From 62f5b3cde86bbab50c76f216b80776672df45425 Mon Sep 17 00:00:00 2001 From: Samir Alajmovic <5246600+alajmo@users.noreply.github.com> Date: Sun, 16 Oct 2022 08:22:12 +0200 Subject: [PATCH 01/18] Release v0.12.1 --- Makefile | 2 +- core/run/exec.go | 6 +++--- core/sake.1 | 2 +- core/utils.go | 4 ++-- docs/changelog.md | 6 ++++++ docs/development.md | 10 +++++----- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 2a384d4..6485cfb 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ NAME := sake PACKAGE := github.com/alajmo/$(NAME) DATE := $(shell date +%FT%T%Z) GIT := $(shell [ -d .git ] && git rev-parse --short HEAD) -VERSION := v0.12.0 +VERSION := v0.12.1 default: build diff --git a/core/run/exec.go b/core/run/exec.go index 80f8d2f..e1c0671 100644 --- a/core/run/exec.go +++ b/core/run/exec.go @@ -393,7 +393,7 @@ func ParseServers(sshConfigFile *string, servers *[]dao.Server, runFlags *core.R port := cfg[proxyJump].Port if port != "" { - p, err := strconv.ParseInt(port, 10, 16) + p, err := strconv.ParseUint(port, 10, 16) if err != nil { errConnect := &ErrConnect{ Name: (*servers)[i].Name, @@ -441,7 +441,7 @@ func ParseServers(sshConfigFile *string, servers *[]dao.Server, runFlags *core.R port := cfg[(*servers)[i].BastionHost].Port if port != "" { - p, err := strconv.ParseInt(port, 10, 16) + p, err := strconv.ParseUint(port, 10, 16) if err != nil { errConnect := &ErrConnect{ Name: (*servers)[i].Name, @@ -486,7 +486,7 @@ func ParseServers(sshConfigFile *string, servers *[]dao.Server, runFlags *core.R // Port port := serv.Port if port != "" { - p, err := strconv.ParseInt(port, 10, 16) + p, err := strconv.ParseUint(port, 10, 16) if err != nil { errConnect := &ErrConnect{ Name: (*servers)[i].Name, diff --git a/core/sake.1 b/core/sake.1 index ca0d044..950517f 100644 --- a/core/sake.1 +++ b/core/sake.1 @@ -1,4 +1,4 @@ -.TH "SAKE" "1" "2022-10-09T22:24:44CEST" "v0.12.0" "Sake Manual" "sake" +.TH "SAKE" "1" "2022-10-16T08:03:53CEST" "v0.12.1" "Sake Manual" "sake" .SH NAME sake - sake is a command runner for local and remote hosts diff --git a/core/utils.go b/core/utils.go index 5cbf844..1ce20bb 100644 --- a/core/utils.go +++ b/core/utils.go @@ -215,7 +215,7 @@ func ParseHostName(hostname string, defaultUser string, defaultPort uint16) (str case 4: if strings.Contains(host, ":") { lastInd := strings.LastIndex(host, ":") - p, err := strconv.ParseInt(host[lastInd+1:], 0, 16) + p, err := strconv.ParseUint(host[lastInd+1:], 10, 16) if err != nil { return "", "", 22, err } @@ -228,7 +228,7 @@ func ParseHostName(hostname string, defaultUser string, defaultPort uint16) (str case 6: if strings.Contains(host, "[") && strings.Contains(host, "]") { if at := strings.LastIndex(host, ":"); at != -1 { - p, err := strconv.ParseInt(host[at+1:], 10, 16) + p, err := strconv.ParseUint(host[at+1:], 10, 16) if err != nil { return "", "", 22, fmt.Errorf("failed to parse %s", hostname) } diff --git a/docs/changelog.md b/docs/changelog.md index be5c907..0823994 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # Changelog +## 0.12.1 + +### Fixes + +- Fix port out of range when using shorthand format for hosts + ## 0.12.0 ### Features diff --git a/docs/development.md b/docs/development.md index 1cd25b3..906dcb1 100644 --- a/docs/development.md +++ b/docs/development.md @@ -42,15 +42,15 @@ go run ../main.go run ping -a The following workflow is used for releasing a new `sake` version: 1. Create pull request with changes -2. Generate manpage - - `make gen-man` -3. Pass all integration and unit tests locally +2. Pass all integration and unit tests locally - `make integration-test` - `make unit-test` -4. Run benchmarks and profiler to check performance +3. Run benchmarks and profiler to check performance - `make benchmark` -5. Verify build works (especially windows build) +4. Verify build works (especially windows build) - `make build-all` +5. Generate manpage + - `make gen-man` 6. Update `Makefile` and `CHANGELOG.md` with correct version, and add all changes to `CHANGELOG.md` 7. Squash-merge to main with `Release vx.y.z` and description of changes 8. Run `make release`, which will: From f989a690e6d82db4eb360e8bea5d17457db10606 Mon Sep 17 00:00:00 2001 From: Samir Alajmovic <5246600+alajmo@users.noreply.github.com> Date: Sun, 4 Dec 2022 21:21:52 +0100 Subject: [PATCH 02/18] Add many new features and some small fixes (#44) --- .github/workflows/build.yml | 4 +- .github/workflows/release.yml | 2 +- Makefile | 8 +- README.md | 24 +- cmd/completion.go | 2 +- cmd/describe.go | 4 +- cmd/describe_servers.go | 7 +- cmd/describe_specs.go | 1 + cmd/describe_targets.go | 1 + cmd/describe_tasks.go | 2 + cmd/edit.go | 4 +- cmd/exec.go | 168 ++++-- cmd/gen.go | 1 + cmd/init.go | 11 +- cmd/list.go | 25 +- cmd/list_servers.go | 20 +- cmd/list_specs.go | 17 +- cmd/list_tags.go | 18 +- cmd/list_targets.go | 16 +- cmd/list_tasks.go | 15 +- cmd/root.go | 21 +- cmd/run.go | 189 ++++-- cmd/ssh.go | 4 +- core/config.man | 151 +++-- core/dao/common.go | 4 + core/dao/config.go | 45 +- core/dao/import_config.go | 16 +- core/dao/import_task.go | 71 ++- core/dao/server.go | 75 ++- core/dao/spec.go | 114 +++- core/dao/target.go | 54 +- core/dao/task.go | 260 ++++---- core/dao/theme.go | 20 +- core/errors.go | 60 +- core/flags.go | 48 +- core/hostname-gen.go | 27 +- core/hostname-gen_test.go | 8 +- core/man.go | 4 +- core/man_gen.go | 6 +- core/print/lib.go | 8 + core/print/print_block.go | 81 ++- core/print/print_table.go | 279 +++++++-- core/print/report.go | 381 ++++++++++++ core/print/table.go | 27 +- core/run/client.go | 23 +- core/run/exec.go | 548 ++++++++++++++--- core/run/exec_test.go | 34 ++ core/run/localhost.go | 74 ++- core/run/ssh.go | 119 ++-- core/run/table.go | 626 ++++++++++++++++---- core/run/text.go | 722 +++++++++++++++++++---- core/run/unix.go | 1 + core/sake.1 | 484 +++++++++------ core/ssh_config.go | 2 + core/utils.go | 22 +- docs/ansible.md | 505 ++++++++++++++++ docs/background.md | 35 ++ docs/changelog.md | 60 ++ docs/command-reference.md | 239 ++++---- docs/config-reference.md | 676 ++++++++++----------- docs/development.md | 13 +- docs/error-handling.md | 145 +++++ docs/examples.md | 56 +- docs/installation.md | 2 +- docs/introduction.md | 4 +- docs/inventory.md | 82 +++ docs/output.md | 198 +++++++ docs/performance.md | 153 +++++ docs/project-background.md | 76 --- docs/recipes.md | 591 +------------------ docs/roadmap.md | 69 +-- docs/task-execution.md | 87 +++ docs/usage.md | 2 +- docs/variables.md | 94 +++ docs/work-dir.md | 108 ++++ go.mod | 19 +- go.sum | 34 +- img/cpu-1.csv | 13 + img/cpu-1.png | Bin 0 -> 42245 bytes img/cpu-2.png | Bin 0 -> 47498 bytes img/dependency-graph.svg | 139 +++++ img/docusaurus.png | Bin 0 -> 19917 bytes img/favicon.ico | Bin 0 -> 15086 bytes img/free-strategy.png | Bin 0 -> 14913 bytes img/host_pinned-strategy.png | Bin 0 -> 19908 bytes img/linear-strategy.png | Bin 0 -> 21359 bytes img/linear-strategy.svg | 16 + img/logo.png | Bin 0 -> 18061 bytes img/logo.svg | 87 +++ img/mem-1.csv | 13 + img/mem-1.png | Bin 0 -> 45203 bytes img/mem-2.png | Bin 0 -> 47289 bytes img/output.gif | Bin 0 -> 234145 bytes img/time-1-short.png | Bin 0 -> 39461 bytes img/time-1.csv | 13 + img/time-1.png | Bin 0 -> 41764 bytes img/time-2-short.png | Bin 0 -> 42032 bytes img/time-2.png | Bin 0 -> 41343 bytes img/undraw_docusaurus_mountain.svg | 170 ++++++ img/undraw_docusaurus_react.svg | 169 ++++++ img/undraw_docusaurus_tree.svg | 1 + test/Dockerfile | 3 + test/benchmark.sh | 25 +- test/benchmarks/list-servers | 4 - test/benchmarks/nested | 4 - test/benchmarks/nested-parallel | 4 - test/benchmarks/ping | 4 - test/benchmarks/ping-parallel | 4 - test/integration/golden/golden-1.stdout | 2 + test/integration/golden/golden-10.stdout | 225 ++++--- test/integration/golden/golden-11.stdout | 103 ++-- test/integration/golden/golden-12.stdout | 35 +- test/integration/golden/golden-13.stdout | 12 +- test/integration/golden/golden-14.stdout | 12 +- test/integration/golden/golden-15.stdout | 17 +- test/integration/golden/golden-16.stdout | 35 +- test/integration/golden/golden-17.stdout | 17 +- test/integration/golden/golden-18.stdout | 55 +- test/integration/golden/golden-19.stdout | 103 ++-- test/integration/golden/golden-2.stdout | 38 +- test/integration/golden/golden-20.stdout | 187 +++--- test/integration/golden/golden-21.stdout | 219 +++---- test/integration/golden/golden-22.stdout | 379 ++++-------- test/integration/golden/golden-23.stdout | 59 +- test/integration/golden/golden-24.stdout | 59 +- test/integration/golden/golden-25.stdout | 59 +- test/integration/golden/golden-26.stdout | 59 +- test/integration/golden/golden-27.stdout | 77 +-- test/integration/golden/golden-28.stdout | 225 ++++++- test/integration/golden/golden-29.stdout | 96 +-- test/integration/golden/golden-3.stdout | 8 +- test/integration/golden/golden-30.stdout | 96 +-- test/integration/golden/golden-31.stdout | 67 ++- test/integration/golden/golden-32.stdout | 129 ++-- test/integration/golden/golden-33.stdout | 31 +- test/integration/golden/golden-34.stdout | 69 ++- test/integration/golden/golden-35.stdout | 61 +- test/integration/golden/golden-36.stdout | 104 ++-- test/integration/golden/golden-37.stdout | 47 ++ test/integration/golden/golden-38.stdout | 47 ++ test/integration/golden/golden-4.stdout | 8 +- test/integration/golden/golden-5.stdout | 8 +- test/integration/main_test.go | 11 +- test/integration/run_test.go | 68 ++- test/playground/env/sake.yaml | 15 +- test/playground/performance/sake.yaml | 1 - test/playground/sake.yaml | 132 ++++- test/playground/{ => tasks}/tasks.yaml | 0 test/profiles/cpu.prof | Bin 4071 -> 0 bytes test/profiles/goroutine.prof | Bin 2100 -> 0 bytes test/profiles/heap.prof | Bin 4780 -> 0 bytes test/profiles/list-servers | 4 +- test/profiles/nested | 6 +- test/profiles/nested-parallel | 6 +- test/profiles/ping | 6 +- test/profiles/ping-no-key | 4 +- test/profiles/ping-parallel | 6 +- test/sake.yaml | 7 +- test/servers.yaml | 2 +- test/tasks.yaml | 67 ++- 160 files changed, 8294 insertions(+), 3664 deletions(-) create mode 100644 core/print/report.go create mode 100644 core/run/exec_test.go create mode 100644 docs/ansible.md create mode 100644 docs/background.md create mode 100644 docs/error-handling.md create mode 100644 docs/inventory.md create mode 100644 docs/output.md create mode 100644 docs/performance.md delete mode 100644 docs/project-background.md create mode 100644 docs/task-execution.md create mode 100644 docs/variables.md create mode 100644 docs/work-dir.md create mode 100644 img/cpu-1.csv create mode 100644 img/cpu-1.png create mode 100644 img/cpu-2.png create mode 100644 img/dependency-graph.svg create mode 100644 img/docusaurus.png create mode 100644 img/favicon.ico create mode 100644 img/free-strategy.png create mode 100644 img/host_pinned-strategy.png create mode 100644 img/linear-strategy.png create mode 100644 img/linear-strategy.svg create mode 100644 img/logo.png create mode 100644 img/logo.svg create mode 100644 img/mem-1.csv create mode 100644 img/mem-1.png create mode 100644 img/mem-2.png create mode 100644 img/output.gif create mode 100644 img/time-1-short.png create mode 100644 img/time-1.csv create mode 100644 img/time-1.png create mode 100644 img/time-2-short.png create mode 100644 img/time-2.png create mode 100644 img/undraw_docusaurus_mountain.svg create mode 100644 img/undraw_docusaurus_react.svg create mode 100644 img/undraw_docusaurus_tree.svg delete mode 100644 test/benchmarks/list-servers delete mode 100644 test/benchmarks/nested delete mode 100644 test/benchmarks/nested-parallel delete mode 100644 test/benchmarks/ping delete mode 100644 test/benchmarks/ping-parallel create mode 100755 test/integration/golden/golden-37.stdout create mode 100755 test/integration/golden/golden-38.stdout rename test/playground/{ => tasks}/tasks.yaml (100%) delete mode 100644 test/profiles/cpu.prof delete mode 100644 test/profiles/goroutine.prof delete mode 100644 test/profiles/heap.prof diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 970b224..218aaa2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,13 +12,13 @@ jobs: strategy: matrix: os: [ubuntu-latest] - go: [1.18] + go: [1.19] steps: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.18 + go-version: 1.19 - name: Check out code uses: actions/checkout@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1aa0c3b..8128662 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.18 + go-version: 1.19 - name: Create release notes run: ./scripts/release.sh diff --git a/Makefile b/Makefile index 6485cfb..79b94e8 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ NAME := sake PACKAGE := github.com/alajmo/$(NAME) DATE := $(shell date +%FT%T%Z) GIT := $(shell [ -d .git ] && git rev-parse --short HEAD) -VERSION := v0.12.1 +VERSION := v0.13.0 default: build @@ -28,8 +28,7 @@ benchmark-save: test: # Unit tests - go test -v ./core/*.go -v - go test -v ./core/dao/*.go -v + go test -v ./core/... # Integration tests cd ./test && docker-compose up -d @@ -37,8 +36,7 @@ test: cd ./test && docker-compose down unit-test: - go test -v ./core/*.go -v - go test -v ./core/dao/*.go -v + go test -v ./core/... integration-test: go test -v ./test/integration/... -clean diff --git a/README.md b/README.md index 496cf6c..3d82873 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ This readme is also accessible on [sakecli.com](https://sakecli.com/). ![demo](res/output.gif) -Interested in managing your git repositiories in a similar way? Check out [mani](https://github.com/alajmo/mani)! +Interested in managing your git repositories in a similar way? Check out [mani](https://github.com/alajmo/mani)! ## Table of Contents @@ -78,7 +78,7 @@ Auto-completion is available via `sake completion bash|zsh|fish` and man page vi ### Building From Source -Requires [go 1.18 or above](https://golang.org/doc/install). +Requires [go 1.19 or above](https://golang.org/doc/install). 1. Clone the repo 2. Build and run the executable @@ -133,7 +133,7 @@ TASK ping: Pong ************ 0.0.0.0 | pong # Count number of files in each server in parallel -$ sake exec --all --output table --parallel 'find . -type f | wc -l' +$ sake exec --all --output table --strategy=free 'find . -type f | wc -l' Server | Output -----------+-------- @@ -150,10 +150,22 @@ Check out the [examples page](/docs/examples.md) for more advanced examples and - [Recipes](docs/recipes.md) - [Config Reference](docs/config-reference.md) - [Command Reference](docs/command-reference.md) +- Documentation + - [Inventory](docs/inventory.md) + - [Task Execution](docs/task-execution.md) + - [Error Handling](docs/error-handling.md) + - [Variables](docs/variables.md) + - [Working Directory](docs/work-dir.md) + - [Output](docs/output.md) +- Project + - [Background](docs/background.md) + - [Roadmap](docs/roadmap.md) + - [Ansible](docs/ansible.md) + - [Performance](docs/performance.md) +- Development + - [Development](docs/development.md) + - [Contributing](docs/contributing.md) - [Changelog](docs/changelog.md) -- [Roadmap](docs/roadmap.md) -- [Project Background](docs/project-background.md) -- [Contributing](docs/contributing.md) ## [License](LICENSE) diff --git a/cmd/completion.go b/cmd/completion.go index eeeb4d2..534ca01 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -51,7 +51,7 @@ PowerShell: # and source this file from your PowerShell profile. `, ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, - Args: cobra.ExactValidArgs(1), + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), Run: generateCompletion, } diff --git a/cmd/describe.go b/cmd/describe.go index 870db04..bd6083d 100644 --- a/cmd/describe.go +++ b/cmd/describe.go @@ -10,8 +10,8 @@ func describeCmd(config *dao.Config, configErr *error) *cobra.Command { cmd := cobra.Command{ Aliases: []string{"desc"}, Use: "describe ", - Short: "Describe servers and tasks", - Long: "Describe servers and tasks.", + Short: "Describe servers, tasks, specs and targets", + Long: "Describe servers, tasks, specs and targets", Example: ` # Describe servers sake describe servers diff --git a/cmd/describe_servers.go b/cmd/describe_servers.go index a686734..0b94af7 100644 --- a/cmd/describe_servers.go +++ b/cmd/describe_servers.go @@ -37,10 +37,7 @@ func describeServersCmd(config *dao.Config, configErr *error) *cobra.Command { return values, cobra.ShellCompDirectiveNoFileComp }, } - - cmd.Flags().StringVarP(&serverFlags.Regex, "regex", "r", "", "filter servers on host regex") - - cmd.Flags().BoolVarP(&serverFlags.Invert, "invert", "v", false, "invert matching on servers") + cmd.Flags().SortFlags = false cmd.Flags().StringSliceVarP(&serverFlags.Tags, "tags", "t", []string{}, "filter servers by their tag") err := cmd.RegisterFlagCompletionFunc("tags", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { @@ -53,6 +50,8 @@ func describeServersCmd(config *dao.Config, configErr *error) *cobra.Command { }) core.CheckIfError(err) + cmd.Flags().StringVarP(&serverFlags.Regex, "regex", "r", "", "filter servers on host regex") + cmd.Flags().BoolVarP(&serverFlags.Invert, "invert", "v", false, "invert matching on servers") cmd.Flags().BoolVarP(&serverFlags.Edit, "edit", "e", false, "edit server") return &cmd diff --git a/cmd/describe_specs.go b/cmd/describe_specs.go index 0cf2282..8aee6bd 100644 --- a/cmd/describe_specs.go +++ b/cmd/describe_specs.go @@ -32,6 +32,7 @@ func describeSpecsCmd(config *dao.Config, configErr *error) *cobra.Command { }, DisableAutoGenTag: true, } + cmd.Flags().SortFlags = false cmd.Flags().BoolVarP(&specFlags.Edit, "edit", "e", false, "edit spec") diff --git a/cmd/describe_targets.go b/cmd/describe_targets.go index 20546c2..c067b31 100644 --- a/cmd/describe_targets.go +++ b/cmd/describe_targets.go @@ -32,6 +32,7 @@ func describeTargetsCmd(config *dao.Config, configErr *error) *cobra.Command { }, DisableAutoGenTag: true, } + cmd.Flags().SortFlags = false cmd.Flags().BoolVarP(&targetFlags.Edit, "edit", "e", false, "edit target") diff --git a/cmd/describe_tasks.go b/cmd/describe_tasks.go index c542940..3f36867 100644 --- a/cmd/describe_tasks.go +++ b/cmd/describe_tasks.go @@ -36,6 +36,8 @@ func describeTasksCmd(config *dao.Config, configErr *error) *cobra.Command { DisableAutoGenTag: true, } + cmd.Flags().SortFlags = false + cmd.Flags().BoolVarP(&taskFlags.Edit, "edit", "e", false, "edit task") return &cmd diff --git a/cmd/edit.go b/cmd/edit.go index 0663b43..807cb5b 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -28,10 +28,10 @@ func editCmd(config *dao.Config, configErr *error) *cobra.Command { } cmd.AddCommand( - editTask(config, configErr), editServer(config, configErr), - editSpec(config, configErr), + editTask(config, configErr), editTarget(config, configErr), + editSpec(config, configErr), ) return &cmd diff --git a/cmd/exec.go b/cmd/exec.go index 73f8db1..6a9f9e3 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -35,12 +35,45 @@ before the command gets executed in each directory.`, // This is necessary since cobra doesn't support pointers for bools // (that would allow us to use nil as default value) - setRunFlags.Local = cmd.Flags().Changed("local") - setRunFlags.Parallel = cmd.Flags().Changed("parallel") - setRunFlags.OmitEmpty = cmd.Flags().Changed("omit-empty") + setRunFlags.All = cmd.Flags().Changed("all") setRunFlags.AnyErrorsFatal = cmd.Flags().Changed("any-errors-fatal") + setRunFlags.Attach = cmd.Flags().Changed("attach") + setRunFlags.Batch = cmd.Flags().Changed("batch") + setRunFlags.BatchP = cmd.Flags().Changed("batch-p") setRunFlags.IgnoreErrors = cmd.Flags().Changed("ignore-error") - setRunFlags.IgnoreUnreachable = cmd.Flags().Changed("ignore_unreachable") + setRunFlags.IgnoreUnreachable = cmd.Flags().Changed("ignore-unreachable") + setRunFlags.Invert = cmd.Flags().Changed("invert") + setRunFlags.Limit = cmd.Flags().Changed("limit") + setRunFlags.LimitP = cmd.Flags().Changed("limit-p") + setRunFlags.ListHosts = cmd.Flags().Changed("describe-hosts") + setRunFlags.Order = cmd.Flags().Changed("order") + setRunFlags.Local = cmd.Flags().Changed("local") + setRunFlags.OmitEmptyRows = cmd.Flags().Changed("omit-empty-rows") + setRunFlags.OmitEmptyColumns = cmd.Flags().Changed("omit-empty-columns") + setRunFlags.Regex = cmd.Flags().Changed("regex") + setRunFlags.Report = cmd.Flags().Changed("report") + setRunFlags.Servers = cmd.Flags().Changed("servers") + setRunFlags.Silent = cmd.Flags().Changed("silent") + setRunFlags.Confirm = cmd.Flags().Changed("confirm") + setRunFlags.Step = cmd.Flags().Changed("step") + setRunFlags.TTY = cmd.Flags().Changed("tty") + setRunFlags.Tags = cmd.Flags().Changed("tags") + setRunFlags.Verbose = cmd.Flags().Changed("verbose") + + maxFailPercentage, err := cmd.Flags().GetUint8("max-fail-percentage") + core.CheckIfError(err) + runFlags.MaxFailPercentage = maxFailPercentage + + forks, err := cmd.Flags().GetUint32("forks") + core.CheckIfError(err) + runFlags.Forks = forks + + batch, err := cmd.Flags().GetUint32("batch") + core.CheckIfError(err) + batchp, err := cmd.Flags().GetUint8("batch-p") + core.CheckIfError(err) + runFlags.Batch = batch + runFlags.BatchP = batchp limit, err := cmd.Flags().GetUint32("limit") core.CheckIfError(err) @@ -55,49 +88,37 @@ before the command gets executed in each directory.`, DisableAutoGenTag: true, } - cmd.Flags().BoolVar(&runFlags.TTY, "tty", false, "replace the current process") - cmd.Flags().BoolVar(&runFlags.Attach, "attach", false, "ssh to server after command") - cmd.Flags().BoolVar(&runFlags.Local, "local", false, "run command on localhost") - cmd.MarkFlagsMutuallyExclusive("tty", "attach", "local") + cmd.PersistentFlags().SortFlags = false + cmd.Flags().SortFlags = false cmd.Flags().BoolVar(&runFlags.DryRun, "dry-run", false, "prints the command to see what will be executed") - cmd.Flags().BoolVarP(&runFlags.Silent, "silent", "S", false, "omit showing loader when running tasks") - cmd.Flags().BoolVar(&runFlags.AnyErrorsFatal, "any-errors-fatal", false, "stop task execution on all servers on error") - cmd.Flags().BoolVar(&runFlags.IgnoreErrors, "ignore-errors", false, "continue task execution on errors") - cmd.Flags().BoolVar(&runFlags.IgnoreUnreachable, "ignore-unreachable", false, "ignore unreachable hosts") - cmd.Flags().BoolVar(&runFlags.OmitEmpty, "omit-empty", false, "omit empty results for table output") - cmd.Flags().BoolVarP(&runFlags.Parallel, "parallel", "p", false, "run server tasks in parallel") - cmd.Flags().StringVarP(&runFlags.IdentityFile, "identity-file", "i", "", "set identity file for all servers") - cmd.Flags().StringVar(&runFlags.Password, "password", "", "set ssh password for all servers") - cmd.Flags().StringVar(&runFlags.KnownHostsFile, "known-hosts-file", "", "set known hosts file") - - cmd.Flags().StringVarP(&runFlags.Regex, "regex", "r", "", "filter servers on host regex") + cmd.Flags().BoolVar(&runFlags.Describe, "describe", false, "print task information") + cmd.Flags().BoolVar(&runFlags.ListHosts, "list-hosts", false, "print hosts that will be targetted") + cmd.Flags().BoolVarP(&runFlags.Verbose, "verbose", "V", false, "enable all diagnostics") - cmd.Flags().Uint32P("limit", "l", 0, "set limit of servers to target") - cmd.Flags().Uint8P("limit-p", "L", 0, "set percentage of servers to target") - cmd.MarkFlagsMutuallyExclusive("limit", "limit-p") - - cmd.Flags().BoolVarP(&runFlags.Invert, "invert", "v", false, "invert matching on servers") - - cmd.Flags().StringVarP(&runFlags.Output, "output", "o", "", "set task output [text|table|table-2|table-3|table-4|html|markdown]") - err := cmd.RegisterFlagCompletionFunc("output", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + cmd.Flags().StringVarP(&runFlags.Strategy, "strategy", "S", "", "set execution strategy [linear|host_pinned|free]") + err := cmd.RegisterFlagCompletionFunc("strategy", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if *configErr != nil { return []string{}, cobra.ShellCompDirectiveDefault } - - valid := []string{"text", "table", "table-2", "table-3", "table-4", "html", "markdown"} - return valid, cobra.ShellCompDirectiveDefault + return strategies, cobra.ShellCompDirectiveDefault }) core.CheckIfError(err) + cmd.Flags().Uint32P("forks", "f", 10000, "max number of concurrent processes") + cmd.Flags().Uint32P("batch", "b", 0, "set number of hosts to run in parallel") + cmd.Flags().Uint8P("batch-p", "B", 0, "set percentage of servers to run in parallel [0-100]") + cmd.MarkFlagsMutuallyExclusive("batch", "batch-p") + cmd.Flags().BoolVarP(&runFlags.All, "all", "a", false, "target all servers") + cmd.Flags().BoolVarP(&runFlags.Invert, "invert", "v", false, "invert matching on servers") + cmd.Flags().StringVarP(&runFlags.Regex, "regex", "r", "", "filter servers on host regex") cmd.Flags().StringSliceVarP(&runFlags.Servers, "servers", "s", []string{}, "target servers by names") err = cmd.RegisterFlagCompletionFunc("servers", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if *configErr != nil { return []string{}, cobra.ShellCompDirectiveDefault } - servers := config.GetServerNameAndDesc() return servers, cobra.ShellCompDirectiveDefault }) @@ -108,24 +129,94 @@ before the command gets executed in each directory.`, if *configErr != nil { return []string{}, cobra.ShellCompDirectiveDefault } - tags := config.GetTags() return tags, cobra.ShellCompDirectiveDefault }) core.CheckIfError(err) + cmd.Flags().StringVarP(&runFlags.Target, "target", "T", "", "target servers by target name") + err = cmd.RegisterFlagCompletionFunc("target", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if *configErr != nil { + return []string{}, cobra.ShellCompDirectiveDefault + } + values := config.GetTargetNames() + return values, cobra.ShellCompDirectiveDefault + }) + core.CheckIfError(err) + + cmd.Flags().StringVar(&runFlags.Order, "order", "", "order hosts") + err = cmd.RegisterFlagCompletionFunc("order", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if *configErr != nil { + return []string{}, cobra.ShellCompDirectiveDefault + } + return orders, cobra.ShellCompDirectiveDefault + }) + core.CheckIfError(err) + + cmd.Flags().Uint32P("limit", "l", 0, "set limit of servers to target") + cmd.Flags().Uint8P("limit-p", "L", 0, "set percentage of servers to target") + cmd.MarkFlagsMutuallyExclusive("limit", "limit-p") + + cmd.Flags().BoolVar(&runFlags.IgnoreUnreachable, "ignore-unreachable", false, "ignore unreachable hosts") + cmd.Flags().Uint8P("max-fail-percentage", "M", 0, "stop task execution on all servers when threshold reached") + cmd.Flags().BoolVar(&runFlags.AnyErrorsFatal, "any-errors-fatal", false, "stop task execution on all servers on error") + cmd.MarkFlagsMutuallyExclusive("any-errors-fatal", "max-fail-percentage") + cmd.Flags().BoolVar(&runFlags.IgnoreErrors, "ignore-errors", false, "continue task execution on errors") + + cmd.Flags().StringVarP(&runFlags.Spec, "spec", "J", "", "set spec") + err = cmd.RegisterFlagCompletionFunc("spec", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if *configErr != nil { + return []string{}, cobra.ShellCompDirectiveDefault + } + values := config.GetSpecNames() + return values, cobra.ShellCompDirectiveDefault + }) + core.CheckIfError(err) + + cmd.Flags().StringVarP(&runFlags.Output, "output", "o", "", "set task output [text|table|table-2|table-3|table-4|html|markdown|json|csv|none]") + err = cmd.RegisterFlagCompletionFunc("output", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if *configErr != nil { + return []string{}, cobra.ShellCompDirectiveDefault + } + valid := []string{"text", "table", "table-2", "table-3", "table-4", "html", "markdown", "json", "csv", "none"} + return valid, cobra.ShellCompDirectiveDefault + }) + core.CheckIfError(err) + + cmd.Flags().BoolVar(&runFlags.OmitEmptyRows, "omit-empty-rows", false, "omit empty row for table output") + cmd.Flags().BoolVar(&runFlags.OmitEmptyColumns, "omit-empty-columns", false, "omit empty column for table output") + cmd.Flags().BoolVarP(&runFlags.Silent, "silent", "q", false, "omit showing loader when running tasks") + cmd.Flags().BoolVar(&runFlags.Confirm, "confirm", false, "confirm root task before running") + cmd.Flags().BoolVar(&runFlags.Step, "step", false, "confirm each task before running") cmd.PersistentFlags().StringVar(&runFlags.Theme, "theme", "default", "set theme") err = cmd.RegisterFlagCompletionFunc("theme", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if *configErr != nil { return []string{}, cobra.ShellCompDirectiveDefault } - names := config.GetThemeNames() - return names, cobra.ShellCompDirectiveDefault }) core.CheckIfError(err) + cmd.Flags().BoolVar(&runFlags.TTY, "tty", false, "replace the current process") + cmd.Flags().BoolVar(&runFlags.Attach, "attach", false, "ssh to server after command") + cmd.Flags().BoolVar(&runFlags.Local, "local", false, "run command on localhost") + cmd.MarkFlagsMutuallyExclusive("tty", "attach", "local") + + cmd.Flags().StringSliceVarP(&runFlags.Report, "report", "R", []string{"recap"}, "reports to show") + err = cmd.RegisterFlagCompletionFunc("report", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if *configErr != nil { + return []string{}, cobra.ShellCompDirectiveDefault + } + return reports, cobra.ShellCompDirectiveDefault + }) + core.CheckIfError(err) + + cmd.Flags().StringVarP(&runFlags.IdentityFile, "identity-file", "i", "", "set identity file for all servers") + cmd.Flags().StringVarP(&runFlags.User, "user", "U", "", "set ssh user") + cmd.Flags().StringVar(&runFlags.Password, "password", "", "set ssh password for all servers") + cmd.Flags().StringVar(&runFlags.KnownHostsFile, "known-hosts-file", "", "set known hosts file") + return &cmd } @@ -150,7 +241,12 @@ func execTask( Cmd: cmdStr, } - task := dao.Task{Tasks: []dao.TaskCmd{cmd}, ID: "output", Name: "output"} + spec, err := config.GetSpec("default") + core.CheckIfError(err) + tt, err := config.GetTarget("default") + core.CheckIfError(err) + + task := dao.Task{Spec: *spec, Target: *tt, Tasks: []dao.TaskCmd{cmd}, ID: "output", Name: "output"} taskErrors := make([]dao.ResourceErrors[dao.Task], 1) var configErr = "" @@ -164,7 +260,7 @@ func execTask( } target := run.Run{Servers: servers, Task: &task, Config: *config} - err := target.RunTask([]string{}, runFlags, setRunFlags) + err = target.RunTask([]string{}, runFlags, setRunFlags) core.CheckIfError(err) } } diff --git a/cmd/gen.go b/cmd/gen.go index 1f4c17e..ead5dec 100644 --- a/cmd/gen.go +++ b/cmd/gen.go @@ -19,6 +19,7 @@ func genCmd() *cobra.Command { DisableAutoGenTag: true, } + cmd.Flags().SortFlags = false cmd.Flags().StringVarP(&dir, "dir", "d", "./", "directory to save manpage to") err := cmd.RegisterFlagCompletionFunc("dir", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { diff --git a/cmd/init.go b/cmd/init.go index d94647b..0c2ba05 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -36,10 +36,10 @@ func PrintServerInit(servers []dao.Server) { } options := print.PrintTableOptions{ - Theme: theme, - OmitEmpty: true, - Output: "table", - SuppressEmptyColumns: false, + Theme: theme, + OmitEmptyRows: true, + OmitEmptyColumns: false, + Output: "table", } data := dao.TableOutput{ @@ -52,5 +52,6 @@ func PrintServerInit(servers []dao.Server) { } fmt.Println("\nFollowing servers were added to sake.yaml") - print.PrintTable(data.Rows, options, data.Headers) + err := print.PrintTable(data.Rows, options, data.Headers, []string{}, true, true) + core.CheckIfError(err) } diff --git a/cmd/list.go b/cmd/list.go index 60d62f4..a778f63 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -13,8 +13,8 @@ func listCmd(config *dao.Config, configErr *error) *cobra.Command { cmd := cobra.Command{ Aliases: []string{"ls", "l"}, Use: "list", - Short: "List servers, tasks and tags", - Long: "List servers, tasks and tags.", + Short: "List servers, tasks, tags, specs and targets", + Long: "List servers, tasks, tags, specs and targets", Example: ` # List all servers sake list servers @@ -25,6 +25,8 @@ func listCmd(config *dao.Config, configErr *error) *cobra.Command { sake list tags`, DisableAutoGenTag: true, } + cmd.PersistentFlags().SortFlags = false + cmd.Flags().SortFlags = false cmd.AddCommand( listServersCmd(config, configErr, &listFlags), @@ -34,26 +36,23 @@ func listCmd(config *dao.Config, configErr *error) *cobra.Command { listSpecsCmd(config, configErr, &listFlags), ) - cmd.PersistentFlags().StringVar(&listFlags.Theme, "theme", "default", "set theme") - err := cmd.RegisterFlagCompletionFunc("theme", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + cmd.PersistentFlags().StringVarP(&listFlags.Output, "output", "o", "table", "set table output [table|table-2|table-3|table-4|html|markdown|json|csv]") + err := cmd.RegisterFlagCompletionFunc("output", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if *configErr != nil { return []string{}, cobra.ShellCompDirectiveDefault } - - names := config.GetThemeNames() - - return names, cobra.ShellCompDirectiveDefault + valid := []string{"table", "table-2", "table-3", "table-4", "html", "markdown", "json", "csv"} + return valid, cobra.ShellCompDirectiveDefault }) core.CheckIfError(err) - cmd.PersistentFlags().StringVarP(&listFlags.Output, "output", "o", "table", "set table output [table|table-2|table-3|table-4|markdown|html]") - err = cmd.RegisterFlagCompletionFunc("output", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + cmd.PersistentFlags().StringVar(&listFlags.Theme, "theme", "default", "set theme") + err = cmd.RegisterFlagCompletionFunc("theme", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if *configErr != nil { return []string{}, cobra.ShellCompDirectiveDefault } - - valid := []string{"table", "table-2", "table-3", "table-4", "markdown", "html"} - return valid, cobra.ShellCompDirectiveDefault + names := config.GetThemeNames() + return names, cobra.ShellCompDirectiveDefault }) core.CheckIfError(err) diff --git a/cmd/list_servers.go b/cmd/list_servers.go index 624f376..c21e53c 100644 --- a/cmd/list_servers.go +++ b/cmd/list_servers.go @@ -42,11 +42,10 @@ func listServersCmd(config *dao.Config, configErr *error, listFlags *core.ListFl }, DisableAutoGenTag: true, } - - cmd.Flags().StringVarP(&serverFlags.Regex, "regex", "r", "", "filter servers on host regex") + cmd.Flags().SortFlags = false cmd.Flags().BoolVarP(&serverFlags.Invert, "invert", "v", false, "invert matching on servers") - + cmd.Flags().StringVarP(&serverFlags.Regex, "regex", "r", "", "filter servers on host regex") cmd.Flags().StringSliceVarP(&serverFlags.Tags, "tags", "t", []string{}, "filter servers by tags") err := cmd.RegisterFlagCompletionFunc("tags", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if *configErr != nil { @@ -59,7 +58,7 @@ func listServersCmd(config *dao.Config, configErr *error, listFlags *core.ListFl core.CheckIfError(err) cmd.Flags().BoolVarP(&serverFlags.AllHeaders, "all-headers", "H", false, "select all server headers") - cmd.Flags().StringSliceVar(&serverFlags.Headers, "headers", []string{"server", "host", "tag", "desc"}, "set headers") + cmd.Flags().StringSliceVar(&serverFlags.Headers, "headers", []string{"server", "host", "tags", "desc"}, "set headers") err = cmd.RegisterFlagCompletionFunc("headers", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if err != nil { return []string{}, cobra.ShellCompDirectiveDefault @@ -103,11 +102,11 @@ func listServers(config *dao.Config, args []string, listFlags *core.ListFlags, s if len(servers) > 0 { options := print.PrintTableOptions{ - Output: listFlags.Output, - Theme: *theme, - OmitEmpty: false, - SuppressEmptyColumns: true, - Resource: "server", + Output: listFlags.Output, + Theme: *theme, + OmitEmptyRows: false, + OmitEmptyColumns: true, + Resource: "server", } var headers []string @@ -118,6 +117,7 @@ func listServers(config *dao.Config, args []string, listFlags *core.ListFlags, s } rows := dao.GetTableData(servers, headers) - print.PrintTable(rows, options, headers) + err := print.PrintTable(rows, options, headers, []string{}, true, true) + core.CheckIfError(err) } } diff --git a/cmd/list_specs.go b/cmd/list_specs.go index 5c5969d..c49d593 100644 --- a/cmd/list_specs.go +++ b/cmd/list_specs.go @@ -8,7 +8,7 @@ import ( "github.com/alajmo/sake/core/print" ) -var specHeaders = []string{"spec", "output", "parallel", "any_errors_fatal", "ignore_errors", "ignore_unreachable", "omit_empty"} +var specHeaders = []string{"spec", "desc", "describe", "list_hosts", "order", "silent", "strategy", "batch", "batch_p", "forks", "output", "any_errors_fatal", "max_fail_percentage", "ignore_errors", "ignore_unreachable", "omit_empty", "report"} func listSpecsCmd(config *dao.Config, configErr *error, listFlags *core.ListFlags) *cobra.Command { var specFlags core.SpecFlags @@ -35,6 +35,8 @@ func listSpecsCmd(config *dao.Config, configErr *error, listFlags *core.ListFlag DisableAutoGenTag: true, } + cmd.Flags().SortFlags = false + cmd.Flags().StringSliceVar(&specFlags.Headers, "headers", specHeaders, "set headers") err := cmd.RegisterFlagCompletionFunc("headers", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if *configErr != nil { @@ -59,11 +61,11 @@ func listSpecs( core.CheckIfError(err) options := print.PrintTableOptions{ - Output: listFlags.Output, - Theme: *theme, - OmitEmpty: false, - SuppressEmptyColumns: true, - Resource: "spec", + Output: listFlags.Output, + Theme: *theme, + OmitEmptyRows: false, + OmitEmptyColumns: true, + Resource: "spec", } var specs []dao.Spec @@ -77,6 +79,7 @@ func listSpecs( if len(specs) > 0 { rows := dao.GetTableData(specs, specFlags.Headers) - print.PrintTable(rows, options, specFlags.Headers) + err := print.PrintTable(rows, options, specFlags.Headers, []string{}, true, true) + core.CheckIfError(err) } } diff --git a/cmd/list_tags.go b/cmd/list_tags.go index 2f41465..d4e7002 100644 --- a/cmd/list_tags.go +++ b/cmd/list_tags.go @@ -35,6 +35,8 @@ func listTagsCmd(config *dao.Config, configErr *error, listFlags *core.ListFlags DisableAutoGenTag: true, } + cmd.Flags().SortFlags = false + cmd.Flags().StringSliceVar(&tagFlags.Headers, "headers", tagHeaders, "set headers") err := cmd.RegisterFlagCompletionFunc("headers", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if *configErr != nil { @@ -59,11 +61,11 @@ func listTags( core.CheckIfError(err) options := print.PrintTableOptions{ - Output: listFlags.Output, - Theme: *theme, - OmitEmpty: false, - SuppressEmptyColumns: true, - Resource: "tag", + Output: listFlags.Output, + Theme: *theme, + OmitEmptyRows: false, + OmitEmptyColumns: true, + Resource: "tag", } allTags := config.GetTags() @@ -79,14 +81,16 @@ func listTags( core.CheckIfError(err) if len(tags) > 0 { - print.PrintTable(tags, options, tagFlags.Headers) + err := print.PrintTable(tags, options, tagFlags.Headers, []string{}, true, true) + core.CheckIfError(err) } } else { tags, err := config.GetTagAssocations(allTags) core.CheckIfError(err) if len(tags) > 0 { rows := dao.GetTableData(tags, tagFlags.Headers) - print.PrintTable(rows, options, tagFlags.Headers) + err := print.PrintTable(rows, options, tagFlags.Headers, []string{}, true, true) + core.CheckIfError(err) } } } diff --git a/cmd/list_targets.go b/cmd/list_targets.go index aac3ef3..4de0dd4 100644 --- a/cmd/list_targets.go +++ b/cmd/list_targets.go @@ -8,7 +8,7 @@ import ( "github.com/alajmo/sake/core/print" ) -var targetHeaders = []string{"target", "all", "servers", "tags", "regex", "invert", "limit", "limit_p"} +var targetHeaders = []string{"target", "desc", "all", "servers", "tags", "regex", "invert", "limit", "limit_p"} func listTargetsCmd(config *dao.Config, configErr *error, listFlags *core.ListFlags) *cobra.Command { var targetFlags core.TargetFlags @@ -34,6 +34,7 @@ func listTargetsCmd(config *dao.Config, configErr *error, listFlags *core.ListFl }, DisableAutoGenTag: true, } + cmd.Flags().SortFlags = false cmd.Flags().StringSliceVar(&targetFlags.Headers, "headers", targetHeaders, "set headers. Available headers: name, regex") err := cmd.RegisterFlagCompletionFunc("headers", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { @@ -59,11 +60,11 @@ func listTargets( core.CheckIfError(err) options := print.PrintTableOptions{ - Output: listFlags.Output, - Theme: *theme, - OmitEmpty: false, - SuppressEmptyColumns: true, - Resource: "target", + Output: listFlags.Output, + Theme: *theme, + OmitEmptyRows: false, + OmitEmptyColumns: true, + Resource: "target", } var targets []dao.Target @@ -77,6 +78,7 @@ func listTargets( if len(targets) > 0 { rows := dao.GetTableData(targets, targetFlags.Headers) - print.PrintTable(rows, options, targetFlags.Headers) + err := print.PrintTable(rows, options, targetFlags.Headers, []string{}, true, true) + core.CheckIfError(err) } } diff --git a/cmd/list_tasks.go b/cmd/list_tasks.go index 4fa96f3..17c4383 100644 --- a/cmd/list_tasks.go +++ b/cmd/list_tasks.go @@ -37,6 +37,8 @@ func listTasksCmd(config *dao.Config, configErr *error, listFlags *core.ListFlag DisableAutoGenTag: true, } + cmd.Flags().SortFlags = false + cmd.Flags().BoolVarP(&taskFlags.AllHeaders, "all-headers", "H", false, "select all task headers") cmd.Flags().StringSliceVar(&taskFlags.Headers, "headers", []string{"task", "desc"}, "set headers") err := cmd.RegisterFlagCompletionFunc("headers", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { @@ -67,11 +69,11 @@ func listTasks( if len(tasks) > 0 { options := print.PrintTableOptions{ - Output: listFlags.Output, - Theme: *theme, - OmitEmpty: false, - SuppressEmptyColumns: true, - Resource: "task", + Output: listFlags.Output, + Theme: *theme, + OmitEmptyRows: false, + OmitEmptyColumns: true, + Resource: "task", } var headers []string @@ -82,6 +84,7 @@ func listTasks( } rows := dao.GetTableData(tasks, headers) - print.PrintTable(rows, options, headers) + err := print.PrintTable(rows, options, headers, []string{}, true, true) + core.CheckIfError(err) } } diff --git a/cmd/root.go b/cmd/root.go index ad836b5..4759bb8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,8 +12,8 @@ import ( const ( appName = "sake" - shortAppDesc = "sake is a command runner for local and remote hosts" - longAppDesc = `sake is a command runner for local and remote hosts. + shortAppDesc = "sake is a task runner for local and remote hosts" + longAppDesc = `sake is a task runner for local and remote hosts. You define servers and tasks in a sake.yaml config file and then run the tasks on the servers. ` @@ -56,22 +56,27 @@ func init() { cobra.OnInitialize(initConfig) + cobra.EnableCommandSorting = false + + rootCmd.Flags().SortFlags = false + rootCmd.PersistentFlags().SortFlags = false + rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "specify config") rootCmd.PersistentFlags().StringVarP(&userConfigPath, "user-config", "u", "", "specify user config") - rootCmd.PersistentFlags().StringVarP(&sshConfigPath, "ssh-config", "U", "", "specify ssh config") + rootCmd.PersistentFlags().StringVar(&sshConfigPath, "ssh-config", "", "specify ssh config") rootCmd.PersistentFlags().BoolVar(&noColor, "no-color", false, "disable color") rootCmd.AddCommand( - completionCmd(), - genCmd(), initCmd(), listCmd(&config, &configErr), describeCmd(&config, &configErr), - editCmd(&config, &configErr), - execCmd(&config, &configErr), runCmd(&config, &configErr), - checkCmd(&config, &configErr), + execCmd(&config, &configErr), sshCmd(&config, &configErr), + editCmd(&config, &configErr), + checkCmd(&config, &configErr), + completionCmd(), + genCmd(), ) rootCmd.SetVersionTemplate(fmt.Sprintf("Version: %-10s\nCommit: %-10s\nDate: %-10s\n", version, commit, date)) diff --git a/cmd/run.go b/cmd/run.go index 8947a3f..85f335f 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -11,6 +11,28 @@ import ( "github.com/alajmo/sake/core/run" ) +var strategies = []string{ + "linear\texecute task for each host before proceeding to the next task (default)", + "host_pinned\texecutes tasks (serial) for a host before proceeding to the next host", + "free\texecutes tasks without waiting for other tasks", +} + +var orders = []string{ + "inventory\tThe order is as provided by the inventory", + "reverse_inventory\tThe order is the reverse of the inventory", + "sorted\tHosts are alphabetically sorted by host", + "reverse_sorted\tHosts are sorted by host in reverse alphabetical order", + "random\tHosts are randomly ordered", +} + +var reports = []string{ + "recap\tshow basic report", + "rc\tshow return code for each host and task", + "task\tshow task status for each host and task", + "time\tshow time report for each host and task", + "all\tshow available reports", +} + func runCmd(config *dao.Config, configErr *error) *cobra.Command { var runFlags core.RunFlags var setRunFlags core.SetRunFlags @@ -34,14 +56,45 @@ func runCmd(config *dao.Config, configErr *error) *cobra.Command { // This is necessary since cobra doesn't support pointers for bools // (that would allow us to use nil as default value) setRunFlags.All = cmd.Flags().Changed("all") - setRunFlags.Invert = cmd.Flags().Changed("invert") - setRunFlags.Local = cmd.Flags().Changed("local") - setRunFlags.TTY = cmd.Flags().Changed("tty") - setRunFlags.Parallel = cmd.Flags().Changed("parallel") - setRunFlags.OmitEmpty = cmd.Flags().Changed("omit-empty") setRunFlags.AnyErrorsFatal = cmd.Flags().Changed("any-errors-fatal") + setRunFlags.Attach = cmd.Flags().Changed("attach") + setRunFlags.Batch = cmd.Flags().Changed("batch") + setRunFlags.BatchP = cmd.Flags().Changed("batch-p") + setRunFlags.Describe = cmd.Flags().Changed("describe") setRunFlags.IgnoreErrors = cmd.Flags().Changed("ignore-errors") setRunFlags.IgnoreUnreachable = cmd.Flags().Changed("ignore-unreachable") + setRunFlags.Invert = cmd.Flags().Changed("invert") + setRunFlags.Limit = cmd.Flags().Changed("limit") + setRunFlags.LimitP = cmd.Flags().Changed("limit-p") + setRunFlags.ListHosts = cmd.Flags().Changed("list-hosts") + setRunFlags.Order = cmd.Flags().Changed("order") + setRunFlags.Local = cmd.Flags().Changed("local") + setRunFlags.OmitEmptyRows = cmd.Flags().Changed("omit-empty-rows") + setRunFlags.OmitEmptyColumns = cmd.Flags().Changed("omit-empty-columns") + setRunFlags.Regex = cmd.Flags().Changed("regex") + setRunFlags.Report = cmd.Flags().Changed("report") + setRunFlags.Servers = cmd.Flags().Changed("servers") + setRunFlags.Silent = cmd.Flags().Changed("silent") + setRunFlags.Confirm = cmd.Flags().Changed("confirm") + setRunFlags.Step = cmd.Flags().Changed("step") + setRunFlags.TTY = cmd.Flags().Changed("tty") + setRunFlags.Tags = cmd.Flags().Changed("tags") + setRunFlags.Verbose = cmd.Flags().Changed("verbose") + + maxFailPercentage, err := cmd.Flags().GetUint8("max-fail-percentage") + core.CheckIfError(err) + runFlags.MaxFailPercentage = maxFailPercentage + + forks, err := cmd.Flags().GetUint32("forks") + core.CheckIfError(err) + runFlags.Forks = forks + + batch, err := cmd.Flags().GetUint32("batch") + core.CheckIfError(err) + batchp, err := cmd.Flags().GetUint8("batch-p") + core.CheckIfError(err) + runFlags.Batch = batch + runFlags.BatchP = batchp limit, err := cmd.Flags().GetUint32("limit") core.CheckIfError(err) @@ -63,79 +116,137 @@ func runCmd(config *dao.Config, configErr *error) *cobra.Command { DisableAutoGenTag: true, } - cmd.Flags().BoolVar(&runFlags.TTY, "tty", false, "replace the current process") - cmd.Flags().BoolVar(&runFlags.Attach, "attach", false, "ssh to server after command") - cmd.Flags().BoolVar(&runFlags.Local, "local", false, "run task on localhost") - cmd.MarkFlagsMutuallyExclusive("tty", "attach", "local") + cmd.PersistentFlags().SortFlags = false + cmd.Flags().SortFlags = false - cmd.Flags().BoolVar(&runFlags.Describe, "describe", false, "print task information") cmd.Flags().BoolVar(&runFlags.DryRun, "dry-run", false, "print the task to see what will be executed") - cmd.Flags().BoolVarP(&runFlags.Silent, "silent", "S", false, "omit showing loader when running tasks") - cmd.Flags().BoolVar(&runFlags.AnyErrorsFatal, "any-errors-fatal", false, "stop task execution on all servers on error") - cmd.Flags().BoolVar(&runFlags.IgnoreErrors, "ignore-errors", false, "continue task execution on errors") - cmd.Flags().BoolVar(&runFlags.IgnoreUnreachable, "ignore-unreachable", false, "ignore unreachable hosts") - cmd.Flags().BoolVar(&runFlags.OmitEmpty, "omit-empty", false, "omit empty results for table output") - cmd.Flags().BoolVarP(&runFlags.Parallel, "parallel", "p", false, "run server tasks in parallel") - cmd.Flags().BoolVarP(&runFlags.Edit, "edit", "e", false, "edit task") - cmd.Flags().StringVarP(&runFlags.IdentityFile, "identity-file", "i", "", "set identity file for all servers") - cmd.Flags().StringVar(&runFlags.Password, "password", "", "set ssh password for all servers") - cmd.Flags().StringVar(&runFlags.KnownHostsFile, "known-hosts-file", "", "set known hosts file") - - cmd.Flags().StringVarP(&runFlags.Regex, "regex", "r", "", "filter servers on host regex") - - cmd.Flags().Uint32P("limit", "l", 0, "set limit of servers to target") - cmd.Flags().Uint8P("limit-p", "L", 0, "set percentage of servers to target [0-100]") - cmd.MarkFlagsMutuallyExclusive("limit", "limit-p") - - cmd.Flags().BoolVarP(&runFlags.Invert, "invert", "v", false, "invert matching on servers") + cmd.Flags().BoolVar(&runFlags.Describe, "describe", false, "print task information") + cmd.Flags().BoolVar(&runFlags.ListHosts, "list-hosts", false, "print hosts that will be targetted") + cmd.Flags().BoolVarP(&runFlags.Verbose, "verbose", "V", false, "enable all diagnostics") - cmd.Flags().StringVarP(&runFlags.Output, "output", "o", "", "set task output [text|table|table-2|table-3|table-4|html|markdown]") - err := cmd.RegisterFlagCompletionFunc("output", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + cmd.Flags().StringVarP(&runFlags.Strategy, "strategy", "S", "", "set execution strategy [linear|host_pinned|free]") + err := cmd.RegisterFlagCompletionFunc("strategy", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if *configErr != nil { return []string{}, cobra.ShellCompDirectiveDefault } - valid := []string{"text", "table", "table-2", "table-3", "table-4", "html", "markdown"} - return valid, cobra.ShellCompDirectiveDefault + return strategies, cobra.ShellCompDirectiveDefault }) core.CheckIfError(err) - cmd.Flags().BoolVarP(&runFlags.All, "all", "a", false, "target all servers") + cmd.Flags().Uint32P("forks", "f", 10000, "max number of concurrent processes") + cmd.Flags().Uint32P("batch", "b", 0, "set number of hosts to run in parallel") + cmd.Flags().Uint8P("batch-p", "B", 0, "set percentage of hosts to run in parallel [0-100]") + cmd.MarkFlagsMutuallyExclusive("batch", "batch-p") + + cmd.Flags().BoolVarP(&runFlags.All, "all", "a", false, "target all hosts") + cmd.Flags().BoolVarP(&runFlags.Invert, "invert", "v", false, "invert matching on hosts") + cmd.Flags().StringVarP(&runFlags.Regex, "regex", "r", "", "target hosts on host regex") cmd.Flags().StringSliceVarP(&runFlags.Servers, "servers", "s", []string{}, "target servers by names") err = cmd.RegisterFlagCompletionFunc("servers", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if *configErr != nil { return []string{}, cobra.ShellCompDirectiveDefault } - servers := config.GetServerNameAndDesc() return servers, cobra.ShellCompDirectiveDefault }) core.CheckIfError(err) - cmd.Flags().StringSliceVarP(&runFlags.Tags, "tags", "t", []string{}, "target servers by tags") + cmd.Flags().StringSliceVarP(&runFlags.Tags, "tags", "t", []string{}, "target hosts by tags") err = cmd.RegisterFlagCompletionFunc("tags", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if *configErr != nil { return []string{}, cobra.ShellCompDirectiveDefault } - tags := config.GetTags() return tags, cobra.ShellCompDirectiveDefault }) core.CheckIfError(err) - cmd.PersistentFlags().StringVar(&runFlags.Theme, "theme", "", "set theme") - err = cmd.RegisterFlagCompletionFunc("theme", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + cmd.Flags().StringVarP(&runFlags.Target, "target", "T", "", "target hosts by target name") + err = cmd.RegisterFlagCompletionFunc("target", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if *configErr != nil { return []string{}, cobra.ShellCompDirectiveDefault } + values := config.GetTargetNames() + return values, cobra.ShellCompDirectiveDefault + }) + core.CheckIfError(err) - names := config.GetThemeNames() + cmd.Flags().StringVar(&runFlags.Order, "order", "", "order hosts") + err = cmd.RegisterFlagCompletionFunc("order", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if *configErr != nil { + return []string{}, cobra.ShellCompDirectiveDefault + } + return orders, cobra.ShellCompDirectiveDefault + }) + core.CheckIfError(err) + + cmd.Flags().Uint32P("limit", "l", 0, "set limit of servers to target") + cmd.Flags().Uint8P("limit-p", "L", 0, "set percentage of servers to target [0-100]") + cmd.MarkFlagsMutuallyExclusive("limit", "limit-p") + + cmd.Flags().BoolVar(&runFlags.IgnoreUnreachable, "ignore-unreachable", false, "ignore unreachable hosts") + cmd.Flags().Uint8P("max-fail-percentage", "M", 0, "stop task execution on all servers when threshold reached") + cmd.Flags().BoolVar(&runFlags.AnyErrorsFatal, "any-errors-fatal", false, "stop task execution on all servers on error") + cmd.MarkFlagsMutuallyExclusive("any-errors-fatal", "max-fail-percentage") + cmd.Flags().BoolVar(&runFlags.IgnoreErrors, "ignore-errors", false, "continue task execution on errors") + + cmd.Flags().StringVarP(&runFlags.Spec, "spec", "J", "", "set spec") + err = cmd.RegisterFlagCompletionFunc("spec", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if *configErr != nil { + return []string{}, cobra.ShellCompDirectiveDefault + } + values := config.GetSpecNames() + return values, cobra.ShellCompDirectiveDefault + }) + core.CheckIfError(err) + + cmd.Flags().StringVarP(&runFlags.Output, "output", "o", "", "set task output [text|table|table-2|table-3|table-4|html|markdown|json|csv|none]") + err = cmd.RegisterFlagCompletionFunc("output", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if *configErr != nil { + return []string{}, cobra.ShellCompDirectiveDefault + } + valid := []string{"text", "table", "table-2", "table-3", "table-4", "html", "markdown", "json", "csv", "none"} + return valid, cobra.ShellCompDirectiveDefault + }) + core.CheckIfError(err) + cmd.Flags().BoolVar(&runFlags.OmitEmptyRows, "omit-empty-rows", false, "omit empty row for table output") + cmd.Flags().BoolVar(&runFlags.OmitEmptyColumns, "omit-empty-columns", false, "omit empty column for table output") + cmd.Flags().BoolVarP(&runFlags.Silent, "silent", "q", false, "omit showing loader when running tasks") + cmd.Flags().BoolVar(&runFlags.Confirm, "confirm", false, "confirm root task before running") + cmd.Flags().BoolVar(&runFlags.Step, "step", false, "confirm each task before running") + cmd.PersistentFlags().StringVar(&runFlags.Theme, "theme", "default", "set theme") + err = cmd.RegisterFlagCompletionFunc("theme", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if *configErr != nil { + return []string{}, cobra.ShellCompDirectiveDefault + } + names := config.GetThemeNames() return names, cobra.ShellCompDirectiveDefault }) core.CheckIfError(err) + cmd.Flags().BoolVar(&runFlags.TTY, "tty", false, "replace the current process") + cmd.Flags().BoolVar(&runFlags.Attach, "attach", false, "ssh to server after command") + cmd.Flags().BoolVar(&runFlags.Local, "local", false, "run task on localhost") + cmd.MarkFlagsMutuallyExclusive("tty", "attach", "local") + cmd.Flags().BoolVarP(&runFlags.Edit, "edit", "e", false, "edit task") + + cmd.Flags().StringSliceVarP(&runFlags.Report, "report", "R", []string{"recap"}, "reports to show") + err = cmd.RegisterFlagCompletionFunc("report", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if *configErr != nil { + return []string{}, cobra.ShellCompDirectiveDefault + } + return reports, cobra.ShellCompDirectiveDefault + }) + core.CheckIfError(err) + + cmd.Flags().StringVarP(&runFlags.IdentityFile, "identity-file", "i", "", "set identity file") + cmd.Flags().StringVarP(&runFlags.User, "user", "U", "", "set ssh user") + cmd.Flags().StringVar(&runFlags.Password, "password", "", "set ssh password") + cmd.Flags().StringVar(&runFlags.KnownHostsFile, "known-hosts-file", "", "set known hosts file") + return &cmd } diff --git a/cmd/ssh.go b/cmd/ssh.go index 17d3711..1c569b6 100644 --- a/cmd/ssh.go +++ b/cmd/ssh.go @@ -33,6 +33,8 @@ func sshCmd(config *dao.Config, configErr *error) *cobra.Command { DisableAutoGenTag: true, } + cmd.Flags().SortFlags = false + cmd.Flags().StringVarP(&runFlags.IdentityFile, "identity-file", "i", "", "set identity file for all servers") cmd.Flags().StringVar(&runFlags.Password, "password", "", "set ssh password for all servers") @@ -44,7 +46,7 @@ func ssh(args []string, config *dao.Config, runFlags *core.RunFlags) { core.CheckIfError(err) servers := []dao.Server{*server} - errConnect, err := run.ParseServers(config.SSHConfigFile, &servers, runFlags) + errConnect, err := run.ParseServers(config.SSHConfigFile, &servers, runFlags, "inventory") if len(errConnect) > 0 { core.Exit(&errConnect[0]) } diff --git a/core/config.man b/core/config.man index 53938aa..0e25328 100644 --- a/core/config.man +++ b/core/config.man @@ -181,65 +181,112 @@ Below is a config file detailing all of the available options and their defaults header: fg: bg: - align: attr: row: fg: bg: - align: attr: row_alt: fg: bg: - align: attr: footer: fg: bg: - align: attr: # List of Specs [optional] specs: default: - # Set task output [text|table|html|markdown] - output: text + # Spec description + desc: default spec + + # Print task description + describe: false + + # Print list of hosts that will be targetted + list_hosts: false + + # Order hosts [inventory|reverse_inventory|sorted|reverse_sorted|random] + order: inventory + + # Omit showing loader when running tasks + silent: false - # Run server tasks in parallel - parallel: false + # Execution strategy [linear|host_pinned|free] + strategy: linear + + # Number of hosts to run in parallel + batch: 1 + + # Number of hosts in percentage to run in parallel [0-100] + # batch_p: 100 + + # Max number of forks + forks: 10000 + + # Set task output [text|table|table-2|table-3|table-4|html|markdown|json|csv|none] + output: text # Continue task execution on errors ignore_errors: true - # Stop task execution on all servers on error + # Stop task execution on any error any_errors_fatal: false + # Max number of tasks to fail before aborting + max_fail_percentage: 100 + # Ignore unreachable hosts ignore_unreachable: false - # Omit empty results for table output - omit_empty: false + # Omit empty rows for table output + omit_empty_rows: false + + # Omit empty columns for table output + omit_empty_columns: false + + # Show task reports [recap|rc|task|time|all] + report: [recap] + + # Verbose turns on describe, list_hosts and report set to all + verbose: false + + # Confirm invoked task before running + confirm: false + + # Confirm each task before running + step: false # List of targets [optional] targets: default: - # Target all servers + # Target description + desc: "" + + # Target all hosts all: false - # Specify servers via server name + # Specify hosts via server name servers: [] - # Specify servers via server tags + # Specify hosts via server tags tags: [] - # Limit of servers to target + # Limit number of hosts to target limit: 0 - # Limit of servers to target in percentage - limit_p: 0 + # Limit number of hosts to target in percentage + limit_p: 100 + + # Invert matching on hosts + invert: false + + # Specify host regex + regex: "" # List of tasks tasks: @@ -273,11 +320,11 @@ Below is a config file detailing all of the available options and their defaults # Or specify specs inline spec: output: table - parallel: true ignore_errors: true ignore_unreachable: true any_errors_fatal: false - omit_empty: true + omit_empty_rows: true + omit_empty_columns: true # Target reference [optional] # target: default @@ -288,7 +335,6 @@ Below is a config file detailing all of the available options and their defaults servers: [media] tags: [remote] limit: 1 - limit_p: 100 # List of environment variables [optional] env: @@ -299,22 +345,15 @@ Below is a config file detailing all of the available options and their defaults num_lines: $(ls -1 | wc -l) # The following variables are available by default: + # S_NAME + # S_HOST + # S_USER + # S_PORT + # S_BASTION + # S_TAGS + # S_IDENTITY # SAKE_DIR # SAKE_PATH - # - # SAKE_TASK_ID - # SAKE_TASK_NAME - # SAKE_TASK_DESC - # SAKE_TASK_LOCAL - # - # SAKE_SERVER_NAME - # SAKE_SERVER_DESC - # SAKE_SERVER_TAGS - # SAKE_SERVER_HOST - # SAKE_SERVER_USER - # SAKE_SERVER_PORT - # SAKE_SERVER_BASTION - # SAKE_SERVER_LOCAL # Run on localhost [optional] local: false @@ -328,7 +367,7 @@ Below is a config file detailing all of the available options and their defaults # Each task can only define: # - a single cmd # - or a single task reference - # - or a list of task references or commands + # - or a list of task references and commands # Single command cmd: | @@ -343,6 +382,7 @@ Below is a config file detailing all of the available options and their defaults # Command - name: inline-command cmd: echo "Hello World" + ignore_errors: true work_dir: /tmp shell: bash env: @@ -352,9 +392,14 @@ Below is a config file detailing all of the available options and their defaults # Nested task referencing is supported and will result in a # flat list of commands - task: simple-1 + ignore_errors: true work_dir: /tmp + register: results env: foo: bar + + - name: output + cmd: echo $results_stdout .RE .SH EXAMPLES @@ -399,29 +444,19 @@ Describe a task .B ~ $ sake describe task ping .nf -Task: ping -Name: ping -Desc: ping server -Local: false -WorkDir: -Theme: default -Target: - All: true - Servers: - Tags: -Spec: - Output: text - Parallel: false - AnyErrorsFatal: false - IgnoreErrors: false - IgnoreUnreachable: false - OmitEmpty: false -Env: - SAKE_TASK_ID: ping - SAKE_TASK_NAME: - SAKE_TASK_DESC: ping server - SAKE_TASK_LOCAL: false -Cmd: +name: ping +desc: ping server +local: false +work_dir: +theme: default +target: + all: true +spec: + output: text + ignore_unreachable: true + omit_empty_rows: true + omit_empty_columns: true +cmd: echo pong .fi diff --git a/core/dao/common.go b/core/dao/common.go index 515d505..a6bd71f 100644 --- a/core/dao/common.go +++ b/core/dao/common.go @@ -106,6 +106,10 @@ func MergeEnvs(envs ...[]string) []string { for _, part := range envs { for _, elem := range part { + if elem == "" { + continue + } + elem = strings.TrimSuffix(elem, "\n") kv := strings.SplitN(elem, "=", 2) diff --git a/core/dao/config.go b/core/dao/config.go index 07c2217..cae43d8 100644 --- a/core/dao/config.go +++ b/core/dao/config.go @@ -2,7 +2,6 @@ package dao import ( "fmt" - "io/ioutil" "os" "os/exec" "path/filepath" @@ -31,16 +30,34 @@ var ( All: false, Servers: []string{}, Tags: []string{}, + Regex: "", + Invert: false, + Limit: 0, + LimitP: 0, } DEFAULT_SPEC = Spec{ Name: "default", + Desc: "the default spec", + Describe: false, + ListHosts: false, + Order: "inventory", + Silent: false, + Strategy: "linear", Output: "text", - Parallel: false, - AnyErrorsFatal: false, - IgnoreUnreachable: false, + Forks: 10000, + MaxFailPercentage: 0, + AnyErrorsFatal: true, IgnoreErrors: false, - OmitEmpty: false, + IgnoreUnreachable: false, + OmitEmptyRows: false, + OmitEmptyColumns: false, + Batch: 0, + BatchP: 0, + Report: []string{"recap"}, + Verbose: false, + Confirm: false, + Step: false, } ) @@ -133,7 +150,7 @@ func ReadConfig(configFilepath string, userConfigPath string, sshConfigFile stri configPath = filename } - dat, err := ioutil.ReadFile(configPath) + dat, err := os.ReadFile(configPath) if err != nil { return Config{}, err } @@ -289,7 +306,7 @@ func (c *Config) EditTask(name string) error { configPath = task.context } - dat, err := ioutil.ReadFile(configPath) + dat, err := os.ReadFile(configPath) if err != nil { return err } @@ -332,7 +349,7 @@ func (c *Config) EditServer(name string) error { configPath = server.context } - dat, err := ioutil.ReadFile(configPath) + dat, err := os.ReadFile(configPath) if err != nil { return err } @@ -373,7 +390,7 @@ func (c *Config) EditTarget(name string) error { configPath = target.context } - dat, err := ioutil.ReadFile(configPath) + dat, err := os.ReadFile(configPath) if err != nil { return err } @@ -414,7 +431,7 @@ func (c *Config) EditSpec(name string) error { configPath = spec.context } - dat, err := ioutil.ReadFile(configPath) + dat, err := os.ReadFile(configPath) if err != nil { return err } @@ -527,9 +544,15 @@ tasks: func (c *Config) ParseInventory(userArgs []string) error { var servers []Server + var shell = DEFAULT_SHELL + if c.Shell != "" { + shell = c.Shell + } + shell = core.FormatShell(shell) + for _, s := range c.Servers { if s.Inventory != "" { - hosts, err := core.EvaluateInventory(s.context, s.Inventory, s.Envs, userArgs) + hosts, err := core.EvaluateInventory(shell, s.context, s.Inventory, s.Envs, userArgs) if err != nil { return err } diff --git a/core/dao/import_config.go b/core/dao/import_config.go index 834bcfc..01ec6a5 100644 --- a/core/dao/import_config.go +++ b/core/dao/import_config.go @@ -2,7 +2,6 @@ package dao import ( "fmt" - "io/ioutil" "os" "path/filepath" "strings" @@ -84,12 +83,12 @@ func (c *FoundCyclicDependency) Error() string { // 2. Perform a depth-first search of imports and collect all specs, targets, themes, tasks, servers and store in intermediate struct ConfigResources // - Nested tasks for tasks are saved as TaskRefYAML // - Spec, Theme, Target are saved as references here as well, if they are specified -// 3. If the default theme, spec, and target objects are not overwritten, then create them -// 3.1. Create default Theme collection -// 3.2. Create default Spec collection -// 3.3. Create default Target collection -// 4. Perform a depth-first search for task references and save them as T -// 5. We check duplicate server hosts in the config collection +// 3. If the default theme, spec, and target objects are not overwritten, then create them +// 3.1. Create default Theme collection +// 3.2. Create default Spec collection +// 3.3. Create default Target collection +// 4. Perform a depth-first search for task references and save them as T +// 5. We check duplicate server hosts in the config collection // // Given config imports, use a Depth-first-search algorithm to recursively // check for resources (tasks, servers, dirs, themes, specs, targets). @@ -151,6 +150,7 @@ func (c *ConfigYAML) parseConfig() (Config, error) { // Create default spec if not exists _, err = cr.GetSpec(DEFAULT_SPEC.Name) if err != nil { + // TODO: Fill in all default values for spec cr.Specs = append(cr.Specs, DEFAULT_SPEC) } @@ -336,7 +336,7 @@ func parseConfigFile(path string, cr *ConfigResources) (ConfigYAML, error) { configYAML.Path = absPath configYAML.Dir = filepath.Dir(absPath) - dat, err := ioutil.ReadFile(absPath) + dat, err := os.ReadFile(absPath) if err != nil { return configYAML, &core.FileError{Err: err.Error()} } diff --git a/core/dao/import_task.go b/core/dao/import_task.go index 0d2bcce..688b457 100644 --- a/core/dao/import_task.go +++ b/core/dao/import_task.go @@ -22,11 +22,12 @@ type TaskLink struct { // The following nomenclature is used: // // tasks: <-- root context -// b: <-- root task -// tasks: <-- child context -// - task: a <-- child task -// env: -// foo: bar +// +// b: <-- root task +// tasks: <-- child context +// - task: a <-- child task +// env: +// foo: bar func dfsTask(task *Task, tn *TaskNode, tm map[string]*TaskNode, cycles *[]TaskLink, cr *ConfigResources) { tn.Visiting = true @@ -57,7 +58,6 @@ func dfsTask(task *Task, tn *TaskNode, tm map[string]*TaskNode, cycles *[]TaskLi if tn.TaskRefs[i].Cmd != "" { // name: <-- task - // cmd: echo 123 // tasks: // - cmd: <-- tn.TaskRefs[i].Cmd @@ -71,22 +71,29 @@ func dfsTask(task *Task, tn *TaskNode, tm map[string]*TaskNode, cycles *[]TaskLi tty = *tn.TaskRefs[i].TTY } + ignoreErrors := task.Spec.IgnoreErrors + if tn.TaskRefs[i].IgnoreErrors != nil { + ignoreErrors = *tn.TaskRefs[i].IgnoreErrors + } + envs := MergeEnvs(tn.TaskRefs[i].Envs, task.Envs) workDir := SelectFirstNonEmpty(tn.TaskRefs[i].WorkDir, task.WorkDir) shell := SelectFirstNonEmpty(tn.TaskRefs[i].Shell, task.Shell) childTask := TaskCmd{ - ID: tn.TaskRefs[i].Task, - Name: tn.TaskRefs[i].Name, - Desc: tn.TaskRefs[i].Desc, - RootDir: filepath.Dir(task.context), - WorkDir: workDir, - Shell: shell, - Cmd: tn.TaskRefs[i].Cmd, - Envs: envs, - Local: local, - TTY: tty, + ID: tn.TaskRefs[i].Task, + Name: tn.TaskRefs[i].Name, + Desc: tn.TaskRefs[i].Desc, + Register: tn.TaskRefs[i].Register, + RootDir: filepath.Dir(task.context), + WorkDir: workDir, + Shell: shell, + Cmd: tn.TaskRefs[i].Cmd, + Envs: envs, + Local: local, + TTY: tty, + IgnoreErrors: ignoreErrors, } task.Tasks = append(task.Tasks, childTask) } else { @@ -129,25 +136,30 @@ func dfsTask(task *Task, tn *TaskNode, tm map[string]*TaskNode, cycles *[]TaskLi tty = *tn.TaskRefs[i].TTY } - envs := MergeEnvs(tn.TaskRefs[i].Envs, task.Envs, childTask.Envs) + ignoreErrors := childTask.Spec.IgnoreErrors + if tn.TaskRefs[i].IgnoreErrors != nil { + ignoreErrors = *tn.TaskRefs[i].IgnoreErrors + } - // The child task default envs like SAKE_TASK_ID should take precedence - envs = MergeEnvs(childTask.GetDefaultEnvs(), envs) + envs := MergeEnvs(tn.TaskRefs[i].Envs, task.Envs, childTask.Envs) workDir := SelectFirstNonEmpty(tn.TaskRefs[i].WorkDir, task.WorkDir, childTask.WorkDir) shell := SelectFirstNonEmpty(tn.TaskRefs[i].Shell, task.Shell, childTask.Shell) + // TODO: Should task.Register be set here? t := TaskCmd{ - ID: childTask.ID, - Name: name, - Desc: childTask.Desc, - RootDir: filepath.Dir(task.context), - WorkDir: workDir, - Shell: shell, - Cmd: childTask.Cmd, - Envs: envs, - Local: local, - TTY: tty, + ID: childTask.ID, + Name: name, + Desc: childTask.Desc, + RootDir: filepath.Dir(task.context), + WorkDir: workDir, + Shell: shell, + Cmd: childTask.Cmd, + Register: tn.TaskRefs[i].Register, + Envs: envs, + Local: local, + TTY: tty, + IgnoreErrors: ignoreErrors, } task.Tasks = append(task.Tasks, t) } else { @@ -181,6 +193,7 @@ func dfsTask(task *Task, tn *TaskNode, tm map[string]*TaskNode, cycles *[]TaskLi // env: <-- tn.TaskRefs[i].Envs, takes first precedence // foo: foo + // TODO: May need to add IgnoreErrors here tnn.TaskRefs = append(tnn.TaskRefs, k) tnn.TaskRefs[j].Envs = MergeEnvs(tn.TaskRefs[i].Envs, tnn.TaskRefs[j].Envs, childTask.Envs) tnn.TaskRefs[j].WorkDir = SelectFirstNonEmpty(tn.TaskRefs[i].WorkDir, tnn.TaskRefs[j].WorkDir, childTask.WorkDir) diff --git a/core/dao/server.go b/core/dao/server.go index 8eb2fcc..4d923a2 100644 --- a/core/dao/server.go +++ b/core/dao/server.go @@ -3,13 +3,16 @@ package dao import ( "errors" "fmt" + "math/rand" "os" "os/user" "path" "path/filepath" "regexp" + "sort" "strconv" "strings" + "time" "gopkg.in/yaml.v3" @@ -38,6 +41,7 @@ type Server struct { Group string PubFile *string + RootDir string // config dir context string // config path contextLine int // defined at } @@ -88,7 +92,7 @@ func (s Server) GetValue(key string, _ int) string { return "" } case "tags": - return strings.Join(s.Tags, "\n") + return strings.Join(s.Tags, ",") } return "" @@ -105,7 +109,7 @@ func (s *Server) GetContextLine() int { func (s *Server) GetNonDefaultEnvs() []string { var envs []string for _, env := range s.Envs { - if !strings.Contains(env, "SAKE_SERVER_") { + if !strings.Contains(env, "S_") { envs = append(envs, env) } } @@ -181,17 +185,14 @@ func (c *ConfigYAML) ParseServersYAML() ([]Server, []ResourceErrors[Server]) { } defaultEnvs := []string{} - if serverYAML.Desc != "" { - defaultEnvs = append(defaultEnvs, fmt.Sprintf("SAKE_SERVER_DESC=%s", serverYAML.Desc)) + if serverYAML.IdentityFile != nil { + defaultEnvs = append(defaultEnvs, fmt.Sprintf("S_IDENTITY=%s", *serverYAML.IdentityFile)) } if len(serverYAML.Tags) > 0 { - defaultEnvs = append(defaultEnvs, fmt.Sprintf("SAKE_SERVER_TAGS=%s", strings.Join(serverYAML.Tags, ","))) + defaultEnvs = append(defaultEnvs, fmt.Sprintf("S_TAGS=%s", strings.Join(serverYAML.Tags, ","))) } if serverYAML.Bastion != "" { - defaultEnvs = append(defaultEnvs, fmt.Sprintf("SAKE_SERVER_BASTION=%s", serverYAML.Bastion)) - } - if serverYAML.Local { - defaultEnvs = append(defaultEnvs, fmt.Sprintf("SAKE_SERVER_LOCAL=%t", serverYAML.Local)) + defaultEnvs = append(defaultEnvs, fmt.Sprintf("S_BASTION=%s", serverYAML.Bastion)) } // Same for all servers @@ -268,10 +269,10 @@ func (c *ConfigYAML) ParseServersYAML() ([]Server, []ResourceErrors[Server]) { } serverEnvs := append(defaultEnvs, []string{ - fmt.Sprintf("SAKE_SERVER_NAME=%s", c.Servers.Content[i].Value), - fmt.Sprintf("SAKE_SERVER_HOST=%s", host), - fmt.Sprintf("SAKE_SERVER_USER=%s", user), - fmt.Sprintf("SAKE_SERVER_PORT=%d", port), + fmt.Sprintf("S_NAME=%s", c.Servers.Content[i].Value), + fmt.Sprintf("S_HOST=%s", host), + fmt.Sprintf("S_USER=%s", user), + fmt.Sprintf("S_PORT=%d", port), }...) serverEnvs = append(serverEnvs, envs...) @@ -294,6 +295,7 @@ func (c *ConfigYAML) ParseServersYAML() ([]Server, []ResourceErrors[Server]) { PubFile: pubKeyFile, Password: password, + RootDir: filepath.Dir(c.Path), context: c.Path, contextLine: c.Servers.Content[i].Line, } @@ -310,10 +312,10 @@ func (c *ConfigYAML) ParseServersYAML() ([]Server, []ResourceErrors[Server]) { } serverEnvs := append(defaultEnvs, []string{ - fmt.Sprintf("SAKE_SERVER_NAME=%s-%d", c.Servers.Content[i].Value, k), - fmt.Sprintf("SAKE_SERVER_HOST=%s", host), - fmt.Sprintf("SAKE_SERVER_USER=%s", user), - fmt.Sprintf("SAKE_SERVER_PORT=%d", port), + fmt.Sprintf("S_NAME=%s-%d", c.Servers.Content[i].Value, k), + fmt.Sprintf("S_HOST=%s", host), + fmt.Sprintf("S_USER=%s", user), + fmt.Sprintf("S_PORT=%d", port), }...) serverEnvs = append(serverEnvs, envs...) @@ -336,6 +338,7 @@ func (c *ConfigYAML) ParseServersYAML() ([]Server, []ResourceErrors[Server]) { PubFile: pubKeyFile, Password: password, + RootDir: filepath.Dir(c.Path), context: c.Path, contextLine: c.Servers.Content[i].Line, } @@ -357,10 +360,10 @@ func (c *ConfigYAML) ParseServersYAML() ([]Server, []ResourceErrors[Server]) { } serverEnvs := append(defaultEnvs, []string{ - fmt.Sprintf("SAKE_SERVER_NAME=%s-%d", c.Servers.Content[i].Value, k), - fmt.Sprintf("SAKE_SERVER_HOST=%s", host), - fmt.Sprintf("SAKE_SERVER_USER=%s", user), - fmt.Sprintf("SAKE_SERVER_PORT=%d", port), + fmt.Sprintf("S_NAME=%s-%d", c.Servers.Content[i].Value, k), + fmt.Sprintf("S_HOST=%s", host), + fmt.Sprintf("S_USER=%s", user), + fmt.Sprintf("S_PORT=%d", port), }...) serverEnvs = append(serverEnvs, envs...) @@ -383,6 +386,7 @@ func (c *ConfigYAML) ParseServersYAML() ([]Server, []ResourceErrors[Server]) { PubFile: pubKeyFile, Password: password, + RootDir: filepath.Dir(c.Path), context: c.Path, contextLine: c.Servers.Content[i].Line, } @@ -410,6 +414,7 @@ func (c *ConfigYAML) ParseServersYAML() ([]Server, []ResourceErrors[Server]) { PubFile: pubKeyFile, Password: password, + RootDir: filepath.Dir(c.Path), context: c.Path, contextLine: c.Servers.Content[i].Line, } @@ -930,10 +935,9 @@ func CreateInventoryServers(inputHost string, i int, server Server, userArgs []s } serverEnvs := append(server.Envs, []string{ - fmt.Sprintf("SAKE_SERVER_NAME=%s-%d", server.Name, i), - fmt.Sprintf("SAKE_SERVER_HOST=%s", host), - fmt.Sprintf("SAKE_SERVER_USER=%s", user), - fmt.Sprintf("SAKE_SERVER_PORT=%d", port), + fmt.Sprintf("S_HOST=%s", host), + fmt.Sprintf("S_USER=%s", user), + fmt.Sprintf("S_PORT=%d", port), }...) serverEnvs = append(serverEnvs, userArgs...) @@ -962,3 +966,24 @@ func CreateInventoryServers(inputHost string, i int, server Server, userArgs []s return *iServer, nil } + +func SortServers(order string, servers *[]Server) { + switch order { + case "inventory": + case "reverse_inventory": + for i, j := 0, len(*servers)-1; i < j; i, j = i+1, j-1 { + (*servers)[i], (*servers)[j] = (*servers)[j], (*servers)[i] + } + case "sorted": + sort.Slice(*servers, func(i, j int) bool { + return (*servers)[i].Host < (*servers)[j].Host + }) + case "reverse_sorted": + sort.Slice(*servers, func(i, j int) bool { + return (*servers)[i].Host > (*servers)[j].Host + }) + case "random": + rand.Seed(time.Now().UnixNano()) + rand.Shuffle(len((*servers)), func(i, j int) { (*servers)[i], (*servers)[j] = (*servers)[j], (*servers)[i] }) + } +} diff --git a/core/dao/spec.go b/core/dao/spec.go index 385c2bd..d3f1e14 100644 --- a/core/dao/spec.go +++ b/core/dao/spec.go @@ -1,7 +1,8 @@ package dao import ( - "errors" + // "errors" + "fmt" "strconv" "strings" @@ -11,13 +12,27 @@ import ( ) type Spec struct { - Name string `yaml:"_"` - Output string `yaml:"output"` - Parallel bool `yaml:"parallel"` - AnyErrorsFatal bool `yaml:"any_errors_fatal"` - IgnoreErrors bool `yaml:"ignore_errors"` - IgnoreUnreachable bool `yaml:"ignore_unreachable"` - OmitEmpty bool `yaml:"omit_empty"` + Name string `yaml:"_"` + Desc string `yaml:"desc"` + Describe bool `yaml:"describe"` + ListHosts bool `yaml:"list_hosts"` + Order string `yaml:"order"` + Silent bool `yaml:"silent"` + Strategy string `yaml:"strategy"` + Batch uint32 `yaml:"batch"` + BatchP uint8 `yaml:"batch_p"` + Forks uint32 `yaml:"forks"` + Output string `yaml:"output"` + MaxFailPercentage uint8 `yaml:"max_fail_percentage"` + AnyErrorsFatal bool `yaml:"any_errors_fatal"` + IgnoreErrors bool `yaml:"ignore_errors"` + IgnoreUnreachable bool `yaml:"ignore_unreachable"` + OmitEmptyRows bool `yaml:"omit_empty_rows"` + OmitEmptyColumns bool `yaml:"omit_empty_columns"` + Report []string `yaml:"report"` + Verbose bool `yaml:"verbose"` + Confirm bool `yaml:"confirm"` + Step bool `yaml:"step"` context string // config path contextLine int // defined at @@ -36,18 +51,40 @@ func (s Spec) GetValue(key string, _ int) string { switch lkey { case "name", "spec": return s.Name + case "desc", "Desc": + return s.Desc + case "describe", "Describe": + return strconv.FormatBool(s.Describe) + case "list_hosts": + return strconv.FormatBool(s.ListHosts) + case "silent", "Silent": + return strconv.FormatBool(s.Silent) + case "strategy": + return s.Strategy + case "forks": + return strconv.Itoa(int(s.Forks)) + case "batch": + return strconv.Itoa(int(s.Batch)) + case "batch_p": + return strconv.Itoa(int(s.BatchP)) case "output": return s.Output - case "parallel": - return strconv.FormatBool(s.Parallel) + case "max_fail_percentage": + return strconv.Itoa(int(s.MaxFailPercentage)) case "any_errors_fatal": return strconv.FormatBool(s.AnyErrorsFatal) case "ignore_errors": return strconv.FormatBool(s.IgnoreErrors) case "ignore_unreachable": return strconv.FormatBool(s.IgnoreUnreachable) - case "omit_empty": - return strconv.FormatBool(s.OmitEmpty) + case "omit_empty_rows": + return strconv.FormatBool(s.OmitEmptyRows) + case "omit_empty_columns": + return strconv.FormatBool(s.OmitEmptyColumns) + case "report", "Report": + return strings.Join(s.Report, "\n") + case "order", "Order": + return s.Order default: return "" } @@ -62,28 +99,49 @@ func (c *ConfigYAML) ParseSpecsYAML() ([]Spec, []ResourceErrors[Spec]) { j := -1 for i := 0; i < count; i += 2 { j += 1 - spec := &Spec{ - Name: c.Specs.Content[i].Value, - context: c.Path, - contextLine: c.Specs.Content[i].Line, - } + spec, serr := c.DecodeSpec(c.Specs.Content[i].Value, *c.Specs.Content[i+1]) re := ResourceErrors[Spec]{Resource: spec, Errors: []error{}} specErrors = append(specErrors, re) - err := c.Specs.Content[i+1].Decode(spec) - if err != nil { - for _, yerr := range err.(*yaml.TypeError).Errors { - specErrors[j].Errors = append(specErrors[j].Errors, errors.New(yerr)) - } - continue - } - + specErrors[j].Errors = append(specErrors[j].Errors, serr...) specs = append(specs, *spec) } return specs, specErrors } +func (c *ConfigYAML) DecodeSpec(name string, specYAML yaml.Node) (*Spec, []error) { + spec := &Spec{ + Name: name, + context: c.Path, + contextLine: specYAML.Line, + } + + specErrors := []error{} + err := specYAML.Decode(spec) + if err != nil { + specErrors = append(specErrors, err) + } + + if spec.AnyErrorsFatal && spec.MaxFailPercentage > 0 { + specErrors = append(specErrors, &core.MultipleFailSet{Name: name}) + } + + if spec.BatchP > 0 && spec.Batch > 0 { + specErrors = append(specErrors, &core.BatchMultipleDef{Name: name}) + } + + if spec.BatchP > 100 { + specErrors = append(specErrors, &core.InvalidPercentInput{Name: "batch_p"}) + } + + if len(spec.Report) == 0 { + spec.Report = []string{"recap"} + } + + return spec, specErrors +} + func (c *Config) GetSpec(name string) (*Spec, error) { for _, spec := range c.Specs { if name == spec.Name { @@ -97,7 +155,11 @@ func (c *Config) GetSpec(name string) (*Spec, error) { func (c *Config) GetSpecNames() []string { names := []string{} for _, spec := range c.Specs { - names = append(names, spec.Name) + if spec.Desc != "" { + names = append(names, fmt.Sprintf("%s\t%s", spec.Name, spec.Desc)) + } else { + names = append(names, spec.Name) + } } return names diff --git a/core/dao/target.go b/core/dao/target.go index a8f80f0..b72ca96 100644 --- a/core/dao/target.go +++ b/core/dao/target.go @@ -1,7 +1,7 @@ package dao import ( - "errors" + "fmt" "strconv" "strings" @@ -12,6 +12,7 @@ import ( type Target struct { Name string `yaml:"name"` + Desc string `yaml:"desc"` All bool `yaml:"all"` Servers []string `yaml:"servers"` Tags []string `yaml:"tags"` @@ -37,6 +38,8 @@ func (t Target) GetValue(key string, _ int) string { switch lkey { case "name", "target": return t.Name + case "desc", "Desc": + return t.Desc case "all": return strconv.FormatBool(t.All) case "servers": @@ -65,26 +68,11 @@ func (c *ConfigYAML) ParseTargetsYAML() ([]Target, []ResourceErrors[Target]) { j := -1 for i := 0; i < count; i += 2 { j += 1 - target := &Target{ - context: c.Path, - contextLine: c.Targets.Content[i].Line, - } + target, terr := c.DecodeTarget(c.Targets.Content[i].Value, *c.Targets.Content[i+1]) re := ResourceErrors[Target]{Resource: target, Errors: []error{}} targetErrors = append(targetErrors, re) - err := c.Targets.Content[i+1].Decode(target) - if err != nil { - for _, yerr := range err.(*yaml.TypeError).Errors { - targetErrors[j].Errors = append(targetErrors[j].Errors, errors.New(yerr)) - } - continue - } - - target.Name = c.Targets.Content[i].Value - - if target.LimitP > 100 { - targetErrors[j].Errors = append(targetErrors[j].Errors, &core.InvalidPercentInput{}) - } + targetErrors[j].Errors = append(targetErrors[j].Errors, terr...) targets = append(targets, *target) } @@ -92,6 +80,30 @@ func (c *ConfigYAML) ParseTargetsYAML() ([]Target, []ResourceErrors[Target]) { return targets, targetErrors } +func (c *ConfigYAML) DecodeTarget(name string, targetYAML yaml.Node) (*Target, []error) { + target := &Target{ + Name: name, + context: c.Path, + contextLine: targetYAML.Line, + } + + targetErrors := []error{} + err := targetYAML.Decode(target) + if err != nil { + targetErrors = append(targetErrors, err) + } + + if target.LimitP > 0 && target.Limit > 0 { + targetErrors = append(targetErrors, &core.LimitMultipleDef{Name: name}) + } + + if target.LimitP > 100 { + targetErrors = append(targetErrors, &core.InvalidPercentInput{Name: "limit_p"}) + } + + return target, targetErrors +} + func (c *Config) GetTarget(name string) (*Target, error) { for _, target := range c.Targets { if name == target.Name { @@ -105,7 +117,11 @@ func (c *Config) GetTarget(name string) (*Target, error) { func (c *Config) GetTargetNames() []string { names := []string{} for _, target := range c.Targets { - names = append(names, target.Name) + if target.Desc != "" { + names = append(names, fmt.Sprintf("%s\t%s", target.Name, target.Desc)) + } else { + names = append(names, target.Name) + } } return names diff --git a/core/dao/task.go b/core/dao/task.go index 57ca262..754ccc3 100644 --- a/core/dao/task.go +++ b/core/dao/task.go @@ -3,39 +3,48 @@ package dao import ( "errors" "fmt" - "gopkg.in/yaml.v3" "math" + "regexp" "strconv" "strings" + "time" + + "gopkg.in/yaml.v3" "github.com/alajmo/sake/core" ) +var REGISTER_REGEX = regexp.MustCompile("^[a-zA-Z_]+[a-zA-Z0-9_]*$") + // This is the struct that is added to the Task.Tasks in import_task.go type TaskCmd struct { - ID string - Name string - Desc string - WorkDir string - Shell string - RootDir string - Cmd string - Local bool - TTY bool - Envs []string + ID string + Name string + Desc string + WorkDir string + Shell string + RootDir string + Register string + Cmd string + Local bool + TTY bool + IgnoreErrors bool + Envs []string } // This is the struct that is added to the Task.TaskRefs type TaskRef struct { - Name string - Desc string - Cmd string - WorkDir string - Shell string - Task string - Local *bool - TTY *bool - Envs []string + Name string + Desc string + Cmd string + WorkDir string + Shell string + Register string + Task string + Local *bool + TTY *bool + IgnoreErrors *bool + Envs []string } type Task struct { @@ -63,6 +72,7 @@ type Task struct { contextLine int // defined at } +// Unmarshaled from YAML type TaskYAML struct { Name string `yaml:"name"` Desc string `yaml:"desc"` @@ -80,17 +90,19 @@ type TaskYAML struct { Theme yaml.Node `yaml:"theme"` } -// This is the struct that will be unmarsheld from YAML +// Unmarshaled from YAML type TaskRefYAML struct { - Name string `yaml:"name"` - Desc string `yaml:"desc"` - WorkDir string `yaml:"work_dir"` - Shell string `yaml:"shell"` - Cmd string `yaml:"cmd"` - Task string `yaml:"task"` - Local *bool `yaml:"local"` - TTY *bool `yaml:"tty"` - Env yaml.Node `yaml:"env"` + Name string `yaml:"name"` + Desc string `yaml:"desc"` + WorkDir string `yaml:"work_dir"` + Shell string `yaml:"shell"` + Cmd string `yaml:"cmd"` + Task string `yaml:"task"` + Register string `yaml:"register"` + Local *bool `yaml:"local"` + IgnoreErrors *bool `yaml:"ignore_errors"` + TTY *bool `yaml:"tty"` + Env yaml.Node `yaml:"env"` } func (t Task) GetValue(key string, _ int) string { @@ -121,28 +133,6 @@ func (t Task) GetValue(key string, _ int) string { } } -func (t *Task) GetDefaultEnvs() []string { - var defaultEnvs []string - for _, env := range t.Envs { - if strings.Contains(env, "SAKE_TASK_") { - defaultEnvs = append(defaultEnvs, env) - } - } - - return defaultEnvs -} - -func (t *Task) GetNonDefaultEnvs() []string { - var envs []string - for _, env := range t.Envs { - if !strings.Contains(env, "SAKE_TASK_") { - envs = append(envs, env) - } - } - - return envs -} - func (t *Task) GetContext() string { return t.context } @@ -155,16 +145,15 @@ func (t *Task) GetContextLine() int { // This function also sets task references. // Valid formats (only one is allowed): // -// cmd: | -// echo pong +// cmd: | +// echo pong // -// task: ping -// -// tasks: -// - task: ping -// - task: ping -// - cmd: echo pong +// task: ping // +// tasks: +// - task: ping +// - task: ping +// - cmd: echo pong func (c *ConfigYAML) ParseTasksYAML() ([]Task, []ResourceErrors[Task]) { var tasks []Task count := len(c.Tasks.Content) @@ -231,18 +220,6 @@ func (c *ConfigYAML) ParseTasksYAML() ([]Task, []ResourceErrors[Task]) { task.Shell = taskYAML.Shell task.Attach = taskYAML.Attach - defaultEnvs := []string{ - fmt.Sprintf("SAKE_TASK_ID=%s", task.ID), - fmt.Sprintf("SAKE_TASK_NAME=%s", taskYAML.Name), - fmt.Sprintf("SAKE_TASK_DESC=%s", taskYAML.Desc), - } - - if taskYAML.Local { - defaultEnvs = append(defaultEnvs, fmt.Sprintf("SAKE_TASK_LOCAL=%t", taskYAML.Local)) - } - - task.Envs = append(task.Envs, defaultEnvs...) - if !IsNullNode(taskYAML.Env) { err := CheckIsMappingNode(taskYAML.Env) if err != nil { @@ -257,16 +234,10 @@ func (c *ConfigYAML) ParseTasksYAML() ([]Task, []ResourceErrors[Task]) { // Spec if len(taskYAML.Spec.Content) > 0 { - // Spec value - spec := &Spec{} - err := taskYAML.Spec.Decode(spec) - if err != nil { - for _, yerr := range err.(*yaml.TypeError).Errors { - taskErrors[j].Errors = append(taskErrors[j].Errors, errors.New(yerr)) - } - } else { - task.Spec = *spec - } + // Inline Spec + spec, specErrors := c.DecodeSpec("", taskYAML.Spec) + taskErrors[j].Errors = append(taskErrors[j].Errors, specErrors...) + task.Spec = *spec } else if taskYAML.Spec.Value != "" { // Spec reference task.SpecRef = taskYAML.Spec.Value @@ -276,16 +247,10 @@ func (c *ConfigYAML) ParseTasksYAML() ([]Task, []ResourceErrors[Task]) { // Target if len(taskYAML.Target.Content) > 0 { - // Target value - target := &Target{} - err := taskYAML.Target.Decode(target) - if err != nil { - for _, yerr := range err.(*yaml.TypeError).Errors { - taskErrors[j].Errors = append(taskErrors[j].Errors, errors.New(yerr)) - } - } else { - task.Target = *target - } + // Inline Target + target, targetErrors := c.DecodeTarget("", taskYAML.Target) + taskErrors[j].Errors = append(taskErrors[j].Errors, targetErrors...) + task.Target = *target } else if taskYAML.Target.Value != "" { // Target reference task.TargetRef = taskYAML.Target.Value @@ -295,7 +260,7 @@ func (c *ConfigYAML) ParseTasksYAML() ([]Task, []ResourceErrors[Task]) { // Theme if len(taskYAML.Theme.Content) > 0 { - // Theme value + // Inline Theme theme := &Theme{} err := taskYAML.Theme.Decode(theme) if err != nil { @@ -324,15 +289,36 @@ func (c *ConfigYAML) ParseTasksYAML() ([]Task, []ResourceErrors[Task]) { // Tasks References for k := range taskYAML.Tasks { tr := TaskRef{ - Name: taskYAML.Tasks[k].Name, - Desc: taskYAML.Tasks[k].Desc, - WorkDir: taskYAML.Tasks[k].WorkDir, - Shell: taskYAML.Tasks[k].Shell, - Local: taskYAML.Tasks[k].Local, - TTY: taskYAML.Tasks[k].TTY, - Envs: ParseNodeEnv(taskYAML.Tasks[k].Env), + Name: taskYAML.Tasks[k].Name, + Desc: taskYAML.Tasks[k].Desc, + WorkDir: taskYAML.Tasks[k].WorkDir, + Shell: taskYAML.Tasks[k].Shell, + Local: taskYAML.Tasks[k].Local, + TTY: taskYAML.Tasks[k].TTY, + IgnoreErrors: taskYAML.Tasks[k].IgnoreErrors, + Envs: ParseNodeEnv(taskYAML.Tasks[k].Env), } + if taskYAML.Tasks[k].Register != "" { + match := REGISTER_REGEX.MatchString(taskYAML.Tasks[k].Register) + if match { + tr.Register = taskYAML.Tasks[k].Register + } else { + taskErrors[j].Errors = append(taskErrors[j].Errors, &core.RegisterInvalidName{Value: taskYAML.Tasks[k].Register}) + continue + } + } + + // TODO: What about this? + // Find servers matching the flag + // var servers []Server + // for _, server := range c.Servers { + // match := pattern.MatchString(server.Host) + // if match { + // servers = append(servers, server) + // } + // } + // Check that only cmd or task is defined if taskYAML.Tasks[k].Cmd != "" && taskYAML.Tasks[k].Task != "" { taskErrors[j].Errors = append(taskErrors[j].Errors, &core.TaskRefMultipleDef{Name: c.Tasks.Content[i].Value}) @@ -381,12 +367,24 @@ func (c *Config) GetTaskServers(task *Task, runFlags *core.RunFlags, setRunFlags // If any runtime target flags are used, disregard config specified task targets if len(runFlags.Servers) > 0 || len(runFlags.Tags) > 0 || runFlags.Regex != "" || setRunFlags.All || setRunFlags.Invert { servers, err = c.FilterServers(runFlags.All, runFlags.Servers, runFlags.Tags, runFlags.Regex, runFlags.Invert) + if err != nil { + return []Server{}, err + } + } else if runFlags.Target != "" { + target, err := c.GetTarget(runFlags.Target) + if err != nil { + return []Server{}, err + } + task.Target = *target + servers, err = c.FilterServers(task.Target.All, task.Target.Servers, task.Target.Tags, task.Target.Regex, runFlags.Invert) + if err != nil { + return []Server{}, err + } } else { servers, err = c.FilterServers(task.Target.All, task.Target.Servers, task.Target.Tags, task.Target.Regex, runFlags.Invert) - } - - if err != nil { - return []Server{}, err + if err != nil { + return []Server{}, err + } } var limit uint32 @@ -406,15 +404,18 @@ func (c *Config) GetTaskServers(task *Task, runFlags *core.RunFlags, setRunFlags if limit > 0 { if limit <= uint32(len(servers)) { return servers[0:limit], nil - } else { - return []Server{}, &core.InvalidLimit{Max: len(servers), Limit: limit} } } else if limitp > 0 { if limitp <= 100 { tot := float64(len(servers)) percentage := float64(limitp) / float64(100) limit := math.Floor(percentage * tot) - return servers[0:int(limit)], nil + + if limit > 0 { + return servers[0:int(limit)], nil + } else { + return servers[0:1], nil + } } else { return []Server{}, &core.InvalidPercentInput{} } @@ -494,3 +495,48 @@ func (c *Config) GetTask(id string) (*Task, error) { return nil, &core.TaskNotFound{IDs: []string{id}} } + +type TaskStatus int64 + +const ( + Skipped TaskStatus = iota + Ok + Failed + Ignored + Unreachable +) + +type Report struct { + ReturnCode int + Duration time.Duration + Status TaskStatus +} + +type ReportRow struct { + Name string + Status map[TaskStatus]int + Rows []Report +} + +type ReportData struct { + Headers []string + Tasks []ReportRow + Status map[TaskStatus]int +} + +func (s TaskStatus) String() string { + switch s { + case Ok: + return "ok" + case Failed: + return "failed" + case Skipped: + return "skipped" + case Ignored: + return "ignored" + case Unreachable: + return "unreachable" + } + + return "" +} diff --git a/core/dao/theme.go b/core/dao/theme.go index 2a7d2b2..c5f019d 100644 --- a/core/dao/theme.go +++ b/core/dao/theme.go @@ -75,6 +75,7 @@ type Row struct { type TableOutput struct { Headers []string Rows []Row + Footers []string } func (t *Theme) GetContext() string { @@ -216,7 +217,7 @@ var DefaultTable = Table{ Fg: core.Ptr(""), Bg: core.Ptr(""), Align: core.Ptr(""), - Attr: core.Ptr("bold"), + Attr: core.Ptr("normal"), Format: core.Ptr("default"), }, @@ -224,7 +225,7 @@ var DefaultTable = Table{ Fg: core.Ptr(""), Bg: core.Ptr(""), Align: core.Ptr(""), - Attr: core.Ptr("bold"), + Attr: core.Ptr("normal"), Format: core.Ptr("default"), }, } @@ -375,9 +376,15 @@ func (c *ConfigYAML) ParseThemesYAML() ([]Theme, []ResourceErrors[Theme]) { if themes[i].Table.Title.Bg == nil { themes[i].Table.Title.Bg = DefaultTable.Title.Bg } + if themes[i].Table.Title.Align == nil { + themes[i].Table.Title.Align = DefaultTable.Title.Align + } if themes[i].Table.Title.Attr == nil { themes[i].Table.Title.Attr = DefaultTable.Title.Attr } + if themes[i].Table.Title.Format == nil { + themes[i].Table.Title.Format = DefaultTable.Title.Format + } } } @@ -395,6 +402,9 @@ func (c *ConfigYAML) ParseThemesYAML() ([]Theme, []ResourceErrors[Theme]) { if themes[i].Table.Header.Bg == nil { themes[i].Table.Header.Bg = DefaultTable.Header.Bg } + if themes[i].Table.Header.Align == nil { + themes[i].Table.Header.Align = DefaultTable.Header.Align + } if themes[i].Table.Header.Attr == nil { themes[i].Table.Header.Attr = DefaultTable.Header.Attr } @@ -418,6 +428,9 @@ func (c *ConfigYAML) ParseThemesYAML() ([]Theme, []ResourceErrors[Theme]) { if themes[i].Table.Row.Bg == nil { themes[i].Table.Row.Bg = DefaultTable.Row.Bg } + if themes[i].Table.Row.Align == nil { + themes[i].Table.Row.Align = DefaultTable.Row.Align + } if themes[i].Table.Row.Attr == nil { themes[i].Table.Row.Attr = DefaultTable.Row.Attr } @@ -441,6 +454,9 @@ func (c *ConfigYAML) ParseThemesYAML() ([]Theme, []ResourceErrors[Theme]) { if themes[i].Table.Footer.Bg == nil { themes[i].Table.Footer.Bg = DefaultTable.Footer.Bg } + if themes[i].Table.Footer.Align == nil { + themes[i].Table.Footer.Align = DefaultTable.Footer.Align + } if themes[i].Table.Footer.Attr == nil { themes[i].Table.Footer.Attr = DefaultTable.Footer.Attr } diff --git a/core/errors.go b/core/errors.go index 136436d..e081257 100644 --- a/core/errors.go +++ b/core/errors.go @@ -86,6 +86,14 @@ func (c *TaskNotFound) Error() string { return fmt.Sprintf("cannot find tasks %s", tasks) } +type OutputFormatNotFound struct { + Name string +} + +func (c *OutputFormatNotFound) Error() string { + return fmt.Sprintf("output option `%s` not found", c.Name) +} + type TaskMultipleDef struct { Name string } @@ -94,19 +102,56 @@ func (c *TaskMultipleDef) Error() string { return fmt.Sprintf("can only define one of the following for task `%s`: cmd, task, tasks", c.Name) } -type InvalidPercentInput struct{} +type LimitMultipleDef struct { + Name string +} + +func (c *LimitMultipleDef) Error() string { + if c.Name != "" { + return fmt.Sprintf("can only define one of the following for target `%s`: limit, limit_p", c.Name) + } + + return "can only define one of the following for target: limit, limit_p" +} + +type MultipleFailSet struct { + Name string +} + +func (c *MultipleFailSet) Error() string { + if c.Name != "" { + return fmt.Sprintf("can only define either `any_errors_fatal` or `max_fail_percentage` but not both spec `%s`", c.Name) + } + + return "can only define either `any_errors_fatal` or `max_fail_percentage` but not both spec" +} + +type BatchMultipleDef struct { + Name string +} + +func (c *BatchMultipleDef) Error() string { + if c.Name != "" { + return fmt.Sprintf("can only define one of the following for spec `%s`: batch, batch_p", c.Name) + } + + return "can only define one of the following for spec: batch, batch_p" +} + +type InvalidPercentInput struct { + Name string +} func (c *InvalidPercentInput) Error() string { - return "Percentage can only be between 0 and 100" + return fmt.Sprintf("percentage can only be between 0 and 100 for property `%s`", c.Name) } -type InvalidLimit struct { - Max int - Limit uint32 +type RegisterInvalidName struct { + Value string } -func (c *InvalidLimit) Error() string { - return fmt.Sprintf("The number of filtered servers is %d, but limit was set to %d", c.Max, c.Limit) +func (c *RegisterInvalidName) Error() string { + return fmt.Sprintf("invalid register variable name '%s', only alphanumeric characters and underscore are allowed and variable cannot start with a digit", c.Value) } type ServerMultipleDef struct { @@ -193,6 +238,7 @@ func (c *NoEditorEnv) Error() string { return "no environment variable `EDITOR` found" } +// If there's a misconfiguration with golang templates (prefix/header for instance in text.go) type TemplateParseError struct { Msg string } diff --git a/core/flags.go b/core/flags.go index 8aca2f3..5a0b8a9 100644 --- a/core/flags.go +++ b/core/flags.go @@ -38,10 +38,17 @@ type TaskFlags struct { type RunFlags struct { // Flags - Edit bool - DryRun bool - Describe bool - Silent bool + Edit bool + DryRun bool + Describe bool + ListHosts bool + Silent bool + Confirm bool + Step bool + Verbose bool + + // Reports + Report []string // Target All bool @@ -52,6 +59,8 @@ type RunFlags struct { Invert bool Limit uint32 LimitP uint8 + Target string + Order string // Config KnownHostsFile string @@ -64,25 +73,48 @@ type RunFlags struct { // Server IdentityFile string + User string Password string // Spec - Parallel bool + Spec string AnyErrorsFatal bool + MaxFailPercentage uint8 IgnoreErrors bool IgnoreUnreachable bool - OmitEmpty bool + OmitEmptyRows bool + OmitEmptyColumns bool + Forks uint32 + Batch uint32 + BatchP uint8 Output string + Strategy string } type SetRunFlags struct { + Silent bool + Describe bool + ListHosts bool + Attach bool All bool Invert bool - Parallel bool - OmitEmpty bool + OmitEmptyRows bool + OmitEmptyColumns bool Local bool TTY bool AnyErrorsFatal bool IgnoreErrors bool IgnoreUnreachable bool + Order bool + Report bool + Batch bool + BatchP bool + Servers bool + Tags bool + Regex bool + Limit bool + LimitP bool + Verbose bool + Confirm bool + Step bool } diff --git a/core/hostname-gen.go b/core/hostname-gen.go index fdd4987..295fc6b 100644 --- a/core/hostname-gen.go +++ b/core/hostname-gen.go @@ -11,13 +11,24 @@ import ( ) // Separate hosts with newline and space/tab -func EvaluateInventory(context string, input string, serverEnvs []string, userEnvs []string) ([]string, error) { - cmd := exec.Command("sh", "-c", input) +func EvaluateInventory( + shell string, + context string, + input string, + serverEnvs []string, + userEnvs []string, +) ([]string, error) { + args := strings.SplitN(shell, " ", 2) + shellProgram := args[0] + shellFlag := append(args[1:], input) + + cmd := exec.Command(shellProgram, shellFlag...) cmd.Env = append(os.Environ(), serverEnvs...) cmd.Env = append(cmd.Env, userEnvs...) cmd.Dir = filepath.Dir(context) out, err := cmd.CombinedOutput() + if err != nil { return []string{}, &InventoryEvalFailed{Err: string(out)} } @@ -195,7 +206,17 @@ func readRange(input string, i int) (HostRange, int, error) { return HostRange{}, i, errors.New("parsing hosts failed, missing end range") } - if r.Start > r.End { + s, err := strconv.Atoi(r.Start) + if err != nil { + return HostRange{}, i, errors.New("start is not a number") + } + + e, err := strconv.Atoi(r.End) + if err != nil { + return HostRange{}, i, errors.New("end is not a number") + } + + if s > e { return HostRange{}, i, errors.New("parsing hosts failed, start cannot be greater than end") } diff --git a/core/hostname-gen_test.go b/core/hostname-gen_test.go index a3dd98a..b2a3d45 100644 --- a/core/hostname-gen_test.go +++ b/core/hostname-gen_test.go @@ -9,7 +9,7 @@ import ( func TestEvaluateInventory(t *testing.T) { // IP inventory 1 host input := `echo 192.168.0.1` - hosts, err := EvaluateInventory("", input, []string{}, []string{}) + hosts, err := EvaluateInventory("sh -c", "", input, []string{}, []string{}) test.CheckErr(t, err) wanted := []string{"192.168.0.1"} for i := range wanted { @@ -18,7 +18,7 @@ func TestEvaluateInventory(t *testing.T) { // IP inventory 2 hosts splitted on space input = `echo "192.168.0.1 192.168.0.2"` - hosts, err = EvaluateInventory("", input, []string{}, []string{}) + hosts, err = EvaluateInventory("sh -c", "", input, []string{}, []string{}) test.CheckErr(t, err) wanted = []string{"192.168.0.1", "192.168.0.2"} for i := range wanted { @@ -27,7 +27,7 @@ func TestEvaluateInventory(t *testing.T) { // IP inventory 2 hosts splitted on newline input = `echo "192.168.0.1\n192.168.0.2"` - hosts, err = EvaluateInventory("", input, []string{}, []string{}) + hosts, err = EvaluateInventory("sh -c", "", input, []string{}, []string{}) test.CheckErr(t, err) wanted = []string{"192.168.0.1", "192.168.0.2"} for i := range wanted { @@ -36,7 +36,7 @@ func TestEvaluateInventory(t *testing.T) { // IP inventory 2 hosts splitted on tab input = `echo "192.168.0.1\t192.168.0.2"` - hosts, err = EvaluateInventory("", input, []string{}, []string{}) + hosts, err = EvaluateInventory("sh -c", "", input, []string{}, []string{}) test.CheckErr(t, err) wanted = []string{"192.168.0.1", "192.168.0.2"} for i := range wanted { diff --git a/core/man.go b/core/man.go index 82d21aa..4de7a70 100644 --- a/core/man.go +++ b/core/man.go @@ -3,7 +3,7 @@ package core import ( _ "embed" "fmt" - "io/ioutil" + "os" "path/filepath" ) @@ -12,7 +12,7 @@ var CONFIG_MAN []byte func GenManPages(dir string) error { manPath := filepath.Join(dir, "sake.1") - err := ioutil.WriteFile(manPath, CONFIG_MAN, 0644) + err := os.WriteFile(manPath, CONFIG_MAN, 0644) CheckIfError(err) fmt.Printf("Created %s\n", manPath) diff --git a/core/man_gen.go b/core/man_gen.go index e0e9f0c..878a3c3 100644 --- a/core/man_gen.go +++ b/core/man_gen.go @@ -11,7 +11,7 @@ import ( _ "embed" "fmt" "io" - "io/ioutil" + "os" "path/filepath" "strings" @@ -47,7 +47,7 @@ func CreateManPage(desc string, version string, date string, rootCmd *cobra.Comm res := genMan(header, rootCmd, cmds...) res = append(res, CONFIG_MD...) manPath := filepath.Join("./core/", "sake.1") - err := ioutil.WriteFile(manPath, res, 0644) + err := os.WriteFile(manPath, res, 0644) if err != nil { return err } @@ -60,7 +60,7 @@ func CreateManPage(desc string, version string, date string, rootCmd *cobra.Comm } mdPath := filepath.Join("./docs/", "command-reference.md") - err = ioutil.WriteFile(mdPath, md, 0644) + err = os.WriteFile(mdPath, md, 0644) if err != nil { return err } diff --git a/core/print/lib.go b/core/print/lib.go index 462466b..ff2486f 100644 --- a/core/print/lib.go +++ b/core/print/lib.go @@ -6,6 +6,14 @@ import ( "github.com/alajmo/sake/core" ) +var NormalPrint = text.Colors{text.Reset} +var OkPrint = text.Colors{text.Reset, text.FgGreen} +var FailedPrint = text.Colors{text.Reset, text.FgRed} +var SkippedPrint = text.Colors{text.Reset, text.FgBlue} +var IgnoredPrint = text.Colors{text.Reset, text.FgMagenta} +var UnreachablePrint = text.Colors{text.Reset, text.FgRed} +var ZeroPrint = text.Colors{text.Reset, text.Faint} + // Format map against go-pretty/table func GetFormat(s string) text.Format { switch s { diff --git a/core/print/print_block.go b/core/print/print_block.go index af925d1..a7cf911 100644 --- a/core/print/print_block.go +++ b/core/print/print_block.go @@ -5,9 +5,31 @@ import ( "fmt" "strings" + "github.com/alajmo/sake/core" "github.com/alajmo/sake/core/dao" ) +func PrintServerList(servers []dao.Server) error { + theme := dao.DEFAULT_THEME + theme.Table.Options.DrawBorder = core.Ptr(false) + theme.Table.Options.SeparateColumns = core.Ptr(false) + theme.Table.Options.SeparateRows = core.Ptr(false) + theme.Table.Options.SeparateHeader = core.Ptr(true) + theme.Table.Options.SeparateFooter = core.Ptr(false) + options := PrintTableOptions{ + Theme: theme, + Output: "table", + OmitEmptyRows: true, + OmitEmptyColumns: true, + } + + headers := []string{"Server", "Host", "Bastion", "Tags"} + rows := dao.GetTableData(servers, headers) + err := PrintTable(rows, options, headers, []string{}, true, false) + + return err +} + func PrintServerBlocks(servers []dao.Server) { if len(servers) == 0 { return @@ -73,12 +95,11 @@ func PrintTaskBlock(tasks []dao.Task) { fmt.Print(output) - PrintTargetBlocks([]dao.Target{task.Target}, true) PrintSpecBlocks([]dao.Spec{task.Spec}, true, false) + PrintTargetBlocks([]dao.Target{task.Target}, true) - envs := task.GetNonDefaultEnvs() - if envs != nil { - printEnv(envs) + if task.Envs != nil { + printEnv(task.Envs) } if task.Cmd != "" { @@ -86,7 +107,7 @@ func PrintTaskBlock(tasks []dao.Task) { printCmd(task.Cmd) } else if len(task.Tasks) > 0 { fmt.Printf("tasks: \n") - for _, st := range task.Tasks { + for i, st := range task.Tasks { if st.Name != "" { if st.Desc != "" { fmt.Printf("%3s - %s: %s\n", " ", st.Name, st.Desc) @@ -94,7 +115,7 @@ func PrintTaskBlock(tasks []dao.Task) { fmt.Printf("%3s - %s\n", " ", st.Name) } } else { - fmt.Printf("%3s - %s\n", " ", "cmd") + fmt.Printf("%3s - %s-%d\n", " ", "task", i) } } } @@ -103,20 +124,9 @@ func PrintTaskBlock(tasks []dao.Task) { fmt.Print("\n--\n\n") } } - fmt.Println() -} -func printCmd(cmd string) { - scanner := bufio.NewScanner(strings.NewReader(cmd)) - for scanner.Scan() { - fmt.Printf("%4s%s\n", " ", scanner.Text()) - } -} - -func printEnv(env []string) { - fmt.Printf("env: \n") - for _, env := range env { - fmt.Printf("%4s%s\n", " ", strings.Replace(strings.TrimSuffix(env, "\n"), "=", ": ", 1)) + if len(tasks) != 1 { + fmt.Println() } } @@ -127,7 +137,9 @@ func PrintTargetBlocks(targets []dao.Target, indent bool) { for i, target := range targets { output := "" + output += printStringField("desc", target.Desc, indent) output += printBoolField("all", target.All, indent) + output += printBoolField("invert", target.Invert, indent) output += printSliceField("servers", target.Servers, indent) output += printStringField("regex", target.Regex, indent) output += printSliceField("tags", target.Tags, indent) @@ -158,15 +170,22 @@ func PrintSpecBlocks(specs []dao.Spec, indent bool, name bool) { for i, spec := range specs { output := "" - if name { - printStringField("name", spec.Name, indent) - } + output += printStringField("desc", spec.Desc, indent) + output += printBoolField("describe", spec.Describe, indent) + output += printBoolField("list_hosts", spec.ListHosts, indent) + output += printStringField("order", spec.Order, indent) + output += printStringField("strategy", spec.Strategy, indent) + output += printNumberField("batch", int(spec.Batch), indent) + output += printNumberField("batch_p", int(spec.BatchP), indent) + output += printNumberField("forks", int(spec.Forks), indent) output += printStringField("output", spec.Output, indent) - output += printBoolField("parallel", spec.Parallel, indent) + output += printNumberField("max_fail_percentage", int(spec.MaxFailPercentage), indent) output += printBoolField("any_errors_fatal", spec.AnyErrorsFatal, indent) output += printBoolField("ignore_errors", spec.IgnoreErrors, indent) output += printBoolField("ignore_unreachable", spec.IgnoreUnreachable, indent) - output += printBoolField("omit_empty", spec.OmitEmpty, indent) + output += printBoolField("omit_empty_rows", spec.OmitEmptyRows, indent) + output += printBoolField("omit_empty_columns", spec.OmitEmptyColumns, indent) + output += printSliceField("report", spec.Report, indent) if output == "" { continue @@ -185,6 +204,20 @@ func PrintSpecBlocks(specs []dao.Spec, indent bool, name bool) { } } +func printCmd(cmd string) { + scanner := bufio.NewScanner(strings.NewReader(cmd)) + for scanner.Scan() { + fmt.Printf("%4s%s\n", " ", scanner.Text()) + } +} + +func printEnv(env []string) { + fmt.Printf("env: \n") + for _, env := range env { + fmt.Printf("%4s%s\n", " ", strings.Replace(strings.TrimSuffix(env, "\n"), "=", ": ", 1)) + } +} + func printStringField(key string, value string, indent bool) string { if value != "" { if indent { diff --git a/core/print/print_table.go b/core/print/print_table.go index 1d4e2f5..fcd311a 100644 --- a/core/print/print_table.go +++ b/core/print/print_table.go @@ -1,50 +1,73 @@ package print import ( + "encoding/json" "fmt" + "strings" "github.com/jedib0t/go-pretty/v6/table" + "github.com/alajmo/sake/core" "github.com/alajmo/sake/core/dao" ) type PrintTableOptions struct { - Title string - Output string - Theme dao.Theme - Resource string - OmitEmpty bool - SuppressEmptyColumns bool + Title string + Output string + Theme dao.Theme + Resource string + OmitEmptyRows bool + OmitEmptyColumns bool } func PrintTable[T dao.Items]( data []T, options PrintTableOptions, headers []string, -) { + footers []string, + padTop bool, + padBottom bool, +) error { + switch options.Output { + case "table", "table-1": + return table1(data, options, headers, footers, padTop, padBottom) case "table-2": - table2(data, options, headers) + return table2(data, options, headers, footers, padTop, padBottom) case "table-3": - table3(data, options, headers) + return table3(data, options, headers, footers, padTop, padBottom) case "table-4": - table4(data, options, headers) + return table4(data, options, headers, footers, padTop, padBottom) + case "csv": + return printCSV(data, options, headers, footers) + case "json": + return printJSON(data, options, headers) default: - table1(data, options, headers) + return table1(data, options, headers, footers, padTop, padBottom) } } /* 1 table, task names in 1st row - Server | Host | Hostname | OS | Kernel + Server | Host | Hostname | OS | Kernel + --------+--------------------+--------------+--------------+-------- - ip6-1 | 2001:3984:3989::10 | 31cb8139dffd | Ubuntu 22.04 | 5.18.0 + + ip6-1 | 2001:3984:3989::10 | 31cb8139dffd | Ubuntu 22.04 | 5.18.0 + --------+--------------------+--------------+--------------+-------- - ip6-2 | 2001:3984:3989::11 | 54666c1891fb | Ubuntu 22.04 | 5.18.0 + ip6-2 | 2001:3984:3989::11 | 54666c1891fb | Ubuntu 22.04 | 5.18.0 */ -func table1[T dao.Items](data []T, options PrintTableOptions, headers []string) { +func table1[T dao.Items]( + data []T, + options PrintTableOptions, + headers []string, + footers []string, + padTop bool, + padBottom bool, +) error { t := CreateTable(options, headers) // Headers @@ -62,7 +85,7 @@ func table1[T dao.Items](data []T, options PrintTableOptions, headers []string) row = append(row, value) } - if options.OmitEmpty { + if options.OmitEmptyRows { empty := true for _, v := range row[1:] { if v != "" { @@ -78,27 +101,50 @@ func table1[T dao.Items](data []T, options PrintTableOptions, headers []string) t.AppendRow(row) } + var tableFooter table.Row + for _, h := range footers { + tableFooter = append(tableFooter, h) + } + t.AppendFooter(tableFooter) + if options.Title != "" { t.SetTitle(options.Title) } - RenderTable(t, options.Output) + RenderTable(t, options.Output, padTop, padBottom) + + return nil } /* 1 table, task names in 1st column - Task | Ip6-1 | Ip6-2 + Task | Ip6-1 | Ip6-2 + ----------+--------------------+-------------------- - Host | 2001:3984:3989::10 | 2001:3984:3989::11 + + Host | 2001:3984:3989::10 | 2001:3984:3989::11 + ----------+--------------------+-------------------- - Hostname | 31cb8139dffd | 54666c1891fb + + Hostname | 31cb8139dffd | 54666c1891fb + ----------+--------------------+-------------------- - OS | Ubuntu 22.04 | Ubuntu 22.04 + + OS | Ubuntu 22.04 | Ubuntu 22.04 + ----------+--------------------+-------------------- - Kernel | 5.18.0 | 5.18.0 + + Kernel | 5.18.0 | 5.18.0 */ -func table2[T dao.Items](data []T, options PrintTableOptions, headers []string) { +func table2[T dao.Items]( + data []T, + options PrintTableOptions, + headers []string, + footers []string, + padTop bool, + padBottom bool, +) error { tableHeaders := table.Row{options.Resource} rh := []string{options.Resource} for _, h := range data { @@ -117,7 +163,7 @@ func table2[T dao.Items](data []T, options PrintTableOptions, headers []string) row = append(row, value) } - if options.OmitEmpty { + if options.OmitEmptyRows { empty := true for _, v := range row[1:] { if v != "" { @@ -132,27 +178,36 @@ func table2[T dao.Items](data []T, options PrintTableOptions, headers []string) t.AppendRow(row) } - RenderTable(t, options.Output) + RenderTable(t, options.Output, padTop, padBottom) + + return nil } /* 1 table per server, task names in 1st row - ip6-1 + ip6-1 - Host | Hostname | OS | Kernel - --------------------+--------------+--------------+-------- - 2001:3984:3989::10 | 31cb8139dffd | Ubuntu 22.04 | 5.18.0 + Host | Hostname | OS | Kernel + --------------------+--------------+--------------+-------- + 2001:3984:3989::10 | 31cb8139dffd | Ubuntu 22.04 | 5.18.0 - ip6-2 + ip6-2 - Host | Hostname | OS | Kernel - --------------------+--------------+--------------+-------- - 2001:3984:3989::11 | 54666c1891fb | Ubuntu 22.04 | 5.18.0 + Host | Hostname | OS | Kernel + --------------------+--------------+--------------+-------- + 2001:3984:3989::11 | 54666c1891fb | Ubuntu 22.04 | 5.18.0 */ -func table3[T dao.Items](data []T, options PrintTableOptions, headers []string) { +func table3[T dao.Items]( + data []T, + options PrintTableOptions, + headers []string, + footers []string, + padTop bool, + padBottom bool, +) error { var tableHeaders table.Row for _, h := range headers[1:] { tableHeaders = append(tableHeaders, h) @@ -173,7 +228,7 @@ func table3[T dao.Items](data []T, options PrintTableOptions, headers []string) } t.AppendRow(row) - if options.OmitEmpty { + if options.OmitEmptyRows { empty := true for _, v := range row { if v != "" { @@ -185,34 +240,59 @@ func table3[T dao.Items](data []T, options PrintTableOptions, headers []string) } } - RenderTable(t, options.Output) + RenderTable(t, options.Output, padTop, padBottom) } + + return nil } /* 1 table per server, task names in 1st column - Task | Ip6-1 + Task | Ip6-1 + ----------+-------------------- - Host | 2001:3984:3989::10 + + Host | 2001:3984:3989::10 + ----------+-------------------- - Hostname | 31cb8139dffd + + Hostname | 31cb8139dffd + ----------+-------------------- - OS | Ubuntu 22.04 + + OS | Ubuntu 22.04 + ----------+-------------------- - Kernel | 5.18.0 - Task | Ip6-2 + Kernel | 5.18.0 + + Task | Ip6-2 + ----------+-------------------- - Host | 2001:3984:3989::11 + + Host | 2001:3984:3989::11 + ----------+-------------------- - Hostname | 54666c1891fb + + Hostname | 54666c1891fb + ----------+-------------------- - OS | Ubuntu 22.04 + + OS | Ubuntu 22.04 + ----------+-------------------- - Kernel | 5.18.0 + + Kernel | 5.18.0 */ -func table4[T dao.Items](data []T, options PrintTableOptions, headers []string) { +func table4[T dao.Items]( + data []T, + options PrintTableOptions, + headers []string, + footers []string, + padTop bool, + padBottom bool, +) error { for _, s := range data { val := s.GetValue(fmt.Sprintf("%v", s), 0) t := CreateTable(options, []string{options.Resource, val}) @@ -226,7 +306,7 @@ func table4[T dao.Items](data []T, options PrintTableOptions, headers []string) row = append(row, h) row = append(row, value) - if options.OmitEmpty { + if options.OmitEmptyRows { empty := true for _, v := range row[1:] { if v != "" { @@ -241,6 +321,107 @@ func table4[T dao.Items](data []T, options PrintTableOptions, headers []string) t.AppendRow(row) } - RenderTable(t, options.Output) + RenderTable(t, options.Output, padTop, padBottom) + } + + return nil +} + +/* +server,host +ip6-1,2001:3984:3989::10 +ip6-2,2001:3984:3989::11 +*/ +func printCSV[T dao.Items](data []T, options PrintTableOptions, headers []string, footers []string) error { + t := CreateTable(options, headers) + + // Headers + var tableHeaders table.Row + for _, h := range headers { + tableHeaders = append(tableHeaders, h) + } + t.AppendHeader(tableHeaders) + + // Rows + for _, item := range data { + var row []any + for i, h := range headers { + value := item.GetValue(fmt.Sprintf("%v", h), i) + switch h { + case "tags": + t := strings.Split(value, "\n") + v := strings.Join(t, ",") + row = append(row, v) + default: + row = append(row, value) + } + } + + if options.OmitEmptyRows { + empty := true + for _, v := range row[1:] { + if v != "" { + empty = false + } + } + + if empty { + continue + } + } + + t.AppendRow(row) } + + if options.Title != "" { + t.SetTitle(options.Title) + } + + RenderTable(t, options.Output, true, true) + + return nil +} + +/* +[ + + { + "ping": "pong" + "server": "list-0", + }, + { + "ping": "pong" + "server": "list-1", + } + +] +*/ +func printJSON[T dao.Items](data []T, options PrintTableOptions, headers []string) error { + var out []map[string]any + for _, v := range data { + m := make(map[string]any) + for j, k := range headers { + value := v.GetValue(k, j) + switch k { + case "servers": + s := core.SplitString(value, "\n") + m[k] = s + case "tags": + t := core.SplitString(value, "\n") + m[k] = t + default: + m[k] = value + } + } + out = append(out, m) + } + + a, err := json.Marshal(out) + if err != nil { + return err + } + + fmt.Println(string(a)) + + return nil } diff --git a/core/print/report.go b/core/print/report.go new file mode 100644 index 0000000..bffec5e --- /dev/null +++ b/core/print/report.go @@ -0,0 +1,381 @@ +package print + +import ( + "fmt" + "strconv" + "strings" + "time" + + "golang.org/x/term" + + "github.com/jedib0t/go-pretty/v6/text" + + "github.com/alajmo/sake/core" + "github.com/alajmo/sake/core/dao" +) + +// TODO: Support csv,html,json,markdown +func PrintReport( + theme *dao.Theme, + reportData dao.ReportData, + spec dao.Spec, +) error { + reportTheme := dao.DEFAULT_THEME + reportTheme.Table.Options.DrawBorder = core.Ptr(false) + reportTheme.Table.Options.SeparateColumns = core.Ptr(false) + reportTheme.Table.Options.SeparateRows = core.Ptr(false) + reportTheme.Table.Options.SeparateHeader = core.Ptr(true) + reportTheme.Table.Options.SeparateFooter = core.Ptr(false) + options := PrintTableOptions{ + Theme: reportTheme, + Output: "table", + OmitEmptyRows: false, + OmitEmptyColumns: false, + } + + summaryTheme := dao.DEFAULT_THEME + summaryTheme.Table.Options.DrawBorder = core.Ptr(false) + summaryTheme.Table.Options.SeparateColumns = core.Ptr(false) + summaryTheme.Table.Options.SeparateRows = core.Ptr(false) + summaryTheme.Table.Options.SeparateHeader = core.Ptr(true) + summaryTheme.Table.Options.SeparateFooter = core.Ptr(false) + summaryOptions := PrintTableOptions{ + Theme: summaryTheme, + Output: "table", + OmitEmptyRows: false, + OmitEmptyColumns: false, + } + + if core.StringInSlice("all", spec.Report) { + printRecapHeader("RETURN CODES ", theme.Text.HeaderFiller) + err := PrintExitReport(&reportTheme, options, reportData) + if err != nil { + return err + } + printRecapHeader("TASK STATUS ", theme.Text.HeaderFiller) + err = PrintTaskReport(&reportTheme, options, reportData) + if err != nil { + return err + } + printRecapHeader("TIME ", theme.Text.HeaderFiller) + err = PrintProfileReport(&reportTheme, options, reportData) + if err != nil { + return err + } + printRecapHeader("RECAP ", theme.Text.HeaderFiller) + err = PrintSummaryReport(&summaryTheme, summaryOptions, reportData) + if err != nil { + return err + } + + return nil + } + + for _, v := range spec.Report { + switch v { + case "recap": + printRecapHeader("RECAP ", theme.Text.HeaderFiller) + err := PrintSummaryReport(&summaryTheme, summaryOptions, reportData) + if err != nil { + return err + } + case "rc": + printRecapHeader("RETURN CODES ", theme.Text.HeaderFiller) + err := PrintExitReport(&reportTheme, options, reportData) + if err != nil { + return err + } + case "task": + printRecapHeader("TASK STATUS ", theme.Text.HeaderFiller) + err := PrintTaskReport(&reportTheme, options, reportData) + if err != nil { + return err + } + case "time": + printRecapHeader("TIME ", theme.Text.HeaderFiller) + err := PrintProfileReport(&reportTheme, options, reportData) + if err != nil { + return err + } + } + } + + return nil +} + +/* + Return Code + Server Output-0 Output-1 Output-2 + +------------------------------------------ + + list-0 1 + list-1 0 1 + list-2 1 + list-3 0 0 0 + list-4 0 0 0 +*/ +func PrintExitReport( + theme *dao.Theme, + options PrintTableOptions, + reportData dao.ReportData, +) error { + theme.Table.Options.SeparateFooter = core.Ptr(false) + var data dao.TableOutput + data.Headers = reportData.Headers + for i := range reportData.Tasks { + name := getStatusName(reportData.Tasks[i].Name, reportData.Tasks[i].Status) + data.Rows = append(data.Rows, dao.Row{Columns: []string{name}}) + + for _, t := range reportData.Tasks[i].Rows { + if t.Status == dao.Skipped || t.Status == dao.Unreachable { + data.Rows[i].Columns = append(data.Rows[i].Columns, "") + } else { + v := strconv.Itoa(t.ReturnCode) + if t.ReturnCode > 0 { + v = FailedPrint.Sprintf(v) + } else { + v = OkPrint.Sprintf(v) + } + data.Rows[i].Columns = append(data.Rows[i].Columns, v) + } + } + } + + err := PrintTable(data.Rows, options, reportData.Headers, []string{}, true, false) + if err != nil { + return err + } + + return nil +} + +/* + Server Output-0 Output-1 Output-2 + +-------------------------------------- + + list-0 0.05 s 0.00 s 0.00 s + list-1 1.05 s 0.01 s 0.00 s + list-2 0.01 s 0.00 s 0.00 s + list-3 1.01 s 0.00 s 0.00 s + list-4 1.01 s 0.00 s 0.00 s +*/ +func PrintProfileReport( + theme *dao.Theme, + options PrintTableOptions, + reportData dao.ReportData, +) error { + theme.Table.Options.SeparateFooter = core.Ptr(true) + var data dao.TableOutput + data.Headers = append(reportData.Headers, "Total") + + var taskDuration []time.Duration + for i := 1; i < len(reportData.Headers); i++ { + taskDuration = append(taskDuration, time.Duration(0)) + } + + for i := range reportData.Tasks { + name := getStatusName(reportData.Tasks[i].Name, reportData.Tasks[i].Status) + data.Rows = append(data.Rows, dao.Row{Columns: []string{name}}) + + var sDuration time.Duration + for k, t := range reportData.Tasks[i].Rows { + if t.Status == dao.Skipped || t.Status == dao.Unreachable { + data.Rows[i].Columns = append(data.Rows[i].Columns, "") + } else { + seconds := NormalPrint.Sprintf("%.2f s", t.Duration.Seconds()) + data.Rows[i].Columns = append(data.Rows[i].Columns, seconds) + sDuration += t.Duration + taskDuration[k] += t.Duration + } + } + tSeconds := NormalPrint.Sprintf("%.2f s", sDuration.Seconds()) + data.Rows[i].Columns = append(data.Rows[i].Columns, tSeconds) + } + + // Don't calculate total if only 1 server + if len(reportData.Tasks) > 1 { + footerName := getStatusName("Total", reportData.Status) + data.Footers = append(data.Footers, footerName) + var tot time.Duration + for _, t := range taskDuration { + v := NormalPrint.Sprintf("%.2f s", t.Seconds()) + data.Footers = append(data.Footers, v) + tot += t + } + data.Footers = append(data.Footers, NormalPrint.Sprintf("%.2f s", tot.Seconds())) + } + + err := PrintTable(data.Rows, options, data.Headers, data.Footers, true, false) + if err != nil { + return err + } + + return nil +} + +/* + Task + Server Output-0 Output-1 Output-2 + +------------------------------------------ + + list-0 ignored ok ok + list-1 ok ignored ok + list-2 ignored ignored ok + list-3 ok ok ok + list-4 ok ok ok +*/ +func PrintTaskReport( + theme *dao.Theme, + options PrintTableOptions, + reportData dao.ReportData, +) error { + theme.Table.Options.SeparateFooter = core.Ptr(false) + var data dao.TableOutput + data.Headers = reportData.Headers + for i := range reportData.Tasks { + name := getStatusName(reportData.Tasks[i].Name, reportData.Tasks[i].Status) + data.Rows = append(data.Rows, dao.Row{Columns: []string{name}}) + + for _, t := range reportData.Tasks[i].Rows { + var v string + switch t.Status { + case dao.Ok: + v = OkPrint.Sprintf(t.Status.String()) + case dao.Skipped: + v = SkippedPrint.Sprintf(t.Status.String()) + case dao.Ignored: + v = IgnoredPrint.Sprintf(t.Status.String()) + case dao.Failed: + v = FailedPrint.Sprintf(t.Status.String()) + case dao.Unreachable: + v = UnreachablePrint.Sprintf(t.Status.String()) + } + + data.Rows[i].Columns = append(data.Rows[i].Columns, v) + } + } + + err := PrintTable(data.Rows, options, reportData.Headers, []string{}, true, false) + if err != nil { + return err + } + + return nil +} + +/* + Summary + +Server Ok Ignored Failed Skipped +------------------------------------------------- +list-0 ok=2 ignored=1 failed=0 skipped=0 +list-1 ok=2 ignored=1 failed=0 skipped=0 +list-2 ok=1 ignored=2 failed=0 skipped=0 +list-3 ok=3 ignored=0 failed=0 skipped=0 +list-4 ok=3 ignored=0 failed=0 skipped=0 +*/ +func PrintSummaryReport( + theme *dao.Theme, + options PrintTableOptions, + reportData dao.ReportData, +) error { + theme.Table.Options.SeparateHeader = core.Ptr(false) + theme.Table.Options.SeparateFooter = core.Ptr(false) + + var data dao.TableOutput + data.Headers = []string{"", "", "", "", "", ""} + var taskStatuses = []dao.TaskStatus{ + dao.Ok, + dao.Unreachable, + dao.Ignored, + dao.Failed, + dao.Skipped, + } + + for i := range reportData.Tasks { + data.Rows = append(data.Rows, dao.Row{}) + name := getStatusName(reportData.Tasks[i].Name, reportData.Tasks[i].Status) + data.Rows[i].Columns = append(data.Rows[i].Columns, name) + for _, s := range taskStatuses { + val := getTotalStatus(s, reportData.Tasks[i].Status) + data.Rows[i].Columns = append(data.Rows[i].Columns, val) + } + } + + // Don't calculate total if only 1 server + if len(reportData.Tasks) > 1 { + theme.Table.Options.SeparateFooter = core.Ptr(true) + if reportData.Status[dao.Failed] == 0 && reportData.Status[dao.Unreachable] == 0 { + tot := OkPrint.Sprintf("%s", "Total") + data.Footers = append(data.Footers, tot) + } else if reportData.Status[dao.Unreachable] > 0 { + data.Footers = append(data.Footers, FailedPrint.Sprintf("%s", "Total")) + } else if reportData.Status[dao.Ok] == 0 { + data.Footers = append(data.Footers, SkippedPrint.Sprintf("%s", "Total")) + } else { + data.Footers = append(data.Footers, FailedPrint.Sprintf("%s", "Total")) + } + for _, s := range taskStatuses { + val := getTotalStatus(s, reportData.Status) + data.Footers = append(data.Footers, val) + } + } + + err := PrintTable(data.Rows, options, data.Headers, data.Footers, false, true) + if err != nil { + return err + } + + return nil +} + +func printRecapHeader(h string, filler string) { + hh := text.Bold.Sprintf(h) + width, _, _ := term.GetSize(0) + headerLength := len(core.Strip(hh)) + if width > 0 { + header := fmt.Sprintf("\n%s%s", hh, strings.Repeat(filler, width-headerLength-1)) + fmt.Println(header) + } +} + +func getStatusName(name string, status map[dao.TaskStatus]int) string { + var out string + if status[dao.Failed] > 0 || status[dao.Unreachable] > 0 { + out = FailedPrint.Sprintf("%s\t", name) + } else if status[dao.Ok] == 0 && status[dao.Skipped] > 0 { + out = SkippedPrint.Sprintf("%s\t", name) + } else { + out = OkPrint.Sprintf("%s\t", name) + } + return out +} + +// func getTotalStatus(s dao.TaskStatus, reportData dao.ReportData) string { +func getTotalStatus(s dao.TaskStatus, status map[dao.TaskStatus]int) string { + var val string + vv := int(status[s]) + v := strconv.Itoa(vv) + + if vv > 0 { + switch s { + case dao.Ok: + val = OkPrint.Sprintf("%s=%s", s, v) + case dao.Skipped: + val = SkippedPrint.Sprintf("%s=%s", s, v) + case dao.Ignored: + val = IgnoredPrint.Sprintf("%s=%s", s, v) + case dao.Failed: + val = FailedPrint.Sprintf("%s=%s", s, v) + case dao.Unreachable: + val = FailedPrint.Sprintf("%s=%s", s, v) + } + } else { + val = ZeroPrint.Sprintf("%s=%s", s, v) + } + + return val +} diff --git a/core/print/table.go b/core/print/table.go index c750db3..6cdda1b 100644 --- a/core/print/table.go +++ b/core/print/table.go @@ -20,7 +20,7 @@ func CreateTable( t.SetOutputMirror(os.Stdout) t.SetStyle(FormatTable(theme)) - if options.SuppressEmptyColumns { + if options.OmitEmptyColumns { t.SuppressEmptyColumns() } @@ -29,10 +29,10 @@ func CreateTable( hh := table.ColumnConfig{ Number: i + 1, AlignHeader: GetAlign(*theme.Table.Header.Align), + Align: GetAlign(*theme.Table.Row.Align), ColorsHeader: combineColors(theme.Table.Header.Fg, theme.Table.Header.Bg, theme.Table.Header.Attr), - - Align: GetAlign(*theme.Table.Row.Align), - Colors: combineColors(theme.Table.Row.Fg, theme.Table.Row.Bg, theme.Table.Row.Attr), + Colors: combineColors(theme.Table.Row.Fg, theme.Table.Row.Bg, theme.Table.Row.Attr), + ColorsFooter: combineColors(theme.Table.Footer.Fg, theme.Table.Footer.Bg, theme.Table.Footer.Attr), } headers = append(headers, hh) @@ -51,6 +51,7 @@ func FormatTable(theme dao.Theme) table.Style { Format: table.FormatOptions{ Header: GetFormat(*theme.Table.Header.Format), Row: GetFormat(*theme.Table.Row.Format), + Footer: GetFormat(*theme.Table.Footer.Format), }, Options: table.Options{ @@ -58,6 +59,7 @@ func FormatTable(theme dao.Theme) table.Style { SeparateColumns: *theme.Table.Options.SeparateColumns, SeparateHeader: *theme.Table.Options.SeparateHeader, SeparateRows: *theme.Table.Options.SeparateRows, + SeparateFooter: *theme.Table.Options.SeparateFooter, }, Title: table.TitleOptions{ @@ -75,15 +77,26 @@ func FormatTable(theme dao.Theme) table.Style { } } -func RenderTable(t table.Writer, output string) { - fmt.Println() +func RenderTable( + t table.Writer, + output string, + padTop bool, + padBottom bool, +) { switch output { case "markdown": t.RenderMarkdown() case "html": t.RenderHTML() + case "csv": + t.RenderCSV() default: + if padTop { + fmt.Println() + } t.Render() + if padBottom { + fmt.Println() + } } - fmt.Println() } diff --git a/core/run/client.go b/core/run/client.go index 2c7cfe1..08d1ad6 100644 --- a/core/run/client.go +++ b/core/run/client.go @@ -7,18 +7,19 @@ import ( ) type Client interface { - Connect(bool, string, *sync.Mutex, SSHDialFunc) *ErrConnect - Run([]string, string, string, string) error - Wait() error - Close() error - Prefix() string - Write(p []byte) (n int, err error) - WriteClose() error - Stdin() io.WriteCloser - Stderr() io.Reader - Stdout() io.Reader - Signal(os.Signal) error + Connect(SSHDialFunc, bool, string, *sync.Mutex) *ErrConnect + Run(int, []string, string, string, string) error + Wait(int) error + Close(int) error + Write(int, []byte) (n int, err error) + WriteClose(int) error + Stdin(int) io.WriteCloser + Stderr(int) io.Reader + Stdout(int) io.Reader + Signal(int, os.Signal) error GetName() string + Prefix() string + Connected() bool } type ErrConnect struct { diff --git a/core/run/exec.go b/core/run/exec.go index e1c0671..002c8fe 100644 --- a/core/run/exec.go +++ b/core/run/exec.go @@ -1,9 +1,11 @@ package run import ( + "bufio" "errors" "fmt" "golang.org/x/crypto/ssh" + "math" "os" "os/signal" "path/filepath" @@ -19,11 +21,12 @@ import ( ) type Run struct { - LocalClients map[string]Client - RemoteClients map[string]Client - Servers []dao.Server - Task *dao.Task - Config dao.Config + LocalClients map[string]Client + RemoteClients map[string]Client + Servers []dao.Server + UnreachableServers []dao.Server + Task *dao.Task + Config dao.Config } type TaskContext struct { @@ -60,7 +63,13 @@ func (run *Run) RunTask( return err } - errConnects, err := ParseServers(run.Config.SSHConfigFile, &run.Servers, runFlags) + err = run.ParseTask(configEnv, userArgs, runFlags, setRunFlags) + if err != nil { + return err + } + run.CheckTaskNoColor() + + errConnects, err := ParseServers(run.Config.SSHConfigFile, &run.Servers, runFlags, run.Task.Spec.Order) if err != nil { return err } @@ -76,29 +85,26 @@ func (run *Run) RunTask( } options := print.PrintTableOptions{ - Theme: task.Theme, - OmitEmpty: task.Spec.OmitEmpty, - Output: task.Spec.Output, - SuppressEmptyColumns: false, - Title: "Parse Errors", + Theme: task.Theme, + OmitEmptyRows: task.Spec.OmitEmptyRows, + OmitEmptyColumns: false, + Output: task.Spec.Output, + Title: "Parse Errors", + } + err = print.PrintTable(parseOutput.Rows, options, parseOutput.Headers, []string{}, true, true) + if err != nil { + return err } - print.PrintTable(parseOutput.Rows, options, parseOutput.Headers) return &core.ExecError{Err: errors.New("Parse Error"), ExitCode: 4} } - err = run.ParseTask(configEnv, userArgs, runFlags, setRunFlags) - if err != nil { - return err - } - run.CheckTaskNoColor() - // Remote + Local clients numClients := len(servers) * 2 clientCh := make(chan Client, numClients) errCh := make(chan ErrConnect, numClients) - errConnect, err := run.SetClients(runFlags, numClients, clientCh, errCh) + errConnect, err := run.SetClients(task, runFlags, numClients, clientCh, errCh) if err != nil { return err } @@ -114,13 +120,16 @@ func (run *Run) RunTask( } options := print.PrintTableOptions{ - Theme: task.Theme, - OmitEmpty: task.Spec.OmitEmpty, - Output: "table", - SuppressEmptyColumns: false, - Title: "\nUnreachable Hosts\n", + Theme: task.Theme, + OmitEmptyRows: task.Spec.OmitEmptyRows, + OmitEmptyColumns: false, + Output: "table", + Title: "\nUnreachable Hosts\n", + } + err := print.PrintTable(unreachableOutput.Rows, options, unreachableOutput.Headers, []string{}, true, true) + if err != nil { + return err } - print.PrintTable(unreachableOutput.Rows, options, unreachableOutput.Headers) if !task.Spec.IgnoreUnreachable { return &core.ExecError{Err: err, ExitCode: 4} @@ -129,6 +138,7 @@ func (run *Run) RunTask( // Get reachable servers var reachableServers []dao.Server + var unreachableServers []dao.Server for _, server := range servers { if server.Local { reachableServers = append(reachableServers, server) @@ -138,49 +148,97 @@ func (run *Run) RunTask( _, reachable := run.RemoteClients[server.Name] if reachable { reachableServers = append(reachableServers, server) + } else { + unreachableServers = append(unreachableServers, server) } } run.Servers = reachableServers + run.UnreachableServers = unreachableServers // Describe task - if runFlags.Describe { + if task.Spec.Describe { + PrintHeader("TASK DESCRIPTION ", run.Task.Theme.Text, false) print.PrintTaskBlock([]dao.Task{*task}) } + // Describe Servers + if task.Spec.ListHosts { + PrintHeader("HOSTS ", run.Task.Theme.Text, false) + err := print.PrintServerList(servers) + if err != nil { + return err + } + } + + if runFlags.Confirm && !confirmExecute(run.Task.Name) { + return nil + } + switch task.Spec.Output { - case "table", "table-1", "table-2", "table-3", "table-4", "html", "markdown": + case "table", "table-1", "table-2", "table-3", "table-4", "html", "markdown", "json", "csv", "none": spinner := core.GetSpinner() - if !runFlags.Silent { + if !task.Spec.Silent && !task.Spec.Step && !task.Spec.Confirm { spinner.Start(" Running", 500) } - data, err := run.Table(runFlags.DryRun) + data, reportData, derr := run.Table(runFlags.DryRun) options := print.PrintTableOptions{ - Theme: task.Theme, - OmitEmpty: task.Spec.OmitEmpty, - Output: task.Spec.Output, - SuppressEmptyColumns: false, - Resource: "task", + Theme: task.Theme, + OmitEmptyRows: task.Spec.OmitEmptyRows, + OmitEmptyColumns: task.Spec.OmitEmptyColumns, + Output: task.Spec.Output, + Resource: "task", } run.CleanupClients() - if !runFlags.Silent { + if !task.Spec.Silent && !task.Spec.Step && !task.Spec.Confirm { spinner.Stop() } - print.PrintTable(data.Rows, options, data.Headers) + if len(run.Servers) > 0 && task.Spec.Output != "none" { + if strings.Contains(task.Spec.Output, "table") { + PrintHeader("TASKS ", run.Task.Theme.Text, true) + } + + err = print.PrintTable(data.Rows, options, data.Headers, []string{}, false, false) + if err != nil { + return err + } + } + + err := print.PrintReport(&run.Task.Theme, reportData, task.Spec) if err != nil { return err } + + if derr != nil { + return derr + } default: - err := run.Text(runFlags.DryRun) + if (len(run.Servers) > 0 && len(run.Task.Tasks) > 1) || run.Task.Spec.Strategy != "linear" { + PrintHeader("TASKS ", run.Task.Theme.Text, true) + } else { + fmt.Println() + } + + reportData, derr := run.Text(runFlags.DryRun) + run.CleanupClients() + err = print.PrintReport(&run.Task.Theme, reportData, task.Spec) if err != nil { return err } + + if err != nil { + return err + } + + if derr != nil { + return derr + } } - if runFlags.Attach || task.Attach { + if task.Attach { server, err := dao.GetFirstRemoteServer(servers) if err != nil { return err @@ -201,12 +259,13 @@ type Signers struct { // SetClients establishes connection to server func (run *Run) SetClients( + task *dao.Task, runFlags *core.RunFlags, numChannels int, clientCh chan Client, errCh chan ErrConnect, ) ([]ErrConnect, error) { - createLocalClient := func(server dao.Server, wg *sync.WaitGroup, mu *sync.Mutex) { + createLocalClient := func(strategy string, numTasks int, server dao.Server, wg *sync.WaitGroup, mu *sync.Mutex) { defer wg.Done() local := &LocalhostClient{ @@ -215,10 +274,26 @@ func (run *Run) SetClients( Host: server.Host, } + switch strategy { + case "free": + for i := 0; i < numTasks; i++ { + local.Sessions = append(local.Sessions, LocalSession{}) + } + default: + local.Sessions = append(local.Sessions, LocalSession{}) + } + clientCh <- local } - createRemoteClient := func(authMethod []ssh.AuthMethod, server dao.Server, wg *sync.WaitGroup, mu *sync.Mutex) { + createRemoteClient := func( + strategy string, + numTasks int, + authMethod []ssh.AuthMethod, + server dao.Server, + wg *sync.WaitGroup, + mu *sync.Mutex, + ) { defer wg.Done() remote := &SSHClient{ @@ -228,28 +303,37 @@ func (run *Run) SetClients( Port: server.Port, AuthMethod: authMethod, } + switch strategy { + case "free": + for i := 0; i < numTasks; i++ { + remote.Sessions = append(remote.Sessions, SSHSession{}) + } + default: + remote.Sessions = append(remote.Sessions, SSHSession{}) + } var bastion *SSHClient if server.BastionHost != "" { bastion = &SSHClient{ + Name: "Bastion", Host: server.BastionHost, User: server.BastionUser, Port: server.BastionPort, AuthMethod: authMethod, } // Connect to bastion - if err := bastion.Connect(run.Config.DisableVerifyHost, run.Config.KnownHostsFile, mu, ssh.Dial); err != nil { + if err := bastion.Connect(ssh.Dial, run.Config.DisableVerifyHost, run.Config.KnownHostsFile, mu); err != nil { errCh <- *err return } // Connect to server through bastion - if err := remote.Connect(run.Config.DisableVerifyHost, run.Config.KnownHostsFile, mu, bastion.DialThrough); err != nil { + if err := remote.Connect(bastion.DialThrough, run.Config.DisableVerifyHost, run.Config.KnownHostsFile, mu); err != nil { errCh <- *err return } } else { - if err := remote.Connect(run.Config.DisableVerifyHost, run.Config.KnownHostsFile, mu, ssh.Dial); err != nil { + if err := remote.Connect(ssh.Dial, run.Config.DisableVerifyHost, run.Config.KnownHostsFile, mu); err != nil { errCh <- *err return } @@ -286,13 +370,14 @@ func (run *Run) SetClients( } } + // TODO: Dont create remote clients if task is set to local for _, server := range run.Servers { wg.Add(1) - go createLocalClient(server, &wg, &mu) + go createLocalClient(task.Spec.Strategy, len(task.Tasks), server, &wg, &mu) if !server.Local { wg.Add(1) authMethods := getAuthMethod(server, &signers) - go createRemoteClient(authMethods, server, &wg, &mu) + go createRemoteClient(task.Spec.Strategy, len(task.Tasks), authMethods, server, &wg, &mu) } } wg.Wait() @@ -335,9 +420,21 @@ func (run *Run) CleanupClients() { return } for _, c := range clients { - err := c.Signal(sig) - if err != nil { - fmt.Fprintf(os.Stderr, "%v", err) + switch c := c.(type) { + case *SSHClient: + for i := range c.Sessions { + err := c.Signal(i, sig) + if err != nil { + fmt.Fprintf(os.Stderr, "%v", err) + } + } + case *LocalhostClient: + for i := range c.Sessions { + err := c.Signal(i, sig) + if err != nil { + fmt.Fprintf(os.Stderr, "%v", err) + } + } } } } @@ -350,19 +447,34 @@ func (run *Run) CleanupClients() { // Close remote connections for _, c := range clients { if remote, ok := c.(*SSHClient); ok { - remote.Close() + for i := range c.(*SSHClient).Sessions { + remote.Close(i) + } } } } -// ParseServers resolves host, port, proxyjump in users ssh config -func ParseServers(sshConfigFile *string, servers *[]dao.Server, runFlags *core.RunFlags) ([]ErrConnect, error) { +// ParseServers resolves host, port, proxyjump in user ssh config +func ParseServers( + sshConfigFile *string, + servers *[]dao.Server, + runFlags *core.RunFlags, + order string, +) ([]ErrConnect, error) { + dao.SortServers(order, servers) + if runFlags.IdentityFile != "" { for i := range *servers { (*servers)[i].IdentityFile = &runFlags.IdentityFile } } + if runFlags.User != "" { + for i := range *servers { + (*servers)[i].User = runFlags.User + } + } + if runFlags.Password != "" { for i := range *servers { (*servers)[i].Password = &runFlags.Password @@ -505,7 +617,12 @@ func ParseServers(sshConfigFile *string, servers *[]dao.Server, runFlags *core.R return errConnects, err } -func (run *Run) ParseTask(configEnv []string, userArgs []string, runFlags *core.RunFlags, setRunFlags *core.SetRunFlags) error { +func (run *Run) ParseTask( + configEnv []string, + userArgs []string, + runFlags *core.RunFlags, + setRunFlags *core.SetRunFlags, +) error { // Update theme property if user flag is provided if runFlags.Theme != "" { theme, err := run.Config.GetTheme(runFlags.Theme) @@ -516,24 +633,109 @@ func (run *Run) ParseTask(configEnv []string, userArgs []string, runFlags *core. run.Task.Theme = *theme } + if runFlags.Spec != "" { + spec, err := run.Config.GetSpec(runFlags.Spec) + if err != nil { + return err + } + run.Task.Spec = *spec + } + + if run.Task.Spec.Forks == 0 { + run.Task.Spec.Forks = 10000 + } + + if setRunFlags.Batch { + run.Task.Spec.Batch = runFlags.Batch + } else if setRunFlags.BatchP { + tot := float64(len(run.Servers)) + percentage := float64(run.Task.Spec.BatchP) / float64(100) + batch := uint32(math.Floor(percentage * tot)) + + if batch > 0 { + run.Task.Spec.Batch = batch + } else { + run.Task.Spec.Batch = 1 + } + } else { + // Batch or BatchP must be > 0 + if run.Task.Spec.Batch == 0 && run.Task.Spec.BatchP == 0 { + run.Task.Spec.Batch = uint32(len(run.Servers)) + } else if run.Task.Spec.BatchP > 0 { + tot := float64(len(run.Servers)) + percentage := float64(run.Task.Spec.BatchP) / float64(100) + batch := uint32(math.Floor(percentage * tot)) + + if batch > 0 { + run.Task.Spec.Batch = batch + } else { + run.Task.Spec.Batch = 1 + } + } + } + + if setRunFlags.Order { + run.Task.Spec.Order = runFlags.Order + } + + // Report + if setRunFlags.Report { + run.Task.Spec.Report = runFlags.Report + } + + // Update describe property if user flag is provided + if setRunFlags.Describe { + run.Task.Spec.Describe = runFlags.Describe + } + + // Update describe property if user flag is provided + if setRunFlags.ListHosts { + run.Task.Spec.ListHosts = runFlags.ListHosts + } + + // Update describe property if user flag is provided + if setRunFlags.Silent { + run.Task.Spec.Silent = runFlags.Silent + } + + // Update describe property if user flag is provided + if setRunFlags.Attach { + run.Task.Attach = runFlags.Attach + } + + // Update strategy property if user flag is provided + if runFlags.Strategy != "" { + run.Task.Spec.Strategy = runFlags.Strategy + } + // Update output property if user flag is provided if runFlags.Output != "" { run.Task.Spec.Output = runFlags.Output } - // Omit servers which provide empty output - if setRunFlags.OmitEmpty { - run.Task.Spec.OmitEmpty = runFlags.OmitEmpty + // Omit empty row + if setRunFlags.OmitEmptyRows { + run.Task.Spec.OmitEmptyRows = runFlags.OmitEmptyRows } - // If parallel flag is set to true, then update task specs - if setRunFlags.Parallel { - run.Task.Spec.Parallel = runFlags.Parallel + // Omit empty column + if setRunFlags.OmitEmptyColumns { + run.Task.Spec.OmitEmptyColumns = runFlags.OmitEmptyColumns } // If AnyErrorsFatal flag is set to true, then tasks execution will stop if error is encountered for all servers if setRunFlags.AnyErrorsFatal { run.Task.Spec.AnyErrorsFatal = runFlags.AnyErrorsFatal + + if run.Task.Spec.AnyErrorsFatal { + run.Task.Spec.MaxFailPercentage = 0 + } else { + run.Task.Spec.MaxFailPercentage = 100 + } + } + + if run.Task.Spec.AnyErrorsFatal { + run.Task.Spec.MaxFailPercentage = 0 } // If IgnoreErrors flag is set to true, then tasks will run regardless of error @@ -551,8 +753,23 @@ func (run *Run) ParseTask(configEnv []string, userArgs []string, runFlags *core. run.Task.TTY = runFlags.TTY } + // Confirm + if setRunFlags.Confirm { + run.Task.Spec.Confirm = runFlags.Confirm + } + + if setRunFlags.Step { + run.Task.Spec.Step = runFlags.Step + } + // Update sub-commands for j := range run.Task.Tasks { + + // If command name is not set, set one + if run.Task.Tasks[j].Name == "" { + run.Task.Tasks[j].Name = fmt.Sprintf("task-%d", j) + } + // If local flag is set to true, then cmd will run locally instead of on remote server if setRunFlags.Local { run.Task.Tasks[j].Local = runFlags.Local @@ -565,9 +782,44 @@ func (run *Run) ParseTask(configEnv []string, userArgs []string, runFlags *core. run.Task.Tasks[j].Envs = envs } + run.ParseTaskTarget(runFlags, setRunFlags) + + if setRunFlags.Verbose || run.Task.Spec.Verbose { + run.Task.Spec.Describe = true + run.Task.Spec.ListHosts = true + run.Task.Spec.Report = []string{"all"} + } + return nil } +func (run *Run) ParseTaskTarget( + runFlags *core.RunFlags, + setRunFlags *core.SetRunFlags, +) { + if setRunFlags.All { + run.Task.Target.All = runFlags.All + } + if setRunFlags.Servers { + run.Task.Target.Servers = runFlags.Servers + } + if setRunFlags.Tags { + run.Task.Target.Tags = runFlags.Tags + } + if setRunFlags.Regex { + run.Task.Target.Regex = runFlags.Regex + } + if setRunFlags.Invert { + run.Task.Target.Invert = runFlags.Invert + } + if setRunFlags.Limit { + run.Task.Target.Limit = runFlags.Limit + } + if setRunFlags.LimitP { + run.Task.Target.LimitP = runFlags.LimitP + } +} + func (run *Run) CheckTaskNoColor() { for _, env := range run.Task.Envs { name := strings.Split(env, "=")[0] @@ -592,32 +844,90 @@ func (run *Run) setKnownHostsFile(knownHostsFileFlag string) error { return nil } -func getWorkDir(cmd dao.TaskCmd, server dao.Server) string { - if cmd.Local || server.Local { - rootDir := os.ExpandEnv(cmd.RootDir) - if cmd.WorkDir != "" { - workDir := os.ExpandEnv(cmd.WorkDir) - if filepath.IsAbs(workDir) { - return workDir - } else { - return filepath.Join(rootDir, workDir) - } - } else if server.WorkDir != "" { - workDir := os.ExpandEnv(server.WorkDir) - if filepath.IsAbs(workDir) { - return workDir - } else { - return filepath.Join(rootDir, workDir) +func getWorkDir( + cmdLocal bool, + serverLocal bool, + cmdWD string, + serverWD string, + cmdDir string, + serverDir string, +) string { + cmdWDTrue := false + if cmdWD != "" { + cmdWDTrue = true + } + + serverWDTrue := false + if serverWD != "" { + serverWDTrue = true + } + + // Remote + + if !cmdLocal && !serverLocal { + if !cmdWDTrue && !serverWDTrue { + return "" + } + + if cmdWDTrue && !serverWDTrue { + return cmdWD + } + + if !cmdWDTrue && serverWDTrue { + return serverWD + } + + // cmdWD relative to serverWD + if cmdWDTrue && serverWDTrue { + if filepath.IsAbs(cmdWD) { + return cmdWD } - } else { - return rootDir + return filepath.Join(serverWD, cmdWD) } - } else if cmd.WorkDir != "" { - // task work_dir - return cmd.WorkDir - } else if server.WorkDir != "" { - // server work_dir - return server.WorkDir + } + + // Local + + // cmd context + if (cmdLocal && serverLocal && !cmdWDTrue && !serverWDTrue) || + (cmdLocal && !serverLocal && !cmdWDTrue && !serverWDTrue) || + (cmdLocal && !serverLocal && !cmdWDTrue && serverWDTrue) || + (!cmdLocal && serverLocal && !cmdWDTrue && !serverWDTrue) { + return cmdDir + } + + // cmdWD relative to serverWD and serverDir + if (cmdLocal && serverLocal && cmdWDTrue && serverWDTrue) || + (!cmdLocal && serverLocal && cmdWDTrue && serverWDTrue) { + if filepath.IsAbs(cmdWD) { + return cmdWD + } + + if filepath.IsAbs(serverWD) { + return filepath.Join(serverWD, cmdWD) + } + + return filepath.Join(serverDir, serverWD, cmdWD) + } + + // cmdWD relative to cmd context + if (cmdLocal && !serverLocal && cmdWDTrue && !serverWDTrue) || + (cmdLocal && !serverLocal && cmdWDTrue && serverWDTrue) || + (cmdLocal && serverLocal && cmdWDTrue && !serverWDTrue) || + (!cmdLocal && serverLocal && cmdWDTrue && !serverWDTrue) { + if filepath.IsAbs(cmdWD) { + return cmdWD + } + return filepath.Join(cmdDir, cmdWD) + } + + // serverWD relative to server context + if (!cmdLocal && serverLocal && !cmdWDTrue && serverWDTrue) || + (cmdLocal && serverLocal && !cmdWDTrue && serverWDTrue) { + if filepath.IsAbs(serverWD) { + return serverWD + } + return filepath.Join(serverDir, serverWD) } return "" @@ -709,3 +1019,77 @@ func getAuthMethod(server dao.Server, signers *Signers) []ssh.AuthMethod { return authMethods } + +func CalcFreeForks(batch int, tasks int, forks uint32) int { + tot := batch * tasks + if tot < int(forks) { + return tot + } + return int(forks) +} + +func CalcForks(batch int, forks uint32) int { + if batch < int(forks) { + return batch + } + return int(forks) +} + +func confirmExecute(taskName string) bool { + var mu sync.Mutex + + mu.Lock() + + reader := bufio.NewReader(os.Stdin) + + fmt.Printf("\nPerform task `%s`: (y)es/(n)o: ", taskName) + + a, err := reader.ReadString('\n') + if err != nil { + return false + } + + mu.Unlock() + + return strings.ToLower(strings.TrimSpace(a)) == "yes" || strings.ToLower(strings.TrimSpace(a)) == "y" +} + +// TODO: Prompt again when invalid answer +func StepTaskExecute(task string, host string, mu *sync.Mutex) (TaskOption, error) { + mu.Lock() + + reader := bufio.NewReader(os.Stdin) + + fmt.Printf("Perform task `%s` on host `%s`: (y)es/(n)o/(c)ontinue: ", task, host) + + a, err := reader.ReadString('\n') + if err != nil { + return Yes, err + } + + option := strings.ToLower(strings.TrimSpace(a)) + var value TaskOption + + switch option { + case "yes", "y": + value = Yes + case "no", "n": + value = No + case "continue", "c": + value = Continue + default: + value = No + } + + mu.Unlock() + + return value, nil +} + +type TaskOption int + +const ( + No = iota + Yes + Continue +) diff --git a/core/run/exec_test.go b/core/run/exec_test.go new file mode 100644 index 0000000..a4770ab --- /dev/null +++ b/core/run/exec_test.go @@ -0,0 +1,34 @@ +package run + +import ( + "testing" + + "github.com/alajmo/sake/core/test" +) + +func TestWorkDir(t *testing.T) { + // Remote + + test.CheckEqS(t, getWorkDir(false, false, "", "", "cmd-root", "server-root"), "") + test.CheckEqS(t, getWorkDir(false, false, "cmd", "", "cmd-root", "server-root"), "cmd") + test.CheckEqS(t, getWorkDir(false, false, "", "server", "", "server-root"), "server") + test.CheckEqS(t, getWorkDir(false, false, "cmd", "server", "cmd-root", "server-root"), "server/cmd") + + // Local + + test.CheckEqS(t, getWorkDir(true, false, "", "", "cmd-root", "server-root"), "cmd-root") + test.CheckEqS(t, getWorkDir(true, true, "", "", "cmd-root", "server-root"), "cmd-root") + test.CheckEqS(t, getWorkDir(true, false, "", "server", "cmd-root", "server-root"), "cmd-root") + test.CheckEqS(t, getWorkDir(false, true, "", "", "cmd-root", "server-root"), "cmd-root") + + test.CheckEqS(t, getWorkDir(false, true, "cmd", "server", "cmd-root", "server-root"), "server-root/server/cmd") + test.CheckEqS(t, getWorkDir(true, true, "cmd", "server", "", "server-root"), "server-root/server/cmd") + + test.CheckEqS(t, getWorkDir(true, false, "cmd", "", "cmd-root", "server-root"), "cmd-root/cmd") + test.CheckEqS(t, getWorkDir(true, false, "cmd", "server", "cmd-root", "server-root"), "cmd-root/cmd") + test.CheckEqS(t, getWorkDir(true, true, "cmd", "", "cmd-root", "server-root"), "cmd-root/cmd") + test.CheckEqS(t, getWorkDir(false, true, "cmd", "", "cmd-root", "server-root"), "cmd-root/cmd") + + test.CheckEqS(t, getWorkDir(false, true, "", "server", "cmd-root", "server-root"), "server-root/server") + test.CheckEqS(t, getWorkDir(true, true, "", "server", "cmd-root", "server-root"), "server-root/server") +} diff --git a/core/run/localhost.go b/core/run/localhost.go index ec8f18d..3209181 100644 --- a/core/run/localhost.go +++ b/core/run/localhost.go @@ -10,12 +10,16 @@ import ( "sync" ) -// Client is a wrapper over the SSH connection/sessions. +// Client is a wrapper over the SSH connection/Sessions. type LocalhostClient struct { Name string User string Host string + Sessions []LocalSession +} + +type LocalSession struct { stdin io.WriteCloser cmd *exec.Cmd stdout io.Reader @@ -23,14 +27,14 @@ type LocalhostClient struct { running bool } -func (c *LocalhostClient) Connect(_ bool, _ string, mu *sync.Mutex, dialer SSHDialFunc) *ErrConnect { +func (c *LocalhostClient) Connect(dialer SSHDialFunc, _ bool, _ string, mu *sync.Mutex) *ErrConnect { return nil } -func (c *LocalhostClient) Run(env []string, workDir string, shell string, cmdStr string) error { +func (c *LocalhostClient) Run(i int, env []string, workDir string, shell string, cmdStr string) error { var err error - if c.running { + if c.Sessions[i].running { return fmt.Errorf("Command already running") } @@ -40,79 +44,89 @@ func (c *LocalhostClient) Run(env []string, workDir string, shell string, cmdStr shell = dao.DEFAULT_SHELL } + var cmdString string + if workDir != "" { + cmdString = fmt.Sprintf("cd %s; %s", workDir, cmdStr) + } else { + cmdString = cmdStr + } + args := strings.SplitN(shell, " ", 2) shellProgram := args[0] - shellFlag := append(args[1:], cmdStr) + shellArgs := append(args[1:], cmdString) - cmd := exec.Command(shellProgram, shellFlag...) + cmd := exec.Command(shellProgram, shellArgs...) cmd.Env = append(userEnv, env...) - cmd.Dir = workDir - c.cmd = cmd + c.Sessions[i].cmd = cmd - c.stdout, err = cmd.StdoutPipe() + c.Sessions[i].stdout, err = cmd.StdoutPipe() if err != nil { return err } - c.stderr, err = cmd.StderrPipe() + c.Sessions[i].stderr, err = cmd.StderrPipe() if err != nil { return err } - c.stdin, err = cmd.StdinPipe() + c.Sessions[i].stdin, err = cmd.StdinPipe() if err != nil { return err } - if err := c.cmd.Start(); err != nil { + if err := c.Sessions[i].cmd.Start(); err != nil { return err } - c.running = true + c.Sessions[i].running = true return nil } -func (c *LocalhostClient) Wait() error { - if !c.running { +func (c *LocalhostClient) Wait(i int) error { + if !c.Sessions[i].running { return fmt.Errorf("Trying to wait on stopped command") } - err := c.cmd.Wait() - c.running = false + err := c.Sessions[i].cmd.Wait() + c.Sessions[i].running = false return err } -func (c *LocalhostClient) Close() error { +func (c *LocalhostClient) Close(i int) error { return nil } -func (c *LocalhostClient) Stdin() io.WriteCloser { - return c.stdin +func (c *LocalhostClient) Stdin(i int) io.WriteCloser { + return c.Sessions[i].stdin } -func (c *LocalhostClient) Write(p []byte) (n int, err error) { - return c.stdin.Write(p) +func (c *LocalhostClient) Write(i int, p []byte) (n int, err error) { + return c.Sessions[i].stdin.Write(p) } -func (c *LocalhostClient) WriteClose() error { - return c.stdin.Close() +func (c *LocalhostClient) WriteClose(i int) error { + return c.Sessions[i].stdin.Close() } -func (c *LocalhostClient) Stderr() io.Reader { - return c.stderr +func (c *LocalhostClient) Stderr(i int) io.Reader { + return c.Sessions[i].stderr } -func (c *LocalhostClient) Stdout() io.Reader { - return c.stdout +func (c *LocalhostClient) Stdout(i int) io.Reader { + return c.Sessions[i].stdout } func (c *LocalhostClient) Prefix() string { return c.Host } -func (c *LocalhostClient) Signal(sig os.Signal) error { - return c.cmd.Process.Signal(sig) +func (c *LocalhostClient) Signal(i int, sig os.Signal) error { + return c.Sessions[i].cmd.Process.Signal(sig) } func (c *LocalhostClient) GetName() string { return c.Name } + +func (c *LocalhostClient) Connected() bool { + return true +} diff --git a/core/run/ssh.go b/core/run/ssh.go index ff278e6..f94456b 100644 --- a/core/run/ssh.go +++ b/core/run/ssh.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net" "os" "strings" @@ -30,7 +29,6 @@ var DefaultTimeout = 20 * time.Second // Client is a wrapper over the SSH connection/sessions. type SSHClient struct { conn *ssh.Client - sess *ssh.Session Name string User string @@ -40,11 +38,17 @@ type SSHClient struct { Password string AuthMethod []ssh.AuthMethod - connString string + connString string + connOpened bool + + Sessions []SSHSession +} + +type SSHSession struct { + sess *ssh.Session remoteStdin io.WriteCloser remoteStdout io.Reader remoteStderr io.Reader - connOpened bool sessOpened bool running bool } @@ -58,13 +62,23 @@ type Identity struct { type SSHDialFunc func(net, addr string, config *ssh.ClientConfig) (*ssh.Client, error) // Connect creates SSH connection to a specified host. -func (c *SSHClient) Connect(disableVerifyHost bool, knownHostsFile string, mu *sync.Mutex, dialer SSHDialFunc) *ErrConnect { +func (c *SSHClient) Connect( + dialer SSHDialFunc, + disableVerifyHost bool, + knownHostsFile string, + mu *sync.Mutex, +) *ErrConnect { return c.ConnectWith(dialer, disableVerifyHost, knownHostsFile, mu) } // ConnectWith creates a SSH connection to a specified host. It will use dialer to establish the // connection. -func (c *SSHClient) ConnectWith(dialer SSHDialFunc, disableVerifyHost bool, knownHostsFile string, mu *sync.Mutex) *ErrConnect { +func (c *SSHClient) ConnectWith( + dialer SSHDialFunc, + disableVerifyHost bool, + knownHostsFile string, + mu *sync.Mutex, +) *ErrConnect { if c.connOpened { return &ErrConnect{ Name: c.Name, @@ -106,30 +120,31 @@ func (c *SSHClient) ConnectWith(dialer SSHDialFunc, disableVerifyHost bool, know } // Run runs a command remotely on c.host. -func (c *SSHClient) Run(env []string, workDir string, shell string, cmdStr string) error { - if c.running { - return fmt.Errorf("Session already running") - } - if c.sessOpened { - return fmt.Errorf("Session already connected") - } +func (c *SSHClient) Run(i int, env []string, workDir string, shell string, cmdStr string) error { + // TODO: What to do about these? + // if c.Sessions[i].running { + // return fmt.Errorf("Session already running") + // } + // if c.Sessions[i].sessOpened { + // return fmt.Errorf("Session already connected") + // } sess, err := c.conn.NewSession() if err != nil { return err } - c.remoteStdin, err = sess.StdinPipe() + c.Sessions[i].remoteStdin, err = sess.StdinPipe() if err != nil { return err } - c.remoteStdout, err = sess.StdoutPipe() + c.Sessions[i].remoteStdout, err = sess.StdoutPipe() if err != nil { return err } - c.remoteStderr, err = sess.StderrPipe() + c.Sessions[i].remoteStderr, err = sess.StderrPipe() if err != nil { return err } @@ -154,33 +169,33 @@ func (c *SSHClient) Run(env []string, workDir string, shell string, cmdStr strin return err } - c.sess = sess - c.sessOpened = true - c.running = true + c.Sessions[i].sess = sess + c.Sessions[i].sessOpened = true + c.Sessions[i].running = true return nil } // Wait waits until the remote command finishes and exits. // It closes the SSH session. -func (c *SSHClient) Wait() error { - if !c.running { +func (c *SSHClient) Wait(i int) error { + if !c.Sessions[i].running { return fmt.Errorf("Trying to wait on stopped session") } - err := c.sess.Wait() - c.sess.Close() - c.running = false - c.sessOpened = false + err := c.Sessions[i].sess.Wait() + c.Sessions[i].sess.Close() + c.Sessions[i].running = false + c.Sessions[i].sessOpened = false return err } // Close closes the underlying SSH connection and session. -func (c *SSHClient) Close() error { - if c.sessOpened { - c.sess.Close() - c.sessOpened = false +func (c *SSHClient) Close(i int) error { + if c.Sessions[i].sessOpened { + c.Sessions[i].sess.Close() + c.Sessions[i].sessOpened = false } if !c.connOpened { return fmt.Errorf("Trying to close the already closed connection") @@ -188,21 +203,21 @@ func (c *SSHClient) Close() error { err := c.conn.Close() c.connOpened = false - c.running = false + c.Sessions[i].running = false return err } -func (c *SSHClient) Stdin() io.WriteCloser { - return c.remoteStdin +func (c *SSHClient) Stdin(i int) io.WriteCloser { + return c.Sessions[i].remoteStdin } -func (c *SSHClient) Stderr() io.Reader { - return c.remoteStderr +func (c *SSHClient) Stderr(i int) io.Reader { + return c.Sessions[i].remoteStderr } -func (c *SSHClient) Stdout() io.Reader { - return c.remoteStdout +func (c *SSHClient) Stdout(i int) io.Reader { + return c.Sessions[i].remoteStdout } // DialThrough will create a new connection from the ssh server c is connected to. DialThrough is an SSHDialer. @@ -222,22 +237,22 @@ func (c *SSHClient) Prefix() string { return c.Host } -func (c *SSHClient) Write(p []byte) (n int, err error) { - return c.remoteStdin.Write(p) +func (c *SSHClient) Write(i int, p []byte) (n int, err error) { + return c.Sessions[i].remoteStdin.Write(p) } -func (c *SSHClient) WriteClose() error { - return c.remoteStdin.Close() +func (c *SSHClient) WriteClose(i int) error { + return c.Sessions[i].remoteStdin.Close() } -func (c *SSHClient) Signal(sig os.Signal) error { - if !c.sessOpened { +func (c *SSHClient) Signal(i int, sig os.Signal) error { + if !c.Sessions[i].sessOpened { return fmt.Errorf("session is not open") } switch sig { case os.Interrupt: - return c.sess.Signal(ssh.SIGINT) + return c.Sessions[i].sess.Signal(ssh.SIGINT) default: return fmt.Errorf("%v not supported", sig) } @@ -339,10 +354,11 @@ func AddKnownHost(host string, remote net.Addr, key ssh.PublicKey, knownFile str // TODO: Replace this method with known_hosts Line method when issue with ip6 formats is fixed. // Supported Host formats: -// 172.24.2.3 -// 172.24.2.3:333 # custom port -// 2001:3984:3989::10 -// [2001:3984:3989::10]:333 # custom port +// +// 172.24.2.3 +// 172.24.2.3:333 # custom port +// 2001:3984:3989::10 +// [2001:3984:3989::10]:333 # custom port func Line(address string, key ssh.PublicKey) string { host, port, err := net.SplitHostPort(address) if err != nil { @@ -425,7 +441,7 @@ func GetPasswordAuth(server dao.Server) (ssh.AuthMethod, error) { func GetPasswordIdentitySigner(server dao.Server) (ssh.Signer, error) { var signer ssh.Signer - data, err := ioutil.ReadFile(*server.IdentityFile) + data, err := os.ReadFile(*server.IdentityFile) if err != nil { return nil, err } @@ -446,7 +462,7 @@ func GetPasswordIdentitySigner(server dao.Server) (ssh.Signer, error) { } func GetFingerprintPubKey(server dao.Server) (string, error) { - data, err := ioutil.ReadFile(*server.PubFile) + data, err := os.ReadFile(*server.PubFile) if err != nil { return "", err } @@ -461,7 +477,7 @@ func GetFingerprintPubKey(server dao.Server) (string, error) { func GetSigner(server dao.Server) (ssh.Signer, error) { var signer ssh.Signer - data, err := ioutil.ReadFile(*server.IdentityFile) + data, err := os.ReadFile(*server.IdentityFile) if err != nil { return nil, err } @@ -491,5 +507,8 @@ func GetSigner(server dao.Server) (ssh.Signer, error) { } return signer, nil +} +func (c *SSHClient) Connected() bool { + return c.connOpened } diff --git a/core/run/table.go b/core/run/table.go index 99894c4..31e31d7 100644 --- a/core/run/table.go +++ b/core/run/table.go @@ -1,180 +1,606 @@ package run import ( + "bytes" "fmt" - "golang.org/x/crypto/ssh" "io" - "io/ioutil" + "math" "os" "os/exec" "strings" "sync" + "time" + + "golang.org/x/crypto/ssh" "github.com/alajmo/sake/core" "github.com/alajmo/sake/core/dao" ) -func (run *Run) Table(dryRun bool) (dao.TableOutput, error) { +type ServerTask struct { + Server *dao.Server + Task *dao.Task + Cmd *dao.TaskCmd + i int + j int +} + +func (run *Run) Table(dryRun bool) (dao.TableOutput, dao.ReportData, error) { task := run.Task servers := run.Servers + uServers := run.UnreachableServers + // TODO: data, reportData should be pointer? var data dao.TableOutput + var reportData dao.ReportData var dataMutex = sync.RWMutex{} - - /** - ** Headers - **/ - data.Headers = append(data.Headers, "server") - + data.Headers = append(reportData.Headers, "host") + reportData.Headers = append(reportData.Headers, "host") // Append Command names if set for _, subTask := range task.Tasks { - if subTask.Name != "" { - data.Headers = append(data.Headers, subTask.Name) - } else { - data.Headers = append(data.Headers, "output") - } + data.Headers = append(data.Headers, subTask.Name) + reportData.Headers = append(reportData.Headers, subTask.Name) } - // Populate the rows (server name is first cell, then commands and cmd output is set to empty string) for i, p := range servers { - data.Rows = append(data.Rows, dao.Row{Columns: []string{p.Name}}) - + data.Rows = append(data.Rows, dao.Row{Columns: []string{p.Host}}) + reportData.Tasks = append(reportData.Tasks, dao.ReportRow{Name: p.Host, Rows: []dao.Report{}}) for range task.Tasks { data.Rows[i].Columns = append(data.Rows[i].Columns, "") + reportData.Tasks[i].Rows = append(reportData.Tasks[i].Rows, dao.Report{}) + } + } + k := len(servers) + for i, p := range uServers { + reportData.Tasks = append(reportData.Tasks, dao.ReportRow{Name: p.Host, Rows: []dao.Report{}}) + for range task.Tasks { + reportData.Tasks[k+i].Rows = append(reportData.Tasks[k+i].Rows, dao.Report{Status: dao.Unreachable}) } } - var wg sync.WaitGroup - /** - ** Values - **/ - for i := range servers { - wg.Add(1) - if task.Spec.Parallel { - go func(i int, wg *sync.WaitGroup) { - defer wg.Done() - // TODO: Handle errors when running tasks in parallel - _ = run.TableWork(i, dryRun, data, &dataMutex) - }(i, &wg) - } else { - err := func(i int, wg *sync.WaitGroup) error { + var err error + switch task.Spec.Strategy { + case "free": + err = run.free(&run.Config, data, reportData, &dataMutex, dryRun) + case "host_pinned": + err = run.hostPinned(&run.Config, data, reportData, &dataMutex, dryRun) + default: + err = run.linear(&run.Config, data, reportData, &dataMutex, dryRun) + } + + reportData.Status = make(map[dao.TaskStatus]int, 5) + for i := range reportData.Tasks { + reportData.Tasks[i].Status = make(map[dao.TaskStatus]int, 5) + for j := range reportData.Tasks[i].Rows { + if reportData.Tasks[i].Rows[j].Status == dao.Unreachable { + status := reportData.Tasks[i].Rows[j].Status + reportData.Tasks[i].Status[status] = 1 + reportData.Status[status] += 1 + break + } else { + status := reportData.Tasks[i].Rows[j].Status + reportData.Tasks[i].Status[status] += 1 + reportData.Status[status] += 1 + } + } + } + + if err != nil { + switch err := err.(type) { + case *ssh.ExitError: + return data, reportData, &core.ExecError{Err: err, ExitCode: err.ExitStatus()} + case *exec.ExitError: + return data, reportData, &core.ExecError{Err: err, ExitCode: err.ExitCode()} + default: + return data, reportData, err + } + } + + return data, reportData, nil +} + +func (run *Run) free( + config *dao.Config, + data dao.TableOutput, + reportData dao.ReportData, + dataMutex *sync.RWMutex, + dryRun bool, +) error { + serverLen := len(run.Servers) + taskLen := len(run.Task.Tasks) + batch := int(run.Task.Spec.Batch) + maxFailPercentage := run.Task.Spec.MaxFailPercentage + var forks int + if run.Task.Spec.Step { + forks = 1 + } else { + forks = CalcForks(batch, run.Task.Spec.Forks) + } + + register := make(map[string]map[string]string) + var runs []ServerTask + for i := range run.Servers { + register[run.Servers[i].Name] = map[string]string{} + for j := range run.Task.Tasks { + runs = append(runs, ServerTask{ + Server: &run.Servers[i], + Task: run.Task, + Cmd: &run.Task.Tasks[j], + i: i, + j: j, + }) + } + } + + // calculate how many total tasks + quotient, remainder := serverLen/batch, serverLen%batch + + if remainder > 0 { + quotient += 1 + } + + taskContinue := false + failedHosts := make(chan bool, serverLen*taskLen) + var mu sync.Mutex + waitChan := make(chan struct{}, forks) + for k := 0; k < quotient; k++ { + var wg sync.WaitGroup + errCh := make(chan error, batch*taskLen) + + start := k * batch * taskLen + end := start + batch*taskLen + + if end > serverLen*taskLen { + end = start + remainder*taskLen + } + + for i := range runs[start:end] { + wg.Add(1) + go func( + r ServerTask, + register map[string]string, + errCh chan<- error, + failedHosts chan<- bool, + wg *sync.WaitGroup, + ) { defer wg.Done() - err := run.TableWork(i, dryRun, data, &dataMutex) - - return err - }(i, &wg) - - if err != nil && run.Task.Spec.AnyErrorsFatal { - // Return proper exit code for failed tasks - switch err := err.(type) { - case *ssh.ExitError: - return data, &core.ExecError{Err: err, ExitCode: err.ExitStatus()} - case *exec.ExitError: - return data, &core.ExecError{Err: err, ExitCode: err.ExitCode()} - default: - return data, err + waitChan <- struct{}{} + + if run.Task.Spec.Step && !taskContinue { + taskOption, err := StepTaskExecute(r.Cmd.Name, r.Server.Host, &mu) + if err != nil { + errCh <- err + failedHosts <- true + } + switch taskOption { + case Yes: + case No: + <-waitChan + return + case Continue: + taskContinue = true + } } + + err := run.tableWork(r, r.j, register, data, reportData, dataMutex, dryRun) + <-waitChan + if err != nil { + errCh <- err + failedHosts <- true + } + }(runs[start+i], register[runs[start+i].Server.Name], errCh, failedHosts, &wg) + } + + wg.Wait() + + percentageFailed := uint8(math.Floor(float64(len(failedHosts)) / float64(serverLen) * 100)) + if percentageFailed > maxFailPercentage { + close(errCh) + return <-errCh + } + + close(errCh) + } + + return nil +} + +func (run *Run) linear( + config *dao.Config, + data dao.TableOutput, + reportData dao.ReportData, + dataMutex *sync.RWMutex, + dryRun bool, +) error { + serverLen := len(run.Servers) + taskLen := len(run.Task.Tasks) + batch := int(run.Task.Spec.Batch) + var forks int + if run.Task.Spec.Step { + forks = 1 + } else { + forks = CalcForks(batch, run.Task.Spec.Forks) + } + maxFailPercentage := run.Task.Spec.MaxFailPercentage + + register := make(map[string]map[string]string) + for i := range run.Servers { + register[run.Servers[i].Name] = map[string]string{} + } + var runs []ServerTask + for i := range run.Task.Tasks { + for j := range run.Servers { + runs = append(runs, ServerTask{ + Server: &run.Servers[j], + Task: run.Task, + Cmd: &run.Task.Tasks[i], + i: j, + j: i, + }) + } + } + + // calculate how many total tasks + quotient, remainder := serverLen/batch, serverLen%batch + + if remainder > 0 { + quotient += 1 + } + + numFailed := 0 + taskContinue := false + failedHosts := make(map[string]bool, serverLen) + waitChan := make(chan struct{}, forks) + var mu sync.Mutex + for t := 0; t < taskLen; t++ { + var wg sync.WaitGroup + + errCh := make(chan error, serverLen) + + for k := 0; k < quotient; k++ { + failedHostsCh := make(chan struct { + string + bool + }, batch) + + start := t*serverLen + k*batch + end := start + batch + + if end > (t+1)*serverLen { + end = start + remainder + } + + for _, r := range runs[start:end] { + if failedHosts[r.Server.Name] { + continue + } + + waitChan <- struct{}{} + + if run.Task.Spec.Step && !taskContinue { + taskOption, err := StepTaskExecute(run.Task.Tasks[t].Name, r.Server.Host, &mu) + if err != nil { + return err + } + switch taskOption { + case Yes: + case No: + <-waitChan + continue + case Continue: + taskContinue = true + } + } + + wg.Add(1) + + go func( + r ServerTask, + register map[string]string, + errCh chan<- error, + failedHosts chan<- struct { + string + bool + }, + wg *sync.WaitGroup, + ) { + defer wg.Done() + + err := run.tableWork(r, 0, register, data, reportData, dataMutex, dryRun) + <-waitChan + if err != nil { + errCh <- err + failedHostsCh <- struct { + string + bool + }{r.Server.Name, true} + } else { + failedHostsCh <- struct { + string + bool + }{r.Server.Name, false} + } + }(r, register[r.Server.Name], errCh, failedHostsCh, &wg) + } + + wg.Wait() + + close(failedHostsCh) + for p := range failedHostsCh { + failedHosts[p.string] = p.bool + if p.bool { + numFailed += 1 + } + } + + percentageFailed := uint8(math.Floor(float64(numFailed) / float64(serverLen) * 100)) + if percentageFailed > maxFailPercentage { + close(errCh) + return <-errCh } } + close(errCh) } - wg.Wait() - return data, nil + return nil } -func (run *Run) TableWork(rIndex int, dryRun bool, data dao.TableOutput, dataMutex *sync.RWMutex) error { - config := run.Config - task := run.Task - server := run.Servers[rIndex] +func (run *Run) hostPinned( + config *dao.Config, + data dao.TableOutput, + reportData dao.ReportData, + dataMutex *sync.RWMutex, + dryRun bool, +) error { + serverLen := len(run.Servers) + taskLen := len(run.Task.Tasks) + batch := int(run.Task.Spec.Batch) + var forks int + if run.Task.Spec.Step { + forks = 1 + } else { + forks = CalcForks(batch, run.Task.Spec.Forks) + } + maxFailPercentage := run.Task.Spec.MaxFailPercentage + + register := make(map[string]map[string]string) + var runs []ServerTask + for i := range run.Servers { + register[run.Servers[i].Name] = map[string]string{} + for j := range run.Task.Tasks { + runs = append(runs, ServerTask{ + Server: &run.Servers[i], + Task: run.Task, + Cmd: &run.Task.Tasks[j], + i: i, + j: j, + }) + } + } + + // calculate how many total tasks + quotient, remainder := serverLen/batch, serverLen%batch + + if remainder > 0 { + quotient += 1 + } + + taskContinue := false + failedHosts := make(chan bool, serverLen) + waitChan := make(chan struct{}, forks) + var mu sync.Mutex + for k := 0; k < quotient; k++ { + var wg sync.WaitGroup + errCh := make(chan error, batch) + + start := k * batch * taskLen + end := start + batch*taskLen + + if end > serverLen*taskLen { + end = start + remainder*taskLen + } + + for t := start; t < end; t = t + taskLen { + wg.Add(1) + go func( + r []ServerTask, + register map[string]map[string]string, + errCh chan<- error, + failedHosts chan<- bool, + wg *sync.WaitGroup, + ) { + defer wg.Done() + for _, j := range r { + waitChan <- struct{}{} + + if run.Task.Spec.Step && !taskContinue { + taskOption, err := StepTaskExecute(j.Cmd.Name, j.Server.Host, &mu) + if err != nil { + errCh <- err + failedHosts <- true + break + } + switch taskOption { + case Yes: + case No: + <-waitChan + continue + case Continue: + taskContinue = true + } + } + + err := run.tableWork(j, 0, register[j.Server.Name], data, reportData, dataMutex, dryRun) + <-waitChan + if err != nil { + errCh <- err + failedHosts <- true + break + } + } + }(runs[t:t+taskLen], register, errCh, failedHosts, &wg) + } + + wg.Wait() + + percentageFailed := uint8(math.Floor(float64(len(failedHosts)) / float64(serverLen) * 100)) + if percentageFailed > maxFailPercentage { + close(errCh) + return <-errCh + } + + close(errCh) + } + + return nil +} + +func (run *Run) tableWork( + r ServerTask, + si int, + register map[string]string, + data dao.TableOutput, + reportData dao.ReportData, + dataMutex *sync.RWMutex, + dryRun bool, +) error { var wg sync.WaitGroup - for j, cmd := range task.Tasks { - combinedEnvs := dao.MergeEnvs(cmd.Envs, server.Envs) - var client Client - if cmd.Local || server.Local { - client = run.LocalClients[server.Name] + var registerEnvs []string + for k, v := range register { + envStdout := fmt.Sprintf("%v=%v", k, v) + registerEnvs = append(registerEnvs, envStdout) + } + combinedEnvs := dao.MergeEnvs(r.Cmd.Envs, r.Server.Envs, registerEnvs) + var client Client + if r.Cmd.Local || r.Server.Local { + client = run.LocalClients[r.Server.Name] + } else { + client = run.RemoteClients[r.Server.Name] + } + + shell := dao.SelectFirstNonEmpty(r.Task.Shell, r.Server.Shell, run.Config.Shell) + shell = core.FormatShell(shell) + workDir := getWorkDir((*r.Cmd).Local, (*r.Server).Local, (*r.Cmd).WorkDir, (*r.Server).WorkDir, (*r.Cmd).RootDir, (*r.Server).RootDir) + t := TaskContext{ + rIndex: r.i, + cIndex: r.j + 1, // first index (0) is server name + client: client, + dryRun: dryRun, + env: combinedEnvs, + workDir: workDir, + shell: shell, + cmd: r.Cmd.Cmd, + tty: r.Task.TTY, + } + + start := time.Now() + out, stdout, stderr, err := runTableCmd(si, t, &wg) + reportData.Tasks[r.i].Rows[r.j].Duration = time.Since(start) + + var errCode int + switch err := err.(type) { + case *ssh.ExitError: + errCode = err.ExitStatus() + case *exec.ExitError: + errCode = err.ExitCode() + } + + // TODO: Are mutex needed, perhaps if we're writing to the same buffer + // dataMutex.Lock() + // out, err := io.ReadAll(client.Stderr()) + // dataMutex.Unlock() + if err != nil { + data.Rows[t.rIndex].Columns[t.cIndex] = fmt.Sprintf("%s\n%s", out, err.Error()) + } else { + data.Rows[t.rIndex].Columns[t.cIndex] = strings.TrimSuffix(out, "\n") + } + + reportData.Tasks[r.i].Rows[r.j].ReturnCode = errCode + + // TODO: Add skipped env variable + if r.Cmd.Register != "" { + register[r.Cmd.Register] = strings.TrimSuffix(out, "\n") + register[r.Cmd.Register+"_stdout"] = stdout + register[r.Cmd.Register+"_stderr"] = stderr + register[r.Cmd.Register+"_rc"] = fmt.Sprint(reportData.Tasks[t.rIndex].Rows[r.j].ReturnCode) + if err != nil { + register[r.Cmd.Register+"_failed"] = "true" + if r.Task.Spec.IgnoreErrors || r.Cmd.IgnoreErrors { + register[r.Cmd.Register+"_status"] = "ignored" + } else { + register[r.Cmd.Register+"_status"] = "failed" + } + } else { + register[r.Cmd.Register+"_failed"] = "false" + register[r.Cmd.Register+"_status"] = "ok" + } + } + + if err != nil { + if r.Task.Spec.IgnoreErrors || r.Cmd.IgnoreErrors { + reportData.Tasks[r.i].Rows[r.j].Status = dao.Ignored + return nil } else { - client = run.RemoteClients[server.Name] - } - - shell := dao.SelectFirstNonEmpty(cmd.Shell, server.Shell, config.Shell) - shell = core.FormatShell(shell) - workDir := getWorkDir(cmd, server) - tableCmd := TaskContext{ - rIndex: rIndex, - cIndex: j + 1, - client: client, - dryRun: dryRun, - env: combinedEnvs, - workDir: workDir, - shell: shell, - cmd: cmd.Cmd, - tty: cmd.TTY, - } - - err := RunTableCmd(tableCmd, data, dataMutex, &wg) - if !task.Spec.IgnoreErrors && err != nil { + reportData.Tasks[r.i].Rows[r.j].Status = dao.Failed return err } } - wg.Wait() + reportData.Tasks[r.i].Rows[r.j].Status = dao.Ok return nil } -func RunTableCmd(t TaskContext, data dao.TableOutput, dataMutex *sync.RWMutex, wg *sync.WaitGroup) error { +func runTableCmd(i int, t TaskContext, wg *sync.WaitGroup) (string, string, string, error) { + buf := new(bytes.Buffer) + bufOut := new(bytes.Buffer) + bufErr := new(bytes.Buffer) + if t.dryRun { - data.Rows[t.rIndex].Columns[t.cIndex] = t.cmd - return nil + return t.cmd, bufOut.String(), bufErr.String(), nil } if t.tty { - return ExecTTY(t.cmd, t.env) + return buf.String(), bufOut.String(), bufErr.String(), ExecTTY(t.cmd, t.env) } - err := t.client.Run(t.env, t.workDir, t.shell, t.cmd) + err := t.client.Run(i, t.env, t.workDir, t.shell, t.cmd) if err != nil { - return err + return buf.String(), bufOut.String(), bufErr.String(), err } // Copy over commands STDOUT. - var stdoutHandler = func(client Client) { + var stdoutHandler = func(i int, client Client) { defer wg.Done() - dataMutex.Lock() - out, err := ioutil.ReadAll(client.Stdout()) - - data.Rows[t.rIndex].Columns[t.cIndex] = fmt.Sprintf("%s%s", data.Rows[t.rIndex].Columns[t.cIndex], strings.TrimSuffix(string(out), "\n")) - dataMutex.Unlock() + mw := io.MultiWriter(buf, bufOut) + _, err = io.Copy(mw, client.Stdout(i)) if err != nil && err != io.EOF { fmt.Fprintf(os.Stderr, "%v", err) } } wg.Add(1) - go stdoutHandler(t.client) + go stdoutHandler(i, t.client) // Copy over tasks's STDERR. - var stderrHandler = func(client Client) { + var stderrHandler = func(i int, client Client) { defer wg.Done() - dataMutex.Lock() - out, err := ioutil.ReadAll(client.Stderr()) - data.Rows[t.rIndex].Columns[t.cIndex] = fmt.Sprintf("%s%s", data.Rows[t.rIndex].Columns[t.cIndex], strings.TrimSuffix(string(out), "\n")) - dataMutex.Unlock() + mw := io.MultiWriter(buf, bufErr) + _, err = io.Copy(mw, client.Stderr(i)) if err != nil && err != io.EOF { fmt.Fprintf(os.Stderr, "%v", err) } } wg.Add(1) - go stderrHandler(t.client) + go stderrHandler(i, t.client) wg.Wait() - if err := t.client.Wait(); err != nil { - data.Rows[t.rIndex].Columns[t.cIndex] = fmt.Sprintf("%s\n%s", data.Rows[t.rIndex].Columns[t.cIndex], err.Error()) - return err + if err := t.client.Wait(i); err != nil { + return buf.String(), bufOut.String(), bufErr.String(), err } - return nil + return buf.String(), bufOut.String(), bufErr.String(), nil } diff --git a/core/run/text.go b/core/run/text.go index bf3563f..b4efa41 100644 --- a/core/run/text.go +++ b/core/run/text.go @@ -9,150 +9,623 @@ import ( "golang.org/x/exp/slices" "golang.org/x/term" "io" + "math" "os" "os/exec" "strings" "sync" "text/template" + "time" "github.com/alajmo/sake/core" "github.com/alajmo/sake/core/dao" "github.com/alajmo/sake/core/print" ) -func (run *Run) Text(dryRun bool) error { +func (run *Run) Text(dryRun bool) (dao.ReportData, error) { + task := run.Task servers := run.Servers + uServers := run.UnreachableServers + prefixMaxLen := calcMaxPrefixLength(run.LocalClients) - var wg sync.WaitGroup - for i := range servers { - wg.Add(1) + // TODO: reportData should be pointer? + var reportData dao.ReportData + var dataMutex = sync.RWMutex{} + reportData.Headers = append(reportData.Headers, "server") + // Append Command names if set + for _, subTask := range task.Tasks { + reportData.Headers = append(reportData.Headers, subTask.Name) + } + // Populate the rows (server name is first cell, then commands and cmd output is set to empty string) + for i, p := range servers { + reportData.Tasks = append(reportData.Tasks, dao.ReportRow{Name: p.Host, Rows: []dao.Report{}}) + for range task.Tasks { + reportData.Tasks[i].Rows = append(reportData.Tasks[i].Rows, dao.Report{}) + } + } - if run.Task.Spec.Parallel { - go func(i int, wg *sync.WaitGroup) { - defer wg.Done() - // TODO: Handle errors when running tasks in parallel - _ = run.TextWork(i, prefixMaxLen, dryRun) - }(i, &wg) - } else { - err := func(i int, wg *sync.WaitGroup) error { + k := len(servers) + for i, p := range uServers { + reportData.Tasks = append(reportData.Tasks, dao.ReportRow{Name: p.Host, Rows: []dao.Report{}}) + for range task.Tasks { + reportData.Tasks[k+i].Rows = append(reportData.Tasks[k+i].Rows, dao.Report{Status: dao.Unreachable}) + } + } + + var err error + switch task.Spec.Strategy { + case "free": + err = run.freeText(&run.Config, prefixMaxLen, reportData, &dataMutex, dryRun) + case "host_pinned": + err = run.hostPinnedText(&run.Config, prefixMaxLen, reportData, &dataMutex, dryRun) + default: // linear + err = run.linearText(&run.Config, prefixMaxLen, reportData, &dataMutex, dryRun) + } + + reportData.Status = make(map[dao.TaskStatus]int, 5) + for i := range reportData.Tasks { + reportData.Tasks[i].Status = make(map[dao.TaskStatus]int, 5) + for j := range reportData.Tasks[i].Rows { + if reportData.Tasks[i].Rows[j].Status == dao.Unreachable { + status := reportData.Tasks[i].Rows[j].Status + reportData.Tasks[i].Status[status] = 1 + reportData.Status[status] += 1 + break + } else { + status := reportData.Tasks[i].Rows[j].Status + reportData.Tasks[i].Status[status] += 1 + reportData.Status[status] += 1 + } + } + } + + if err != nil && run.Task.Spec.AnyErrorsFatal { + switch err := err.(type) { + case *ssh.ExitError: + return reportData, &core.ExecError{Err: err, ExitCode: err.ExitStatus()} + case *exec.ExitError: + return reportData, &core.ExecError{Err: err, ExitCode: err.ExitCode()} + default: + return reportData, err + } + } + + return reportData, nil +} + +func (run *Run) freeText( + config *dao.Config, + prefixMaxLen int, + reportData dao.ReportData, + dataMutex *sync.RWMutex, + dryRun bool, +) error { + serverLen := len(run.Servers) + taskLen := len(run.Task.Tasks) + batch := int(run.Task.Spec.Batch) + maxFailPercentage := run.Task.Spec.MaxFailPercentage + var forks int + if run.Task.Spec.Step { + forks = 1 + } else { + forks = CalcForks(batch, run.Task.Spec.Forks) + } + + register := make(map[string]map[string]string) + var runs []ServerTask + for i := range run.Servers { + register[run.Servers[i].Name] = map[string]string{} + for j := range run.Task.Tasks { + runs = append(runs, ServerTask{ + Server: &run.Servers[i], + Task: run.Task, + Cmd: &run.Task.Tasks[j], + i: i, + j: j, + }) + } + } + + // calculate how many total tasks + quotient, remainder := serverLen/batch, serverLen%batch + + if remainder > 0 { + quotient += 1 + } + + taskContinue := false + failedHosts := make(chan bool, serverLen*taskLen) + var mu sync.Mutex + waitChan := make(chan struct{}, forks) + for k := 0; k < quotient; k++ { + var wg sync.WaitGroup + errCh := make(chan error, batch*taskLen) + + start := k * batch * taskLen + end := start + batch*taskLen + + if end > serverLen*taskLen { + end = start + remainder*taskLen + } + + // For each server task + for i := range runs[start:end] { + wg.Add(1) + + go func( + r ServerTask, + register map[string]string, + errCh chan<- error, + failedHosts chan<- bool, + wg *sync.WaitGroup, + ) { defer wg.Done() - err := run.TextWork(i, prefixMaxLen, dryRun) - return err - }(i, &wg) + waitChan <- struct{}{} + + if run.Task.Spec.Step && !taskContinue { + taskOption, err := StepTaskExecute(r.Cmd.Name, r.Server.Host, &mu) + if err != nil { + errCh <- err + failedHosts <- true + } + switch taskOption { + case Yes: + case No: + <-waitChan + return + case Continue: + taskContinue = true + } + } + err := run.textWork(r, r.j, register, prefixMaxLen, reportData, dataMutex, dryRun, batch) + <-waitChan + if err != nil { + errCh <- err + failedHosts <- true + } + }(runs[start+i], register[runs[start+i].Server.Name], errCh, failedHosts, &wg) + } + + wg.Wait() + + percentageFailed := uint8(math.Floor(float64(len(failedHosts)) / float64(serverLen) * 100)) + if percentageFailed > maxFailPercentage { + close(errCh) + return <-errCh + } + + close(errCh) + } + + return nil +} + +func (run *Run) linearText( + config *dao.Config, + prefixMaxLen int, + reportData dao.ReportData, + dataMutex *sync.RWMutex, + dryRun bool, +) error { + serverLen := len(run.Servers) + taskLen := len(run.Task.Tasks) + batch := int(run.Task.Spec.Batch) + var forks int + if run.Task.Spec.Step { + forks = 1 + } else { + forks = CalcForks(batch, run.Task.Spec.Forks) + } + maxFailPercentage := run.Task.Spec.MaxFailPercentage + + register := make(map[string]map[string]string) + for i := range run.Servers { + register[run.Servers[i].Name] = map[string]string{} + } + var runs []ServerTask + for i := range run.Task.Tasks { + for j := range run.Servers { + runs = append(runs, ServerTask{ + Server: &run.Servers[j], + Task: run.Task, + Cmd: &run.Task.Tasks[i], + i: j, + j: i, + }) + } + } + + // calculate how many total tasks + quotient, remainder := serverLen/batch, serverLen%batch + + if remainder > 0 { + quotient += 1 + } + numFailed := 0 + taskContinue := false + failedHosts := make(map[string]bool, serverLen) + waitChan := make(chan struct{}, forks) + var mu sync.Mutex + for t := 0; t < taskLen; t++ { + var wg sync.WaitGroup + + errCh := make(chan error, serverLen) + + if run.Task.Theme.Text.Header != "" { + if t > 0 { + fmt.Println() + } + err := printTaskHeader(t, taskLen, run.Task.Tasks[t].Name, run.Task.Tasks[t].Desc, run.Task.Theme.Text) if err != nil { - switch err.(type) { - case *template.ExecError: - return err - case *core.TemplateParseError: - return err - default: - if run.Task.Spec.AnyErrorsFatal { - // Return proper exit code for failed tasks - switch err := err.(type) { - case *ssh.ExitError: - return &core.ExecError{Err: err, ExitCode: err.ExitStatus()} - case *exec.ExitError: - return &core.ExecError{Err: err, ExitCode: err.ExitCode()} - default: - return err - } + return err + } + fmt.Println() + } + + // Per batch + for k := 0; k < quotient; k++ { + failedHostsCh := make(chan struct { + string + bool + }, batch) + + start := t*serverLen + k*batch + end := start + batch + + if end > (t+1)*serverLen { + end = start + remainder + } + + // Per task + for _, r := range runs[start:end] { + if failedHosts[r.Server.Name] { + continue + } + + waitChan <- struct{}{} + + if run.Task.Spec.Step && !taskContinue { + taskOption, err := StepTaskExecute(run.Task.Tasks[t].Name, r.Server.Host, &mu) + if err != nil { + return err } + + switch taskOption { + case Yes: + case No: + <-waitChan + continue + case Continue: + taskContinue = true + } + } + + wg.Add(1) + + go func( + r ServerTask, + register map[string]string, + errCh chan<- error, + failedHosts chan<- struct { + string + bool + }, + wg *sync.WaitGroup, + ) { + defer wg.Done() + + err := run.textWork(r, 0, register, prefixMaxLen, reportData, dataMutex, dryRun, batch) + <-waitChan + if err != nil { + errCh <- err + failedHostsCh <- struct { + string + bool + }{r.Server.Name, true} + } else { + failedHostsCh <- struct { + string + bool + }{r.Server.Name, false} + } + }(r, register[r.Server.Name], errCh, failedHostsCh, &wg) + } + + wg.Wait() + + close(failedHostsCh) + for p := range failedHostsCh { + failedHosts[p.string] = p.bool + if p.bool { + numFailed += 1 } } + + percentageFailed := uint8(math.Floor(float64(numFailed) / float64(serverLen) * 100)) + if percentageFailed > maxFailPercentage { + close(errCh) + return <-errCh + } } - } - wg.Wait() + close(errCh) + } return nil } -func (run *Run) TextWork(rIndex int, prefixMaxLen int, dryRun bool) error { - config := run.Config - task := run.Task - server := run.Servers[rIndex] - prefix := getPrefixer(run.LocalClients[server.Name], rIndex, prefixMaxLen, task.Theme.Text, task.Spec.Parallel) +func (run *Run) hostPinnedText( + config *dao.Config, + prefixMaxLen int, + reportData dao.ReportData, + dataMutex *sync.RWMutex, + dryRun bool, +) error { + serverLen := len(run.Servers) + taskLen := len(run.Task.Tasks) + batch := int(run.Task.Spec.Batch) + var forks int + if run.Task.Spec.Step { + forks = 1 + } else { + forks = CalcForks(batch, run.Task.Spec.Forks) + } + maxFailPercentage := run.Task.Spec.MaxFailPercentage + + register := make(map[string]map[string]string) + var runs []ServerTask + for i := range run.Servers { + register[run.Servers[i].Name] = map[string]string{} + for j := range run.Task.Tasks { + runs = append(runs, ServerTask{ + Server: &run.Servers[i], + Task: run.Task, + Cmd: &run.Task.Tasks[j], + i: i, + j: j, + }) + } + } - numTasks := len(task.Tasks) + // calculate how many total tasks + quotient, remainder := serverLen/batch, serverLen%batch - var wg sync.WaitGroup - for j, cmd := range task.Tasks { - var client Client - combinedEnvs := dao.MergeEnvs(cmd.Envs, server.Envs) - if cmd.Local || server.Local { - client = run.LocalClients[server.Name] - } else { - client = run.RemoteClients[server.Name] + if remainder > 0 { + quotient += 1 + } + + failedHosts := make(chan bool, serverLen) + taskContinue := false + waitChan := make(chan struct{}, forks) + var mu sync.Mutex + // Per batch + for k := 0; k < quotient; k++ { + var wg sync.WaitGroup + errCh := make(chan error, batch) + + start := k * batch * taskLen + end := start + batch*taskLen + + if end > serverLen*taskLen { + end = start + remainder*taskLen } - shell := dao.SelectFirstNonEmpty(cmd.Shell, server.Shell, config.Shell) - shell = core.FormatShell(shell) - workDir := getWorkDir(cmd, server) - args := TaskContext{ - rIndex: rIndex, - cIndex: j, - client: client, - dryRun: dryRun, - env: combinedEnvs, - workDir: workDir, - shell: shell, - cmd: cmd.Cmd, - desc: cmd.Desc, - name: cmd.Name, - numTasks: numTasks, - tty: cmd.TTY, + // Per server + for t := start; t < end; t = t + taskLen { + wg.Add(1) + go func( + r []ServerTask, + register map[string]map[string]string, + errCh chan<- error, + failedHosts chan<- bool, + wg *sync.WaitGroup, + ) { + defer wg.Done() + for i, j := range r { + waitChan <- struct{}{} + + if run.Task.Spec.Step && !taskContinue { + taskOption, err := StepTaskExecute(j.Cmd.Name, j.Server.Host, &mu) + if err != nil { + <-waitChan + errCh <- err + failedHosts <- true + break + } + switch taskOption { + case Yes: + case No: + <-waitChan + continue + case Continue: + taskContinue = true + } + } + + if run.Task.Theme.Text.Header != "" && batch == 1 { + fmt.Println() + err := printTaskHeader(i, taskLen, j.Cmd.Name, j.Cmd.Desc, run.Task.Theme.Text) + fmt.Println() + if err != nil { + <-waitChan + errCh <- err + failedHosts <- true + break + } + } + + err := run.textWork(j, 0, register[j.Server.Name], prefixMaxLen, reportData, dataMutex, dryRun, batch) + <-waitChan + if err != nil { + errCh <- err + failedHosts <- true + break + } + } + }(runs[t:t+taskLen], register, errCh, failedHosts, &wg) } - err := RunTextCmd(args, task.Theme.Text, prefix, task.Spec.Parallel, &wg) - switch err.(type) { - case *template.ExecError: - return err - case *core.TemplateParseError: - return err - default: - if !task.Spec.IgnoreErrors && err != nil { - return err - } + wg.Wait() + + percentageFailed := uint8(math.Floor(float64(len(failedHosts)) / float64(serverLen) * 100)) + if percentageFailed > maxFailPercentage { + close(errCh) + return <-errCh } - } - wg.Wait() + close(errCh) + } return nil } -func RunTextCmd(t TaskContext, textStyle dao.Text, prefix string, parallel bool, wg *sync.WaitGroup) error { - if textStyle.Header != "" && !parallel { - err := printHeader(t.cIndex, t.numTasks, t.name, t.desc, textStyle) +func (run *Run) textWork( + r ServerTask, + si int, + register map[string]string, + prefixMaxLen int, + reportData dao.ReportData, + dataMutex *sync.RWMutex, + dryRun bool, + batch int, +) error { + prefix := getPrefixer(run.LocalClients[r.Server.Name], r.i, prefixMaxLen, r.Task.Theme.Text, batch) + + numTasks := len(r.Task.Tasks) + + var registerEnvs []string + for k, v := range register { + envStdout := fmt.Sprintf("%v=%v", k, v) + registerEnvs = append(registerEnvs, envStdout) + } + combinedEnvs := dao.MergeEnvs(r.Cmd.Envs, r.Server.Envs, registerEnvs) + var client Client + if r.Cmd.Local || r.Server.Local { + client = run.LocalClients[r.Server.Name] + } else { + client = run.RemoteClients[r.Server.Name] + } + + shell := dao.SelectFirstNonEmpty(r.Task.Shell, r.Server.Shell, run.Config.Shell) + shell = core.FormatShell(shell) + workDir := getWorkDir((*r.Cmd).Local, (*r.Server).Local, (*r.Cmd).WorkDir, (*r.Server).WorkDir, (*r.Cmd).RootDir, (*r.Server).RootDir) + t := TaskContext{ + rIndex: r.i, + cIndex: r.j, + client: client, + dryRun: dryRun, + env: combinedEnvs, + workDir: workDir, + shell: shell, + cmd: r.Cmd.Cmd, + desc: r.Cmd.Desc, + name: r.Cmd.Name, + numTasks: numTasks, + tty: r.Cmd.TTY, + } + + start := time.Now() + var wg sync.WaitGroup + out, stdout, stderr, err := runTextCmd(si, t, r.Task.Theme.Text, prefix, r.Cmd.Register, &wg) + reportData.Tasks[r.i].Rows[r.j].Duration = time.Since(start) + + // Add exit code to reportData + var errCode int + switch err := err.(type) { + case *ssh.ExitError: + errCode = err.ExitStatus() + case *exec.ExitError: + errCode = err.ExitCode() + case *template.ExecError: + return err + case *core.TemplateParseError: + return err + } + + reportData.Tasks[r.i].Rows[r.j].ReturnCode = errCode + + // TODO: Add skipped env variable + if r.Cmd.Register != "" { + register[r.Cmd.Register] = strings.TrimSuffix(out, "\n") + register[r.Cmd.Register+"_stdout"] = stdout + register[r.Cmd.Register+"_stderr"] = stderr + register[r.Cmd.Register+"_rc"] = fmt.Sprint(reportData.Tasks[t.rIndex].Rows[r.j].ReturnCode) if err != nil { + register[r.Cmd.Register+"_failed"] = "true" + if r.Task.Spec.IgnoreErrors || r.Cmd.IgnoreErrors { + register[r.Cmd.Register+"_status"] = "ignored" + } else { + register[r.Cmd.Register+"_status"] = "failed" + } + } else { + register[r.Cmd.Register+"_failed"] = "false" + register[r.Cmd.Register+"_status"] = "ok" + } + } + + if err != nil { + if r.Task.Spec.IgnoreErrors || r.Cmd.IgnoreErrors { + reportData.Tasks[r.i].Rows[r.j].Status = dao.Ignored + return nil + } else { + reportData.Tasks[r.i].Rows[r.j].Status = dao.Failed return err } } + reportData.Tasks[r.i].Rows[r.j].Status = dao.Ok + + return nil +} + +func runTextCmd( + i int, + t TaskContext, + textStyle dao.Text, + prefix string, + register string, + wg *sync.WaitGroup, +) (string, string, string, error) { + buf := new(bytes.Buffer) + bufOut := new(bytes.Buffer) + bufErr := new(bytes.Buffer) + if t.dryRun { printCmd(prefix, t.cmd) - return nil + return buf.String(), bufOut.String(), bufErr.String(), nil } if t.tty { - return ExecTTY(t.cmd, t.env) + return buf.String(), bufOut.String(), bufErr.String(), ExecTTY(t.cmd, t.env) } - err := t.client.Run(t.env, t.workDir, t.shell, t.cmd) + err := t.client.Run(i, t.env, t.workDir, t.shell, t.cmd) if err != nil { - return err + return buf.String(), bufOut.String(), bufErr.String(), err } // Copy over commands STDOUT. go func(client Client) { defer wg.Done() var err error - if prefix != "" { - _, err = io.Copy(os.Stdout, core.NewPrefixer(client.Stdout(), prefix)) + + if register == "" { + if prefix != "" { + _, err = io.Copy(os.Stdout, core.NewPrefixer(client.Stdout(i), prefix)) + } else { + _, err = io.Copy(os.Stdout, client.Stdout(i)) + } } else { - _, err = io.Copy(os.Stdout, client.Stdout()) + mw := io.MultiWriter(buf, bufOut) + r := io.TeeReader(client.Stdout(i), mw) + // TODO: Refactor to NewReader: https://pkg.go.dev/golang.org/x/text/transform?utm_source=godoc#NewReader + if prefix != "" { + _, err = io.Copy(os.Stdout, core.NewPrefixer(r, prefix)) + } else { + _, err = io.Copy(os.Stdout, r) + } } if err != nil && err != io.EOF { @@ -165,11 +638,23 @@ func RunTextCmd(t TaskContext, textStyle dao.Text, prefix string, parallel bool, go func(client Client) { defer wg.Done() var err error - if prefix != "" { - _, err = io.Copy(os.Stderr, core.NewPrefixer(client.Stderr(), prefix)) + + if register == "" { + if prefix != "" { + _, err = io.Copy(os.Stderr, core.NewPrefixer(client.Stderr(i), prefix)) + } else { + _, err = io.Copy(os.Stderr, client.Stderr(i)) + } } else { - _, err = io.Copy(os.Stderr, client.Stderr()) + mw := io.MultiWriter(buf, bufErr) + r := io.TeeReader(client.Stderr(i), mw) + if prefix != "" { + _, err = io.Copy(os.Stderr, core.NewPrefixer(r, prefix)) + } else { + _, err = io.Copy(os.Stderr, r) + } } + if err != nil && err != io.EOF { fmt.Fprintf(os.Stderr, "%v", err) } @@ -178,20 +663,20 @@ func RunTextCmd(t TaskContext, textStyle dao.Text, prefix string, parallel bool, wg.Wait() - if err := t.client.Wait(); err != nil { + if err := t.client.Wait(i); err != nil { if prefix != "" { fmt.Printf("%s%s\n", prefix, err.Error()) } else { fmt.Printf("%s\n", err.Error()) } - return err + return buf.String(), bufOut.String(), bufErr.String(), err } - return nil + return buf.String(), bufOut.String(), bufErr.String(), nil } -func HeaderTemplate(header string, data HeaderData) (string, error) { +func headerTemplate(header string, data HeaderData) (string, error) { tmpl, err := template.New("header.tmpl").Parse(header) if err != nil { return "", &core.TemplateParseError{Msg: err.Error()} @@ -236,14 +721,32 @@ func (h HeaderData) Style(s any, args ...string) string { return colors.Sprintf(v) } -func printHeader(i int, numTasks int, name string, desc string, ts dao.Text) error { +func PrintHeader(value string, ts dao.Text, padding bool) { + width, _, _ := term.GetSize(0) + headerLength := len(core.Strip(value)) + headerName := text.Colors{text.Reset, text.Bold} + var header string + if ts.HeaderFiller != "" { + header = fmt.Sprintf("\n%s%s\n", headerName.Sprintf(value), strings.Repeat(ts.HeaderFiller, width-headerLength-1)) + } else { + header = fmt.Sprintf("\n%s\n", headerName.Sprintf(value)) + } + + if padding { + fmt.Println(header) + } else { + fmt.Printf("%s", header) + } +} + +func printTaskHeader(i int, numTasks int, name string, desc string, ts dao.Text) error { data := HeaderData{ Name: name, Desc: desc, Index: i + 1, NumTasks: numTasks, } - header, err := HeaderTemplate(ts.Header, data) + header, err := headerTemplate(ts.Header, data) if err != nil { return err } @@ -251,9 +754,7 @@ func printHeader(i int, numTasks int, name string, desc string, ts dao.Text) err width, _, _ := term.GetSize(0) headerLength := len(core.Strip(header)) if width > 0 && ts.HeaderFiller != "" { - header = fmt.Sprintf("\n%s%s\n", header, strings.Repeat(ts.HeaderFiller, width-headerLength-1)) - } else { - header = fmt.Sprintf("\n%s\n", header) + header = fmt.Sprintf("%s%s", header, strings.Repeat(ts.HeaderFiller, width-headerLength-1)) } fmt.Println(header) @@ -261,7 +762,7 @@ func printHeader(i int, numTasks int, name string, desc string, ts dao.Text) err return nil } -func getPrefixer(client Client, i, prefixMaxLen int, textStyle dao.Text, parallel bool) string { +func getPrefixer(client Client, i, prefixMaxLen int, textStyle dao.Text, batch int) string { if !textStyle.Prefix { return "" } @@ -275,20 +776,19 @@ func getPrefixer(client Client, i, prefixMaxLen int, textStyle dao.Text, paralle prefixColor = print.GetFg(textStyle.PrefixColors[i%len(textStyle.PrefixColors)]) } - if (textStyle.Header == "" || parallel) && len(prefix) < prefixMaxLen { // Left padding. - prefixString := prefix + strings.Repeat(" ", prefixMaxLen-prefixLen) + " | " - if prefixColor != nil { - prefix = prefixColor.Sprintf(prefixString) - } else { - prefix = prefixString - } + // When batch = 1 correctly align the prefix to current host + // When batch > 1 correctly align the prefix to the largest host + var prefixString string + if batch > 1 && len(prefix) < prefixMaxLen { // Left padding. + prefixString = prefix + strings.Repeat(" ", prefixMaxLen-prefixLen) + " | " } else { - prefixString := prefix + " | " - if prefixColor != nil { - prefix = prefixColor.Sprintf(prefixString) - } else { - prefix = prefixString - } + prefixString = prefix + " | " + } + + if prefixColor != nil { + prefix = prefixColor.Sprintf(prefixString) + } else { + prefix = prefixString } return prefix diff --git a/core/run/unix.go b/core/run/unix.go index fddb48a..72832dd 100644 --- a/core/run/unix.go +++ b/core/run/unix.go @@ -49,6 +49,7 @@ func ExecTTY(cmd string, envs []string) error { } userEnv := append(os.Environ(), envs...) + // TODO: default shell err = unix.Exec(execBin, []string{"bash", "-c", cmd}, userEnv) if err != nil { return err diff --git a/core/sake.1 b/core/sake.1 index 950517f..d338756 100644 --- a/core/sake.1 +++ b/core/sake.1 @@ -1,12 +1,12 @@ -.TH "SAKE" "1" "2022-10-16T08:03:53CEST" "v0.12.1" "Sake Manual" "sake" +.TH "SAKE" "1" "2022-12-04T21:04:49CET" "v0.13.0" "Sake Manual" "sake" .SH NAME -sake - sake is a command runner for local and remote hosts +sake - sake is a task runner for local and remote hosts .SH SYNOPSIS .B sake [command] [flags] .SH DESCRIPTION -sake is a command runner for local and remote hosts. +sake is a task runner for local and remote hosts. You define servers and tasks in a sake.yaml config file and then run the tasks on the servers. @@ -16,17 +16,17 @@ You define servers and tasks in a sake.yaml config file and then run the tasks o \fB-c, --config=""\fR specify config .TP -\fB-h, --help[=false]\fR -help for sake +\fB-u, --user-config=""\fR +specify user config +.TP +\fB--ssh-config=""\fR +specify ssh config .TP \fB--no-color[=false]\fR disable color .TP -\fB-U, --ssh-config=""\fR -specify ssh config -.TP -\fB-u, --user-config=""\fR -specify user config +\fB-h, --help[=false]\fR +help for sake .SH COMMANDS .TP @@ -42,38 +42,50 @@ Run tasks specified in a sake.yaml file. .RS .RS .TP -\fB-a, --all[=false]\fR -target all servers -.TP -\fB--any-errors-fatal[=false]\fR -stop task execution on all servers on error -.TP -\fB--attach[=false]\fR -ssh to server after command +\fB--dry-run[=false]\fR +print the task to see what will be executed .TP \fB--describe[=false]\fR print task information .TP -\fB--dry-run[=false]\fR -print the task to see what will be executed +\fB--list-hosts[=false]\fR +print hosts that will be targetted .TP -\fB-e, --edit[=false]\fR -edit task +\fB-V, --verbose[=false]\fR +enable all diagnostics .TP -\fB-i, --identity-file=""\fR -set identity file for all servers +\fB-S, --strategy=""\fR +set execution strategy [linear|host_pinned|free] .TP -\fB--ignore-errors[=false]\fR -continue task execution on errors +\fB-f, --forks=10000\fR +max number of concurrent processes .TP -\fB--ignore-unreachable[=false]\fR -ignore unreachable hosts +\fB-b, --batch=0\fR +set number of hosts to run in parallel +.TP +\fB-B, --batch-p=0\fR +set percentage of hosts to run in parallel [0-100] +.TP +\fB-a, --all[=false]\fR +target all hosts .TP \fB-v, --invert[=false]\fR -invert matching on servers +invert matching on hosts .TP -\fB--known-hosts-file=""\fR -set known hosts file +\fB-r, --regex=""\fR +target hosts on host regex +.TP +\fB-s, --servers=[]\fR +target servers by names +.TP +\fB-t, --tags=[]\fR +target hosts by tags +.TP +\fB-T, --target=""\fR +target hosts by target name +.TP +\fB--order=""\fR +order hosts .TP \fB-l, --limit=0\fR set limit of servers to target @@ -81,38 +93,68 @@ set limit of servers to target \fB-L, --limit-p=0\fR set percentage of servers to target [0-100] .TP -\fB--local[=false]\fR -run task on localhost +\fB--ignore-unreachable[=false]\fR +ignore unreachable hosts .TP -\fB--omit-empty[=false]\fR -omit empty results for table output +\fB-M, --max-fail-percentage=0\fR +stop task execution on all servers when threshold reached .TP -\fB-o, --output=""\fR -set task output [text|table|table-2|table-3|table-4|html|markdown] +\fB--any-errors-fatal[=false]\fR +stop task execution on all servers on error .TP -\fB-p, --parallel[=false]\fR -run server tasks in parallel +\fB--ignore-errors[=false]\fR +continue task execution on errors .TP -\fB--password=""\fR -set ssh password for all servers +\fB-J, --spec=""\fR +set spec .TP -\fB-r, --regex=""\fR -filter servers on host regex +\fB-o, --output=""\fR +set task output [text|table|table-2|table-3|table-4|html|markdown|json|csv|none] .TP -\fB-s, --servers=[]\fR -target servers by names +\fB--omit-empty-rows[=false]\fR +omit empty row for table output +.TP +\fB--omit-empty-columns[=false]\fR +omit empty column for table output .TP -\fB-S, --silent[=false]\fR +\fB-q, --silent[=false]\fR omit showing loader when running tasks .TP -\fB-t, --tags=[]\fR -target servers by tags +\fB--confirm[=false]\fR +confirm root task before running .TP -\fB--theme=""\fR -set theme +\fB--step[=false]\fR +confirm each task before running .TP \fB--tty[=false]\fR replace the current process +.TP +\fB--attach[=false]\fR +ssh to server after command +.TP +\fB--local[=false]\fR +run task on localhost +.TP +\fB--theme="default"\fR +set theme +.TP +\fB-e, --edit[=false]\fR +edit task +.TP +\fB-R, --report=[recap]\fR +reports to show +.TP +\fB-i, --identity-file=""\fR +set identity file +.TP +\fB-U, --user=""\fR +set ssh user +.TP +\fB--password=""\fR +set ssh password +.TP +\fB--known-hosts-file=""\fR +set known hosts file .RE .RE .TP @@ -128,32 +170,50 @@ before the command gets executed in each directory. .RS .RS .TP -\fB-a, --all[=false]\fR -target all servers +\fB--dry-run[=false]\fR +prints the command to see what will be executed .TP -\fB--any-errors-fatal[=false]\fR -stop task execution on all servers on error +\fB--describe[=false]\fR +print task information .TP -\fB--attach[=false]\fR -ssh to server after command +\fB--list-hosts[=false]\fR +print hosts that will be targetted .TP -\fB--dry-run[=false]\fR -prints the command to see what will be executed +\fB-V, --verbose[=false]\fR +enable all diagnostics .TP -\fB-i, --identity-file=""\fR -set identity file for all servers +\fB-S, --strategy=""\fR +set execution strategy [linear|host_pinned|free] .TP -\fB--ignore-errors[=false]\fR -continue task execution on errors +\fB-f, --forks=10000\fR +max number of concurrent processes .TP -\fB--ignore-unreachable[=false]\fR -ignore unreachable hosts +\fB-b, --batch=0\fR +set number of hosts to run in parallel +.TP +\fB-B, --batch-p=0\fR +set percentage of servers to run in parallel [0-100] +.TP +\fB-a, --all[=false]\fR +target all servers .TP \fB-v, --invert[=false]\fR invert matching on servers .TP -\fB--known-hosts-file=""\fR -set known hosts file +\fB-r, --regex=""\fR +filter servers on host regex +.TP +\fB-s, --servers=[]\fR +target servers by names +.TP +\fB-t, --tags=[]\fR +target servers by tags +.TP +\fB-T, --target=""\fR +target servers by target name +.TP +\fB--order=""\fR +order hosts .TP \fB-l, --limit=0\fR set limit of servers to target @@ -161,38 +221,65 @@ set limit of servers to target \fB-L, --limit-p=0\fR set percentage of servers to target .TP -\fB--local[=false]\fR -run command on localhost +\fB--ignore-unreachable[=false]\fR +ignore unreachable hosts .TP -\fB--omit-empty[=false]\fR -omit empty results for table output +\fB-M, --max-fail-percentage=0\fR +stop task execution on all servers when threshold reached .TP -\fB-o, --output=""\fR -set task output [text|table|table-2|table-3|table-4|html|markdown] +\fB--any-errors-fatal[=false]\fR +stop task execution on all servers on error .TP -\fB-p, --parallel[=false]\fR -run server tasks in parallel +\fB--ignore-errors[=false]\fR +continue task execution on errors .TP -\fB--password=""\fR -set ssh password for all servers +\fB-J, --spec=""\fR +set spec .TP -\fB-r, --regex=""\fR -filter servers on host regex +\fB-o, --output=""\fR +set task output [text|table|table-2|table-3|table-4|html|markdown|json|csv|none] .TP -\fB-s, --servers=[]\fR -target servers by names +\fB--omit-empty-rows[=false]\fR +omit empty row for table output .TP -\fB-S, --silent[=false]\fR +\fB--omit-empty-columns[=false]\fR +omit empty column for table output +.TP +\fB-q, --silent[=false]\fR omit showing loader when running tasks .TP -\fB-t, --tags=[]\fR -target servers by tags +\fB--confirm[=false]\fR +confirm root task before running .TP -\fB--theme="default"\fR -set theme +\fB--step[=false]\fR +confirm each task before running .TP \fB--tty[=false]\fR replace the current process +.TP +\fB--attach[=false]\fR +ssh to server after command +.TP +\fB--local[=false]\fR +run command on localhost +.TP +\fB--theme="default"\fR +set theme +.TP +\fB-R, --report=[recap]\fR +reports to show +.TP +\fB-i, --identity-file=""\fR +set identity file for all servers +.TP +\fB-U, --user=""\fR +set ssh user +.TP +\fB--password=""\fR +set ssh password for all servers +.TP +\fB--known-hosts-file=""\fR +set known hosts file .RE .RE .TP @@ -208,16 +295,16 @@ Open up sake config file in $EDITOR. Open up sake config file in $EDITOR and go to servers section. .TP -.B edit spec [spec] -Open up sake config file in $EDITOR and go to specs section. +.B edit task [task] +Open up sake config file in $EDITOR and go to tasks section. .TP .B edit target [target] Open up sake config file in $EDITOR and go to targets section. .TP -.B edit task [task] -Open up sake config file in $EDITOR and go to tasks section. +.B edit spec [spec] +Open up sake config file in $EDITOR and go to specs section. .TP .B list servers [servers] [flags] @@ -228,12 +315,6 @@ List servers. .RS .RS .TP -\fB-H, --all-headers[=false]\fR -select all server headers -.TP -\fB--headers=[server,host,tag,desc]\fR -set headers -.TP \fB-v, --invert[=false]\fR invert matching on servers .TP @@ -243,8 +324,14 @@ filter servers on host regex \fB-t, --tags=[]\fR filter servers by tags .TP +\fB-H, --all-headers[=false]\fR +select all server headers +.TP +\fB--headers=[server,host,tags,desc]\fR +set headers +.TP \fB-o, --output="table"\fR -set table output [table|table-2|table-3|table-4|markdown|html] +set table output [table|table-2|table-3|table-4|html|markdown|json|csv] .TP \fB--theme="default"\fR set theme @@ -252,19 +339,22 @@ set theme .RE .RE .TP -.B list specs [specs] [flags] -List specs. +.B list tasks [tasks] [flags] +List tasks. .B Available Options: .RS .RS .TP -\fB--headers=[spec,output,parallel,any_errors_fatal,ignore_errors,ignore_unreachable,omit_empty]\fR +\fB-H, --all-headers[=false]\fR +select all task headers +.TP +\fB--headers=[task,desc]\fR set headers .TP \fB-o, --output="table"\fR -set table output [table|table-2|table-3|table-4|markdown|html] +set table output [table|table-2|table-3|table-4|html|markdown|json|csv] .TP \fB--theme="default"\fR set theme @@ -284,7 +374,7 @@ List tags. set headers .TP \fB-o, --output="table"\fR -set table output [table|table-2|table-3|table-4|markdown|html] +set table output [table|table-2|table-3|table-4|html|markdown|json|csv] .TP \fB--theme="default"\fR set theme @@ -300,11 +390,11 @@ List targets. .RS .RS .TP -\fB--headers=[target,all,servers,tags,regex,invert,limit,limit_p]\fR +\fB--headers=[target,desc,all,servers,tags,regex,invert,limit,limit_p]\fR set headers. Available headers: name, regex .TP \fB-o, --output="table"\fR -set table output [table|table-2|table-3|table-4|markdown|html] +set table output [table|table-2|table-3|table-4|html|markdown|json|csv] .TP \fB--theme="default"\fR set theme @@ -312,22 +402,19 @@ set theme .RE .RE .TP -.B list tasks [tasks] [flags] -List tasks. +.B list specs [specs] [flags] +List specs. .B Available Options: .RS .RS .TP -\fB-H, --all-headers[=false]\fR -select all task headers -.TP -\fB--headers=[task,desc]\fR +\fB--headers=[spec,desc,describe,list_hosts,order,silent,strategy,batch,batch_p,forks,output,any_errors_fatal,max_fail_percentage,ignore_errors,ignore_unreachable,omit_empty,report]\fR set headers .TP \fB-o, --output="table"\fR -set table output [table|table-2|table-3|table-4|markdown|html] +set table output [table|table-2|table-3|table-4|html|markdown|json|csv] .TP \fB--theme="default"\fR set theme @@ -343,22 +430,22 @@ Describe servers. .RS .RS .TP -\fB-e, --edit[=false]\fR -edit server -.TP -\fB-v, --invert[=false]\fR -invert matching on servers +\fB-t, --tags=[]\fR +filter servers by their tag .TP \fB-r, --regex=""\fR filter servers on host regex .TP -\fB-t, --tags=[]\fR -filter servers by their tag +\fB-v, --invert[=false]\fR +invert matching on servers +.TP +\fB-e, --edit[=false]\fR +edit server .RE .RE .TP -.B describe specs [specs] [flags] -Describe specs. +.B describe tasks [tasks] [flags] +Describe tasks. .B Available Options: @@ -366,7 +453,7 @@ Describe specs. .RS .TP \fB-e, --edit[=false]\fR -edit spec +edit task .RE .RE .TP @@ -383,8 +470,8 @@ edit target .RE .RE .TP -.B describe tasks [tasks] [flags] -Describe tasks. +.B describe specs [specs] [flags] +Describe specs. .B Available Options: @@ -392,7 +479,7 @@ Describe tasks. .RS .TP \fB-e, --edit[=false]\fR -edit task +edit spec .RE .RE .TP @@ -607,65 +694,112 @@ Below is a config file detailing all of the available options and their defaults header: fg: bg: - align: attr: row: fg: bg: - align: attr: row_alt: fg: bg: - align: attr: footer: fg: bg: - align: attr: # List of Specs [optional] specs: default: - # Set task output [text|table|html|markdown] - output: text + # Spec description + desc: default spec + + # Print task description + describe: false + + # Print list of hosts that will be targetted + list_hosts: false + + # Order hosts [inventory|reverse_inventory|sorted|reverse_sorted|random] + order: inventory + + # Omit showing loader when running tasks + silent: false - # Run server tasks in parallel - parallel: false + # Execution strategy [linear|host_pinned|free] + strategy: linear + + # Number of hosts to run in parallel + batch: 1 + + # Number of hosts in percentage to run in parallel [0-100] + # batch_p: 100 + + # Max number of forks + forks: 10000 + + # Set task output [text|table|table-2|table-3|table-4|html|markdown|json|csv|none] + output: text # Continue task execution on errors ignore_errors: true - # Stop task execution on all servers on error + # Stop task execution on any error any_errors_fatal: false + # Max number of tasks to fail before aborting + max_fail_percentage: 100 + # Ignore unreachable hosts ignore_unreachable: false - # Omit empty results for table output - omit_empty: false + # Omit empty rows for table output + omit_empty_rows: false + + # Omit empty columns for table output + omit_empty_columns: false + + # Show task reports [recap|rc|task|time|all] + report: [recap] + + # Verbose turns on describe, list_hosts and report set to all + verbose: false + + # Confirm invoked task before running + confirm: false + + # Confirm each task before running + step: false # List of targets [optional] targets: default: - # Target all servers + # Target description + desc: "" + + # Target all hosts all: false - # Specify servers via server name + # Specify hosts via server name servers: [] - # Specify servers via server tags + # Specify hosts via server tags tags: [] - # Limit of servers to target + # Limit number of hosts to target limit: 0 - # Limit of servers to target in percentage - limit_p: 0 + # Limit number of hosts to target in percentage + limit_p: 100 + + # Invert matching on hosts + invert: false + + # Specify host regex + regex: "" # List of tasks tasks: @@ -699,11 +833,11 @@ Below is a config file detailing all of the available options and their defaults # Or specify specs inline spec: output: table - parallel: true ignore_errors: true ignore_unreachable: true any_errors_fatal: false - omit_empty: true + omit_empty_rows: true + omit_empty_columns: true # Target reference [optional] # target: default @@ -714,7 +848,6 @@ Below is a config file detailing all of the available options and their defaults servers: [media] tags: [remote] limit: 1 - limit_p: 100 # List of environment variables [optional] env: @@ -725,22 +858,15 @@ Below is a config file detailing all of the available options and their defaults num_lines: $(ls -1 | wc -l) # The following variables are available by default: + # S_NAME + # S_HOST + # S_USER + # S_PORT + # S_BASTION + # S_TAGS + # S_IDENTITY # SAKE_DIR # SAKE_PATH - # - # SAKE_TASK_ID - # SAKE_TASK_NAME - # SAKE_TASK_DESC - # SAKE_TASK_LOCAL - # - # SAKE_SERVER_NAME - # SAKE_SERVER_DESC - # SAKE_SERVER_TAGS - # SAKE_SERVER_HOST - # SAKE_SERVER_USER - # SAKE_SERVER_PORT - # SAKE_SERVER_BASTION - # SAKE_SERVER_LOCAL # Run on localhost [optional] local: false @@ -754,7 +880,7 @@ Below is a config file detailing all of the available options and their defaults # Each task can only define: # - a single cmd # - or a single task reference - # - or a list of task references or commands + # - or a list of task references and commands # Single command cmd: | @@ -769,6 +895,7 @@ Below is a config file detailing all of the available options and their defaults # Command - name: inline-command cmd: echo "Hello World" + ignore_errors: true work_dir: /tmp shell: bash env: @@ -778,9 +905,14 @@ Below is a config file detailing all of the available options and their defaults # Nested task referencing is supported and will result in a # flat list of commands - task: simple-1 + ignore_errors: true work_dir: /tmp + register: results env: foo: bar + + - name: output + cmd: echo $results_stdout .RE .SH EXAMPLES @@ -825,29 +957,19 @@ Describe a task .B ~ $ sake describe task ping .nf -Task: ping -Name: ping -Desc: ping server -Local: false -WorkDir: -Theme: default -Target: - All: true - Servers: - Tags: -Spec: - Output: text - Parallel: false - AnyErrorsFatal: false - IgnoreErrors: false - IgnoreUnreachable: false - OmitEmpty: false -Env: - SAKE_TASK_ID: ping - SAKE_TASK_NAME: - SAKE_TASK_DESC: ping server - SAKE_TASK_LOCAL: false -Cmd: +name: ping +desc: ping server +local: false +work_dir: +theme: default +target: + all: true +spec: + output: text + ignore_unreachable: true + omit_empty_rows: true + omit_empty_columns: true +cmd: echo pong .fi diff --git a/core/ssh_config.go b/core/ssh_config.go index 13861ce..cc50636 100644 --- a/core/ssh_config.go +++ b/core/ssh_config.go @@ -53,6 +53,7 @@ func ParseReader(r io.Reader, cfg string) ([]*Endpoint, error) { if err != nil { return err } + g, err := glob.Compile(k) if err != nil { return fmt.Errorf("%s: invalid Host: %q: %w", cfg, k, err) @@ -61,6 +62,7 @@ func ParseReader(r io.Reader, cfg string) ([]*Endpoint, error) { if g.Match(name) || (info.HostName != "" && g.Match(info.HostName)) { info = mergeHostinfo(info, v) } + return nil }); err != nil { return err diff --git a/core/utils.go b/core/utils.go index 1ce20bb..66709cd 100644 --- a/core/utils.go +++ b/core/utils.go @@ -61,13 +61,14 @@ func FindFileInParentDirs(path string, files []string) (string, error) { // Get the absolute path // Need to support following path types: -// lala/land -// ./lala/land -// ../lala/land -// /lala/land -// $HOME/lala/land -// ~/lala/land -// ~root/lala/land +// +// lala/land +// ./lala/land +// ../lala/land +// /lala/land +// $HOME/lala/land +// ~/lala/land +// ~root/lala/land func GetAbsolutePath(configDir string, path string, name string) (string, error) { path = os.ExpandEnv(path) @@ -269,3 +270,10 @@ func IsDigit(s string) bool { } return true } + +func SplitString(s, sep string) []string { + if len(s) == 0 { + return []string{} + } + return strings.Split(s, sep) +} diff --git a/docs/ansible.md b/docs/ansible.md new file mode 100644 index 0000000..2c64416 --- /dev/null +++ b/docs/ansible.md @@ -0,0 +1,505 @@ +# Ansible + +Ansible is by far the most well-recognized software when it comes to configuring servers and/or running tasks and as such, warrants an in-depth comparison. + +Ansible and sake overlap in one segment, namely ad-hoc task execution over remote hosts using ssh. They both allow you to write commands in YAML and target multiple hosts in parallel. + +Where they notably differ is that Ansible has modules which allows you to easily write idempotent tasks for common use-cases , whereas with sake it's up to the developers to write tasks. + +## Comparison Table + +| | sake | Ansible +---|---|--- + Dependencies | Single Portable Binary | Python runtime on control node and hosts + Host Filtering | sake can filter on name, range, tags, and regex via config or CLI | Ansible can filter on name, group, range, regex via config or CLI + Tasks | User writes tasks | Provides declarative idempotent modules + Real-time output | Yes | No, however, there's some workarounds with async actions/polling + Templating | No built-in templating | Jinja templates + Auto-completion | Rich | Limited + Config Format | YAML, same file for sake config, tasks, and inventory | YAML/Jinja + INI inventory, separate files for Ansible config, tasks, and inventory + Community | Non-existent community | Big community + Stable | No, and 1 core developer | Yes, and a lot of core developers + Opionated | No | Yes + Learning curve | sake has less concepts to learn but puts more work on the user | While not complex, it's safe to say you'll be revisiting Ansible docs + Documentation | Yes | Yes, Ansible has a lot more Stackoverflow posts, blogs, etc. + Performance | [sake is around 4-18 times faster than Ansible depending on the number of hosts ](/performance) | Lacking, however there's a lot of optimization configurations you can do to increase performance + +## Should I use Ansible or Sake? + +You can use Ansible and sake side by side, perhaps using Ansible playbooks/roles for configuring your servers, and leveraging sakes performance for running ad-hoc tasks. + +That said, if you wish to use only one: + +- Choose **sake** when: + - you prefer or mostly rely on shell scripts + - you want real-time output + - you want the most performant task runner + - you prefer sakes simplicity + +- Choose **Ansible** when: + - you want to use idempotent and declarative configurations + - you are already familiar with Ansible, and don't find any of sakes features particularly useful + - you want to use established software and relevant industry skills + - you manage complex infrastructure + +## Background + +### What is Ansible + +> Ansible is a radically simple IT automation system. It handles configuration management, application deployment, cloud provisioning, ad-hoc task execution, network automation, and multi-node orchestration + +Ansible provides over 3000 ready-to-use modules, such as `shell`, `apt`, `file`, `copy`, etc. which you compose as playbooks and roles that you then execute for many hosts. The configuration files are written in a declarative DSL (YAML + Jinja templates). + +Almost all Ansible modules are idempotent, meaning that if you run the same playbook multiple times, you will get the same results. Furthermore, Ansible is quite opinionated in terms of how you set up your roles: + +> An Ansible role has a defined directory structure with eight main standard directories. You must include at least one of these directories in each role. You can omit any directories the role does not use +> +> [docs.ansible.com](https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html#id2) + +This makes sense for multiple reasons. First, it gives developers a common language to configure servers and removes yak-shaving because there are enforced decisions on how you're supposed to structure projects, and secondly, you can easily switch between projects that use Ansible. + +There are some drawbacks though, the user has to become familiar with the opinionated and implicit file structure of Ansible, and there's a lot of boilerplate (more than 8 directories/files per role that you can use to customize roles: tasks, handlers, templates, files, vars, defaults, meta, module_utils, lookup_plugins, library, etc.). + +### What is sake + +> sake is a task runner for remote and local hosts + +That's it basically. Its focus is the `ad-hoc task execution` segment that Ansible also does, but it tries to do it a bit better and more performant. You can use it to configure, deploy, and automate tasks, just like Ansible, but it's not as capable or robust as Ansible in that area. + +Sake provides only one module, namely the `shell` module. The `shell` module allows you to write commands in your preferred language that you then execute for many hosts. + +Similar to Ansible, the configuration files in sake are written in YAML but without any support for Jinja templates. Instead, all conditional logic is handled inside the task definitions. + +So, as you can see, sake doesn't have as valiant a goal as Ansible, since to accomplish what Ansible has, you'd have to write over 3387 (and counting) modules! The benefit is that if you already have basic shell knowledge, you don't have to learn another software to set up your infrastructure, except of course for how to write tasks in sake, but that's quite simple. + +Furthermore, there is no best-practice setup of how you organize your configuration files, that's all up to the user. + +When it comes to idempotency, sake doesn't provide any guarantee that the tasks you write are idempotent (this is similar to Ansible if you use the `shell` module). Instead, that responsibility falls on the developer and the software you use. Fortunately, a lot of software is already idempotent by default: + +- `docker-compose up` will not recreate your containers unless there is a newer image and you've set your Docker compose file to use `latest`. +- `rsync` won't upload files if there hasn't been any change +- `apt` won't reinstall your package if it's already installed +- `useradd` won't create a new user if it already exists + +## Comparisons + +### Dependencies + +Both sake and Ansible are agentless; they don't have to be installed on the remote hosts for them to work. + +Sake is delivered as a single static library that uses the ssh protocol to execute tasks on remote nodes. The remote nodes don't require `sake` to be installed. + +Ansible requires Python to be installed on the control node and utilizes the ssh protocol to execute tasks on the remote nodes. +If you use Ansible modules then the Python runtime has to be installed on the remote nodes as well. The only Ansible module which doesn't require Python to be installed on the remote nodes is the `shell` module. + +### Inventory + +Both sake and Ansible support creating a list of hosts that you can target. In Ansible, inventory files can be defined using multiple formats. The most common one is called `INI` and looks something like this: + +``` +mail.example.com + +[webservers] +foo.example.com +bar.example.com + +[dbservers] +one.example.com +two.example.com + +[prod:children] +webservers +dbservers +``` + +Another format is YAML which looks like this: +```yaml +all: + hosts: + mail.example.com: + children: + webservers: + hosts: + foo.example.com: + bar.example.com: + dbservers: + hosts: + one.example.com: + two.example.com: + prod: + hosts: + foo.example.com: + bar.example.com: + one.example.com: + two.example.com: +``` + +To list hosts you can run: + +```sh +$ ansible all --list-hosts + + hosts (5): + mail.example.com + foo.example.com + bar.example.com + one.example.com + two.example.com +``` + +sake on the other hand only has one file format for all configurations, a YAML configuration file: + +```yaml +servers: + mail: + host: mail.example.com + + webservers: + hosts: + - foo.example.com + - bar.example.com + tags: [prod] + + dbservers: + hosts: + - one.example.com + - two.example.com + tags: [prod] +``` + +To list all hosts you can run: + +```sh +$ sake list servers + + server | host | tags +--------------+------------------+------ + mail | mail.example.com | + webservers-0 | foo.example.com | prod + webservers-1 | bar.example.com | prod + dbservers-0 | one.example.com | prod + dbservers-1 | two.example.com | prod +``` + +You can define dynamic inventories in both Ansible and sake. + +The only difference between sake and Ansible is how hosts belonging to multiple groups work. In Ansible you have to create a new group and specify the hosts belonging to it, whereas in sake you simply add a tag to the hosts. + +### Modules and Idempotency + +A big part of Ansible, and what makes it popular, is that it provides idempotent ready-to-use modules that you can use to describe your wanted configuration state in a declarative way. + +For instance, to install `htop` in Ansible, you could do something like this (using the builtin `apt` module): + +```yaml +- name: install + become: true + tasks: + - name: apt + apt: + name: htop + state: present +``` + +or this (using Ansibles `shell` module): + +```yaml +- name: install + tasks: + - shell: sudo apt-get install --no-upgrade htop -y +``` + +In sake you do this: + +```yaml +tasks: + install: + cmd: sudo apt-get install htop --no-upgrade -y +``` + +The difference is that in Ansible, the first definition is declarative (you declare the wanted state, and Ansible takes care of the steps to get there), whereas, in the second and third definitions, you write the steps yourself to get to your desired state. + +The thing is, `apt-get install --no-upgrade` is already idempotent, if the package is installed, it won't do anything, if it's not, then it will install the package. So if you already know the syntax for `apt`, there's little benefit to this particular Ansible module, other than the consistency of always using Ansible modules where possible. In fact, The `shell` method is faster than the built-in `apt` module. I suspect the `apt` module is slower because when you run tasks in Ansible, it first has to copy over the playbook to the host and then run it (via Python), whereas, with the `shell` module, it's running the command directly. + +Both methods have their pros and cons, on one hand, when you're wrapping software (as Ansible does), you provide the users with a nice looking DSL and cover most basic functionality, but on the other hand, it's up to Ansible to update their wrappers when the underlying software changes. For `apt` that is stable and I assume hasn't changed over the years, it's fine. But when you have software like `rsync`, which has over 40 flags for modifying its behavior, it's not as pragmatic. Ansible has a `rsync` module but it doesn't wrap around all of the `rsync` flags that are available. + +### Task Output + +Ansibles task output is optimized for showing changes made when you run tasks, for instance: + +```bash +$ ansible-playbook web_playbook.yaml -i hosts + +PLAY [Setup] ******************************************************************************************************* + +TASK [ping] ******************************************************************************************************** +changed: [172.24.2.9] + +PLAY RECAP ********************************************************************************************************* +172.24.2.9 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +To get the `pong` output, there are multiple ways: + +1. Add `stdout_callback = minimal` to Ansible config or define a custom callback +2. Add debug task and register out variable +3. Using debug flags `-v` + +There are probably other ways, but I think it shows that it's quite bothersome to execute singular tasks and show the output of the task without resorting to editing the configs. + +Sake on the other hand optimizes for showing task output. There are multiple formats (text, tables, markdown, CSV, JSON, YAML) and it's easy to change the output format for a task inline or at the command line: + +```bash +$ sake run ping + +TASK [ping] *************** + +172.24.2.9 | pong + +$ sake run ping --output table + + Server | Ping +--------+------ + 172.24.2.9 | pong + +$ sake run ping --output markdown + +| server | ping | +|:--- |:--- | +| 172.24.2.9 | pong | +``` + +### Host Filtering + +Both sake and Ansible support host filtering at the task level. For instance, in Ansible, you'd write: + +```yaml +- name: Ping + hosts: prod + tasks: + - name: ping + shell: echo pong +``` + +to target all `prod` hosts. + +In sake it's: + +```yaml +tasks: + ping: + target: + tags: [prod] + cmd: echo pong +``` + +If you want to override the hosts from the command line you can write: + +Ansible: +```sh +ansible-playbook playbook.yml -l +``` + +sake: +```sh +sake run ping -t dev +``` + +The big difference is that with sake you get auto-completion for all the hosts and tags, whereas in Ansible you have to know the group or hosts beforehand. + +### Directory Structure + +In Ansible you define hosts, playbooks, and optionally, roles. We've covered hosts already, but let's look at playbooks and roles: + +- Playbooks are simply a collection of tasks that you define to run for a set of hosts. +- Roles are a collection of tasks, files, templates, handlers, and a bunch of other stuff, which you can define to improve reusability. + +What you normally do is define one or multiple playbooks that are entry points to configuring servers. The playbook then refers to a set of roles, that further define the tasks, handlers, files, etc. that you use to configure your server. + +The roles have an opinionated and strict directory structure that you must adhere to (for instance, the `tasks` directory must have a `main.yaml`file for it to be picked up). + +One limitation with Ansible tasks is that you can only import task files, not individual tasks from a file. What you can do instead is define a tag for that specific task and then run the playbook with the `--tags` flag. I'm not sure why Ansible doesn't allow you to import specific tasks only. Also, be aware of cyclic dependencies, Ansible doesn't detect them so you can end up with infinitive loops. + +sake on the other hand is like any other programming language, you import config files and can then reference specific tasks you intend to use. So you're free to structure your project any way you see fit. + +### IDE & Auto-completion + +Ansible has basic auto-completion for commands and flags, but it doesn't seem to be able to autocomplete single tasks, groups, tags, etc. + +There's also autocompletion for running modules ad-hoc on the command line, however, when you run `ansible -m `, you get a list of all modules, which is over 3000, so the initial load time is substantial. + +sake supports rich autocompletion for both commands, flags, and values for hosts, tasks, tags, etc. + +For instance, when you type `sake run --server --tags `, you get the following autocompletions: + +- first `` autocompletes all available tasks +- second `` autocompletes all available hosts +- third `` autocompletes all available tags + +Ansible has IDE integrations for vscode, vim, and other IDEs. It seems to support key attributes (gather_facts, etc.) but doesn't provide completion for hosts or tasks. + +Sake doesn't have any IDE integrations yet, but I plan on adding them at a later stage when sake reaches maturity and the API has stabilized. + +### Stability & Community + +Ansible has a big community and is considered stable (reached v1). + +Sake is still in early development and the community is non-existent. + +### Misc + +Sake has a list of quality-of-life features that I've not found in Ansible: + +- Edit the `sake` config file via the `sake edit` command, which opens up the config file in your preferred editor, this also works for servers, tasks, targets and specs +- `sake ssh ` gives me a list of all the hosts and the ability to ssh into them +- `sake run --edit` opens up the editor and navigates to the line where the task is defined, this is quite helpful when you're debugging a task +- `sake describe/list servers --tags web` lists/describes all the servers that have the tag `web` +- `sake run --attach ` will run the task, then ssh into the server and give you a tty +- `sake run --tty container=my-container`, enables you to replace the current process and attach to a Docker container running on a remote server. The `` can be defined as `ssh -t $S_USER@$S_HOST "docker exec -it $container"` + +### Performance + +Ansible is not known for its performance, in fact, just performing a simple ping to a single host over LAN takes over a second, whereas sake only takes 200 ms. And it gets worse when you increase the number of hosts, for instance, running ping on 100 hosts takes Ansible around 3.5 seconds to complete (Pipelining enabled, gather facts off and forks set to 10), and for sake only 350 ms. +Additionally, I found sake to consume less memory and CPU. + +Some more extensive benchmarks can be found [here](/performance). + +## Example + +Now let's compare Ansible and sake with a simple demo. We'll begin with the problem definition and then look at a possible solution, using Ansible and sake: + +1. Connect to a machine +2. Install `htop` +3. Start a docker container +4. Read file content +5. Upload file +6. ssh and run `htop` + +### Ansible + +We need to create 3 files: + +```toml title=ansible.cfg +[defaults] +host_key_checking = false +``` + +```toml title=hosts +172.24.2.2 +``` + +```yaml title=playbook.yaml +- hosts: all + become: true + vars: + contents: "{{ lookup('file','ansible-upload.txt') }}" + + tasks: + - name: Install htop + ansible.builtin.apt: + name: htop + state: present + update_cache: no + + - name: docker-compose up + community.docker.docker_compose: + project_src: /opt + build: false + + - name: print file + ansible.builtin.debug: + msg: "{{ contents }}" + + - name: Upload file + ansible.builtin.copy: + src: ./file.txt + dest: /home/test/ +``` + +To run all tasks: + +```bash +$ ansible-playbook -i hosts playbook.yaml + +# Run htop on host +$ ssh test@172.24.2.2 -t 'htop' +``` + +### Sake + +With sake we create 1 file containing the config, inventory, and tasks: + +```yaml title=sake.yaml +disable_verify_host: true + +servers: + server-1: + host: test@172.24.2.2:22 + +tasks: + many-tasks: + tasks: + - name: Install htop + cmd: sudo apt-get install htop --no-upgrade -y + + - name: docker-compose up + cmd: docker-compose up -d + + - name: Print file + cmd: cat file.txt + + - name: Upload file + local: true + env: + file: file.txt + cmd: scp -P "$S_PORT" "$file" $S_USER@$S_HOST:/home/test + + - name: Run htop + tty: true + cmd: ssh $S_USER@$S_HOST -t "htop" +``` + +To run all tasks: + +```bash +$ sake run many-tasks -a +``` + +Note, you can create a generic task for the commands, and then reference them like this: + +```yaml +tasks: + upload: + local: true + cmd: scp -P "$S_PORT" "$from" $S_USER@$S_HOST:"$to" + env: + from: + to: /home/test + + upload-file: + task: upload + env: + from: file.txt + to: /home/test +``` + +### Recap + +As we can see, with Ansible we had to define 3 files: + +- Ansible configuration file `ansible.cfg` +- Ansible inventory `hosts` +- Ansible playbook `playbook.yaml` + +With sake, we only had to define 1 file. You could split it up into 3 separate files if you wanted to, but it's not required. + +The main difference is that for Ansible, you have to read Ansible documentation for all the modules to know how to configure them: + +- apt module +- docker-compose module +- debug bultin +- file upload module + +Additionally, you have to run the `ssh` commands at the end to run `htop`, something which you don't have to do in sake. + +So, if you already have basic unix knowledge, it's a lot quicker to get started with sake. diff --git a/docs/background.md b/docs/background.md new file mode 100644 index 0000000..37041dc --- /dev/null +++ b/docs/background.md @@ -0,0 +1,35 @@ +# Background + +> To configure servers, all you need is bash scripting, environment variables, and some know-how. + +`sake` came about because I needed a simple tool to run tasks on remote hosts. There's tons of software in this category, Ansible, pyinfra, Puppet, Chef, Salt, Sup, and probably many more. However, some of them can be quite complex to master, have complicated DSLs and for simple situations are not quite ergonomic. So, `sake` was born, an ergonomic utility CLI tool to run tasks on servers. + +## Premise + +The premise is you have a bunch of servers and want the following: + +1. A central place for your servers, containing name, host, and a small description of the servers +2. Ability to run ad-hoc commands (perhaps `hostnamectl` to see system hostname) on 1, a subset, or all of the servers +3. Ability to run defined tasks on 1, a subset, or all of the servers +4. Ability to get an overview of 1, a subset, or all of the servers and tasks + +## Design + +`sake` prioritizes simplicity and flexibility, principles that are at odds with each other at times: + +1. Simplicity - both the implementation and the UX is designed to be simple. However, this rule can be bent to some degree if the reward is substantial +2. Flexibility - provide the user with the ability to shape `sake` to their user case, instead of providing an opinionated setup + +With these principles in mind, I've elected not to create a complex DSL (see Terraform, Puppet, Ansible, etc.), but instead, just add a few primitives and let the user leverage their existing sysadmin knowledge to create their ideal setup. This results in not forcing users to learn yet another DSL, avoiding continuous lookup of `sake` commands, and updating `sake` to get new features. + +It does, however, push complexity onto the user, for instance, there's no built-in primitive to download files and the user must define a task to do so. It would be foolish to aim for feature parity with 3rd party software like rsync for downloading files (there are over 150 flags for rsync). + +So what config format is best suited for this purpose? In my opinion, YAML is a suitable candidate. While it has its issues, I think its purpose as a human-readable config/state file works well. It has all the primitives you'd need in a config language, simple key/value entries, dictionaries, and lists, as well as supporting comments (something which JSON doesn't). We could create a custom format, but then users would have to learn that syntax, so in this case, YAML has a major advantage, many developers are familiar with it. + +I don't intend to introduce any templating capability via Jinja templates or similar, instead, the user will have to leverage shell or any other programming to compose more complex tasks. + +In this sense, `sake` follows the Unix principle: + +- Write programs that do one thing and do it well - run tasks on multiple servers +- Write programs to work together - invoke other programs to do your bidding (rsync) +- Write programs to handle text streams - output of all `sake` commands is just text diff --git a/docs/changelog.md b/docs/changelog.md index 0823994..ba352d2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,65 @@ # Changelog +## 0.13.0 + +### Features + +- Add new task strategies: linear, host_pinned, free + - `linear`: execute task for each host before proceeding to the next task (default) + - `host_pinned`: executes tasks (serial) for a host before proceeding to the next host + - `free`: tasks without waiting for other tasks +- Add host ordering + - `inventory`: The order is as provided by the inventory + - `reverse_inventory`: The order is the reverse of the inventory + - `sorted`: Hosts are alphabetically sorted by host + - `reverse_sorted`: Hosts are sorted by host in reverse alphabetical order + - `random`: Hosts are randomly ordered +- Determine number of hosts to run in parallel + - `batch`: specify number of hosts + - `batch_p`: specify number of hosts in percentage + - `forks`: max number of concurrent processes +- Add ability to register variables which are available to the next tasks +- Add option to display reports at end of tasks by using `--report` flag or specifying it in `spec` definition + - `recap`: show basic report + - `rc`: show return code for each host and task + - `task`: show task status for each host and task + - `time`: show time report for each host and task + - `all`: show all reports +- Add flag/spec option to list targetted hosts +- Add option to ignore errors for indiviual tasks +- Add confirm/step task capability + - `confirm`: for the root task + - `step`: per task and host + +### Fixes + +- Fix omitting attribute `align` when creating a theme +- Abort tasks prematurely when running in parallel and AnyErrorsFatal set to true +- Fix server range (previously `[2:100]` didn't work as strings were compared) +- Fix empty error for non existing working directory and update how work_dir works + +### Changes + +- Switch to default shell when evaluating inventory +- If no command name is set on nested tasks, assign `task-$i` instead of `task` +- If `--limit` flag is higher than available hosts, then select all hosts filtered +- Building `sake` with go 1.19 +- Shorthand flag for silent is now `Q` +- Deprecated the parallel flag, use batch/batch_p/forks instead +- Update flag sorting +- Rename `--omit-empty` to `--omit-empty-rowss` +- [BREAKING CHANGE]: Rename default environment variables from `SAKE_SERVER_*` to `S_*`, and remove task default environment variables + +### Minor + +- Add description to targets and specs +- Add option to specify target and spec via flags +- Support output options `csv`/`json`/`none` +- Add server identity to environment variables +- Add silent/describe attribute to spec definition +- Add ssh user flag option +- Add option to omit empty columns via flag `--omit-empty-columns` and spec `omit_empty_columns` + ## 0.12.1 ### Fixes diff --git a/docs/command-reference.md b/docs/command-reference.md index 9b10f6b..03712cc 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -2,11 +2,11 @@ ## sake -sake is a command runner for local and remote hosts +sake is a task runner for local and remote hosts ### Synopsis -sake is a command runner for local and remote hosts. +sake is a task runner for local and remote hosts. You define servers and tasks in a sake.yaml config file and then run the tasks on the servers. @@ -15,10 +15,10 @@ You define servers and tasks in a sake.yaml config file and then run the tasks o ``` -c, --config string specify config - -h, --help help for sake - --no-color disable color - -U, --ssh-config string specify ssh config -u, --user-config string specify user config + --ssh-config string specify ssh config + --no-color disable color + -h, --help help for sake ``` ## check @@ -74,31 +74,45 @@ run [flags] ### Options ``` - -a, --all target all servers - --any-errors-fatal stop task execution on all servers on error - --attach ssh to server after command - --describe print task information - --dry-run print the task to see what will be executed - -e, --edit edit task - -h, --help help for run - -i, --identity-file string set identity file for all servers - --ignore-errors continue task execution on errors - --ignore-unreachable ignore unreachable hosts - -v, --invert invert matching on servers - --known-hosts-file string set known hosts file - -l, --limit uint32 set limit of servers to target - -L, --limit-p uint8 set percentage of servers to target [0-100] - --local run task on localhost - --omit-empty omit empty results for table output - -o, --output string set task output [text|table|table-2|table-3|table-4|html|markdown] - -p, --parallel run server tasks in parallel - --password string set ssh password for all servers - -r, --regex string filter servers on host regex - -s, --servers strings target servers by names - -S, --silent omit showing loader when running tasks - -t, --tags strings target servers by tags - --theme string set theme - --tty replace the current process + --dry-run print the task to see what will be executed + --describe print task information + --list-hosts print hosts that will be targetted + -V, --verbose enable all diagnostics + -S, --strategy string set execution strategy [linear|host_pinned|free] + -f, --forks uint32 max number of concurrent processes (default 10000) + -b, --batch uint32 set number of hosts to run in parallel + -B, --batch-p uint8 set percentage of hosts to run in parallel [0-100] + -a, --all target all hosts + -v, --invert invert matching on hosts + -r, --regex string target hosts on host regex + -s, --servers strings target servers by names + -t, --tags strings target hosts by tags + -T, --target string target hosts by target name + --order string order hosts + -l, --limit uint32 set limit of servers to target + -L, --limit-p uint8 set percentage of servers to target [0-100] + --ignore-unreachable ignore unreachable hosts + -M, --max-fail-percentage uint8 stop task execution on all servers when threshold reached + --any-errors-fatal stop task execution on all servers on error + --ignore-errors continue task execution on errors + -J, --spec string set spec + -o, --output string set task output [text|table|table-2|table-3|table-4|html|markdown|json|csv|none] + --omit-empty-rows omit empty row for table output + --omit-empty-columns omit empty column for table output + -q, --silent omit showing loader when running tasks + --confirm confirm root task before running + --step confirm each task before running + --tty replace the current process + --attach ssh to server after command + --local run task on localhost + --theme string set theme (default "default") + -e, --edit edit task + -R, --report strings reports to show (default [recap]) + -i, --identity-file string set identity file + -U, --user string set ssh user + --password string set ssh password + --known-hosts-file string set known hosts file + -h, --help help for run ``` ## exec @@ -130,29 +144,44 @@ exec [flags] ### Options ``` - -a, --all target all servers - --any-errors-fatal stop task execution on all servers on error - --attach ssh to server after command - --dry-run prints the command to see what will be executed - -h, --help help for exec - -i, --identity-file string set identity file for all servers - --ignore-errors continue task execution on errors - --ignore-unreachable ignore unreachable hosts - -v, --invert invert matching on servers - --known-hosts-file string set known hosts file - -l, --limit uint32 set limit of servers to target - -L, --limit-p uint8 set percentage of servers to target - --local run command on localhost - --omit-empty omit empty results for table output - -o, --output string set task output [text|table|table-2|table-3|table-4|html|markdown] - -p, --parallel run server tasks in parallel - --password string set ssh password for all servers - -r, --regex string filter servers on host regex - -s, --servers strings target servers by names - -S, --silent omit showing loader when running tasks - -t, --tags strings target servers by tags - --theme string set theme (default "default") - --tty replace the current process + --dry-run prints the command to see what will be executed + --describe print task information + --list-hosts print hosts that will be targetted + -V, --verbose enable all diagnostics + -S, --strategy string set execution strategy [linear|host_pinned|free] + -f, --forks uint32 max number of concurrent processes (default 10000) + -b, --batch uint32 set number of hosts to run in parallel + -B, --batch-p uint8 set percentage of servers to run in parallel [0-100] + -a, --all target all servers + -v, --invert invert matching on servers + -r, --regex string filter servers on host regex + -s, --servers strings target servers by names + -t, --tags strings target servers by tags + -T, --target string target servers by target name + --order string order hosts + -l, --limit uint32 set limit of servers to target + -L, --limit-p uint8 set percentage of servers to target + --ignore-unreachable ignore unreachable hosts + -M, --max-fail-percentage uint8 stop task execution on all servers when threshold reached + --any-errors-fatal stop task execution on all servers on error + --ignore-errors continue task execution on errors + -J, --spec string set spec + -o, --output string set task output [text|table|table-2|table-3|table-4|html|markdown|json|csv|none] + --omit-empty-rows omit empty row for table output + --omit-empty-columns omit empty column for table output + -q, --silent omit showing loader when running tasks + --confirm confirm root task before running + --step confirm each task before running + --tty replace the current process + --attach ssh to server after command + --local run command on localhost + --theme string set theme (default "default") + -R, --report strings reports to show (default [recap]) + -i, --identity-file string set identity file for all servers + -U, --user string set ssh user + --password string set ssh password for all servers + --known-hosts-file string set known hosts file + -h, --help help for exec ``` ## init @@ -345,51 +374,55 @@ list servers [servers] [flags] ### Options ``` - -H, --all-headers select all server headers - --headers strings set headers (default [server,host,tag,desc]) - -h, --help help for servers -v, --invert invert matching on servers -r, --regex string filter servers on host regex -t, --tags strings filter servers by tags + -H, --all-headers select all server headers + --headers strings set headers (default [server,host,tags,desc]) + -h, --help help for servers ``` ### Options inherited from parent commands ``` - -o, --output string set table output [table|table-2|table-3|table-4|markdown|html] (default "table") + -o, --output string set table output [table|table-2|table-3|table-4|html|markdown|json|csv] (default "table") --theme string set theme (default "default") ``` -## list specs +## list tasks -List specs +List tasks ### Synopsis -List specs. +List tasks. ``` -list specs [specs] [flags] +list tasks [tasks] [flags] ``` ### Examples ``` - # List all specs - sake list specs + # List all tasks + sake list tasks + + # List task + sake list task ``` ### Options ``` - --headers strings set headers (default [spec,output,parallel,any_errors_fatal,ignore_errors,ignore_unreachable,omit_empty]) - -h, --help help for specs + -H, --all-headers select all task headers + --headers strings set headers (default [task,desc]) + -h, --help help for tasks ``` ### Options inherited from parent commands ``` - -o, --output string set table output [table|table-2|table-3|table-4|markdown|html] (default "table") + -o, --output string set table output [table|table-2|table-3|table-4|html|markdown|json|csv] (default "table") --theme string set theme (default "default") ``` @@ -422,7 +455,7 @@ list tags [tags] [flags] ### Options inherited from parent commands ``` - -o, --output string set table output [table|table-2|table-3|table-4|markdown|html] (default "table") + -o, --output string set table output [table|table-2|table-3|table-4|html|markdown|json|csv] (default "table") --theme string set theme (default "default") ``` @@ -448,51 +481,47 @@ list targets [targets] [flags] ### Options ``` - --headers strings set headers. Available headers: name, regex (default [target,all,servers,tags,regex,invert,limit,limit_p]) + --headers strings set headers. Available headers: name, regex (default [target,desc,all,servers,tags,regex,invert,limit,limit_p]) -h, --help help for targets ``` ### Options inherited from parent commands ``` - -o, --output string set table output [table|table-2|table-3|table-4|markdown|html] (default "table") + -o, --output string set table output [table|table-2|table-3|table-4|html|markdown|json|csv] (default "table") --theme string set theme (default "default") ``` -## list tasks +## list specs -List tasks +List specs ### Synopsis -List tasks. +List specs. ``` -list tasks [tasks] [flags] +list specs [specs] [flags] ``` ### Examples ``` - # List all tasks - sake list tasks - - # List task - sake list task + # List all specs + sake list specs ``` ### Options ``` - -H, --all-headers select all task headers - --headers strings set headers (default [task,desc]) - -h, --help help for tasks + --headers strings set headers (default [spec,desc,describe,list_hosts,order,silent,strategy,batch,batch_p,forks,output,any_errors_fatal,max_fail_percentage,ignore_errors,ignore_unreachable,omit_empty,report]) + -h, --help help for specs ``` ### Options inherited from parent commands ``` - -o, --output string set table output [table|table-2|table-3|table-4|markdown|html] (default "table") + -o, --output string set table output [table|table-2|table-3|table-4|html|markdown|json|csv] (default "table") --theme string set theme (default "default") ``` @@ -521,37 +550,40 @@ describe servers [servers] [flags] ### Options ``` + -t, --tags strings filter servers by their tag + -r, --regex string filter servers on host regex + -v, --invert invert matching on servers -e, --edit edit server -h, --help help for servers - -v, --invert invert matching on servers - -r, --regex string filter servers on host regex - -t, --tags strings filter servers by their tag ``` -## describe specs +## describe tasks -Describe specs +Describe tasks ### Synopsis -Describe specs. +Describe tasks. ``` -describe specs [specs] [flags] +describe tasks [tasks] [flags] ``` ### Examples ``` - # Describe all specs - sake describe specs + # Describe all tasks + sake describe tasks + + # Describe task + sake describe task ``` ### Options ``` - -e, --edit edit spec - -h, --help help for specs + -e, --edit edit task + -h, --help help for tasks ``` ## describe targets @@ -580,33 +612,30 @@ describe targets [targets] [flags] -h, --help help for targets ``` -## describe tasks +## describe specs -Describe tasks +Describe specs ### Synopsis -Describe tasks. +Describe specs. ``` -describe tasks [tasks] [flags] +describe specs [specs] [flags] ``` ### Examples ``` - # Describe all tasks - sake describe tasks - - # Describe task - sake describe task + # Describe all specs + sake describe specs ``` ### Options ``` - -e, --edit edit task - -h, --help help for tasks + -e, --edit edit spec + -h, --help help for specs ``` ## ssh @@ -631,9 +660,9 @@ ssh [flags] ### Options ``` - -h, --help help for ssh -i, --identity-file string set identity file for all servers --password string set ssh password for all servers + -h, --help help for ssh ``` ## gen diff --git a/docs/config-reference.md b/docs/config-reference.md index 142e14b..5f71297 100644 --- a/docs/config-reference.md +++ b/docs/config-reference.md @@ -7,7 +7,7 @@ The sake.yaml config is based on the following concepts: - **specs** are configs that alter **task** execution and output - **targets** are configs that provide shorthand filtering of **servers** when executing **tasks** - **themes** are used to modify the output of `sake` commands -- **envs** are environment variables that can be defined globally, per server and per task +- **env** are environment variables that can be defined globally, per server and per task **Specs**, **targets** and **themes** come with a default setting that the user can override. @@ -18,13 +18,13 @@ Below is a config file detailing all of the available options and their defaults ```yaml # Import servers/tasks/env/specs/themes/targets from other configs [optional] import: - - ./some-dir/sake.yaml + - ./some-dir/sake.yaml # Verify SSH host connections. Set this to true if you wish to circumvent verify host [optional] disable_verify_host: false # Set known_hosts_file path. Default is users ssh home directory [optional] -known_hosts_file: $HOME/.ssh/known_hosts +# known_hosts_file: $HOME/.ssh/known_hosts # Shell used for commands [optional] # If you use any other program than bash, zsh, sh, node, or python @@ -34,382 +34,365 @@ shell: bash # List of Servers servers: - # Server name [required] - media: - # Server description [optional] - desc: media server + # Server name [required] + media: + # Server description [optional] + desc: media server - # Host [required] - host: media.lan + # Host [required] + host: media.lan + # one-line for setting user and port + # host: samir@media.lan:22 - # User to connect as. It defaults to the current user [optional] - user: whoami + # Specify multiple hosts: + # hosts: + # - samir@192.168.0.1:22 + # - samir@l92.168.1.1:22 - # Port for ssh [optional] - port: 22 + # or use a host range generator + # hosts: samir@192.168.[0:1].1:22 - # Shell used for commands [optional] - shell: bash + # generate hosts by local command + # inventory: echo samir@192.168.0.1:22 samir@192.168.1.1:22 - # Set identity file. By default it will attempt to establish a connection using a SSH auth agent [optional] - identity_file: ./id_rsa + # Bastion [optional] + bastion: samir@192.168.1.1:2222 - # Set password. Accepts either a string or a shell command [optional] - password: $(echo $MY_SECRET_PASSWORD) + # User to connect as. It defaults to the current user [optional] + user: samir - # Run on localhost [optional] - local: false + # Port for ssh [optional] + port: 22 - # Set default working directory for task execution [optional] - work_dir: "" + # Shell used for commands [optional] + shell: bash - # List of tags [optional] - tags: [remote] + # Run on localhost [optional] + local: false - # List of server specific environment variables [optional] - env: - # Simple string value - key: value + # Set default working directory for task execution [optional] + work_dir: "" - # Shell command substitution (evaluated on localhost) - date: $(date -u +"%Y-%m-%dT%H:%M:%S%Z") + # Set identity file. By default it will attempt to establish a connection using a SSH auth agent [optional] + # sake respects users ssh config, so you can set auth credentials in the users ssh config + identity_file: ./id_rsa + + # Set password. Accepts either a string or a shell command [optional] + password: $(echo $MY_SECRET_PASSWORD) + + # List of tags [optional] + tags: [remote] + + # List of server specific environment variables [optional] + env: + # Simple string value + key: value + + # Shell command substitution (evaluated on localhost) + date: $(date -u +"%Y-%m-%dT%H:%M:%S%Z") # List of environment variables that are available to all tasks env: - # Simple string value - AUTHOR: "alajmo" + # Simple string value + AUTHOR: "alajmo" - # Shell command substitution (evaluated on localhost) - DATE: $(date -u +"%Y-%m-%dT%H:%M:%S%Z") + # Shell command substitution (evaluated on localhost) + DATE: $(date -u +"%Y-%m-%dT%H:%M:%S%Z") # List of themes themes: - # Theme name - default: - # Text options [optional] - text: - # Include server name prefix for each line [optional] - prefix: true - - # Colors to alternate between for each server prefix [optional] - # Available options: green, blue, red, yellow, magenta, cyan - prefix_colors: ["green", "blue", "red", "yellow", "magenta", "cyan"] - - # Customize the task header that is printed before each task when output is set to text (to opt out, set it to empty string) [optional] - # Available variables: `.Name`, `.Desc`, `.Index`, `.NumTasks` - # Available methods: `.Style`, which takes in 1 or more parameters, first is the string to be styled, and the rest are styling options - # Available styling options: - # Colors (prefix with `fg_` for foreground, and `bg_` for background): black, red, green, yellow, blue, magenta, cyan, white, hi_black, hi_red, hi_green, hi_yellow, hi_blue, hi_magenta, hi_cyan, hi_white - # Attributes: normal, bold, faint, italic, underline crossed_out - header: '{{ .Style "TASK" "bold" }}{{ if ne .NumTasks 1 }} ({{ .Index }}/{{ .NumTasks }}){{end}}{{ if and .Name .Desc }} [{{.Style .Name "bold"}}: {{ .Desc }}] {{ else if .Name }} [{{ .Name }}] {{ else if .Desc }} [{{ .Desc }}] {{end}}' - - # Fill remaining spaces with a character after the prefix, if set to empty string, no filler characters will be displayed [optional] - header_filler: "*" - - # Table options [optional] - table: - # Table style [optional] - # Available options: ascii, default - style: ascii - - # Text format options for headers and rows in table output [optional] - # Available options: default, lower, title, upper - format: - header: default - row: default - - # Border options for table output [optional] - options: - draw_border: false - separate_columns: true - separate_header: true - separate_rows: false - separate_footer: false - - # Color, attr and align options [optional] - # Available options for fg/bg: green, blue, red, yellow, magenta, cyan - # Available options for align: left, center, justify, right - # Available options for attr: normal, bold, faint, italic, underline, crossed_out - color: - header: - server: - fg: - bg: - align: - attr: - - user: - fg: - bg: - align: - attr: - - host: - fg: - bg: - align: - attr: - - port: - fg: - bg: - align: - attr: - - local: - fg: - bg: - align: - attr: - - tag: - fg: - bg: - align: - attr: - - desc: - fg: - bg: - align: - attr: - - task: - fg: - bg: - align: - attr: - - output: - fg: - bg: - align: - attr: - - row: - server: - fg: - bg: - align: - attr: - - user: - fg: - bg: - align: - attr: - - host: - fg: - bg: - align: - attr: - - port: - fg: - bg: - align: - attr: - - local: - fg: - bg: - align: - attr: - - tag: - fg: - bg: - align: - attr: - - desc: - fg: - bg: - align: - attr: - - task: - fg: - bg: - align: - attr: - - output: - fg: - bg: - align: - attr: - - border: - header: - fg: - bg: - align: - attr: - - row: - fg: - bg: - align: - attr: - - row_alt: - fg: - bg: - align: - attr: - - footer: - fg: - bg: - align: - attr: + # Theme name + default: + # Text options [optional] + text: + # Include server name prefix for each line [optional] + prefix: true + + # Colors to alternate between for each server prefix [optional] + # Available options: green, blue, red, yellow, magenta, cyan + prefix_colors: ["green", "blue", "red", "yellow", "magenta", "cyan"] + + # Customize the task header that is printed before each task when output is set to text (to opt out, set it to empty string) [optional] + # Available variables: `.Name`, `.Desc`, `.Index`, `.NumTasks` + # Available methods: `.Style`, which takes in 1 or more parameters, first is the string to be styled, and the rest are styling options + # Available styling options: + # Colors (prefix with `fg_` for foreground, and `bg_` for background): black, red, green, yellow, blue, magenta, cyan, white, hi_black, hi_red, hi_green, hi_yellow, hi_blue, hi_magenta, hi_cyan, hi_white + # Attributes: normal, bold, faint, italic, underline crossed_out + header: '{{ .Style "TASK" "bold" }}{{ if ne .NumTasks 1 }} ({{ .Index }}/{{ .NumTasks }}){{end}}{{ if and .Name .Desc }} [{{.Style .Name "bold"}}: {{ .Desc }}] {{ else if .Name }} [{{ .Name }}] {{ else if .Desc }} [{{ .Desc }}] {{end}}' + + # Fill remaining spaces with a character after the header, if set to empty string, no filler characters will be displayed [optional] + header_filler: "*" + + # Table options [optional] + table: + # Table style [optional] + # Available options: ascii, connected-light + style: ascii + + # Border options for table output [optional] + options: + draw_border: false + separate_columns: true + separate_header: true + separate_rows: false + separate_footer: false + + # Color, attr, align, and format options [optional] + # Available options for fg/bg: green, blue, red, yellow, magenta, cyan, hi_green, hi_blue, hi_red, hi_yellow, hi_magenta, hi_cyan + # Available options for align: left, center, justify, right + # Available options for attr: normal, bold, faint, italic, underline, crossed_out + # Available options for format: default, lower, title, upper + title: + fg: + bg: + align: + attr: + format: + + header: + fg: + bg: + align: + attr: + format: + + row: + fg: + bg: + align: + attr: + format: + + footer: + fg: + bg: + align: + attr: + format: + + border: + header: + fg: + bg: + attr: + + row: + fg: + bg: + attr: + + row_alt: + fg: + bg: + attr: + + footer: + fg: + bg: + attr: # List of Specs [optional] specs: - default: - # Set task output [text|table|html|markdown] - output: text + default: + # Spec description + desc: default spec + + # Print task description + describe: false + + # Print list of hosts that will be targetted + list_hosts: false + + # Order hosts [inventory|reverse_inventory|sorted|reverse_sorted|random] + order: inventory + + # Omit showing loader when running tasks + silent: false + + # Execution strategy [linear|host_pinned|free] + strategy: linear + + # Number of hosts to run in parallel + batch: 1 + + # Number of hosts in percentage to run in parallel [0-100] + # batch_p: 100 + + # Max number of forks + forks: 10000 - # Run server tasks in parallel - parallel: false + # Set task output [text|table|table-2|table-3|table-4|html|markdown|json|csv|none] + output: text - # Continue task execution on errors - ignore_errors: true + # Continue task execution on errors + ignore_errors: true - # Stop task execution on all servers on error - any_errors_fatal: false + # Stop task execution on any error + any_errors_fatal: false - # Ignore unreachable hosts - ignore_unreachable: false + # Max number of tasks to fail before aborting + max_fail_percentage: 100 - # Omit empty results for table output - omit_empty: false + # Ignore unreachable hosts + ignore_unreachable: false + + # Omit empty rows for table output + omit_empty_rows: false + + # Omit empty columns for table output + omit_empty_columns: false + + # Show task reports [recap|rc|task|time|all] + report: [recap] + + # Verbose turns on describe, list_hosts and report set to all + verbose: false + + # Confirm invoked task before running + confirm: false + + # Confirm each task before running + step: false # List of targets [optional] targets: - default: - # Target all servers - all: false + default: + # Target description + desc: "" - # Specify servers via server name - servers: [] + # Target all hosts + all: false - # Specify servers via server tags - tags: [] + # Specify hosts via server name + servers: [] + + # Specify hosts via server tags + tags: [] + + # Limit number of hosts to target + limit: 0 + + # Limit number of hosts to target in percentage + limit_p: 100 + + # Invert matching on hosts + invert: false + + # Specify host regex + regex: "" # List of tasks tasks: - # Command ID [required] - simple-1: - # The name that will be displayed when executing or listing tasks. Defaults to task ID [optional] - name: Simple - - # Script to run - cmd: | - echo "hello world" - desc: simple command 1 - - # Short-form for a command - simple-2: echo "hello world" - - # Command ID [required] - advanced-command: - # The name that will be displayed when executing or listing tasks. Defaults to task ID [optional] - name: Advanced Command - - # Task description [optional] - desc: Advanced task - - # Specify theme [optional] - theme: default - - # Spec reference [optional] - # spec: default - - # Or specify specs inline - spec: - output: table - parallel: true - ignore_errors: true - ignore_unreachable: true - any_errors_fatal: false - omit_empty: true - - # Target reference [optional] - # target: default - - # Or specify targets inline - target: - all: true - servers: [media] - tags: [remote] - - # List of environment variables [optional] - env: - # Simple string value - release: v1.0.0 - - # Shell command substitution - num_lines: $(ls -1 | wc -l) - - # The following variables are available by default: - # SAKE_DIR - # SAKE_PATH - # - # SAKE_TASK_ID - # SAKE_TASK_NAME - # SAKE_TASK_DESC - # SAKE_TASK_LOCAL - # - # SAKE_SERVER_NAME - # SAKE_SERVER_DESC - # SAKE_SERVER_TAGS - # SAKE_SERVER_HOST - # SAKE_SERVER_USER - # SAKE_SERVER_PORT - # SAKE_SERVER_LOCAL - - # Run on localhost [optional] - local: false - - # Set default working directory for task [optional] - work_dir: "" - - # Shell used for commands [optional] - shell: bash - - # Each task can only define: - # - a single cmd - # - or a single task reference - # - or a list of task references or commands - - # Single command - cmd: | - echo complex - echo command - - # Task reference. work_dir and env variables are passed down. - task: simple-1 - - # List of task references or commands. - tasks: - # Command - - name: inline-command - cmd: echo "Hello World" - work_dir: /tmp - shell: bash - env: - foo: bar - - # Task reference. work_dir and env variables are passed down. - # Nested task referencing is supported and will result in a - # flat list of commands - - task: simple-1 - work_dir: /tmp - env: - foo: bar + # Command ID [required] + simple-1: + # The name that will be displayed when executing or listing tasks. Defaults to task ID [optional] + name: Simple + + # Script to run + cmd: | + echo "hello world" + desc: simple command 1 + + # Short-form for a command + simple-2: echo "hello world" + + # Command ID [required] + advanced-command: + # The name that will be displayed when executing or listing tasks. Defaults to task ID [optional] + name: Advanced Command + + # Task description [optional] + desc: Advanced task + + # Specify theme [optional] + theme: default + + # Spec reference [optional] + # spec: default + + # Or specify specs inline + spec: + output: table + ignore_errors: true + ignore_unreachable: true + any_errors_fatal: false + omit_empty_rows: true + omit_empty_columns: true + + # Target reference [optional] + # target: default + + # Or specify targets inline + target: + all: true + servers: [media] + tags: [remote] + limit: 1 + + # List of environment variables [optional] + env: + # Simple string value + release: v1.0.0 + + # Shell command substitution + num_lines: $(ls -1 | wc -l) + + # The following variables are available by default: + # S_NAME + # S_HOST + # S_USER + # S_PORT + # S_BASTION + # S_TAGS + # S_IDENTITY + # SAKE_DIR + # SAKE_PATH + + # Run on localhost [optional] + local: false + + # Set default working directory for task [optional] + work_dir: "" + + # Shell used for commands [optional] + shell: bash + + # Each task can only define: + # - a single cmd + # - or a single task reference + # - or a list of task references and commands + + # Single command + cmd: | + echo complex + echo command + + # Task reference. work_dir and env variables are passed down + task: simple-1 + + # List of task references or commands + tasks: + # Command + - name: inline-command + cmd: echo "Hello World" + ignore_errors: true + work_dir: /tmp + shell: bash + env: + foo: bar + + # Task reference. work_dir and env variables are passed down. + # Nested task referencing is supported and will result in a + # flat list of commands + - task: simple-1 + ignore_errors: true + work_dir: /tmp + register: results + env: + foo: bar + + - name: output + cmd: echo $results_stdout ``` ## Files @@ -438,9 +421,6 @@ SAKE_SSH_CONFIG SAKE_KNOWN_HOSTS_FILE Override known_hosts file path -SAKE_PASSWORD - Override SSH password - NO_COLOR If this env variable is set (regardless of value) then all colors will be disabled ``` diff --git a/docs/development.md b/docs/development.md index 906dcb1..54e1610 100644 --- a/docs/development.md +++ b/docs/development.md @@ -4,7 +4,7 @@ ### Prerequisites -- [go 1.18 or above](https://golang.org/doc/install) +- [go 1.19 or above](https://golang.org/doc/install) - [goreleaser](https://goreleaser.com/install/) (optional) - [golangci-lint](https://github.com/golangci/golangci-lint) (optional) @@ -49,7 +49,7 @@ The following workflow is used for releasing a new `sake` version: - `make benchmark` 4. Verify build works (especially windows build) - `make build-all` -5. Generate manpage +5. Update `config-reference.md` and `config.man` if any config changes and generate manpage - `make gen-man` 6. Update `Makefile` and `CHANGELOG.md` with correct version, and add all changes to `CHANGELOG.md` 7. Squash-merge to main with `Release vx.y.z` and description of changes @@ -57,6 +57,15 @@ The following workflow is used for releasing a new `sake` version: - Create a git tag with release notes - Trigger a build in Github that builds cross-platform binaries and generates release notes of changes between current and previous tag +## Overview of How Sake Works + +1. Parse & validate CLI arguments +2. Parse `sake` config files and create config, inventory, tasks, specs, and target states +3. Create clients for remote and local task execution for the selected hosts/tasks +4. Execute tasks on remote/local hosts +6. Disconnect from remote hosts +7. Print any output (results, reports, errors, etc.) + ## Dependency Graph Create SVG dependency graphs using graphviz and [goda](https://github.com/loov/goda). diff --git a/docs/error-handling.md b/docs/error-handling.md new file mode 100644 index 0000000..e7244bc --- /dev/null +++ b/docs/error-handling.md @@ -0,0 +1,145 @@ +# Error Handling + +`sake` has multiple ways to deal with errors, and depending on the task execution strategy and which error flags are used, you will get different behavior. The following properties can be used to control task execution and how errors are handled: + +- **any_errors_fatal**: stop task execution on all servers on error, this is the same as setting `max_fail_percentage` to zero + - note that when you run tasks in parallel, it will wait for the current tasks to finish before aborting +- **max_fail_percentage**: stop task execution on all servers when threshold reached +- **ignore_errors**: continue task execution on errors +- **ignore_unreachable**: ignore unreachable hosts + +## Aborting on the First Error + +If you wish to abort all tasks on all errors in case an error is encountered for any task, use the flag `--any-errors-fatal` or specify it in the task `spec`. + +- `any-errors-fatal` set to false + + ```bash + $ sake run fatal --any-errors-fatal=false + + TASKS ******************************************************************* + + Host | Task-0 | Task-1 | Task-2 + ------------+--------+------------------------------+-------- + 172.24.2.2 | 123 | | + | | Process exited with status 1 | + ------------+--------+------------------------------+-------- + 172.24.2.2 | 123 | | + | | Process exited with status 1 | + + RECAP ******************************************************************* + + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + --------------------------------------------------------------------- + Total ok=2 unreachable=0 ignored=0 failed=2 skipped=2 + ``` + +- `any-errors-fatal` set to true + ```bash + $ sake run fatal --any-errors-fatal=true + + TASKS ****************************************************************** + + Host | Task-0 | Task-1 | Task-2 + ------------+--------+------------------------------+-------- + 172.24.2.2 | 123 | | + | | Process exited with status 1 | + ------------+--------+------------------------------+-------- + 172.24.2.2 | | | + + RECAP ****************************************************************** + + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=0 skipped=3 + --------------------------------------------------------------------- + Total ok=1 unreachable=0 ignored=0 failed=1 skipped=4 + ``` + +## Ignoring Task Errors + +If you wish to continue task execution even if an error is encountered, use the flag `--ignore-errors` or specify it in the task `spec`. + +- `ignore-errors` set to false + ```bash + $ sake run errors --ignore-errors=false + + TASKS ****************************************************************** + + Host | Task-0 | Task-1 | Task-2 + ------------+--------+------------------------------+-------- + 172.24.2.2 | 123 | | + | | Process exited with status 1 | + ------------+--------+------------------------------+-------- + 172.24.2.2 | 123 | | + | | Process exited with status 1 | + + RECAP ****************************************************************** + + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + --------------------------------------------------------------------- + Total ok=2 unreachable=0 ignored=0 failed=2 skipped=2 + ``` + +- `ignore-errors` set to true + ```bash + $ sake run errors --ignore-errors=true + + TASKS ******************************************************************** + + Host | Task-0 | Task-1 | Task-2 + ------------+--------+------------------------------+-------- + 172.24.2.2 | 123 | | 321 + | | Process exited with status 1 | + ------------+--------+------------------------------+-------- + 172.24.2.2 | 123 | | 321 + | | Process exited with status 1 | + + RECAP ******************************************************************** + + 172.24.2.2 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + --------------------------------------------------------------------- + Total ok=4 unreachable=0 ignored=2 failed=0 skipped=0 + ``` + +## Ignoring Unreachable Hosts + +Sometimes you want to ignore remote hosts which are unreachable, for instance if it's a host that is flaky, then you can either use the `--ignore-unreachable` flag or specify it in the task `spec`. + +- `ignore-unreachable` set to false + ```bash + $ sake run unreachable --ignore-unreachable=false + + Unreachable Hosts + + Server | Host | User | Port | Error + --------+--------------+------+------+----------------------------------------------------- + list-1 | 172.24.2.222 | test | 22 | dial tcp 172.24.2.222:22: connect: no route to host + ``` + +- `ignore-unreachable` set to false + ```bash + $ sake run unreachable --ignore-unreachable=true + + Unreachable Hosts + + Server | Host | User | Port | Error + --------+--------------+------+------+----------------------------------------------------- + list-1 | 172.24.2.222 | test | 22 | dial tcp 172.24.2.222:22: connect: no route to host + + TASKS ************************************************************************************** + + Host | Task-0 + ------------+-------- + 172.24.2.2 | 123 + + RECAP ************************************************************************************** + + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.222 ok=0 unreachable=1 ignored=0 failed=0 skipped=0 + ----------------------------------------------------------------------- + Total ok=1 unreachable=1 ignored=0 failed=0 skipped=0 + + ``` diff --git a/docs/examples.md b/docs/examples.md index f4ad6ab..5c6fee9 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -32,7 +32,7 @@ tasks: output: table target: all: true - cmd: echo $SAKE_SERVER_HOST + cmd: echo $S_HOST info: desc: get remote info @@ -61,29 +61,16 @@ $ sake list servers # Describe task $ sake describe task print-host -Task: print-host -Name: Host -Desc: print host -Local: false -Theme: default -Target: - All: true - Servers: - Tags: -Spec: - Output: table - Parallel: false - AnyErrorsFatal: false - IgnoreErrors: false - IgnoreUnreachable: false - OmitEmpty: false -Env: - SAKE_TASK_ID: print-host - SAKE_TASK_NAME: Host - SAKE_TASK_DESC: print host - SAKE_TASK_LOCAL: false -Cmd: - echo $SAKE_HOST +task: print-host +name: Host +desc: print host +theme: default +target: + all: true +spec: + output: table +cmd: + echo $S_HOST # Run a task targeting servers with tag `local` @@ -116,7 +103,7 @@ TASK (3/3) Command ******************** localhost | Done # Run runtime defined command for all servers -$ sake exec --all --output table --parallel 'cd ~ && ls -al | wc -l' +$ sake exec --all --output table --strategy=free 'cd ~ && ls -al | wc -l' Server | Output -----------+-------- @@ -181,10 +168,11 @@ specs: info: output: table ignore_errors: true - omit_empty: true + omit_empty_rows: true + omit_empty_columns: true any_fatal_errors: false ignore_unreachable: true - parallel: true + strategy: free targets: all: @@ -204,7 +192,7 @@ tasks: name: Host desc: print host target: all - cmd: echo $SAKE_SERVER_HOST + cmd: echo $S_HOST print-hostname: name: Hostname @@ -312,7 +300,7 @@ targets: specs: info: output: table - parallel: true + strategy: free ignore_errors: true ignore_unreachable: true any_errors_fatal: false @@ -328,7 +316,7 @@ tasks: desc: ping server target: all local: true - cmd: ping $SAKE_SERVER_HOST -c 2 + cmd: ping $S_HOST -c 2 # Setup setup-pi: @@ -458,7 +446,7 @@ tasks: desc: print host spec: info target: all - cmd: echo $SAKE_SERVER_HOST + cmd: echo $S_HOST print-hostname: name: Hostname @@ -558,7 +546,7 @@ tasks: SRC: "" DEST: "" local: true - cmd: rsync --recursive --verbose --archive --update $SRC $SAKE_SERVER_HOST:$DEST + cmd: rsync --recursive --verbose --archive --update $SRC $S_HOST:$DEST # Docker @@ -567,7 +555,7 @@ tasks: env: NAME: "" tty: true - cmd: ssh -t $SAKE_SERVER_USER@$SAKE_SERVER_HOST "docker exec -it $NAME bash" + cmd: ssh -t $S_USER@$S_HOST "docker exec -it $NAME bash" docker-start: desc: create and start services @@ -637,7 +625,7 @@ tasks: ### docker-compose.yaml -This is a docker-compose file used to start multiple Docker containers. Currently three servies are ran `syncthing`, `mealie`, and `Node-RED`. +This is a docker-compose file used to start multiple Docker containers. Currently three services are ran `syncthing`, `mealie`, and `Node-RED`. ```yaml title="docker-compose.yaml" version: "3.9" diff --git a/docs/installation.md b/docs/installation.md index cbc0ec3..57f1318 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -22,7 +22,7 @@ ## Building From Source -Requires [go 1.18 or above](https://golang.org/doc/install). +Requires [go 1.19 or above](https://golang.org/doc/install). 1. Clone the repo 2. Build and run the executable diff --git a/docs/introduction.md b/docs/introduction.md index bb5aa81..cc9f248 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -52,7 +52,7 @@ tasks: print-host: name: Host desc: print host - cmd: echo $SAKE_SERVER_HOST + cmd: echo $S_HOST print-os: name: OS @@ -73,7 +73,7 @@ tasks: tags: [remote] spec: output: table - parallel: true + strategy: free ignore_errors: true ignore_unreachable: true tasks: diff --git a/docs/inventory.md b/docs/inventory.md new file mode 100644 index 0000000..46259a8 --- /dev/null +++ b/docs/inventory.md @@ -0,0 +1,82 @@ +# Inventory + +Inventory is a collection of hosts that you can execute tasks on. There are 4 ways to specify hosts: + +```yaml +servers: + # Single Host + single-1: + host: 192.168.1.1 + user: test + port: 33 + bastion: test@192.168.0.1:22 + tags: [web] + + # Multipe hosts using a list + many-1: + hosts: + - test@192.168.1.1:22 + - test@192.168.1.2:22 + tags: [web, prod] + + # Multiple hosts using a ranges + many-2: + hosts: test@192.168.1.[1:2]:22 + tags: [web, prod] + + # Multiple hosts by invoking a shell command + many-3: + inventory: echo test@192.168.1.1:22 test@192.168.1.2:22 + tags: [web, prod] +``` + +To target the hosts in a task there's multiple ways: + +- **all**: target +- **servers**: a list of single hosts or group of hosts + - supports range as well, for instance `--server "list[0:1]"`, select first and second host +- **tags**: target hosts that have a specific tag +- **regex**: target hosts on host regex +- **invert**: invert matching on hosts + +Furthermore, to limit the number of targetted servers, you can use one of following properties: + +- **limit**: limit the number of targetted hosts +- **limit_p**: limit the number of targetted hosts in percentage + +## Provide Identity and Password Credentials + +By default `sake` will attempt to load identity keys from an SSH agent if it's running in the background. However, if you wish to provide credentials manually, you can do so by (first takes precedence): + +1. setting `--identity-file` and/or `--password` flags +2. specifying it in the server definition + +The type of auth used is determined by: + +- if `identity-file` and `password` are provided, then it assumes password protected identity key +- if only `identity-file` is provided, then it first tries without passphrase, if file is encrypted, it will prompt for passphrase +- if only `password` is provided, then it assumes password protected auth + +```yaml +servers: + server-1: + host: server-1.lan + identity_file: id_rsa + password: $(echo $MY_SECRET_PASSWORD) +``` + +You can also define entries in your `~/.ssh/config` file and `sake` will try to resolve them. + +## Known Hosts + +By default a `known_hosts` file is used to verify host connections. If you wish to disable verification, set the global property `disable_verify_host` to true: + +```yaml +disable_verify_host: true +``` + +The default location of the known hosts file is `$HOME/.ssh/known_hosts`. If you wish change this to another file, then set the global property `known_hosts_file` to your desired filepath: + +```yaml +known_hosts_file: ./known_hosts +``` diff --git a/docs/output.md b/docs/output.md new file mode 100644 index 0000000..781a709 --- /dev/null +++ b/docs/output.md @@ -0,0 +1,198 @@ +# Output + +`sake` supports different output formats for tasks. By default it will use `text` output, but it's possible to change this via the `--output` flag or specify it in the task `spec`. + +The following output formats are available: + +- **text** (default), use this when you want streamed output to terminal + ``` + TASK (1/2) [task-0] *********** + + 172.24.2.2 | ping + 172.24.2.2 | ping + + TASK (2/2) [task-1] *********** + + 172.24.2.2 | pong + 172.24.2.2 | pong + ``` +- **table**, useful when you have many hosts but few tasks + ``` + Host | Task-0 | Task-1 + ------------+--------+-------- + 172.24.2.2 | ping | pong + ------------+--------+-------- + 172.24.2.2 | ping | pong + ``` +- **table-2**, useful when you have many tasks but few hosts + ``` + Task | 172.24.2.2 | 172.24.2.2 + --------+------------+------------ + task-0 | ping | ping + --------+------------+------------ + task-1 | pong | pong + ``` +- **table-3**, useful when you want separate tables per host + ``` + 172.24.2.2 + + Task-0 | Task-1 + --------+-------- + ping | pong + + 172.24.2.2 + + Task-0 | Task-1 + --------+-------- + ping | pong + ``` +- **table-4**, useful when you have many hosts and many tasks + ``` + Task | 172.24.2.2 + --------+------------ + task-0 | ping + --------+------------ + task-1 | pong + + Task | 172.24.2.2 + --------+------------ + task-0 | ping + --------+------------ + task-1 | pong + ``` +- **html** + ```html + + + + + + + + + + + + + + + + + + + + +
hosttask-0task-1
172.24.2.2pingpong
172.24.2.2pingpong
+ ``` +- **markdown** + ```markdown + | host | task-0 | task-1 | + |:--- |:--- |:--- | + | 172.24.2.2 | ping | pong | + | 172.24.2.2 | ping | pong | + | | | | + ``` +- **json** + ```json + [ + { + "host": "172.24.2.2", + "task-0": "ping", + "task-1": "pong" + }, + { + "host": "172.24.2.2", + "task-0": "ping", + "task-1": "pong" + } + ] + ``` +- **csv** + ```csv + host,task-0,task-1 + 172.24.2.2,ping,pong + 172.24.2.2,ping,pong + ``` +- none + +## Omit Empty Table Rows and Columns + +If you wish to omit rows/columns that return empty outputs, you can do so via the `--omit-empty-rows`/`--omit-empty-columns` flag or specify it in the task `spec`. Note, this only works for the tables, json, csv, markdown, and html. + +See below for an example: + +```bash +$ sake run empty -s server-3,server-4 -o table + +TASKS ******************************* + + Host | Task-0 | Task-1 +------------+--------+-------- + 172.24.2.4 | 123 | +------------+--------+-------- + 172.24.2.5 | | + +$ sake run empty -s server-3,server-4 -o table --omit-empty-rows --omit-empty-columns + +TASKS ******************************* + + Host | Task-0 +------------+-------- + 172.24.2.4 | 123 +``` + +## Print Reports + +`sake` comes with a few reports that gives you an overview of task execution: + +The available reports are: + +- **recap**: show basic report (default) +- **rc**: show return code for each host and task +- **task**: show task status for each host and task +- **time**: show time report for each host and task +- **all**: show available reports + +```bash +$ sake run task --report=all + +TASKS ************************************************************************************** + + Host | Task-0 | Task-1 | Task-2 +------------+------------------------------+------------------------------+-------- + 172.24.2.2 | foo | bar | xyz +------------+------------------------------+------------------------------+-------- + 172.24.2.2 | | | + | Process exited with status 1 | Process exited with status 1 | + +RETURN CODES ******************************************************************************* + + host task-0 task-1 task-2 +---------------------------------------- + 172.24.2.2 0 0 0 + 172.24.2.2 1 1 + +TASK STATUS ******************************************************************************** + + host task-0 task-1 task-2 +------------------------------------------ + 172.24.2.2 ok ok ok + 172.24.2.2 ignored failed skipped + +TIME *************************************************************************************** + + host task-0 task-1 task-2 Total +------------------------------------------------ + 172.24.2.2 0.09 s 0.01 s 0.01 s 0.10 s + 172.24.2.2 0.08 s 0.01 s 0.09 s +------------------------------------------------ + Total 0.17 s 0.02 s 0.01 s 0.20 s + +RECAP ************************************************************************************** + + 172.24.2.2 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=0 unreachable=0 ignored=1 failed=1 skipped=1 +--------------------------------------------------------------------- + Total ok=3 unreachable=0 ignored=1 failed=1 skipped=1 +``` + diff --git a/docs/performance.md b/docs/performance.md new file mode 100644 index 0000000..849dd48 --- /dev/null +++ b/docs/performance.md @@ -0,0 +1,153 @@ +# Performance + +These are benchmarks for sake. The implementation is in a separate repository at [github.com/alajmo/sake-performance](https://github.com/alajmo/sake-performance). + +sake is between 6 and 8 faster than pyinfra and between 4 and 18 times faster than Ansible, depending on the number of hosts. + +The benchmarks are generated by running each test 20 times and taking an average for each of the following metrics: + +- **Elapsed Time**: The elapsed real (wall clock) time (seconds) used by the process +- **CPU**: Percentage of the CPU that this job got (`(user + sys time) / tot time` +- **Memory**: Maximum memory (Megabyte) usage of the process during its lifetime + +## Ping + +![time](/img/time-1-short.png) +![time](/img/time-1.png) + +## Modules + +![time](/img/time-2-short.png) +![time](/img/time-2.png) + +## Test Case 1 + +A simple ping. + +### Graphs + +![time](/img/time-1.png) +![cpu](/img/cpu-1.png) +![mem](/img/mem-1.png) + +### Data + +Elapsed Time (seconds) + +| name | sake | pyinfra | ansible | +| --- | --- | --- | --- | +| 1 | 0.143 | 0.888 | 0.602 | +| 3 | 0.125 | 0.948 | 0.621 | +| 5 | 0.131 | 0.951 | 0.637 | +| 8 | 0.157 | 0.972 | 0.671 | +| 10 | 0.137 | 0.968 | 0.701 | +| 25 | 0.158 | 1.117 | 0.957 | +| 50 | 0.175 | 1.364 | 1.419 | +| 100 | 0.320 | 1.879 | 2.463 | +| 200 | 0.559 | 2.914 | 4.308 | +| 300 | 0.826 | 4.050 | 6.240 | +| 400 | 1.112 | 5.148 | 8.137 | +| 500 | 1.400 | 6.332 | 10.152 | + +CPU (%) + +| name | sake | pyinfra | ansible | +| --- | --- | --- | --- | +| 1 | 16 | 89 | 86 | +| 3 | 19 | 87 | 95 | +| 5 | 28 | 87 | 103 | +| 8 | 34 | 90 | 113 | +| 10 | 34 | 92 | 120 | +| 25 | 48 | 96 | 183 | +| 50 | 48 | 103 | 236 | +| 100 | 53 | 113 | 311 | +| 200 | 52 | 126 | 332 | +| 300 | 51 | 136 | 357 | +| 400 | 49 | 144 | 361 | +| 500 | 49 | 152 | 377 | + +Memory (MB) + +| name | sake | pyinfra | ansible | +| --- | --- | --- | --- | +| 1 | 15 | 56 | 55 | +| 3 | 16 | 56 | 55 | +| 5 | 18 | 56 | 55 | +| 8 | 21 | 58 | 56 | +| 10 | 21 | 58 | 55 | +| 25 | 25 | 60 | 55 | +| 50 | 27 | 61 | 56 | +| 100 | 29 | 63 | 58 | +| 200 | 40 | 68 | 61 | +| 300 | 48 | 72 | 65 | +| 400 | 59 | 76 | 70 | +| 500 | 66 | 80 | 76 | + +## Test Case 2 + +The following tasks are ran: + +1. Add a user +2. Add a file +3. Copy a file + +Note, after the first command is ran, the subsequent commands won't do anything since the user and files already exists, so all tasks are idempotent. + +### Graphs + +![time](/img/time-2.png) +![cpu](/img/cpu-2.png) +![mem](/img/mem-2.png) + +### Data + +Elapsed Time (seconds) + +| name | sake | pyinfra | ansible | +| --- | --- | --- | --- | +| 1 | 0.156 | 1.568 | 1.174 | +| 3 | 0.159 | 1.487 | 1.210 | +| 5 | 0.175 | 1.456 | 1.268 | +| 8 | 0.203 | 1.489 | 1.436 | +| 10 | 0.246 | 1.489 | 1.535 | +| 25 | 0.274 | 2.180 | 2.742 | +| 50 | 0.439 | 2.982 | 4.717 | +| 100 | 0.664 | 4.711 | 8.947 | +| 200 | 1.006 | 8.371 | 17.410 | +| 300 | 1.425 | 12.108 | 26.110 | +| 400 | 1.911 | 15.797 | 34.888 | +| 500 | 2.443 | 19.418 | 44.075 | + +CPU (%) + +| name | sake | pyinfra | ansible | +| --- | --- | --- | --- | +| 1 | 14 | 54 | 67 | +| 3 | 27 | 65 | 86 | +| 5 | 39 | 70 | 104 | +| 8 | 53 | 74 | 124 | +| 10 | 61 | 76 | 138 | +| 25 | 70 | 77 | 236 | +| 50 | 111 | 86 | 258 | +| 100 | 168 | 95 | 305 | +| 200 | 147 | 101 | 324 | +| 300 | 138 | 105 | 339 | +| 400 | 135 | 109 | 352 | +| 500 | 157 | 112 | 365 | + +Memory (MB) + +| name | sake | pyinfra | ansible | +| --- | --- | --- | --- | +| 1 | 15 | 55 | 55 | +| 3 | 19 | 57 | 55 | +| 5 | 20 | 57 | 56 | +| 8 | 21 | 58 | 55 | +| 10 | 22 | 59 | 55 | +| 25 | 25 | 61 | 56 | +| 50 | 29 | 63 | 56 | +| 100 | 34 | 67 | 59 | +| 200 | 45 | 74 | 63 | +| 300 | 55 | 80 | 68 | +| 400 | 66 | 89 | 73 | +| 500 | 76 | 96 | 79 | diff --git a/docs/project-background.md b/docs/project-background.md deleted file mode 100644 index a8b5067..0000000 --- a/docs/project-background.md +++ /dev/null @@ -1,76 +0,0 @@ -# Project Background - -> To configure servers, all you need is bash scripting, environment variables, and some know-how. - -`sake` came about because I needed a simple tool to manage my servers. There's tons of software in this category, Ansible, Puppet, Chef, Salt, Sup, and probably many more. However, most of them are geared towards enterprise server management and are often not the most ergonomic. So, `sake` was born, an ergonomic CLI tool to configure servers. - -## Premise - -The premise is you have a bunch of servers and want the following: - -1. A central place for your servers, containing name, host, and a small description of the servers -2. Ability to run ad-hoc commands (perhaps `hostnamectl` to see system hostname) on 1, a subset, or all of the servers -3. Ability to run defined tasks on 1, a subset, or all of the servers -4. Ability to get an overview of 1, a subset, or all of the servers and tasks - -## Design - -`sake` prioritizes simplicity and flexibility, principles that are at odds with each other at times: - -1. Simplicity - both the implementation and the UX is designed to be simple. However, this rule can be bent to some degree if the reward is substantial -2. Flexibility - provide the user with the ability to shape `sake` to their user case, instead of providing an opinionated setup - -With these principles in mind, I've elected not to create a complex DSL (see Terraform, Puppet, Ansible, etc.), but instead just add a few primitives and let the user leverage their existing sysadmin knowledge to create their ideal setup. This results in not forcing users to learn yet another DSL, avoiding continuous lookup of `sake` commands, and updating `sake` to get new features. - -It does, however, push complexity onto the user, for instance, there's no built-in primitive to download files and the user must define a task to do so. It would be foolish for me to aim for feature parity with 3rd party software like rsync for downloading files (there are over 150 flags for rsync). -In this sense, `sake` follows the Unix principle: - -- Write programs that do one thing and do it well - run commands on multiple servers -- Write programs to work together - invoke other programs to do your bidding (rsync) -- Write programs to handle text streams - output of all `sake` commands is just text - -## Comparisons - -A lot of the alternatives to `sake` are meant to be used in large teams, which often results in: - -1. Opinionated and implicit file structure that the user is required to know -2. Some of them are not daemon-less, there's a background process keeping track of application state (see Kubernetes, Chef, Puppet), which increases complexity and debuggability -3. Lots of boilerplate (with Ansible there are more than 8 directories/files per role that you can use to customize roles: tasks, handlers, templates, files, vars, defaults, meta, module_utils, lookup_plugins, library, etc.) - -For DIY hobby projects, where you're only interested in: - -1. Connecting to a machine -2. Installing some software -3. Starting a service -4. Querying some data (uptime, disk size, etc.) -5. Uploading/Downloading files - -the alternatives can be overkill. - -In terms of features, `sake` mostly resembles [Sup](https://github.com/pressly/sup), an excellent deployment tool, which `sake` has drawn inspiration from. There are however some notable differences: - -- `sake` has a more extensive and easy to use filtering system -- `sake` provides both text and table output -- `sake` has auto-completion -- `sake` has a sub-command to easily ssh into servers -- `sake` supports tasks -- `sake` has native import capability (in Sup you're left with manually invoking the Sup binary, which is by design as they want to keep it as close to `make` as possible it seems) -- `sup` is not maintained anymore -- Better CLI ergonomics in my opinion, `sake run -s server-1` versus `sup ` (I often forget which one comes first, network or task) - -### User Experience - -These features make using `sake` feel more effortless: - -- Single binary -- Rich auto-completion -- Edit the `sake` config file via the `sake edit` command, which opens up the config file in your preferred editor -- Multiple output formats (text, table, HTML, Markdown) -- Default target filtering for tasks - -## Similar Software - -- [Ansible](https://www.ansible.com) -- [Chef](https://www.chef.io) -- [Puppet](https://puppet.com) -- [Sup](https://pressly.github.io/sup) diff --git a/docs/recipes.md b/docs/recipes.md index 3c4e9ce..f7295ed 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -2,79 +2,6 @@ A list of useful recipes. -- [Specify multiple hosts](#specify-multiple-hosts) -- [Validate Config](#validate-config) -- [Upload File](#upload-file) -- [Download File](#download-file) -- [SSH to Server Using `sake`](#ssh-to-server-using-sake) -- [List Servers, Tasks and Tags](#list-servers-tasks-and-tags) -- [Describe Servers and Tasks](#describe-servers-and-tasks) -- [Edit a Config, Task or Server via `sake`](#edit-a-config-task-or-server-via-sake) -- [Run Command and SSH Afterwords](#run-command-and-ssh-afterwords) -- [Create SSH Tunnel / Port Forward](#create-ssh-tunnel--port-forward) -- [Attach to a Docker Container on a Remote Server](#attach-to-a-docker-container-on-a-remote-server) -- [Run a Local Script on a Remote Server](#run-a-local-script-on-a-remote-server) -- [Replace Current Process](#replace-current-process) -- [Run Server Tasks in Parallel](#run-server-tasks-in-parallel) -- [Aborting on the First Error](#aborting-on-the-first-error) -- [Ignoring Task Errors](#ignoring-task-errors) -- [Ignoring Unreachable Hosts](#ignoring-unreachable-hosts) -- [Omit Table Rows That Return Empty Output](#omit-table-rows-that-return-empty-output) -- [Change Task Output](#change-task-output) -- [Change Working Directory](#change-working-directory) -- [Provide Identity and Password Credentials](#provide-identity-and-password-credentials) -- [Disable Verify Host](#disable-verify-host) -- [Change known_hosts Path](#change-known_hosts-path) -- [Pass Variables from CLI](#pass-variables-from-cli) -- [List Default Environment Variables](#list-default-variables) -- [Change Default Behavior of `sake`](#change-default-behavior-of-sake) -- [Invoke `sake` From Any Directory](#invoke-sake-from-any-directory) -- [Import a Default User Config for Any `sake` Project](#import-a-default-user-config-for-any-sake-project) -- [What's the Difference Between TTY, Attach and Local?](#whats-the-difference-between-tty-attach-and-local) -- [Disable Colors](#disable-colors) -- [Performing a Dry Run](#performing-a-dry-run) -- [Modify Theme](#modify-theme) - -## Specify multiple hosts - -In `sake` there's 3 ways to specify multiple hosts: - -```yaml -servers: - # Option 1 - many-1: - hosts: - - samir@192.168.1.1:22 - - samir@192.168.1.2:22 - - # Option 2 - many-2: - hosts: samir@192.168.1.[1:2]:22 - - # Option 3 - many-3: - inventory: echo samir@192.168.1.1:22 samir@192.168.1.2:22 -``` - -```bash -sake list servers - - Server | Host -----------+------------- - many-1-0 | 192.168.1.1 -----------+------------- - many-1-1 | 192.168.1.2 -----------+------------- - many-2-0 | 192.168.1.1 -----------+------------- - many-2-1 | 192.168.1.2 -----------+------------- - many-3-0 | 192.168.1.1 -----------+------------- - many-3-1 | 192.168.1.2 - -``` - ## Validate Config To check for syntax errors and invalid configurations run: @@ -94,7 +21,7 @@ upload: SRC: "" DEST: "" local: true # Command should be run from local host - cmd: rsync --recursive --verbose --archive --update $SRC $SAKE_SERVER_HOST:$DEST + cmd: rsync --recursive --verbose --archive --update $SRC $S_HOST:$DEST ``` Then you can refer to the `upload` task: @@ -135,7 +62,7 @@ download: SRC: "" DEST: "" local: true # Command should be run from local host - cmd: rsync --recursive --verbose --archive --update $SAKE_SERVER_HOST:$SRC $DEST + cmd: rsync --recursive --verbose --archive --update $S_HOST:$SRC $DEST ``` Then you can refer to the `download` task: @@ -167,98 +94,6 @@ Note that rsync is required both on the client and remote machine. You can SSH to any server via `sake ssh `. -## List Servers, Tasks and Tags - -The list sub-command will list servers, tasks, and tags in a table, HTML, or Markdown format. - -- **Servers**: To list servers run `sake list servers [--tags=] [server]` - - ```bash - $ sake list servers --tags remote - - Server | Host | Tag | Description - -----------+--------------+------------+------------------------ - server-1 | server-1.lan | remote, pi | hosts mealie, node-red - pihole | pihole.lan | remote, pi | runs pihole - ``` - -- **Tasks**: To list tasks run `sake list tasks [task]` - - ```bash - $ sake list tasks - - Task | Description - -------------+------------------------------------- - ping | ping server - Host | print host - Hostname | print hostname - OS | print OS - Kernel | Print kernel version - ``` - - -- **Tags**: To list tags run `sake list tags [tag]` - - ```bash - $ sake list tags - - Tag | Server - --------+----------- - local | localhost - remote | server-1 - | pihole - pi | server-1 - | pihole - ``` - -## Describe Servers and Tasks - -The describe sub-command describes servers and tasks. - -- **Servers**: To describe all servers run `sake describe servers [--tags=] [server]` - - ```bash - $ sake describe server pihole - - Name: pihole - User: samir - Host: pihole.lan - Port: 22 - WorkDir: - Desc: runs pihole - Tags: remote, pi - ``` - -- **Tasks**: To describe all tasks run `sake describe tasks [task]` - - ```bash - $ sake describe task info - - Task: info - Name: info - Desc: get remote info - WorkDir: - Theme: default - Target: - All: true - Spec: - Output: table - Parallel: true - IgnoreErrors: true - IgnoreUnreachable: true - Tasks: - - OS: print OS - - Kernel: Print kernel version - - Disk: print disk usage - - Memory: print memory stats - - CPU: print memory stats - - Uptime: print uptime - ``` - -## Edit a Config, Task or Server via `sake` - -You can open up your preferred editor and edit a `sake` config directly via `sake edit [task|server] [name]`. For this to work, the `EDITOR` environment variable must be set. - ## Run Command and SSH Afterwords Sometimes you want to run a command and then `ssh` into the server: @@ -293,7 +128,7 @@ ssh-tunnel: env: LOCAL: REMOTE: - cmd: ssh $SAKE_SERVER_USER@$SAKE_SERVER_HOST -N -L $LOCAL:localhost:$REMOTE + cmd: ssh $S_USER@$S_HOST -N -L $LOCAL:localhost:$REMOTE ``` Then run: @@ -312,7 +147,7 @@ docker-exec: env: NAME: "" tty: true # Replacing the current process is necessary since SSH requires TTY if you wish to exec to a container - cmd: ssh -t $SAKE_SERVER_USER@$SAKE_SERVER_HOST "docker exec -it $NAME bash" + cmd: ssh -t $S_USER@$S_HOST "docker exec -it $NAME bash" ``` Then you can run: @@ -340,13 +175,13 @@ script: temp_file="$(mktemp /tmp/$FILE.XXXXXXXXX -u)" # Upload script - rsync --compress --recursive --archive --update $FILE $SAKE_SERVER_HOST:$temp_file + rsync --compress --recursive --archive --update $FILE $S_HOST:$temp_file # Run script - ssh $SAKE_SERVER_USER@$SAKE_SERVER_HOST "$temp_file" + ssh $S_USER@$S_HOST "$temp_file" # Remove script - ssh $SAKE_SERVER_USER@$SAKE_SERVER_HOST "rm $temp_file" + ssh $S_USER@$S_HOST "rm $temp_file" ``` Then run: @@ -365,251 +200,6 @@ echo: cmd: echo 123 ``` -## Run Server Tasks in Parallel - -Sometimes you wish to run tasks in parallel, especially when you're just querying information from the machines. In this case, you can use the `--parallel` flag or specify it in the task `spec`. Note that if your tasks has multiple commands, the commands will still execute sequentially for each server, it's just that the overall server execution will happen in parallel. - -```yaml -tasks: - print-kernel: - name: Kernel - desc: Print kernel version - spec: - parallel: true - cmd: uname -r | awk -v FS='-' '{print $1}' -``` - -```bash -$ sake run print-kernel --all -``` - -## Aborting on the First Error - -If you wish to abort all tasks on all errors in case an error is encountered for any task, use the flag `--any-errors-fatal` or specify it in the task `spec`. - -```yaml -fatal: - spec: - any_errors_fatal: true - tasks: - - cmd: echo 123 - - cmd: exit 1 - - cmd: echo 321 -``` - -See example: - -```bash -# any-errors-fatal set to false -$ sake run fatal --all --output table --any-errors-fatal=false - - Server | Output | Output | Output ------------+--------+------------------------------+-------- - localhost | 123 | | - | | exit status 1 | - server-1 | 123 | | - | | Process exited with status 1 | - pihole | 123 | | - | | Process exited with status 1 | - -# any-errors-fatal set to true -$ sake run fatal --all --output table --any-errors-fatal=true - -Server | Output | Output | Output -----------+--------+---------------+-------- -localhost | 123 | | - | | exit status 1 | -server-1 | | | -pihole | | | -``` - -## Ignoring Task Errors - -If you wish to continue task execution even if an error is encountered, use the flag `--ignore-errors` or specify it in the task `spec`. - -```yaml -errors: - spec: - ignore_errors: false - tasks: - - cmd: echo 123 - - cmd: exit 1 - - cmd: echo 321 -``` - -See example: - -```bash -# ignore-errors set to false -$ sake run errors --all --output table --ignore-errors=false - - Server | Output | Output | Output ------------+--------+------------------------------+-------- - localhost | 123 | | - | | exit status 1 | - server-1 | 123 | | - | | Process exited with status 1 | - pihole | 123 | | - | | Process exited with status 1 | - -# ignore-errors set to true -$ sake run errors --all --output table --ignore-errors=true - - Server | Output | Output | Output ------------+--------+-------------------------------+-------- - localhost | 123 | | 321 - | | exit status 65 | - server-1 | 123 | | 321 - | | Process exited with status 65 | - pihole | 123 | | 321 - | | Process exited with status 65 | -``` - -## Ignoring Unreachable Hosts - -Sometimes you want to ignore remote hosts which are unreachable, for instance if it's a host that is flaky, then you can either use the `--ignore-unreachable` flag or specify it in the task `spec`. - -```yaml -unreachable: - spec: - ignore_unreachable: false - cmd: echo 123 -``` - -See example: - -```bash -# ignore-unreachable set to false -$ sake run unreachable --all --output table --ignore-unreachable=false - -Unreachable Hosts - - Server | Host | User | Port | Error -----------+-------------+-------+------+---------------------------------------------------------------- - server-1 | server1.lan | samir | 22 | dial tcp: lookup server1.lan on x.y.z.k:33: no such host - -# ignore-unreachable set to true -$ sake run unreachable --all --output table --ignore-unreachable=true - -Unreachable Hosts - - Server | Host | User | Port | Error -----------+-------------+-------+------+---------------------------------------------------------------- - server-1 | server1.lan | samir | 22 | dial tcp: lookup server1.lan on 192.168.1.209:53: no such host - - Server | Unreachable ------------+------------- - localhost | 123 - pihole | 123 -``` - -## Omit Table Rows That Return Empty Output - -If you wish to omit rows that return empty outputs, you can do so via the `--omit-empty` flag or specify it in the task `spec`. -For instance, the below command will check if a directory `.ssh` exists in the users home directory. - -```yaml -empty: - spec: - omit_empty: false - cmd: | - if [[ -d ".ssh" ]] - then - echo "Exists" - fi -``` - -See example: - -```bash -# omit-empty set to false -$ sake run empty --all --output table --omit-empty=false - - Server | Empty ------------+-------- - localhost | - server-1 | Exists - pihole | Exists - -# omit-empty set to true -$ sake run empty --all --output table --omit-empty=true - - Server | Empty -----------+-------- - server-1 | Exists - pihole | Exists -``` - -## Change Task Output - -`sake` supports different output formats for tasks. By default it will use `text` output, but it's possible to change this via the `--output` flag or specify it in the task `spec`. Possible formats are `text`, `table`, `html` and `markdown`. - -```yaml -output: - spec: - output: text - tasks: - - cmd: echo "Hello world" - - cmd: echo "Bye world" - - cmd: echo "Hello again world" -``` - -See example: - -```bash -# output set to text -$ sake run output --all --output text - -TASK (1/3) Command ********************** - -server-1.lan | Hello world - -TASK (2/3) Command ********************** - -server-1.lan | Bye world - -TASK (3/3) Command ********************** - -server-1.lan | Hello again world - -# output set to table -$ sake run output --all --output table - - Server | Output | Output | Output -----------+-------------+-----------+------------------- - server-1 | Hello world | Bye world | Hello again world - -# output set to html -$ sake run output --all --output html - - - - - - - - - - - - - - - - - - -
serveroutputoutputoutput
server-1Hello worldBye worldHello again world
- -# output set to markdown -$ sake run output --all --output markdown - -| server | output | output | output | -|:--- |:--- |:--- |:--- | -| server-1 | Hello world | Bye world | Hello again world | -``` - - ## Change Shell You can change the default `shell` for tasks by setting the `shell` property in the global scope, server section or the task section (nested tasks/commands included). @@ -654,169 +244,6 @@ tasks: shell: bash # 1 ``` -## Change Working Directory - -You can change the default `work_dir` in the server section and the task section (nested tasks/commands included). -The order of precedence is as follows: - -1. task list -2. task -3. referenced task -4. server -5. default, which is the current working directory for local clients and `/home/user` for remote clients - -```yaml -servers: - localhost: - host: localhost - work_dir: "/opt" # 4 - local: true - -tasks: - work-ref: - name: pwd - work_dir: "/usr" # 3 - cmd: pwd - - work-dir: - work_dir: "/home" # 2 - tasks: - - task: work-ref - - - cmd: pwd - name: pwd - - - cmd: pwd - name: pwd - work_dir: "/" # 1 -``` - -See example: - -```bash -$ sake run work-dir --output table - - Server | Pwd | Pwd | Pwd ------------+-------+-------+----- - localhost | /home | /home | / - -# if we comment work_dir (# 2) then we get - - Server | Pwd | Pwd | Pwd ------------+------+------+----- - localhost | /usr | /opt | / -``` - -## Provide Identity and Password Credentials - -By default `sake` will attempt to load identity keys from an SSH agent if it's running in the background. However, if you wish to provide credentials manually, you can do so by (first takes precedence): - -1. setting `--identity-file` and/or `--password` flags -2. specifying it in the server definition - -The type of auth used is determined by: - -- if `identity-file` and `password` are provided, then it assumes password protected identity key -- if only `identity-file` is provided, then it first tries without passphrase, if file is encrypted, it will prompt for passphrase -- if only `password` is provided, then it assumes password protected auth - -```yaml -servers: - server-1: - host: server-1.lan - identity_file: id_rsa - password: $(echo $MY_SECRET_PASSWORD) -``` - -You can also define entries in your `~/.ssh/config` file and `sake` will try to resolve them. - -## Disable Verify Host - -By default a `known_hosts` file is used to verify host connections. If you wish to disable verification, set the global property `disable_verify_host` to true: - -```yaml -disable_verify_host: true -``` - -## Change known_hosts Path - -By default a `known_hosts` file is used to verify host connections. It's default location is `$HOME/.ssh/known_hosts`. If you wish change this to another file, then set the global property `known_hosts_file` to your desired filepath: - -```yaml -known_hosts_file: ./known_hosts -``` - -## Pass Variables from CLI - -If you wish to pass CLI variables you can run the following: - -```bash -$ sake run hello option=123 -``` - -```yaml -hello: - cmd: echo $option -``` - -## List Default Environment Variables - -Each task has access to a number of default environment variables. - -```yaml - env: - cmd: | - echo "# SERVER" - echo "SAKE_SERVER_NAME $SAKE_SERVER_NAME" - echo "SAKE_SERVER_DESC $SAKE_SERVER_DESC" - echo "SAKE_SERVER_TAGS $SAKE_SERVER_TAGS" - echo "SAKE_SERVER_HOST $SAKE_SERVER_HOST" - echo "SAKE_SERVER_USER $SAKE_SERVER_USER" - echo "SAKE_SERVER_PORT $SAKE_SERVER_PORT" - echo "SAKE_SERVER_LOCAL $SAKE_SERVER_LOCAL" - - echo - echo "# TASK" - echo "SAKE_TASK_ID $SAKE_TASK_ID" - echo "SAKE_TASK_NAME $SAKE_TASK_NAME" - echo "SAKE_TASK_DESC $SAKE_TASK_DESC" - echo "SAKE_TASK_LOCAL $SAKE_TASK_LOCAL" - - echo - echo "# CONFIG" - echo "SAKE_DIR $SAKE_DIR" - echo "SAKE_PATH $SAKE_PATH" - echo "SAKE_KNOWN_HOSTS_FILE $SAKE_KNOWN_HOSTS_FILE" -``` - -See example: - -```bash -$ sake run env -s server-1 - - Server | Env -----------+--------------------------------------------------------------- - server-1 | # SERVER - | SAKE_SERVER_NAME server-1 - | SAKE_SERVER_DESC server-1 description - | SAKE_SERVER_TAGS remote,pi - | SAKE_SERVER_HOST server-1.lan - | SAKE_SERVER_USER test - | SAKE_SERVER_PORT 22 - | SAKE_SERVER_LOCAL false - | - | # TASK - | SAKE_TASK_ID env - | SAKE_TASK_NAME - | SAKE_TASK_DESC print all default env variables - | SAKE_TASK_LOCAL false - | - | # CONFIG - | SAKE_DIR /tmp - | SAKE_PATH /tmp/sake.yaml - | SAKE_KNOWN_HOSTS_FILE -``` - ## Change Default Behavior of `sake` `sake` comes with default definitions for `specs`, `targets` and `themes` (see [config reference](config-reference) for their default values). This means when you run `sake list servers` or `sake run ` without specifying any spec/target/theme on the command line or in the config, it will use the default definition for those primitives. @@ -865,6 +292,10 @@ To disable colors from `sake`, either add the flag `--no-color` or set the envir If you wish to perform a dry run you can do so by adding the flag `--dry-run`. It will then only print out the task for each server. +## Edit a Config, Task or Server via `sake` + +You can open up your preferred editor and edit a `sake` config directly via `sake edit [task|server] [name]`. For this to work, the `EDITOR` environment variable must be set. + ## Modify Theme `sake` allows you to modify the output of tasks by creating themes for different situations. You can do so either inline when defining tasks, refer to a theme in from the global `themes` definition, or provide the `--theme` flag. diff --git a/docs/roadmap.md b/docs/roadmap.md index c652be0..2f3ac50 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,42 +1,33 @@ # Roadmap -`sake` is under active development. Before **v1.0.0**, I want to finish the following tasks, some miscellaneous fixes and improve code documentation: +`sake` is under active development. Before **v1.0.0**, I want to finish the following tasks, some miscellaneous fixes, improve code documentation and refactor: -- [x] Improve servers - - [x] Resolve hostnames from ssh_config - - [x] Support Bastion/Jumphost - - [x] Define multiple hosts without creating individual servers - - [x] Dynamically fetch hosts - - [x] Regex filtering of servers - - [x] Support glob pattern for Hosts (`Host *`) - - [x] Support resolving Includes in ssh config (`~/.ssh/config`) - - [x] Add limit and limit-p flag/target - - [x] Add filtering servers on host regex - - [x] Add invert flag on filtering servers - -- [x] Improve output and add new table outputs - - [x] Tasks in 1st column, output in 2nd, one table per server - - [x] Tasks in column, project output in row - -- [ ] Improve tasks - - [ ] Return correct error exit codes when running tasks - - [x] serial playbook - - [ ] parallel playbook - - [ ] Omit certain tasks from auto-completion and/or being called directly (mainly tasks which are called by other tasks) - - [ ] Repress certain task output if exit code is 0, otherwise displayed - - [ ] Summary of task execution at the end - - [ ] Pass environment variables between tasks - - [ ] Access exit code of the previous task - - [ ] Conditional task execution - - [ ] Tags/servers filtering launching different comands on different servers #6 - - [ ] Ensure command (check existence of file, software, etc.) - - [ ] on-error/on-success task execution - - [ ] Cleanup task that is always ran - - [ ] Log task execution to a file - - [ ] Abort if certain env variables are not present (required envs) - - [ ] Add --step mode flag or config setting to prompt before executing a task - - [ ] Add yaml to command mapper - -## Future - -After **v1.0.0**, focus will be directed to implementing a `tui` for `sake`. The idea is to create something similar to `k9s`, where you have can peruse your servers, tasks, and tags via a `tui`, and execute tasks for selected servers, ssh into servers, etc. +- [ ] Task not callable, only from another task (as not to accidently call it) +- [ ] Hide tasks from auto-completion via `hidden: true` attribute +- [ ] Silent output from task via `silent: true` (and flag) +- [ ] ExecTTY should be config shell +- [ ] Add flag `default_timeout_s` +- [ ] Use `chdir` for tasks, `work_dir` for servers +- [ ] Move limit/limitp to spec, or move order to target +- [ ] Figure out changed/skipped/when +- [ ] Conditional tasks (success, error, skip) +- [ ] Add callbacks (success/error) +- [ ] Loader show current task and how many left on table +- [ ] Add retries to task +- [ ] Add required envs +- [ ] Add option to prompt for envs +- [ ] Handle `Match *` in ssh config for inventory as well +- [ ] Something similar to play, to trigger multiple tasks (with their own context) +- [ ] Add env variables to multiple servers +- [ ] Run one task, save output from all, and then have one task handle differences +- [ ] Save logs/output to files (remote/local) +- [ ] Diff task +- [ ] Inherit default from `default` spec/target +- [ ] Add yaml to command mapper +- [ ] Implement facts +- [ ] Configure what to show, host/ip or name, configure via theme/cli flags +- [ ] - Template for server prefix, similar to header +- [ ] - Add colors to describe (key bold, value color), true (green), false (red) +- [ ] - Add Tree output +- [ ] Fix hashed ip6 with port 22 does not work, all other combinations work +- [ ] Fix `sake ssh inv` not working diff --git a/docs/task-execution.md b/docs/task-execution.md new file mode 100644 index 0000000..d836475 --- /dev/null +++ b/docs/task-execution.md @@ -0,0 +1,87 @@ +# Task Execution + +Sake offers multiple execution strategies that controls how tasks are executed across the hosts. The available ones are: + +- **linear**: execute task for each host before proceeding to the next task (default) +- **host_pinned**: executes tasks (serial) for a host before proceeding to the next host +- **free**: executes tasks without waiting for other tasks + +You can set the strategy via the `stragegy` property in a `spec` definition or via a flag `--strategy [option]`. + +Additionally, the following properties are available to further control task execution: + +- **batch**: number of hosts to run in parallel +- **batch_p**: percentage of hosts to run in parallel +- **forks**: max number of concurrent processes + +## Linear Strategy + +When the following properties are set: + +- **strategy**: linear +- **batch**: 2 +- **forks**: 10000 + +Sake will execute according to the following image: + +![linear](/img/linear-strategy.png) + +1. Task `T1` will run in parallel for hosts `H1` and `H2` +2. Task `T1` will run for host `H3` +3. Task `T2` will run in parallel for hosts `H1` and `H2` +4. Task `T2` will run for host `H3` +5. Task `T3` will run in parallel for hosts `H1` and `H2` +6. Task `T3` will run for host `H3` + +## Host Pinned Strategy + +When the following properties are set: + +- **strategy**: host_pinned +- **batch**: 2 +- **forks**: 10000 + +Sake will execute according to the following image: + +![linear](/img/host_pinned-strategy.png) + +1. Tasks `T1 - T3` will run serially for `H1` and `H2` (in parallel) +2. Tasks `T1 - T3` will run serially for `H3` + +## Free Strategy + +When the following properties are set: + +- **strategy**: free +- **batch_p**: 100% +- **forks**: 10000 + +Sake will execute according to the following image: + +![linear](/img/free-strategy.png) + +1. All tasks for all hosts will run in parallel + +## Ordering Hosts + +There are multiple host ordering options available: + +- **inventory**: the order is as provided by the inventory +- **reverse_inventory**: the order is the reverse of the inventory +- **sorted**: hosts are alphabetically sorted by host +- **reverse_sorted**: hosts are sorted by host in reverse alphabetical order +- **random**: hosts are randomly ordered + +## Confirm Before Running Tasks + +sake comes with two options to confirm tasks before execution: + +- **confirm**: this property is used when you want to simply confirm the task you invoked before running + - it can be specified either via flag `--confirm` or the spec property `confirm` +- **step**: this property is used when you want to confirm each task individually + - it can be specified either via flag `--step` or the spec property `step` + - Invoking `--step` will provide the user 3 options: + - `yes`: run the task + - `no`: skip the task + - `continue`: run the task and don't prompt for the next tasks + diff --git a/docs/usage.md b/docs/usage.md index 89891b4..a437b0c 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -58,7 +58,7 @@ TASK ping: Pong ************ 0.0.0.0 | pong # Count number of files in each servers in parallel -$ sake exec --all --output table --parallel 'find . -type f | wc -l' +$ sake exec --all --output table --strategy=free 'find . -type f | wc -l' Server | Output -----------+-------- diff --git a/docs/variables.md b/docs/variables.md new file mode 100644 index 0000000..057127b --- /dev/null +++ b/docs/variables.md @@ -0,0 +1,94 @@ +# Variables + +sake supports setting variables for both servers and tasks. The variable can either be a string or a command (in which case it's encapsulated by `$()`) which will be evaluated (once) for each task. + +```yaml +servers: + webserver: + host: 172.1.2.3 + env: + string: hello world + +tasks: + ping: + cmd: echo "$msg" + env: + msg: pong + date: $(date) +``` + +Additionally, the following environment variables are available by default for all tasks: + +- Server specific: + - `S_NAME` + - `S_HOST` + - `S_USER` + - `S_PORT` + - `S_TAGS` + +- Config specific: + - `SAKE_DIR` + - `SAKE_PATH` + - `SAKE_KNOWN_HOSTS_FILE` + +## Pass Variables from CLI + +To pass variables from the CLI prompt, simply pass an argument, for instance: + +```bash +sake run msg option=123 +``` + +Now the environment variable `option` can be used in the task. + +## Register Variables in Tasks + +To access a previous tasks output, you can register a variable in the previous task, which will be available as an environment variable in the current task. In addition to just capturing output, the following environment variables will be available: + +- `_status`: +- `_rc`: +- `_failed`: +- `_stdout`: +- `_stdout`: +- `_status`: + +```yaml +tasks: + ping: + tasks: + - cmd: echo "foo" && >&2 echo "error" + register: out + + - cmd: | + echo "status: $out_status" + echo "rc: $out_rc" + echo "failed: $out_failed" + echo "stdout: $out_stdout" + echo "stderr: $out_stderr" + echo "out:" + echo "$out" +``` + +Output: + +```bash +$ sake run ping + +TASKS ****************************** + +TASK (1/2) [task-0] **************** + +172.24.2.2 | error +172.24.2.2 | foo + +TASK (2/2) [task-1] **************** + +172.24.2.2 | status: ok +172.24.2.2 | rc: 0 +172.24.2.2 | failed: false +172.24.2.2 | stdout: foo +172.24.2.2 | stderr: error +172.24.2.2 | out: +172.24.2.2 | error +172.24.2.2 | foo +``` diff --git a/docs/work-dir.md b/docs/work-dir.md new file mode 100644 index 0000000..53dd21a --- /dev/null +++ b/docs/work-dir.md @@ -0,0 +1,108 @@ +# Working Directory + +## Change Working Directory + +You can change the default `work_dir` in the server section and the task section (nested tasks/commands included). +The order of precedence is as follows: + +1. task list +2. task +3. referenced task +4. server +5. default, which is + - the executed task directory for local clients + - the users home directory for remote clients + +```yaml +servers: + localhost: + host: localhost + work_dir: "/opt" # 4 + local: true + +tasks: + work-ref: + name: pwd + work_dir: "/usr" # 3 + cmd: pwd + + work-dir: + work_dir: "/home" # 2 + tasks: + - task: work-ref + + - cmd: pwd + name: pwd + + - cmd: pwd + name: pwd + work_dir: "/" # 1 +``` + +See example: + +```bash +$ sake run work-dir --output table + + Server | Pwd | Pwd | Pwd +-----------+-------+-------+----- + localhost | /home | /home | / + +# if we comment work_dir (# 2) then we get + + Server | Pwd | Pwd | Pwd +-----------+------+------+----- + localhost | /usr | /opt | / +``` + +The complete decision tree for composing a working directory is found below. + +Note: + - Absolute directories (`/opt`) won't be joined. + - The variables `Task Context` and `Server Context` are the local directories where the tasks/servers are defined. + +## Remote Tasks + +Resolve `work_dir` according to `Server Dir` and `Task Dir`: + + +| Host | Task | Server Dir | Task Dir | work_dir | +|--------|--------|------------|----------|---------------------------| +| remote | remote | "" | "" | `/home/user` | +| remote | remote | "" | "task" | `/home/user/opt` | +| remote | remote | "server" | "" | `/home/user/server` | +| remote | remote | "server" | "task" | `/home/user/server/task` | + +## Local Tasks + +Resolve `work_dir` according to `Task Context`: + +| Host | Task | Server Dir | Task Dir | work_dir | +|---------|--------|------------|----------|---------------------------| +| local | local | "" | "" | `[Task Context]` | +| local | remote | "" | "" | `[Task Context]` | +| remote | local | "" | "" | `[Task Context]` | +| remote | local | "server" | "" | `[Task Context]` | + +Resolve `work_dir` according to `Server Context`, `Server Dir` and `Task Dir`: + +| Host | Task | Server Dir | Task Dir | work_dir | +|--------|--------|------------|-----------|--------------------------------| +| local | local | "server" | "task" | `[Server Context]/server/cmd` | +| local | local | "server" | "task" | `[Server Context]/server/cmd` | + +Resolve `work_dir` according to `Task Context` and `Task Dir`: + +| Host | Task | Server Dir | Task Dir | work_dir | +|--------|--------|------------|-----------|---------------------------| +| local | local | "" | "task" | `[Task Context]/task` | +| local | remote | "" | "task" | `[Task Context]/task` | +| remote | local | "" | "task" | `[Task Context]/task` | +| remote | local | "server" | "task" | `[Task Context]/task` | + +Resolve `work_dir` according to `Server Context` and `Server Dir`: + +| Host | Task | Server Dir | Task Dir | work_dir | +|--------|--------|------------|----------|---------------------------| +| local | remote | "server" | "" | `[Server Context]/server` | +| local | local | "server" | "" | `[Server Context]/server` | diff --git a/go.mod b/go.mod index ce299d0..5a134c8 100644 --- a/go.mod +++ b/go.mod @@ -1,32 +1,31 @@ module github.com/alajmo/sake -go 1.18 +go 1.19 require ( github.com/gobwas/glob v0.2.3 - github.com/jedib0t/go-pretty/v6 v6.4.0 + github.com/jedib0t/go-pretty/v6 v6.4.3 github.com/kevinburke/ssh_config v1.2.0 github.com/kr/pretty v0.2.1 - github.com/spf13/cobra v1.5.0 + github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 github.com/theckman/yacspin v0.13.12 - golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b - golang.org/x/exp v0.0.0-20221006183845-316c7553db56 - golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 - golang.org/x/term v0.0.0-20220919170432-7a66f970e087 + golang.org/x/crypto v0.3.0 + golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 + golang.org/x/sys v0.2.0 + golang.org/x/term v0.2.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/fatih/color v1.13.0 // indirect - github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/text v0.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect - github.com/rivo/uniseg v0.4.2 // indirect + github.com/rivo/uniseg v0.4.3 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 3b04003..0689113 100644 --- a/go.sum +++ b/go.sum @@ -8,11 +8,11 @@ github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYF github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jedib0t/go-pretty/v6 v6.4.0 h1:YlI/2zYDrweA4MThiYMKtGRfT+2qZOO65ulej8GTcVI= -github.com/jedib0t/go-pretty/v6 v6.4.0/go.mod h1:MgmISkTWDSFu0xOqiZ0mKNntMQ2mDgOcwOkwBEkMDJI= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jedib0t/go-pretty/v6 v6.4.3 h1:2n9BZ0YQiXGESUSR+6FLg0WWWE80u+mIz35f0uHWcIE= +github.com/jedib0t/go-pretty/v6 v6.4.3/go.mod h1:MgmISkTWDSFu0xOqiZ0mKNntMQ2mDgOcwOkwBEkMDJI= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= @@ -34,12 +34,12 @@ github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdL github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= -github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= +github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= -github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -49,24 +49,22 @@ github.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvH github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4= github.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg= -golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0= -golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/exp v0.0.0-20221006183845-316c7553db56 h1:BrYbdKcCNjLyrN6aKqXy4hPw9qGI8IATkj4EWv9Q+kQ= -golang.org/x/exp v0.0.0-20221006183845-316c7553db56/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 h1:yZNXmy+j/JpX19vZkVktWqAo7Gny4PBWYYK3zskGpx4= +golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 h1:AzgQNqF+FKwyQ5LbVrVqOcuuFB67N47F9+htZYH0wFM= -golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20220919170432-7a66f970e087 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w= -golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/img/cpu-1.csv b/img/cpu-1.csv new file mode 100644 index 0000000..130b52f --- /dev/null +++ b/img/cpu-1.csv @@ -0,0 +1,13 @@ +name,sake,pyinfra,ansible +1,8,90,86 +3,16,86,96 +5,32,88,106 +8,31,88,118 +10,40,92,125 +25,41,97,151 +50,50,102,267 +100,47,111,288 +200,48,126,284 +300,50,136,270 +400,50,144,262 +500,48,151,254 diff --git a/img/cpu-1.png b/img/cpu-1.png new file mode 100644 index 0000000000000000000000000000000000000000..d6cfbc5d697a475ad9cc2be7634f97d62679dbbc GIT binary patch literal 42245 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 XF(hM#NKw}x1|aZs^>bP0l+XkK?=P74 literal 0 HcmV?d00001 diff --git a/img/cpu-2.png b/img/cpu-2.png new file mode 100644 index 0000000000000000000000000000000000000000..c0edc337b723a4abc008e0573beb2d0397a59f74 GIT binary patch literal 47498 zcmeFZbyQXT+Ad6Y2vQP~(%s!4UDB~ADQT%iOE(AzNOwqxl+*%r0iqz?E#0t??lafp ze)ie>-S2z;`^Naj_nm)$FW=HW?t88nEsf{6Smam;2ne{!O7c1g2uKhF1jJJe zG~mjz(fu+40u6$)yo@dww4aL()>R;l?Bdv=l06!eQILMOB&I^3RdTUZqME^I#==^` ztU@3!FO5iC>Juz4{eiBdG$nk3Y^YqoF*JVqWqD zMK?<%wKtgDh>TxQ-XlD=K;l%9?uG6Z@eOoa{p&v-p%{X%WGw#mlA8k(JA_lBLtOap zd;fY62`C8duP6QeSko*7>{?Y#F8suQ8Ak0*L`MEU2mR}#8H`_`dt>7RVb=dTip9!? z-M`LH?Mo@F;0vHVQDkC;xYP|G(S4mg+~_i^1F)IpPU%!Fi!kQP1@&ZT$|4 zN~wQU;by}QHfxUqCRsB)RaM3;2bz~x+K+<2)R;7{ ztn7r;52t;7)7ddv?Xae{r`q}p+S^sX+6-IF}=D;H53^y?U5OKKJRwR4% zS|@8b)yqQ#94zLBA@@A~Ya`3!(PkffiqH7e5+rm1r!$xf9sc;rqEuMK>~C1p*a{uD zH0);z@&?bueD1Ej~x zu*&xKeV33&7C3TyY0k&*@uayQfup1`;%*rBKbDR7j1a!;`6G1q)rlS5hcC}1eM9pW z+hvNiO+akg5aH|7-9xR-Bsp2FT5arVlN)W;QKi}BQTra)=-Q`;X5q&x|vE46goMn?XrZfJ47cXJ#VcaUMYRW1?}#pYCc*Z zW*ZdE#mg0iB>H;TLKg?sCj_v$I#&E!6}?pbk1>xgk2X1IhfbY1&4j6`=9(KIc~mQ< z{qvt6-hAM*wcaVZ+J;>>obQ+S8}R-k{=ChX{z$mg=+@}4{A9ZJA)B6>uY5l#+<*If zoj2xDr}??{#0;kWc-u35c9WMzcWuEBh;yHh*n=bkZyzlzG~(rM3{Tbct=do*d4ABj zwBM-LWor%2yTPXKQ$zT1laU`BdhN9*+!$=&!p;;vEpF~81&S$4pfHAwihCVNARHdL z7~bBCH@+JEiFQ!jm&9|Ot1Nu8C4xgGjV>PI>e^L8TU@8219|iBW4ZSyx*lUK5 z;3+|f;^3TgvHn`Q*>a3tcVqt!U$Di{t-|(nIg_Z*u0?D3q3=n$bIbiv*T^0&vr&=o z+-HXfTI3s}o@maG%Pr4hNx5R5VQ*QVFQC#n$<|C2fN~9ZE;^n>CnWSPpvJlHPCT)1 zGM)%(BC#EQAA5Us64Tjvcj&K{ErM=dV-i<2{V^_SpIQn}X0%9w=k=vUoleV6w(If6 zG{y0+uX11QFW<@tvZYK6wRR1yjeG66-`^_8%gfWWNOQlMtPZIwem>*YbjZcPK$QRS zhET|LQL!^XMwXW;kpkJ=+#EZK;$g8TR6*5!GwDI5Mkg+9^1};8i=~@5_uC$pY#{^E zhkNw=>O0fw93U#u(9lR%S0<4OlBKcge|K|nC~a~n-h`bU_tyIR%$d$#)#McoPO_z! zrkI^XtfnSNB5T*WIICp}Yd~UnYhh(f5Z&q9+rVZU`pORbYyY>vp|=s)%C9KqS6?Fx zW(&B_oczS)`G-Nfks$VBxf&V@i+0RXjgInc&)#tmh~2>Jv_`ca5i2A8=N}gKI!n?UOaLB`Ce2+^&qR8MrlgvG(qv# z1#(pQ`imUq2>17@&~iAczGNp_ku7bZJdDeQfV zq96SGL$nd7;aYCA2fsb2r-x;Hw8Ap5j#nwZxTu)t;jWi%sm1juuO(mu3+;RlxkDzY zS5>O#EZ8-AP`f7=tC!hlZ&nj_bZ%=$#QZ4Ur1{9=B;9poXNN_fA`A)H`Py+7!tyjK z+zR%l69aE3X;JE+H&WdsTxtCIt!|!pjO^aDSV&$zNTMx9h|QoD)ORT1Pmm)e#cAI9 ziU5Q~0IrcEUB@2+Hflfg*Dnk4fKv>UuEoT1&3vn^3zOdL$WyEzuggC!D%k(1-g&{E zk?hv|YMOwU?Ok7zl+(bN_#8cCjj@yMTc`V-9bK78C2Wj|{Zx6t#7xY3$)B&@vnzn- z@6@y)EnNo{vpyHEOHp!9o@!rm=>0Uf=USkaIW|-0++cAln=of3;4&{{KF?jQ$7XzN zRu@ng?O%%Ps48XXV4x)DmwIs_l>z&fEiqD}ja)a{m)y&Km>*P6s7@|%0~T+PWM*vi z-J6b33u?9rduznnh$LdWz_5RJWsuDyk1L0xVKT9$Q$}b`X^4Jo2D?cJm*fo!?ydwjE-gV>E7?qK zGZ^_Ti09&Lw10!{`=IlC1*=mn2*C4;o8 z+p`{~S;Ib!ZqK#Sll;tezPc+NFc`T{Xx6C-H(q8c{ZS-EG(J~6 zEPZ-3kA0*;&c(@d@jM&Fp`pNQM+z*QNGXxnP@Wh)=}pd-a5Zw@!ED?Zy`l5!Ce1IQ z7weM|m}TASE3}l1?^TQ8+3>4O(KQLD`A%f1gWDm-skt0n+L~(I%(LV?7$XBiG&dgB zaiJzBvu=ddz@P$P%;h9@_rh@lr1wlq6aSF=!qU5q@$w3p54f$9m%xvB__`P zGSi~pJL1*pyRP|w)BGpMeu3B3tut-TVuHbU3YDIEjXyqWp9or^rt0$!LMcv8Sv065 zEXD(pmXO`9lO9S_@{Uu>tfrIv-ELt)L>PFVF_?SL&cQe8sublK`7*$q5 zctWn@PZyW=Lt7ilR#EUBGP0#me5A^;MJZ3KfqZWzg(H zyON`A;^fd+!b$JnT>+4WV-$tGg_LJ28$jMamtd> zc$wkrLt6u|a>X}wKZ9!F;PW4>?^w)O$5~3jYiQ~wv!)qFE|zoBiq?i#UkMu>Ch5i{ zf0DkBn7Bpz(nc4-4AE)kslF*AUJ4JFrSKt1n1m6HsH)_x(f7ziUkj;L1s5_ z2byeGsKoq)6>$PTTV+o(v<*SYlRiU0XsXt3)o)9zas?vsgS#%lVSAKcA8EBnd> zK2Dte7k8nfYmBqimGfemwkxHVrv~P}B)f$=RRlyz6uXt>t=HxU8=`=t60PQgc%t02 zayWbUj5B=7he@u=f@d4XKW@!09mBl--pCvo>13$tlPA0UPK>{Y4`-VsRC5SZQ(#;9 zTc12Go*v_af22H(pa_Ttq0S?INv3Nw`IyP31tJH&is!V7-^XU_AhIy!} zb^F6F%Pcksm8eg*R0d~xHd2xymFR1ny{TXhtMui3Aquml?e~ZL?`_03u(+tUM+%fx zlWo?T^L^~d=-f`GIEYvcF+dmgw8g+d3VfVV3M9hIir6v8-~w{<|{=C~6!SA_0} zibp>gXl`Gx!8@?-I)8Q)dKwuwmY^Noz2&H1E|#{_3)4Y834BS^r)v4E)@vJ!KK}Bm zkIE`dxE<=4esn4*u7jam<(=zxGekvs?M!WtT;Rpi2Lp*dM47En&iZz?;{BaybLa{T zS8@gHI={1s`SobUlk-eKqh2J35uVT}wlQ_R77x1QIjHlqxsHx|)T@fhn=A|Zl z``RGT80OGaZFa-SY~INZM4-}c{P#+(P>07vF+`Ymu{LoEc&2-@OrFW%QZccFMl4F} zJD}{(0qQR+XKIq@>i@O~G|QgiaMCFgc||@Q(1tf)P))K%!%}TrN{E9- z&peM9$e@{>#ym-0Bb_zSKbdba+2`0>Ca_wdUj-Ye?OM4Ai&6kBJ;PS7f3D!NQtD5} z8_?JyL^yBoY7zyLZllYr^E42Ll9%x7)rlpvg~y5*{BV!_p4*0ep8zCU9j?z?TwJX1 zIl4cMb!F~s@ABY6j#;lV{JXpb38=wT$6l#ju%;&)$M2ANnA=9a0PNf^kOL|VduCrM zVe=Gpau$_D*)8_C{EZ+t35$+lcsAJAH%4zNwR|W#!Jnm9ydA0Z2qdY(E@$6HnAe;nadL13@#Mz~ew(h=p6H^sx z$1Xn0`si^S99ZA!htzQi)PbBjn&M1j;RlKA)FDcdDu&PRd_#Jo@#G3pN49(FZPm%w z4qVK1YzhiGm0?0IORR!H$?Q@FWsTql=_{ui15&_s$8xk8!>s!>=_F~N4jX)+(=epB zEPorRDfK`oxgFHxss(#q-ymN_C z+EZkkKE}kf3VYfSLa!}I-D7$9JQk)yK{O;oK|OqbMGB>du59f^)!PjGYWnO9v(3{ev?`P1xZA1@$wJ(_{_5wk(QC0F_++4--#-j0^5r~(+ zeIxnp?{WN`Faw9j-!BIK$fcnO*j!Q=YBcDWS^-X}B+l9pFb@HCb))R_*DuJ0v3v_h zNICz`DesitL>R<0e7mS-MtWDCDTrPso+8qW9}49F#2Hm51hbptSbjO)*EeLB_kY7r z7#5clvf3AA6cUa*1c0T710gk+p5h6W1Wv5f;i4<|;X-?q*?jY+eUQWRWwGYIKhsj9 zd4>oAl+)0Em0bU?athb_{vXqwLrkCl0^q3iZv2JYW>2B1k8A5e_XSfm1{H-LS<9Vh;13Tom zq%!d1_tsJ-T+**6jE;}*i6v%pgZ+|mr%s>{`c`e!@UnwxSDerH?HXD>WAExf*Jm0A zK{e0i5?u*?PCbJ}lONvseVRGYfT{O4heVYTT{C z@lTNTED9!UbM>Vf?9wBq9bP2cKL5AD$;7;?AA$CGZGVylT&_gnwbmwNH(M-bn6DkL zJtAT^M7X;-SFC-yC_lf&W!iHaq3XW#+Xn3GR|p`K_X2^jhF)qc_zbV;6_dKJy&Sa< zdbiwvPdtHifuG!W98+TvvqrlBq*=OaNvmoSul1;rJktw|d5_m>but#6af5lBW_*5s z%SOU+g%E~z3UK^o8aiG=+`Z}Uh1qfU-BW5ZNk2*rsynLA&d#E$%S$jsrRO^Tvpuc0>4^s~cRe8R zEoC!ZezliB+nwZo=*VQ&DdE%o22nZf&r|SYnCr=`Q2A zyd9{0L_iS2TdKO2%R@iXVu>6TpFyFl4!3VCctPe3Hi7+EiE_H4$LY5ANPnJJp}6G*4mMgsRLfYG^M*$#jYhD)*FOeewC>Ac>`K z?_$;-QBj3)F8USOoRop>ek$=h&5>I_Pm-P*!R|oWJ&xU0R{s+(~Sd(HBBWQF1iO^YKDYV zf#GP;s!az7KS7bc6CXe6O>J$`v&7R5Uu?LfN<2Vf6n-%s*gjHf!gYFhEa-O;U@}ph zHUO;FVtjg!-R$GtttzEtz{IA{laNcHRYt}K$zd0LMY2NKSD*P0tGT%-bg|;HhLm1_ z9f&V_R+b-8=9ok7N3%4K$Mf>*?54fhLFcqX=9d%{)4_}&3MmIxBxK|s)|w{IWC$)9 zFH(6DwY=7A;V$_YU!yY<5oQ3}|bhKDT0zICLA;TZ;5NRF;|`yf&x zQgpCiS!LendtU3Q;1TY3>_)N}uKjkxrLZx|r`OVfjVEH+zU0E5 z44{BcR>dSr+g5{=wG2?e*G-?E-eddO+sJ zZFF9K0_b5;0aqt0lYZ*3Gg3#mxbXs$%f_dk|vi%Oqy76+k{8f<&-72&r_QD0Jc1TCZ z=qDL7fVdNSz3QU-maYTtOU!qAOx@vpgKyaKGV{IMo})?Q&YSFJ9YCQFg>Y`h#H|5k zRna__=I?L>p@kFTu{Bg^-Iv&scAMn-L8lbfjW6}9Gz0W{hK!jZBz&#BLu&17chu$m zytQ?U@BSW{${t-qRQ+bM(( z)RTI>hSsDH&2qX*ZWPIveOU*6o9g!}ZL8eOjSSO#3&Y1FAG zQXi1A8frO(&Ogk@s2DlSr*`m$3mpp+BQVmwnJ>W(f9qvC{21Sy`RjJx6f7%rR( z8Md^w?pFiv%=YI?S?(U|mcD}U4!R17?0Kg!r!nvLz8 z$Y$E2mFNQbxpOrzc+ddb+?-wU|EoaH{2l>wZknR&D^rz}{Ep7EP5xfGml*_NtBXqGVMYg!S8%rB@9k(LxOBm6Xkk!37h}-TR z5J%V>Ct?cL%#}=CfiNeqExH10R08ij#+c)tY1YI)-MtrNmJbq;sCu4cJ$+#^j^nUB zaxb8kEB_@^Se0Xb?8{ct=F6dSQ05Q1rAfiho;KvMNPBf6DbK@U7e z%>Z=jkGB|_)@wa3!J@c>*KX0t*5BgdNlXMKRj1Qlk7vDmi$*S|c=G$We?upd#bh!5 zn>@<_uazRpAanZl*an+cl|aQg^yIyy7h!)|;E*Et;;x<=o0O|B+A{2u)g%+0T;v&L znVRbPWaQxGY85U9E~Wy!O6K$(0Ww$W2k4 zWxZ7TWhzw~5Y`%xfbGhe{iu`bPY=jZom?h8eEp~3eov(<0+_@vG(bN)HtPDEx4Xypr9R4b6 zNk66k0?qDY%!&F3_4=!94^ey*AyhjkoRsk|Ove{KtEbs+PdWc)cfjm^lOvKWK?K*t zLI2XOvd#nau=qMbeOvw}X@GNocLYHA$z1#i_%HbU=Ra@zFh0RH>oB)g{w^B>Wa9q~ z6MZ&xdG|>;hT~;TTef}z65>;(u_upyGtfGwUA>BW#7wpw1KUQ~K2?lr>`Qo=Ae|fl z0+SJ-)2|Ja3!|Hv@pqBt+&xI3FSe~_k5=em;K9BWM}vEl6k?FwrYl#D)*`hs?+;45 z{7vgMft|kDmshuYNXm~eSPd5`5XG)VrctpEjW9qGN7uDk@j~cv68s9yR(Hdz{07~l zdCM!%4P_$`$F7xsgH=5QWjiXVgY^#pcT3&_6rrv>&j`G+!PVqL0*XN~0zX62weTVV z`7$L$7=Yj(IJR3!OOn8@h3r#}iUALaWrq+VvVH=Fg`|FivT+5IfUYhZVEui0zzye# z+qn@2bPB+;5Qq(dKbMrPSJE=1uxkTufnltNfgAjvp8~T;ru&~J1dNx_LGK6rQc4D% zC5@^N{OL1h2+R_=ofcf* zvtmW??IHUA$o6!dA3CFiOGz7kh?SNu2oOqn?(hA_&PJFp>mL>TtD*hpNs8Y}h&F~^ z&k(3?DW{gn2svbl3#C5yw^`9U?*R@ZN9sRsIk|G2u8y2PRsJBVA?$_}Nm?oL5c%6a zi5=3P2L?WV()S;S==C&M4=l{zmn7Zr`pJadgVJOj4-LXMUK>kxU9xX<3L!)0z}GsyG+2xZtMQTGN}5@1MkQum=a|bs#=&5 zWxOsD)z=icT**KmQQvXk4_*&Do3m*X#>{&o^NeaqwBS$P`F{!XPe1BCdMf^A*FmjJ z$jQ?KImI9bS3%9n$(Tcf=&;uSIY2q)wP3BH#Pum%+ zC@Ok`eo;t*$%ar34pS{jDiK9Wu%cxuB?YpXkUt8x zYgv-gNBF!}Pe1*~ZXB%FPRn^5vg-mn5qOI+QJyGwwjNXSYZITuDz5FUu=WLYWF;5E zE3Y=Hb={{RJsm>WNTZ4qiRD7OUCPVr#qOIg-RO^|ZFKDQ<|suijcCgZSJ%_5{nW#? zUUCopyned}1f4?3I}JY$r;>vO1J;SWK%_~}q#l|N_dRFI}^+*&h_ zfJV50MclxjDe?^By%;`Y=ttOEh9{!EE@w-dL@wn=Tw$?`Mkg z&8pY(6xO&mmE%Q#UE83vr6vW0h}QoM5$d-rg8G5GD7t+~S*xxOx5$+}@+Mz7UXVnJ zo~hAddF_2QtZyv&k*WtifOw&H2G3EN;O*hdG_5`}~?VwSmKR&itP;ev^5 zSE`}VJ@zq?aeN>+e*7e+idc!~hjIntYBY8+cnWW6!7)ZsfXOl(b=9ajxdJ zClEfmEcKkJ&nA3YL#6VQR(ADcU>hkFElF(V-enkRS6LPaQU7q5l@GEq=J{+aB1po9 zR@|)F#^FFZ9OZqKq_y0NY6e)Ydd}{toYbZL%1=howP_=8>G4`|#IxTotl;U$?yq$8 zkrlFQ@{qaennXolJ}Kln^M-^iX3{aMo@v4n!TO-B0M~ek-B%raU|AKL6t?@(c?xsy z)QL_G)ux-9jMsT+d&{nAWr(4N;oGjGLBZ9j_54=hOO~Gej&}~(i4efN$<;|f4!1z? z`1fJyAeC$x>V=qnNwNy&JJz06vQ>eR#@_V#yF4O)3Qy!@uG4SjX z_|5@I_q%4yUkOhDp77XYz%1tMHtHie!Y(QV-q$F)qobeMT2QfK7n&94EEI!T4N!Gi zeOGM9fD|u2UToGt?lR6a>wQT5riB7O$4`zOGUf&Z_cb6;C+8FXm2Dm2*>*SqTBQ;l zbv>b+x}bA=@G$hoN~|WXPF|TH*b!5k*#rc7I}1PpA&L=7-CpN8dHd-g2IFEomgBt| zh{K?aQ|=@VVCrKzr857P=<_MUGw_5SWj0f&R)Tc*vWSEf1!Ri(Z-z z<*Wt*m@XaHBh!v3)|~;ILU6%Wg9V=bOV8mLh320aWw^vBxJ1dP!1bo>(l9yB15;QI zJc&;&9eC>YIvT27QES#jRicj_*tDB&?Z-kw3M3>{S~*vC2(NVWZVDVq>G;DD&D*~b zjn{q22d-PxBt|bCubf7=C!WYr@SNzDBti?JQ*SIC8=KXIL7_Z{+t;E^ooLzPBK|yG1lNC%Z%LF)%)R+*Ld& z^?;yd<$IYGB#4HBBq^CbtuS_lqhb8|zQV&UDe)2LY?)LoN?a=%Ix1J-sWlbxGM$%+ zjOD@kpp7mYi4^dSrE=g0d>zAl(DuMSjh{fjP@v!0mlhs+=N9LgM$|naCus5WB<&)3 z_xPuhD9*;sMAO@zqU^@kh~9_Sn04;%6?d;Ver8J`M_`i@)&k}fh>~WIA2HqPQPv55 z>Ozk$6L=i@LKK)FLeM><2p&f0=={y>FvIYD*)%&H)q8wJF0O3H*VoZT2H+GNLX+f&^iKVc zt6#)C7s~*IoNEI2cGUw3O^hSbJ)r`h;pWj`m>Wd*K!IXqvBekr)DIcxCU&u$1*GM8 za!C|3F(297$U$!BetCFn8CXJG9I1(dzxR-8xyUNwHw4OdhN){2NV1fZ%dcn1+!(hh z)3BoYqLL*I>sqKyb~>1wzuo)G$!`S)A{NBOw*-m9VGjIdgtk31{dJFanNv21beuWn zd3a=@RNC=R^8^c@Y^K>bD(hc8i*hV^XIqtK|AD7=uJ>f+Gq}@12{A+|OPIK=kiIZB zF5-GU#_qVeF)RDM1)rz&kFk;Irv4CjMy^bg3Mv;qE{-DoDj{V<==ua=i?ki+keq>G z$I|T!(=z9qZdy6+fJUq~)RU0)LDkAEF1dCKT3uX<(pSs#a&a>#TlXCv-)S1=l>#7! z>WaGj@uz9}N%`U-Jicj}`q{Xe`A7r=tD?vLCjvjtP zSVjPTy`h;N2-#Bt%{s?vNS=F6!yaxLu(hltnv79XmM-Oe?+?z7CsZh9mOc(av&A1U zF)`osdodm^cO`i_N=0^cQFEGghMv1Z(#l|;UyV}2i$tmA${K;8+f<#k86%;SeUQsXa~ zhlld5{J0ZLdjZztNV$)}{yPtWe=%>;(->=hG@K_fzkb~sc7hSbB_vQs`)Fw^rYlg- zj}{|zN>z-Rg`8*c=%etzQc%r%D9N4YOY}~yzeU@5QGR8^)#za-9k@e1N=9{4nJ-S& zcG$tUSW*heNIUOdYhqW#3T>f3V zonS!xRxs@DbCm2B0^ZIDD~)?ZL=^3dO%i4mI9+Q(KesLp(Fn5gknv%M80Dj!O|p#) zW#SEX=BpL$tzh026pke1qSPg`Vn;*>VSjr4YwYJ(%7%2t!b@I2{q?0r{p0wH8xa@z zsu`n&9?iNr%zH>S=GzlZVx0HR*M^gAr{EiopLxfer+yvZ2ios$a-%hsq{FVgdcQut z`i%-uy1;89V<7k3$Gli<$3B3zj<_tMXyJTNL^f;A8|e~z5zh0n{1ePJDr;>Q(Bu_H}Y74{Px2vU<_p8usER?UAGa zbVc)J%6x5-CCcYHmgxU{0Fe0qU9j>45rfVKDd;?98a%|dP0fvaCoc~17fT$hyrzwS zRT!0wA4dHxiUCmVzi3dyj?gMB>RM_&z#-i_P6jhNo~ z?kox@P5+bnbP@}zw6c=-<5A%&iw$J8Raq`+Wyv@^;p!LkV29Kfl2ZL2WT}sw5P(|R zcN$_J{O;9?$WGi4)U8S^5y7^sn3g_&0^ofg;N<^D#VUgs6oxqG+7;qGYFcr)AQdc2tU)s_ywcul}1*; z*rNZ3u?+}8lMh2*oWH%a*T^U0v&6x86Ep$dj!b+rI?iS=ga@3|IN$((guT99D|eP# zIxC!H=3OJS4del_JKOD}ry2V|$#GCpOgr@te)d6K(KGKrI>^}>*}wV0(DpQM1fLRV zQ7qLG2j}Nx1Ms*bK&bdjJVQ+#AGXk<%>jsv_1jwq)Q;Ute5xfst|$nUNSTekjk7~kyj8tM)3~Jo-WAdA&tZ$CKkYj zQw`ETx+qnj4e8&z^okr(}2%9aNjz`s=8;A zp^@O20Gwh^GRG7>-8Vn^zIF;7jZB6I0?;1VTm<*DX%7%ZScN11QeF+hVY{9qE~fZ{ zDX3afe$31itOgM-o2J_)jrufacFlm79-RW_(dcE3s4}0MgHg?ejiCd#ROSq%W`555*4ndq@D@SRAes>*|*!gANL`#bpYoq76!Lo zADb2Rl1E|S7tA3nCBHD4gQjlz?h^G~SbAt0RzDmo(SDS{_{|?vt!$6{Yi~2I5lS@X zD8?>{^Pun$M`nf+>npJ}p@nuL;jScS%a?z=2R}=9r7-y*mFhf`2!p_VjD<4w@at9I z)a}gy{JYB^BWDyi#qI5c+U35(@Z)?2Kh88}2tx%!CBvu98AHPP@A4_LcE=6Je&b7_ z@mQFwF_nf1>Yy0ns(P!mDTaEPJQ+8Gt)CA^3r(*sZ(3+il{YW4W)164M6q4HDXJYWBnJ2t=EE~k48;6d`O$-gVm>zGdv+_zNWG$r@9b2O&shP=WBp>K@NF1&hN{w{ z$-aD?U-L5yij%UTf14+ZbDAkTUeU;C`OMu5{}Uz`H>NYftpt*MbciWn+nm1>cQOFa zdXDGt7A}-~#K9_Em)>|UIoW%=pvNncEiM!sP2#ux7`OaMT`gUIzQ_w$RL<*NR)84L zW&5@d5iJP^-ZtpXzAPlB=t{AfqO^K@Wrmz*tT|rk^joD`1axi`Ekk4PVd%oe4rvXH ztv5zi z*2*s(BBuP-NbmO3a#W3C{X>`4|mVP6LQ?H?845Vg< z+x3sYgWS>n0UOFOYri5KG%rUNztQtKdN1GX}#yH_wGO>uQ zI-SXvs)AF*^-&T-aGJz1u;8tve}wc%*sGu=X`~DW_B+uoD~-g=4D6+t`$+Gkzv|J(Q?>TLdjY0O$f}kl1eBc!YdR&H#NxkX z@i?#k_PU73&&G!|b^chsfld!<257FJoTj}V8q4?Rx%jlZppxx?_4{Yk4=&Bf<|#3@ z@xEyl8bH}W1aVgM0|!F*=RnSK0A=RCz~eWxBMI3!)-<(qhEj}x$KRJ~XTfyr-#GN9 zl?b3|dNEoyDb-x&Iw+*;TkiDKR+FS?mL9byT83(dTE-90+)IykF3R#J-G-7EhkkZv6WjW3 z7?Xf(hWPKy1 zp-ziL7x3kIIM&Fks{E)OpRzQg)bC@J)j7{cZEc*+O$VTk_@z1R zkr6^$Gor;dz^T0ZQx`dN!mQOmKlpYw!fu2Bpnj`~$bLOI=(tXICQ_2=Gy&*?u92)xs*rVU={RkeqmR@S7w-^RRrTj5#u z1~9hY8FTO|aOgjhb9(r+eYoxP#mG=F{w~FIuRT0D3)wJwnzbYDLOfKGi_3tRtchGLE8d z2RW3cs}8|AuTf`a6ChGNDO|V%irhU{z>&jkj_)yu(DY#c!wBW=B17AfBwZ5c#<;-s z=0Va+DxO7lh{KB#Clmrq0@+s%rGgBC^3`LJy%8~b^WY8#kC(;@#bb0cY}CK~(dfGj zxI;Ta75dq(Rf0XysPywpVs3aY*eBzzNxI)RsG$+{d|Y_47KSba4ghS$ z^RzlTzf{;bww$=Gu}q36{VmA-MJsK-VW~Qa@zMIEF>UjeOu4^E(f7=#X@ZN|^}i%N zi9gI9-a{ahLAb<}>{iT_6Q*mpykbhW^}YwyCRF7Ylr-U>AKCXGB-QVVJ)5n z%ANNkXS?W=A0CfvjCs7rmd`d}X-`yFr%k9BI~Kpl<12RD&=p#hSA-P*H=LAXds@_NH#(kargwZ7)c7^9Cwt7-!Cze&uD67t3GXVBgdBGo)5Xk zR;l0T>AP=m0zNkg)S?6SKiK`t<`@8NeI2LO+9Jfb{p=PMx(am95Hzg|uJ(o}4oI0X z)-DxGfV`VD+NB>BPtnpVqGVimGG^S?9;sksw?4XJ!vR{43hFrkLc^3eJv%P z7dA6(vo3T(OE)yGMY1ZLqBKxtYiWhUYayWg^SkaJocO;{|7V9$1O+4@IQG>Yepx;$sHItS}yHBVK!}f3trR<2M{&4xM-ty4_{jO z+=$m+Ovz;a<_Deu2}3(f*AyO1%R0;SfJTOTqEKn~CWbbmthf5^<$81LIOv=4=$qB( z44;kD^j*SJu7R0_Yw7?|Q2CObe6C zr6?ix=1v5u9czu1Jwj^2F zD|!dx=XX(#j?sj&MSzG?GPMi!X1<4GB!7+@^BLW(ewJ$xV4oGmqUHbe75;!N_a5Nr zt$9}k+HC%5f?~taRHkIZ=5zW>-*#G2K}_Vxc)rTSCE&YXpe>^n%Ppmb2OLI4EgNUHHK6WKw8*0y35lElG*Auh zE>=LUSY1p0bWcIUjPZ@I)PmheW%ne_HV`ZS`;T*v^)f`yoe?aiL@Jp;ghsD~`y6q9 z%xABoUuY!A#T=yUbKYAMK-aoCnvY7)pw)8?h1w2}WG&I&We8qZs5og_U@i%k?H%>qdo_Z>ajUgzc<#(29w^+6l;bkcOY_2KFZW;%#Tl9?T%5uU4P z@b{^bE@d@QSd89^*e_MbL~Nwku(@6B>Zwr*q16Ik6Jw`IMNsu+bF9=v6X?jq2}3{a z271Cic)TsWb|EMJedSN1>?#k|y=G#XU9^`MULc1c0Zk?&Nq*?p7bG)g0$gU`+igq8 zXs2E!ZP#oKQ`vA_P)dvA&f!!i0Aj9Q={m`LJDv5M=Jk0WBu9X>FC2$*VuzW6j<4U8y;Kh0G3m zYYeV6ZI3**eqMM-;abW9sWIm71HQGUP^csq#LPtf=sN!?@rOVPT(llR7nHj?>k)oO zw4*>Nxl;YD^%}O%2Xg56MMe!}J4?Ypn=h-u$9R=A*5cP`{S~@(gscW1LdQx~*}Q)a zwH%MkaEHJ69M-}V9UUhP4k(#nCV5JY+OJGONWtpTZO<=T$}4d()vLM}!y8hnHNr#| z5r|b(povKI8pwyS5_>4}+9Yx+$p_#tc{gKOpY#z1t8^24XN#yKvd zdIsR-o=B_S*kbVUDzkBQ#ifeW_|esoyuR)KBJ8W8@D>ahA$AlL$g+FZgOCKonFxKt>pOIEJeK z7_p~~8|{>|w*9W9Nv&Ax<-o48;rGrTP$`&f?^z_KjZ>Pyqq#U;u-Z3%+}QMoH;7em zx(3kT`Q}j94uPU(Ox5e6F?l8V_RcV@F}mgL^^ju%mTaR2G$zx^_={GbkjRx1Rw6+J zJ{nP6Roty%ydy?o>V;n}=85v#1R3}l)Cg$$05^l5jcl7l>K_Kr*tt@%ToO;f9Tv@S zwk(Jxofq>rZ9gs1b-JA+Flu;*%2#HJc+1`r&*A?ZC{}P@UD_>Tw;3%i1#urQclZj| z`(r`P3iy~(lMo&~L48=DD%-W=G-}Rdl=sy#zh3Vs9`?RgKpu59Bde9^=7-Sq z^|bNQO;0EVQ2(&7xOjBeJWDuLI_HMJJ<#Z}%VG{hEmOU8h@Y)%j(?`G_)c39iFSun zSeffg)Dagn3)4?m#h>(K_H-@J)w0&h4zCh;F>pj>IWFB`dHK$5Pec;3vO~hPzv`i` z_oqP;wZCeSNJw0Esr}^K+iE~*!0{Q>1oc`4OZ1&3BGS^w zj{<{$Mmp77La$IZhBHD^?55f`*p|$%&xL?yAnZ7GikF^kvyB*3&y0hCwpTw5EA+lu ze>1IiRL6OE-viww1i>10`~*l!AQ>Be24$}PMGvu4erfbo=-YqE?#oY%()VI zFAear!nJ4QD+TSYj9=&2>SPisuIj!k=7?-x<*L4?}4(uquSMh2lOE7 zO4lEU`Dl-Co7a$50lEX~4xo+RZ+D~AQ$Uky?fy+s=|tRqj{DSI3_C3e zSN>Qs;}i4?i!UNlMuSC5x&bXoZ6-EG()BpS^HH*nMMyhC82Kk%OAFWU0`Z!G*%b)> z9{vgSfSUPr(9e>pKCy9Tb5~;(Lb(m-VBfXt>-ic%+q+-Ob4iMQj+tcvki>j#VPRpI z(d==-^UnafzsmHJ%d&pSJk;N*2*ue@L%m|ri7wTRJ6w`-b)9R7FYT4fDj0bqxH}1@ z39)~aYBFx?;$JL4Xu_;o_Nx&fxJdeE0|$VhuTy&ht=t6b{ERJIzzyL_@bp9diB$k! zQNEY7`~C%{g)C5OUr{sH=sC}x$fn!zE`v6hFL(18vM40!$NVA?Ok>?03#O-`&CFk` zn$Rs#`ywro=v|4cp!F;WAs?-yEI_N&!;OqFKBP{HF#LSmsq2b&h-|Y_&$f2X-(>&9*~21XGZ>K zD4#^v>~I(xc2=#bls18xi4<>fLPz72x>K_wXtjaw(|ShBS>T01x>|jjrWt~}Rs;L| zekWQGhw57T+Yec&-;D7BW<$h>t={l48CvxOq@)y5TV)2;VnNykSAz@NTebGOr;(c+ z?{6TNrE9#{erSReO50#X$2^A{Z{_A}&hn`-G_jCYH54U-5Z+kzw``?T@e7gs^MJ|J zSaTM_ZwHjYcEigN9$wDf8%vtuGG3K~Q@(0Ho%%7{hoV{EKx?gfTn~o8ldgy zHFP3j#|*X6jGu9L&Ua@C3RTGQh%l*mFb{`5CDauIc***3u8pcnn#yP ztCL$5jSY(!;?zX0=D}hXOcH6DYpy=ouFg!d*1Mg^7LfJ+OcmB}ARoYR$$bH>4I?Qn z)4b5=|A304`j$X99YTh?E6Nqc%3y}cuJ+u}xw|lyN+O_K=ogEsmT&fF5vf%>a-e*C z`u%fDYalqLaivEo-?~7=U#viov#f7@4s32ftBZiK7e@?qu{?Xe@eTj{Fd~{QmB7WQa^JRc ze?urzh_{zuob0%aVt)DXiNF45$``J5Z-x!i^>uF=5$|i#unW+YfxK35=57ez3#7~# zlaQTJDM(&&kI~_!GoviVZrnvZ|B?E7ful!`IsJI3%&sJBQxrVqiYkTcZn&d&kW4*d zG&~$@_(k2f24zP+N5xqEkI7LhmNs~waB!JesXix`D_F_s^TjEXBIY_$gqiIpcc_tV{dKM(r$(&eOOol0Tv6fF2g}Is9 zOQ^Np*ofjx)0CS{HY639DDEOXj||Hfu6md_`c&V1kgN@10a3lZB%rI1-Kuz8&$=_6 z-lIeyW^R(z#_<#4^6IKxwM~DzN(`^^sKOhS#B_XNOk_Jgcl-`SS6YK)vy&i@i8^9)bQTk^)*Vpk%W-HiH;x|1hYug|e1;Y%uv zR#k2T8};S`w$AS&d3R@4(p7AjGQznGUO}~cg!4U8?rC?9I@Izjuqn)1qt{)F6k|mt z3t1uQBfr7)SCthmS8)#Vn_FGZFAsylET$G@w$bLc!wQd9V-{(IsV$3$wNPuLCceMs z`f>R2`*YR|qaUksG?bI+qJz%@w=4Tg<9L}liS9sT5B z)b`ooPS;?aFcn+cw%$r0eqC=p`NY#4>W1kXZ|q`pWR>*|e2_7(>86y8^Xr+F&NJuk zK4j3$+A|fxwY?q!tEoEHmLYJ--Yh`C^7}U^gKDETJME8dv~m?@8n8m&GDN0?GwEs< zX29^vy-(W6XsEU$<3#1IRg5(jX}f1KeVh=k9S`L|&CC>^(&jK;KA!hCVwg-1Uh$)U zH%Xk~$^C1i!v{?6%k%Qca-qSObr5f3%fdh~cJ^Jo?z`^~abJpw`8PS-j_$KE85Ps) z2@Hv{^G?0%|Fqs!xfWv~XgudU!R!39ud+6g;C*n)6QG~&O<};D=wzAtY=MPBDrjeT zfQG1AkA#qMK7UACsa(XDG+|rWil>Z!{IM|%0pPk-S^kIX9*LN=k?~M#J6L%YCK-Ze zaa(W*TT$eBd+-!U80VhHnZx&&ursU`Y)z>v5DZcrf^u4AoOe2ygBeAF;}{+cZ+cU! z4mika$`!u8WtF7SaQvj`C=-b}^1N^X*X&_mFPo;H7E4yCe$4DRqkdy8q(%yGk&|Js)wwfgAn^eeMRxHwX8-YBym4bEW9LKd^*W>_^dajO0^QF6L zld5TSq*weKqsm%3x!^Pvy1}n{y#(`b>lHC|1 z>AuQUa?JfYWUej5tEDbLnZu?|+xpqbdBo`b*-w9$J`t#ipnESx} z-uXDcla1_+qm1N>!#6im&HiMzTY=h7q|Dikec;O*#MAx z?L68VK?9U7&uA4tzrbjGIZhX=l{%T#(-p2ces))*hz^t_cIZ`HNDaI)PcT24&$#7( zcldnew7{p9J3DdHuXsRaF0&&=%- zLXx@cKcFXUy>osB$bWi|e=67sur;48Jc%fO8F?6?h8B6)=6(Ks+vEHo@>lE`ek3a) zu2D@rKV^&F5iTJKA=yDBzD$Q?$i7X#%{1WQG2CKZfz&^aoddoJY5b-l!IwsFqN6s7 zph^R`h+M5VZn$6XS|DBd{_K$tpP96hig=w9Ey>Z#LZ=PeRxW~8zdV1;qxIFK;xS-` zQIe2h9lvo>fhXlISz&TmRIg47PW7%@Z9*W8t_N4V4612tca>`^B|{m>`EjCGrkC=-M5b{_QZ8(F0S5h0xC&Z8Y!J?|gQN z(!D@vUb&dINsr6%i$iqs4Qbm7eMx|PB>i1}pRC*GUcf&W8tmV$x1)+WvychrYXKPm zNmT=5Ak!m~o2Tlak@40lspON+2U=PsvQTc8-4$*7A~K8jL~Ao_(n zb44l>XfM#E&QfwU7pL>;E`6IOY&~kSIBL)aHod%Qc?{j z?yl0%164~7I8ToiWLQRR(u(quMhGJID1Q!ZO!w?^4%mMi5i+f!Cg7CHidrrL+NzO1 ziLB6L{@yQXc9(igpn13VGt#Zqyb{VTkUZmcVk~4&X zD2BZRg~B62e&2zsc>fVV|9+)^9k>$yGMpn_EXbo*2tv1q|p`!g|NsOnXT>`!GT@31zk?^0Ql}ru$`}wUWbL>><;F zQmh0_%{p891Nku7=&Unq$$f%lxTr4q)!qu+|Ki~8*87TtWcPmLsO1h-s79t-RxfQz$wtPlmH^%g9E3e|0x>m^& zvtp>J){baXcyEM(_Vi6L|5d`)f4&;x$alj+ffLDF@#q>h1oKl-&^A zf8O9#6fX*~XwM4zKF94*F0!>yj)3{Pv)n?WznB^IM?;$1AnQxbwXlSs1Z8ApXe~3u zDM}O6Df4>#0b058kzWmeh;D4lRB9Z3!n$LvP{=fmL=!&bqtr6jo8UWlo_#hu^>s9K z)UL=}#kcJ;1^=T3sJtD*?2^AYU)5is!7R4CBlJvI56KO($WuxJlQDl1n+XQ94rGB@ zd+$G)z;_cF543Wb$TKFOi@#ss$sP#`F(={sK|{bad>YRfYFa$b%{$jIU>?7wk$-KJ zPhYU>8YliVGVoV4cx{3Y`H^?)z=2l&%pa|s{Qlz&eD@|;WNs#8p38+bez!(1Od41g zvtSRj^|NvgeYf5EzIDdegU=lTd6?DSW-7Jqdy2DACTOOSl{%7{%H*sLqW=mXPx$9i zx-&P|mVj+Gc`w-XW0o~6*@m7UkNYC{RP3Me?lmZ5A5$1Fz2c&^}%SVSSZ?!a-c zx?yg9cmLUAZm!zXb}fV%kHT91tt+ja&r3ZH zL@B?VjBQ1kCF>+#akuo2uK#_LVVT87IO-L=ba8a@V6P{JOm$KGnNAMO0Lhv&18j%$ zrP~!Si2%YeU>BE}<<~9x@Wt_;NkD5+mJWjX=#V%Qx@1uvEXkEW4Ey05*b(MrOC?C= zYW?c#UTe%a#Q9)tIo#-_ORju;&em)W1t3g+gp7<_11}P4Jl`pYJ9;+)vni2)CatSN zlU9L`MY&r6tpB)fZjk{@Mou0LbXtZ&bLBusH;+~tdZotbUVh+|^5Pei9jewS4dndn z=m_40UEr9%J`%$St=p;~&i|Fpz zBE`0oV_sPbs#BE?$M{XDV(-3@=9eND&3+jBw`)%b%e#nD+q9;QoaxcZ&A%`VsDqRd zG|5$<+UOI~0%+x^db}w4u(=1ebjeyY&2toss0oGUQ+uBB@pPODgdCARrkgUr&qFCW z*fZNe5n)(VDQa|9s zJFPJ)UIGsb&MBYlTT5lrbaUE49GyJi*KQqWUg{uA-OxWB7?WKkV8a}1n{c7_qm78{ zH&IaSK|zi!sr&ZGK1M!%)= zdzb_3cL4Oie|0cPD2|;BgPhI_Oms@GZXzD2W_jgGZ5~{>L7owuV3(MAfSX?97|rv_ zsqHtyvUm-n2j+X`GUih^-z?uGoWOi{f=Z2b!0`1Yj)W0*D=RCdYfEYtoiYYdom|8( zFj9KUG=8*l`+Cw+1t^b#f-iZ9u81Z#foIq>I@@)WHchjHgv4P>NeP8pz4Hm-iw5OS z&mWF?<)OMDt2WJ-pbi)M%%;Hvfzp+f)EeBp1%w;+s@=?Xj9fz*-#@ zHizKvV>6mor}l*!*KChVeAVn%^i*FD*tSv0b2SMAyG|R{Zut`h>1-=1z@cSOMb75t zz^X6GDnpbQ5 zNW69g+v${|^>G-x%@wCz40IP~-28$oS9$DB+^)$?%SL>UWw*ULC8THNz&sD+OBa6>l?%R;wjaALX0C36YuMJ*6+v;X5 zIZco@9*;>J*e*SZjfT1T`xjDA9>5&ecWO^@u&B5m2vk{(!##jzv)HU}l(b4TP!A4{ zU@P5<-{vb&`@s>k>Ex;87>KFR3CR9@({CXs zn=0!mO6Zl^ZR&ED35jr9#$Cc-Uew4jygP7m4L9H*%Z9NmsT81TfRv#cluA(T*a;^I zv~n{oUer9;9Fkog0br+dVZ(l@+*F!YKf&j*AuKXtw=9y{1bn@*#-Gn{ zDR+o&a@*vALh$+i)B9#|qKKi(Lm5rPTooLV`0UnhKJHnL=U^dMd<4TCuoH1p=e1Z*^=EX^67NU8*omnUMfmCrOi)U znRUpx-SV%$9u^YbU)7v{9t7|mKq^UShw~5F3@b@9Eqby+qcmHK)g?unC&iz%UQbmJ zK_`a}8?{;CLld{|l+sRTONDTvVUZVpyQQUi z-u5wmKG_ySD)O6|>uD|-6C zb|wmD`^w16Bk1I3mK4dOPxVdSui%suesRz3RCT^;lrper-xAU*Buo6%tAywWxu_1A zZOoEf`8y`&a~?Ac*m>;cy%WbbNqR9jodf%9RlpSlE{QK_F!;lQPvnDab3PiXro!$# zk8s`h7GlEUC_DT5aG2v6<$xEoI9>J=pc4y|lZ+R3F2BG1z_Q+ZNF#gjD_e{TG?uun zw_PjiFCn`#Q8dyhqp4MDq)rv-E;{S-69g*N$I9Y!{2;b8O)OYJ70;+z;J=s|tm`c( z75$SE<>e=o))a>{$)b$KLHH6@%fqj?zKBYnxK4s<4_X^b+Iab&@;|T?>CbuB$Wn0S z7ukVtGOL{nK!ZY9{}J%1oEFpsSza3Mmq~**e;S5o6%I>h8a4dlD}MC&(aPOz7{A*U z`qlaNdj*04i-q7my~?pCKo>6Agr2jLg-M|N%j~TA(I+9h&vy*k5Gt*Z7#NT9M-Ff;u#+ARQ?*_ToIa&Cr$$)mW@ynQlmMdz0I${gi zyOZD`8v223sd=5R#wUjOOa=;K@#+mLOPdz8P3@OaiCuHribpa9!}~j!<*Og1oJ#a? zB|O{S!wZZHF0D5~yVFIX3B7SS@|E#hBMsNcmeXQCOxvox4^KsW2(P(ahZY}|sgxCX z`$eMubBitLVAU;K=}oq;w7M=Bfm32*6K`1JvoM(D-?zTx zeEo@W8ZV-xO^m%C@4TX?wF5{3U1=0n`fN|!U5uHX7t$GFlEdTYrj zIVkp#4p?7F0I2+ZeXWL#14ybjo;pGnB_#u$f#%Nl?I1-O?}D$~+B7+J(k6*T z+-cqnWtL2PxVeRF!aA-fjQ5SI+|RGNpe^BK=TxcXr@n}ZY?^3npLsjT+A7NXYiFOr zO=m_QI=Q$>I-x9YQhoOrDSv`9@(ne8hc94Y&l88VpD1-~^{mve9<)uWQTXx%LK?;K ztv$5FaCv1b@O_9kjdHWUA4#~{xazaM!AYFEhaD)WmtBu3WkSh1CsAABv)AgcZL_*w zm^TW@Tn)FVA}GrftzIuR!l-y3NNJBhvx8 zYk2b5y#&K1*C#}=Kjg|iCNAX8hDEZA=8O&HkdeD$)&n=oY0sA^nh>d9Jhft!vmU!V z7K5M3PC3;aikRRqevt+Z!<`@Mx7`fe*<(a+FIHBz`1X<6uC@bvG7koNJY-|**t+C$P>tY z;JL1Jw!ZyNAxP`IB27He7UvgQXSa#jh;KLDDHrF83aN2O^4plsq*UA(Yr#w3{3#u< z2}0t%-Hfr3?bx5I)P|23L}*wZ?Z!`}MQZ0y4hCR>TX>t7k@Seu%3*(b9b@hyH6x zk$ogZoMvNM-;CQHa{Fb5yKaxIY*pe}&sDMcWxmky*Iby`uC4&}aYa+DMj)ZCSZu-6 zM?`CQ9M4=^nnh5{?M`H!3~I^mxHqW3zM-HU$HJ=yxjY z8|%GNbg22-X*m2$V1Cz8E2`{1=3NKk+`q)Ux&WRLFEeg?56!fw@E#Vu-P;8k6;a!a z(Oo5wWs3Q~2ZAB!uUVIoyz_n5u<`s;HBMwjo*P7j=a-w?desg=cRwl?lMWa_-R0OvS|TnT^!^kAlyd&6@Ba2HMLtc0xDFUexukNyd#r(YIo@#Z<-N z60{>#_PuSJROG=Cg$&C*QDpMC+^CJ~+o8lmoXXR&3isY~GVf0}*1x@HE5*;$>dgwv zc}#KvFY-7_5%38S$9)h7lsrc|whjeV!LC{sA?Mwpf-;_k<3Chc~p%*<>^k~aBv*R;WyVzB42Huy}eSfuITYGgB5-T7? zozZmpxd8bHl}-(0Nfhy?aPUvekvo0!c6=vv{Rz2H6@icm`LtERVk+CAI%UyH0j5X6 z?7Mu~jm8GGJlB^ONNpXaQA+J3<2jkcu0t*Jz-2d+t|Hpy>G{nc8MHEMQ z6my0MjQzd}wq!mGZdPS4VEqw6IkKq4934Tx zc0~SeJHPo70CE3890Zu&fUM6I_dVX6>hC39Ekdx2AsksNHn4Gz)^rhd_ZPzuFirl~ zBozQOs{S1`{-(F0V1g#CD0K)8u(4HcS4zWBfNt8<71IB?HhhmgPA>v_E(?IZZ_#1;A_# zpJW7rfh38O0+-&-PqkYoCX_}PJ2c(VOmuKg~d^*qat&(4xb@U&;w ztNLh)i1y6e=Y<+9$SZZkQn=!`DlvVR;&8b9p=y`-)Dv*p9oPunLG>LuBQ=33rvE>QgRdVNLdXXzH^J<=CT(byxz2+5!V{5V& zk(9?K7AKc6^#A*0h=a)W%2f=R-raRMOOFH5{f6E~tp!|%uo7?R6Yz1-p<0eVGjv&# zPhZXb7K*tSWTV-WyYvNWJ^4Ms*r|3?0$6R`uzwgd4rE$XRPSQfJUZh!iPYn>?sK4wP!l}|JhvNf-9Z#y%=5|Qc(f; zK2!yK*1#MYv?Aha~2s(|Ql1one_ces9{pPsmtT4zwFT{wDF+#5e!ai(+ zeVZe;0~_|EsP!{+pnhum`y2Xz_||~OF1&bY3S6WAL~E~z$rQ*KG|0MJ~vo47$c5Eojpo z?C#d@*4wWF)=+e|g1}+2zpb;o(9VPZ7rAw~F3_~A!n%Vzni9^v!}HdhiLntG2$DPi z7UJ)|kZ*cAHcQ78><<^O zpH_HNNj2~I7~)(5{omrX$<*rQouvduMZ$rVx_2vo2W(mXsd#NeX(2vKJ`{ol?d{^P z`#ut}rm#z%z17!_o@BR^M>|}tPQj-2NcITvI7UaBvIpjHx1fJdgOLk5KP(!TUNlug z?|4oOB=%7HWzFzS$lmklC4=p-6EVO{&Ab7&IdoKtZxE6xLN!2a?2*8tcpV`3+TcN7 z)MKS12A-xOeSaGjV3vz9sBj%%_W89lbZsrz5X*$L-wgZyihlIxEpG1ocTQ{!?7g%p zTK2sxaBO$D(?6Us(4dY_IG1d?JbPq%_5-i6*PfWykduik7J&E;fVAD;NNUu>6n(BR z&oc*kc@#_v(MWOtt-9~&ne{Zj8d4&#sEZ;6e&epsv)N|+fy{T)VE9ZQitWvC+7uYE z2I#>*QL6o@TOqN>abPN#Xs8^Xb$k*k2(ED~<^P;-!|I{FCx& zZI0JA^N6MtO^@Iqia1&w7aUZbk59XS*VB!7EI+StI3F);MEWS|faaJ-i#7h?A6+!? za?IO(yE%B2ZAuMn=+**Lp@t&#nH4+`9_fa{qBHhG z9!SHcFVU-+O^dOIGDRZ>g)pBQ#-@j!ON(JR@jOjt?D}Jc?NLO- z&d@p2a3YR94(8A(=}vJVM9cX{Vo0Pnx)*5NUHPt$1_a9UJghG>X^J&i;t}7~863&6 zneYrLO@w8(`af*TI0sK!=|0aH#HI7QKx^$CqNTGJl)7G-<#v2M_zCw2j}A}+d7!U9 z-*Q9%U{?lU=>N{Q{Ttk|=@tnpq&$1?U@Yk8szfUn*ZDputn$q|?r+<~1(UJrU>i8@ zJ!I9?P}(e#LdV1cHW9948V8ZGl;;iX@B__ zVfSxo=5xx}jz=mt@E zzR;U&h1jdm`fsNO5jx6bUldM1f4UoxloCJ(1f`vnz%P^`{1w{qc^}*m^U(M}jqA+t zuCPc$@xsoFe?uf>Xoj}k_aXDq?2xY?D+{RE#Z*b7ph4H!#=T`gORI`(^2h2x+fJ-| zlmSFhe@>4BUbw-_er@=i=jv?dZj|%%fDgsr=mM`Kg+qtaz3D48b@SE7Pz;e}yo<-2 z5TsY{Iv>GYTzWcI4L@*r`bimixjKZI68-G>>a3~OHX0ZD4sc|(VIshW7(e>Yta%d3 zVIK4NbGsp*XK-KY)*rtvy?fo(wm4rn%%NoneU}1|_t*Q`e87g#9XRkiUGo=YetI(9 z3n8B0%H*Iq^yr(MAE*oxAoqJj@V7l` z1s&l|+WBHR9$En}aS`ZGVGYt^77GI2GEK(kXqGf>@H4gU0j15@tL}K2a01qX-@uj` zih+VIJ513DyXx1Ewr)bFwh>zxr{?i4u{ydCVKYolSs2 z{*h$@9M64tieqB`)Z+lssS3Om6QC5X>TBwXLxO{YV>y@jOf7!?$d=C(PK|V)R|YOs z`_{@Q3mi*b?d&Dk@t2Ah2SRVc6@mJzF<(N{xE8-dlJxX+^mR_a>V2N2?x_Hi{A;w4 z3Tvf^c6Ysh_h3DInL0fqXj#~_)A7VX z%BM!CX^z4qu4db5dBzj4+^evA56<@w9^43|jn2Yl*L-#)F^e_{Gu|its-H{BZd4yG z(J5;_RnvEm0+iB)30PaE%)f0q8Hb7hr!}Dvaz(aUqo@EX_g<2zqd@#{IGkcV<-r^n zi5~_p#t0p;vGSM3v=ULha2A3&n= zUM76;O-uf2Qp-gt>=O>|9x%V&aFb6E)Mh%=SZB#~8+h|oJGb!@%%Q#t(M1IwdSykS z?lbZXY4R%uJoPZ;4DNuxf7vwW0=h;gj^y1JDw&ZREq)yL;s-n5-=WW_35e{4L+A<7 zT`<7>GHE>*`+hhR0t6{La)d_4#_H?yf>%*oD}XqnziH|mL+!f~bB*;JQe5U`u^w_K zPkILVho@y^8(I3XwJChr`^>s)46#IrswNjJc)3&u&7w&GbO)TUEzh;Pg=?*u8L#wM zOw4eqUIJCqtJ%iMrPE7g!qDodYFhJNr=DuvyN{i4G2{5IIO;LJ;ukSz;A`Vsst+$- znk!{KXK+V7OD(nWa^WXwo5Bu6I_?&WFK)d|an^>AQx)K9FJDpBv-O%T$@Lu++N-V1`;*4x&3HZD%v;wZY;Y`m}aBUklN2X94vl8y|$Hxin z_+db<>t1AWBp>wh6s#Sz9^D1yhV4M9-{cw)G~Jp8>KZ4V3XDL38ow z%505SjxO^qioNOcuTYN`&=EOs`HSCNpgP_4FT%i=r{~y!H*$FLXPo#yiBP~C;a9t& z{n}-*_itNl66C40*rY_KB@)6uYfPcB#4&c$j!#SFNL_cwaJr)iM=xz1>B&SKXmq~)3M|jcy2HiT z@5YzNI!}@g&9{5(F0F==29`A@N*LFNjn+46@YRREo0bmb@pa_Q&AM z1hA&}{O{{0`#&JUJ>h5m8Uk&(+2dl!`i%dki>-HG9D+D~Di?1P$MEFvv!Ew=D8^3I zcZ$X@9&l=C$jR;@b-Pe=)qLRJ5a(}2+ehYR(f6WT4drs5aO$Wk9|^=RO_>U2xA@RB zmsq8o>GAvX3iqnW2K6X+KG~dRZg#J`cTw(d7# z!1+(aa8E=kpg7`s@F5{RULAE2t0(x0(W~eW_E_Q!etpNaFq`-C?3`@1Y{~mEBV*xz zd(sof=ycO|=#c$IW_`ei7nD>PgZqYjAAy3uyd(Ljpk8B z$v8s?6G<^uv)a#B*n;U4ZboPK=Bx3RCcoxoElv)>jw6X5qB(Ze7xm}BifKqH1py(J zv*t$`sI~;>3eUw#M0j;|m=aofw>E&^KQZGy@bd$?T%%&2td>(4)x5%N^l%7!9OD{4mpG1zr5WOg&22;O``D54I#?(HN9xL=zl-zg#G`iezYGKv z#<#M!N1Xt+b0&}uIUl0!Qc5AGu3T+D-{^F*8Y|Y9`exjMFm45pWSO)LCL0bo#{-V` z0s5bhHsm*h`g>7cLVbOX9-F@&2>9^259)nqP|sbY*Z5o|OYnWn{28^}&;qoFI{<7K z_xomM`OlmAo-M{FfZ%Q7(3)Zf=P6<>Z7o%T7Rs$(JB!edXH0Qt{fqrv+jB;rE& zsyD%Pn#jwOtiF%myOGhnI*!H~+E_@{WYVpATIHYgB<=fQ9sllD#yx8eh(Z4v4*$zo zX%5?9G`j4;!rp{$etm}7u3Wd;-Of!~kLFRb=-v`?Z#=8|?+*z~Kt8IHj_{DByzoUt zQcAsqkSM2dQA#17Nu-@+j9Rs;{hknjb5H+HIRDNr7^6B4d`BVXy=vj!nXN@69J+o) z3;2W19%Z&6L9fDls{QZe?r+b63r^!-!^kXDj{2}U+!+n=@4!XU{&ox@nJh6TX!3CA z1fZ6B4T$-_jUX7t;Or5!REY;Nu45-JGm?}qeu1(#x*4?TQOisS(7|P8}8>$Gm9SO6E(u@apM?Cm08W&da*_>j%X z;!c}RgNxx?3<>RGe5wGgZoWD=O7tO#0!&}%JW!qcO$0a&Ez{J zxFvP5R=JIHj}l-}2M_-b`$jt~p^JEL1p#EKWYnm5zD^8D{rDX~-o4UnUexFGqfiH- zx^&w83^e}E0H{D~<<&Jnrj3G~)O1LWlr4|R$wm1S-ll4FUT@>2H~Ao=;?mXdTND8Q z_52?QJwSktpC3F8-{x*sNqq1xhlMo+o}MNm(V&<9_ffZOl)Z-lSpUZw&^P-{JG3)O z2_pF?lkdV+l z%LQ0++r1^*0c^sbqO0Gx4kz@si2H^yTCV*I4$9m4qn7G=?@iv;zRO1%`6@x3jPM$M z&4M=i_D#T=0V3iby*29L0F}S=@2EggLcg#~E(t{~5e>?v#Fna;o8bW*b@hLF0I&>1`3khwz3%ru zfg2Tfr8|n~N(l|01y_)E7#j54CY{mij3W3fqftMd8CcM>%KHV`{f7l%hWchQas;Z9 z%N+)WaNf4Jj_AK0ydy|1g`jwxUs%ttAb&Gyz#o6ZCO+wC_un4I2_5`|)1g~VgB7p~ zPwi~a&K+si7Sduj#Aj3`@4PDY{>Ys|*r)jDp`W$#qwetOFEH8kj7R|5phe;U;`bP0 zK5hN7q1bSu`A2bC*F`Kg#j$^Ad4N=nRiTvv>S1+P1kUjqDi;FiDoeoi0D^4XcrG!- zF%BK=MCSKjXJ6@6edS!BRqmNww|qYMFiP<$F4wHgubdbpx1D>0O>NVN?pBGI1(^5Lx7X}OPN)?U!MT_B}NYqiJs~arrUl!LZ<}Wef zb@CXfKbn9asu+7i!I#xD&CbSKcOdTq-R@8`jC&vXlDh;FwD}7G{^Im4KBzhFROB!voxMwUHWoug<1F9?)vSr2@ zU{6pYYigfBTF6>06et}_QKoS zUj<9AznnW-Fv`eSrXu!S@^Fuqd~NN(uJZB9MfG*O``m=YHOb)~3cUT55QW(UWpYpu zVmbu{Ha1&FodGr2gSDgHMV@xt*+YE3kAZO?|E zpznOh_AqKQwwU8ibOn}eciPCtq*ZU-<$=a6g-RE`8cF1cfQ>J`o-{)kvC;tDRutRk zPHRR&p>y|$TFCN7aH*M2j*Z21irYsxOu0J~>|zpOCt^hIirkLgEQhod;gjG^x1NJ! z(#2~CAwVi(;->5&@G5P5FWK`Y570f!|8K3BJf1?s>sbZs=>mFNt#ZC!vXIgB~G5(E3uZ-Ga=#QBK5|b9Mh<HJ1p-SjhA&&dY^GKrw!zVocFED z{rEVG$Z63HCZo#oDCm-lFM>M&;i8po0nPtbNW|#m!Tw#QA*-Vez=km(nRePwk3X1& zcc1>?KJ-}n%IkNGK2bD3vhpnT?sp>O_Roos|0z7W2Q6P}V3cBB#M-Qh?Ld7_SGO1( zWW_5y_R9JFQUXw%Gd!UrgH}#ruw%j8ypu{=Di73!^z%C7o6I}Kd@BZkS6+UI2avL~ z)~XA=gVsEbNn1%F(R8}oBY8cTi8yHRUFtGY``OTkZ8$wJfetfvjx#s%Tob#KT+p*S51T;6)B>{CQVYCjPK|~uD80-k`mOUSBogU;=3FMR@ao2tyP8z7-3XC$YCdU;Yr(jm!_cEbL4a00MGn4O>Iym zT}J3Kt|`NFq=h>7T;aAK%-opY>Z`cPAvTE%*kG_53SW=Q{k^`H7G^fwW?1%|p5{u| z`Szr)SE_$I940t)F_9^xzL-2z(_hoY3Dd=5k!A+db-LFPIzIFkT#PHRjDuri;YUi6 zg&aOU{+)`&uq88U8ucA_2S-Q4Tzt#LKZQ(a!Dzw!8^F^y=OjZlrY0vT`$@zu8|k8H zN$|3J8te~Uo~{#Ud?-%);DBBX=NQ`Z8Do3&&i_u662m$;_1gXcZB791enqXQNx@Z{ zKUajAeP-0$JrC;3>4H4R#<~q{5u`+*AR!gLd$Udwu+EzXPd?}vq#@I|?ONPH- zr1V8U1gr`IM*$H`j+n0kHr`Agh!EFh5K&?ef3`klwFO0b)a*g zaOz-bHFW7*{!3Qxf~;;1|Fm=rh{=WY5IG?!-Wzix)xFYL#+CM*3w-iXM$WI^#A!jT zAjJpawoRpDDyaQSr!Mj65sKd&d{FbWQl0vlybqm=Eb(L#cHrFa$eG9J$0we|$;;CU zUQB$)917$)GX+4^{-dR3tR7K>ZUvN`U#0`(HfwGvs5}RnPX@IM4^acdu<^(T66kk| zp1$&1GU}fHX*(lhb)q(!0NCO;etH0FRGutiQuF=J9wk92*< z2W_xQa>y}3H18}#BVeQ!@9)It;V@~gPuc*V@j_(hZ#B8htM^)fMV2h^EW<44xvigV zYi|aZ2=E+Lf#qibB^9tYNe?J++*mh|b_4`wTjYs_1gpsL4=XMU@@ z+I}%7(Mc*TZ~KkLm99Rnvy|%J0ZL?ZFHgvZy<9`j?T=6kaihXcNSAQ8*FqVTm!A%! z2M~V;Clq!hXOyimuAxt)9qmSX-aGZY$^NZsiHhE@0(y`AV{`fq!LZKM zw{*o}xFX{*eTI-1fRF3+sS{_>nyo__zt#NtQ8ItSJ;B;~x7GV1j_$uDBs=(yL0(B0 z`K!~)%59m2^30h}DL_I)FAxTqLZqea5ZTcgq?ccvG5n$3e;i$n$Rg34do%9)5#H{@ zwpaNSgPuXS9&bmo*CSZ9F_MLL!=rh;;MnTQvx98iQ4^y_tl@5U|R4x%REBf;k&pC_}tKaer3l9&^8YV!TiTm&sN8I5n zY!C}uR`KsBa@h%TK8AiDjh1BEC3g2cXM=G27@zSQgr(~Mmetubi-|@27xhZ}Eh$bM zB?!hnM(OFxHctV7$9nOZRpi}y}2U5)mF8oH}{QNw;J(27=i&y!Wbd(F|YIUJ% zlBrjsXYog{Uz=jJAcUq$Qb>Mi!N6grw_J~hxM8Ci$z2=grlfCv32}l26hB@AOoY*ARGZ|dHDtrVi@JgiEXfS4C~ zme5K6>f(g3ms7j47bn-Dvvh?#e-P5%3xC5!)Ck;tHP9aMAJ2|Fc+0{u`}cE?9;^8) zKHI$!)DiCkm_bjiOX}Ybk41$Q^A1Mc!|6%2Wp1miA@X%6A^h30pZ25NY1rg;J|4Qd z<9AEs zN!inwP(qOE)5cKCHx-on3Wqen0VYAcHRj`ID6&c)GurHqsV_ndYi=Pi^aFg4g=bKw zfBGINW;aI}@c2D?UL1fY6xSOm0{Pr?8$dDYpj&Vx>DAoIL>=4nSLhPOGnNjCGrluY zO!7R@TSjdQMF!v?m0C5$=AZiSbVZ8IAAl>zpy*?Y-|YB*w!{LarCFl`990nc?=+3S z%)Nl(f|Kni?w!{EWU{hK2a^4E5+}uZA;G_OaNrtv5zxk66m0k(`ufYT59e>;DkCY1 z|HNsl>Va#B=4 zJaBbZCH4<1kRJHA(IJeS&;2@^C?PHKii(rbYM4q|T3UjFgEF=&H~jNcxyIyKmmPb0 z*xo-BEhIiqvs;8U7hqks_hNk%->4(|51jjJ$Uq)#6Ub1}{*zs)bDOl}i?A)}Z?x?F z39D*eFY=S`@VdK?_op4PS;d|hb5}7*39$4cXWu3@rfRJY#7i_3Jt;&fw2vW{)-)d@ zU6WLhF_3^h%{|fT`+Av`yze@j*HkZ0`@wtKU>@=$wQwAF>13^CU{g<0E`4?F5zpwS z4K9dwZvT3-yMYlx1)m3e^*}+E7|7Dkp{lAn-dd)1EHH^Zx__RyzzwB5e&4TEnb>44|L{dGt;8)aYDW=M*1Cl!}igxhOXaw z-tXPIzOT3JI&I<8k{B^y>lv-^KBz@@eeCS%98x3*nW8DV}Dety7 z%iK)&JYfE16&-SfVZ~^+)q8j4;(}X;4_=&&X--L;89)XDcip}rZc!)ka7MH?dtlRs za^R>^4K8cL?)#5b`;U$GDMCeR12Kv6eEt}ZlM_s4iz2MWF6L}xzu6#K4%pUOMZ&cN z5loIR^J??^R~O1cM$qi8@)>}_JSP|UAPAk4o@M;@++{ta7t)vbg*r4czH=i1bn3Iwa|y=0 zQ83K7md@G#n5`Vj+1JEGGoIYCzFPV)93_h6K(Go# zwPomr1O+bi-2Fb^b^zWND_xzi|0>~gUP&dVig>oA_J!QAQRr2x_W)H92gk!Gd zYq}h4R|tpATnRCVSN2*_kZV>T&Rk~-MGui)?0XU@?~stxd_%x$)~!PGyJ}iEk_jp zdOpbp8^BkopPYKdsWmlbDeDU)teL7Y%e!(Q@Y9H(g5CzCL-|tf%htycesuU?iU)dT zg+Bl~Gtsfv%q3vih9GW{c|1Xmyzd(Q2BYr{$TH4kcE#??3y#L8qC7>m%IksQ@oOST zZV+yR}JCgqdb%Oy($tC@{P+vGc5=_IX?1ofCDfvmCdZ#^Y0dQQGXtVOHo*VisM z`t-GF>6<2+&2i9y3P?NB{8SF8T!`^>3$$f8qq#5S%r`UF@F6Q*_ zA%PqI%B_Ox+@drkjqrOlJ*^_f>Bcu(SVuf)dkC@56#OO3^Q~?hOqmCQ{QbyQVd6S+ zyCm5OX^r{mO~{bgfmJoM``&mIF+3%(!WX>3G}{nValg{dCw^((dQ~&s1MM!@KafDW zwt;hCIcouDqk~t{{Nz0FZJ%46$28BR>*V{k?iEQVKkra&90f~5+NW0Z^lFI3DuYvf zuEL5!T}k{>)&P&B?M2831Y3$gettgh(S>AfoRi=&*DtSBCj)JjR!&4!MNx?0s$>TH znOtWGE6$YSYE+|9H%ND?eAAC4QHg}JFg3R&aX$~ddUd$(5n;xIxFgLc?yP_86CdY$ z#vS|BAAW_{$0xj}7j}>{?K~m!tfu$KNb`vx^w>>^gu@UUKu%Xl?cQrPR4cxo!HLG#F}`2iKjgJY00G8?V@Bg zQNyJ5@y|SSB^aI}M;(uzB28p&l8={xlIr()*jXg~o!ip8l8wl($$RkKdC@VwFTtE+ zhKdSNG)hv|tURId+IN7Od;?p2#XNlJCaQ!j>|V&eQxn{!6=G2qXq$3rZQ^_0KH7!i z>(R8~QB}!2lERj4>Ej(0ZsUAs#GN60Uq9iP!NysSQOSy`LFzzC_5_oJCokUc(z}>tzb`G)Al{ zVbKD67=~Rbc+0vX*YFnv+b^UK?p7?7tI?J+J8KXN+}vgFUtmj!+8)XX+wA7%2ZOuhD!62YQb|8Lp*-&A&mc63jLAM_TiofvX{a`65BhkE4 z*;@9~gAw0(dJF0d0XuytPcU(7+5ch)ZyG%d?oL=7?<-{EmV6e8y>YEv`e`Y1P#p<% z>F<#1AMYGL5GVtt4$GVnoiNp836S`Zu#gz`QBJqkm?6=m0AZq8XwOF({X0o8BCXf^ z64If8cA6u`==|-cJZybZuE1#RSOhfCXhStIG z(Ui0ZK56<-yVXLn@OWNncj2XJmEbJA5yyavY6Q1fNK(I&NJW`aH|nn$9i)X;e)$XU z+o>bQAU93vwV*}Ak<>apDZlA=Ez(#vL6wtrFJ({?ZdMC3t=k|0Pz~hCkh<7$?)d8K zqi@$csBhl9dFU*5$F42X$Z)zx{>J1E3gD8>dzyASE$r~{pp^9z1gNg+zDN!){@gbr2^V)Gu31UHsb=<13(9jg4BCq^=pv7>0(1nbVs)7 z5afS(VUnflpr5$QFFnO6d+yNvSpk(A)S&=yb4c2p z7iqWKL6P)RF5p3NDqnl13}S|$`;}+0HFo1tT|{_!C!6}oCN0MG84Uf&Rb5@(7|W|cQBqzd-jy`DTO#pClKF0lsnrH-$x zv7|`+Qd&n_N8H0y%=x3l2M=S*9zHmwdTl@C`wDF@h);|xbsilIxfbX@s@%MwI_v*k z#z0ym$Kq@2a8~KttAXD|cLJN>YyT>nf?}YssAHc(z*XCXW(Rt;%RZj#ralJKjf1N- zwiQ7imfn{?aW)!jOrdoywtCW2sP2>z*Q6x{4` zcx7#w|24-^MaMR$<6)nbkoKy;Gv6JQ>OVV^_{YXM!{xqcY|y-Hxs7C+NLnw(Tklh| z_c7IquDy)xLh6H(G`G5);Ubud(ui1Yc5Ftsi#%NZEUTWP|NDxDPM3K*zHLfoH|0jk ze}f0TX*chq$=F8cF|o)W^VehGztEY@`>1ZV(cCIo@MF>bSqZz45TbwczF>7{>S}Ah z@-UIVo50SFb>F;C{drOFpm@1WEyrd9x7)~V-X~;irA~^oK@|EumkRz)YZ{))KWTUCe*k>9VwwN| literal 0 HcmV?d00001 diff --git a/img/dependency-graph.svg b/img/dependency-graph.svg new file mode 100644 index 0000000..1efafc2 --- /dev/null +++ b/img/dependency-graph.svg @@ -0,0 +1,139 @@ + + + + + + +Codestin Search App + + + +Codestin Search App + + +github.com/alajmo/sake +7 / 87B + + + + + +Codestin Search App + + +github.com/alajmo/sake/cmd +1044 / 34.5KB + + + + + +Codestin Search App + + + + + +Codestin Search App + + +github.com/alajmo/sake/core +669 / 16.3KB + + + + + +Codestin Search App + + + + + +Codestin Search App + + +github.com/alajmo/sake/core/dao +2882 / 81.1KB + + + + + +Codestin Search App + + + + + +Codestin Search App + + +github.com/alajmo/sake/core/print +446 / 13.4KB + + + + + +Codestin Search App + + + + + +Codestin Search App + + +github.com/alajmo/sake/core/run +1213 / 32.3KB + + + + + +Codestin Search App + + + + + +Codestin Search App + + + + + +Codestin Search App + + + + + +Codestin Search App + + + + + +Codestin Search App + + + + + +Codestin Search App + + + + + +Codestin Search App + + + + + diff --git a/img/docusaurus.png b/img/docusaurus.png new file mode 100644 index 0000000000000000000000000000000000000000..e7266fb25cad095dbb329eee6aee301481ddd766 GIT binary patch literal 19917 zcmeFZWmIHMvMyY>yL(Z%ySqcvXw$g6ySq!{4vkAU(73z1TjTD~xI0|lcjnBQv%dT1 zto7Z0Q?+*0-jNYcMm!liE3>k)A{6DNkl^v*0RRA!jI_A&$9v1ikp~O?@oG=3=lf#;KS4KMzihpo*K51U&bJrOcW);cU(82*KR=tj$>aB@$Lm;xmyR~?)b}G% zo43cX%gXnu*_`9XP95r(4)OD$^MxFQTOpipTlXlFIS6fj#CbQW?|ajC=f^qs?5}hRPw~4sDIg!=CciXHp zzn0SDbJfQ|H^15u&G&Wc>K9#y%OF3Uv8&k1_qzjyx0T6v?F(Mn7lj0@9F2qk86)>jBRd)S1yxY z7hZy|aA|v9Gqu_GkJ&yQ6K0N)G$)FW_&t4!Z)N@Xo@tjIRjOC_*CF^`+KY635gueh zNbgOl2i~?qBwRTIFqL6yp<$5*=eUgT51p6KLb`{~&W{HUEG(~1C)bbykt2Zsogfi}bAGC4qUOcm;g^)VZ zR~vpO*0W9qaK04%3`J?@*-vs}Y~D}t@|C48*;W3;5V)abB*(F!Y-Llw(Ab}%Y5QYt zzG8l5MJ(3e=o6vqy&novhaPTzF)|Eu%CiX1ORxcneE9skYbS*JMDAcU4 z=v%hm;#nvCJ(Fvz^3%IX<3;Koqujk;uVd=ZXOxuZbJu4sRV~vDkHKU0A!cW#bLof6 zd@lAkGOE|tV`C&U8@u$4G4XF!)9Z`wbs8OQW(3uOMlNsFw;H1pJCB+bujRF&mG#5@ zYCEmG%EWqFdnaKd+|}b#x%f0i7<3WRlpdf(|1u!=+!&m3~8D z=F72~f4rG_3FejjrH7a9X|Lo|W(?MHMVZd&N*M0@tg_Dg69lmnL>|Uin-I_V8eBtT z(q**ovv8S4`^KlM-YkqqS<5-AHp(0s^ic$(y@d-9tJRZwl0-27YP;3kCAx*`uo2d`7dP_CIVp2ph7-v10x?ukvSYqSZ zw_zVx)#KY|@kMPvpX<)YZ?O*~2_)OQdmPqTZH;he zt}d?$3L(CY46Xnz1z+aGzN3Im1ahv1z1(3bzKq(vo&6+0`{_SCKml!qrgQ?JA1F{e z#kC+eFexC{N-u=`?5upR0sBLz;~=EnPWZuNexE`@+w-f1-}7?Lx&g6I{cGCxb1Jdp znW;z)T6fU#rL8BpjqLMK%PUc2NNDXrR`Vxt>?iBXAmTs^{}C@Y##f0r13+Vwk#?!> zc_6@WvOQZkpO{Qnn@c8xJbkEgv9bIE~^c7g4*C%hHIJ_2{=qt&t(4(m#v z|53lp7X&=Q(>7j4cJh3P9MUO~^pr;`VTq^}APA0H7s@Q4Dem(RlL3imK&$z{UM2Uw zn#ToLP=H0!Nn<+<4*!NxzSuIp@e@KFM$h9Bk~>6TpGqt@o8XuE_H*{GZLaEk+y>Hx zZSR2mqXY=JG@5g4Qq4!s_MRiuY;!34NPU>BW$!trrf2twezOuL#w+m|EbHi2zU_Dd za*Q6!oi|?p8ME9`?S56G&f)q2ofi2HoGu306ut zd|`l#`=y&nQ3KBn2>?Spv&Z75i06fbO40Ao_w*g74CqP`?-sVh<&L@{DEqv%-&hhT z^iC5~;fJH{_Y4owuPELVEf$GKjmW%}Vf5oFe$Rn+o>~w{W}%=#{|wl%ccCY~gV~KQ zA*Nm!yzca;h=21xr5>Bh4XOzMfckvFq6)S91pOb%lKECQxA5^4Ep`!Wa1El*q$4n4 z41)HA64MZf#5Y6SJ7b*D;`wjN&s6~NhpE38eiJM<*as2MQ<@10YlYI5FL9N{?oQoe zHHll3_&T8V+M6j`dS8x5lT8HUYA`9h#XELf1ges9p20iy!Ui}YCNMiaZnjiGB@K6o z
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 Wns#$Lb_7mVM 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
kBO1h;!w;6~XMbBEAMbXLs45^+`v(9;)!HTGx z9meuw9Nt)ljiS4D@!fmLU8x#p*}w_AOShvQS$RCAooTT-S*~r6F6LFXxQJ7?BJd2^ z=p1LoRTct^Ey)M9)6_*&NdoXx$qdlAe}s`VfPzp^F6`mVEx`v!kO6DMF&p@E!W7`=4TJ@@L|6GpDR??kY8VH1_Naun-rY$x4*{m*1(+`S%|I~#x19+9>t(rEmx6&=0Gdm)yjW!q7U0K&kJY|$ z?KGLtQ5~=b+oE>ozqKKp@FpD%9onQcIqfHM!3b$DmD`%#%lYxt2Wtb_oJr zuXEmgq>8|_KLMovR&AmF7l@*S&b{UNj)gebR|i}&lhh4Sc?}{3W7ZD|R!7V)$O~<` zC(htQ)Z4eFI0x|0>(}_<)NTe%;1|l(F!X^f}ya?+4BZfu-L77;< zRAp|B*dr)Y3Ycm`<)?LU3l?Bo>Ms_JoWR^Rh(I}5$nzM$dpddaEbk>Oz4R;zzlKS+ z!*>78ycAJ=*S6QUN59(Xj4TBHp7*K(awO@x7yB=(vmXky9=AvC=3%Y8T?M;I-t&&I zu29e2Sw`#rMAjQWQ0mk!Gp#3*uXCJv=(kg;aA5nzpgdXBV=J*?A8g^D0X)o;lsf({ zn+J-rcaxY<>m;$<)UOd@IvW|t^?s9I!2Q|)hcGwz^JDvuo~0)U(tg&gbu^j*+I;mL zA5g&3uCdqau}L}qHn(6l`>SptfL+XM@A!R^&nPB>p5D1juDR6x0gpu|gdc8=Ez-Qu z?BAHk{j6Dd8|>O?dIVMe%>dNQe6>qhY*6(@cRlF?Xw7IIl6ip;>^&0yLaS^g4Njp=(u6!Tb@eos@ik5Qr2?jdCm)0-LL*qH%_T5 z#h(W0&P48dXDPzu%QK@7Ge~sG{$_2FReQ0f2o+E z%<##2;#7!oPneT*>-=vu^Iru5{fDaqc`nP{u0MU&`w(~qN_(e&^K4na?z8`|8vF{4i2iW~=s?YFVic}^s z3%vHQ1Av1l+O*H%(+O>rck&=(BF;T75^6&U0KPx)+r}>bIu22bgt&FB$$LkU3C$E_ z`6^fV=mJDLflHN51N6c}fW&$cH1Aj7pT=3LE>`KY1@C3tvgOksu#5|o%g^|!cuy=? zVW7wxJv&Aw6I1$F%zXnPO}+CuTI-vxv`mVp^_+elOY(cxfqTZOZr%z2ZVP4e<>Z#Hn@#>`LJV+p@gu8B+)+uL8 z`mZ1gyJf$Z*opvgrtd@4mafsIPR4bjj_!hT${utXuPtjBP?LN}x>Got zJ|M9YvQ&^ZVlO-Q=`XeNJv-a}GS(t4ZNJc!uDMH>dKE?V$)6))R&rRg)8RmikVStnr~ z-o(PuRxE_~ZFh!XzJ@!|TF37hTUtbonQ?0EqTy!ajNo^ZsDF_E;qOWFR|!SXlI8A> zC(#*oPILJaUO=?_yrx`vh$_eeGd>fyo+Rz(9~PD!dfo!yUDi$k-~6kiA^U*KBm*BR^Woa}-@(e@$94+@Ca&iidio2qDx)!9Cr53<-wgwL93`a#4k-?%2o{rY5T0MIma zX1kR%CTTO8z`GU^O{`Dhd!u<#i(eCgaK>#r&zmCQv87uxNO0~_LUv!V-xow6eGggLW{cc)IOn%!3Y`!+&fmB<=YysWmb*#{?)uDzQl37 zsY@5BeNC@422G;CpWb)$tK5O-QPi%z54Jc|I9SC&O*qle(mj0DDwSEGXE@oNvh+&_ zRzC{iZJm-Ir|*aV!Sc7mdqnfxMkEYMCEK(gi^q2lc>~#FOSJ=GxIpOW(oNg|%4;(r zyb`1i&f8mzl#E9k#~|{s>l|?W03XZ)z65(lneEBa>a}ply)SY zRpv%?S@Y&TFKQ3ddf6XW}Wnn9#Xdn%hwva_^Aoz^`2GsNV_jR$2ZDW1s^`hD$rP`(yloqz9*?$S{! zX`U{n?&v`l>}}iwK#F4Pbz%)C3)ptT?H=?`3%1t`Wy^GS{t|FP?nRw?08@p|z1((Z zxtAnw0i6vVLgH^?a!ACTzv=iPNmj^ZBy*PLRQnBWEhBjQ)1_*=2Ks3KqHrRGGdmO7 ziXmF!%3}r?6IUWPRqgFA!N9|=`L9orm#7{M7fAfwCCoM8g3`Q0^3+t3&*hAUp3mI( zjPHmI6lB1z`@=FliQ#zEBQ>+2q77Bk{0za!N6I-G=B*DX3r*{*X3biY<>0l9L}WPD z)Kp63&%I_R1)?2n8}Ee9NdYgPa~(!%6BWc`f1OR189#3E^_$WI=ZcT zQtHPS2izzf69efY$kT^+B=N(Ln~;l483Oq^LE-ZM=WR|nRA&8&!e5EA(!UTWLSPfa z61yKzj?}T@o|T2N*dkUV`+JDVx;lZUth6R1_H%X!KaLB7(N@c*U}!4hK&SqCEY{uWsIR z;6fnw$@Xl`m*wVdGk;Z!W;%Z&|C!Br;vEJ|JOt+*~$pf^>i(nHzK}He)ZJr_61b2Q3A)pU~01;c>H`k8?$TI+)$Rl3ns=bF2 zDLFf%W$7nvz_2ipqO&4h3j){&JpJf_0L*l#G(aJyF&;!}#bkg!m=K^h015jsnkc}& zqF+;yN;3zj!qn1Xx(6fJjXRK+Jznr~BM8SzWYF;G49TAO?O+=OQFt64bRfiBA8Udk zb%X@rl1Ol`x$ra@*&EUKA4VEFjG}iUk>xPO$Jt03`Q*F`9nl;)n4Vy4;vFRCXaNH_ zUsc&g;BxZ}fbxa4DH*_!gRcP0UQgjJ1xP$T$OR2xsdLDvaKOJ#?lZtd%PrQXo*E(?3BnA`sn`N7oD%$HGOhRT9)}{JWi?4G(AE`!+)TY3 z`76GM_2=1rJHRQjv^wGt4yJt74@>oT`EOEM^HR*%5HjFfBO93oQ20x+MM2LwXY++tfwGf)wu42-DZNwLyvD15kD2z7aqA#c5L83rt~J zxu$FyHb0M5>ziep0f;FM>-(&L(k@}uOW*^{6xH2CwD~>!{7ll#BWs(W=|a&9AeEo6 zwZnjs72_NziL1Z^^_x_slR`*6!rNxH$){Qu)Cgf0&wcB^lr<gJk=kF!#BxSxCniTgtiffQ^|lg;y7CW7RcTOi=dTd%ON zu(yT>Xkzco?nq{gFDLaor9FwJspzmO|8{yvjfGujtINo@ zCd+aUnE8&@M^dr_E|frRL`np((upr4X4;Zh2d?wND}CXRku(7nGA>h=>Ec`fjS-GT z6;NoAfcJ+>WpR~!h-}ZiO@1nQ2I@K!`O+=dr|Bi2G$VlSzK5_)r#-TJC^YY$@YP{T z-9II(Vn!42(ABG>5rcG!RbcTPn!IS)5tXU;*w#mwj9&q0%$5x83jnfYmHVc4&5v;< z=FHDa)4;hrQ|n|>DLb9I0SXC;H`kW~EfP3{1q+oJF&%+!ZUv~nfkiY%1`^G$Y$WQazca3}XMNxeTpufdW$1SCxOS|Q zI3Ep(rD$6Gc@NB1YF##`J2QuXIVDYF5zxFEk@!urBOY#OaZ~~P;%1zG+O>H)&(dEu z6FR1!_p_H5zWWV|0xYn#jzW$#BIUcu6+5_<(s%d0d}T_$b62*JTv&nkfA#B_x{es? zeSqTF_6QEe_MozWs54JL-U1egPY_%Bk8aGpIwp`u=}sXC?^8dhz!aISwQ5*C`V3ro zMKnEh?cP6@L4Sh{m*iX_-3Q>2eNm5)F*`x~aYbBU7!9MdWY|q4<1qYq7)vFjZZnvg zb4SY?1>2;aNC8Iczao|9!}dXq1^6?#124w1SB}RtQhI-bc zNNRLLd5I%+*h`;f{}_H6Qly&?Bovct>R}koe)WiCg2~HodUT&Rd4ta$)~2!n>#R=) zu2A7&QAm16=smNIErtSRQ$i6N2w3=6JbFI6HBsEjWGiO{brG;(m)i?N(g`LzbxvmmkLHo<+=mE*gZDfZ_1;1NvPPk`)Yn6s@`y%19 zVf}=jSI8WoiUI|@r#8!=8o=}$<`(w~@R_Z9hIz_KD^&wr34mEYy>8JNJ?SY85PbE^ zPA_u!OX_n2Wd)XlqpiYEvG0)#QT6CO*m$@Qp4l|oE*h#&ccPCoz?jU+vJ=G{WFiSK z7Teyah`$=_!bXeJqRCf7RCxeXjaf{YAXK1!SCd*Mr4*Tv;#&)E=!B)qQw}HK^Xq_KfA25gf3WL1nOR%fD>gtu-my z+PWYXyVz6Qs`jJ%tq9)6~FcWh8;H#7?|@1DxVUhDy=4(y ze)GOKJ^$u~CBc&wwM}fsvz9+MmsP-oT;kF5f;#G!sMnrplLey${Ze#okia6jt`?O3 zdx*O{RM?#4rSwnFIj5~Y&)N`Dj8w4>V{`G!pFr4|pnMJ+IvA&Nb4j&#Nu!!LU`F$n zbvsx~e@QlI;zt8tvUT~rH?OB^41OOnp)u%h!5OeSTQlLRS;Eqe{Rv_I64APf!T3It z{80;f9d2d-^*hcXk^^LE^y zv~UAHEqZ73F4qe1OP%6`+-gQljLER2^?UMetvU@bobX%Skc0*ZSrumTR zW%GGH(aVC6I*S83%1XKQ9_$BAnc?X|`WWyB{IvqT$1QA`9?iy@ zHneBHEZ4ck*(BlQ#@8G>x7H6tLQwf}+Zpxm$ogf-`u=>)D(9*Ia>@J>xn~m_WeSZ0 z$#S3W_OF;!AXw}9sv|e+D=kh{)_>c!r`5WU_@iPO{@WKR5uU9YN4WkYEk1ecb;>Sx zYsga#yRJnMS%+VavwyDqUlpHw`L=8D;#w?5gQPS3s{axy|NOKzQUblyQbk(B`!{39 zYRf{4Z-t6IoNFK}4qj}?U^`G)_zy%B3*{tH#;%j9{EKC1C)yLM5U?k_T@B8qIx^Uy z_qU{T>#*s=UvB`G_$+m-Q%9tRbKe*Oow#?LUH+za@v}|J8vBpNiM0+GU@jtxZRk8g zR3y$qGBYi8NKEx#-So)mieuG9;;atb@jU3T;SV78r0@7SSQJ9s;gaiaql{HG_Pgjx z_z%zZBcGN4#!~7K*VZ7^SPbEUqgJfNXHR})m^wzZ#pT4{_1DT$2jHMq>Et@9cJ$GtJEmwXv4e0L4PmkbM?#rrw2igiCf79C!nz1E<2)Y&` z555Rr!~NSA!y=f-?%N0o1k-8d0=%+uN%|%7APG<{QbK-YCFUt$SDsyOej*Rp0U`UR z7886$%+nkz_J`{T8$r+F9npX(X+?C;N{D3=RMqJI{H=yv8N?YNs3f4j2zKQLX1g4o zAb?;l0K9hGcjKU~mRMx_A>(wWvtpU#w{!?R;;_KN=AOxyH><=rjs)^6~z8(BG9Xu*Fw>@1|VfQgpE#&Xv5Q}5on=}5)>lWu( zgVXJ4x!petPkYAQx^~sadak4gQ1o~HR2F~#C1ln4`SNrpc0T;N)}qBJ`mE+bN3zpw zt?o^6mwbNXN{2^rS_8X3?aORJu2N>I%?&GUXhOck& z_tB`ZW6#x@fc}2h{@WsjtF3z_?pJ?*?^jsA@h|xse|a8o*kIL7HI{YDvK7T3>hRU6 zs_yM&%-NE!jjLs=mPVHpK9(sKdwI4yDvmR1OJ^)( zSW3q9>;v4WKpDTpI1_nT)3$7YE4uQvbr?_G=tYy~;lK?;uu!qzN+MyaWG_Ci%6e%1 zL?i3Yo}^i&Y8`1lTR8HrwDS29nJQ$J`Om*SU5Rafju#QSF19DVW5U;$a9D_dk!PAB z$63>fW2tO)=CEE`3!@!~hHu+Oz~1HC&o(Kee7WcUr(^aSg6Jo>ed2!zBgdv!M?nyl?0N|)5;vML{XhjLkh|yd1{z)dP*(l%_TJ|L+&AZIvC*qBxB9OC{_5E(FN!xX z@6)AaQ#aJ)7Ut1L!(Pkwn>ikT{}@rdt;rdiSiY8V`fgZkgS>1Ip_;nSJ$@hrhN6@L3Cw10uHupoW zW%`snM!3+b`X%=McJe0dSUr(5flUN?s`xA_Wx&sCx$nvUM{%)GCUkVWT! z`;+`{7Y0f9{7kOyvw3HyhX|*$lgWFAzxI~iiJ>&fN1Ck{9y)bq2x!cvlHL$1=GM)8 zeL$jcbz&&J+dN70J^jn-#4T#BNe!GIAG0(*W=WU|9V4L}T3^2us*zgMKg3?;mV!|{ zMlNid)L3vaBZhMck9yOXb7UQ))=_sHCW`MyEa1U&JVylTx|PfFf~CnBM3I-j_Urzl z7FH=FzhYGySJ4F>f0WohQ!kUf`1ZVBAfO^sNluj5FAe4;d;N%+EPbwQ{l31S^6M|^74(Uv6Eu*BT(vOq zqgfwHxM557^kr{_fPOrs{!9W`0n7z@1{@khC7Uc4scwpI|^c?JU?QWv3Ni(`W zg@gHbsXEl_)c0hSOs%|W<$f(bi(s-Z)5#LoeNOjl$(Lm*@c5%z$PhbR98n*?>fInQ z%S)sty;b~b%7~S80Pp!%MC0$huF185SZ&4cx4v;$yO(kuJ>XOq(}s@T)HkGYZ_g)ugfy3f|x!YRt@T}dyOIde(1mSC<;L7o?OPMPTOeYo&bo(y{AbA=hQ{{&N+B zUKIE@osh1J_IW{GnMrMWsr}yd1cX$|nwFpY8@TLzi9Y|bBvqfA7HnHoHZ0RKE_)RA zI*d(y(wbJ2Qs$GHfEU3_T-H)sWYoKk4{K#?+2;~ECAxHzu6HUdX&U>fIFOXs7w=w@ zb5v8y;A^uSus(frYSQAqN5c~e!zE!eQ}}Zb-Y7%&vq3iO2`w3CBD8#u`)j9zLF{{fI$2z@;>8vsKbixb*n1`LpqHl66?C#XuLispbhhattLwiVP8r^-aKLeCX7oTq z)%b0x32usX7&_hxVMXH_I?dHOPL_%~P4Ut$+H^lu?88;QTGQrbBA&_G- zca2ekCy(P`P@}kmjC>xO{)bGbpfc0vU`z;P8}xHO3)KFvipXaz|J$R! z#A9PNhlR)dyQDS2_-yJy)h>15ow_1VP6S}KI>%dyx2QPc_cut}#&=rJW~gkNXT=#m zlDq^J3bUh|<=e4zJ52rP>FUVdo26pnF{8HN{$Tuav_26@l_BZ5Zx>BnUB1kPhS4@2 zj(!B?%W36W8_9K?(gL?LDzMrr5Av`2P7yVHkUPlqE0CE(gsS6-pw=8s0VptGB=UDC z%bs5ul}^}QBI69glB42W-%s;7ET9ph4pyLU49lqJ&Cud;!BVNaVJnLF7zS5HJ^I|V zW6q}D^%H7zlWj16m_AVN|6Aeue_8E;5AWfN``2-?y}{nnt@IM4wk#q-s7ODqF-8j2=HQUuIr zZ%78c`y_wIe$2?UnLW#Eo^*J9EmpV`NE;^!Z%~|U*K)NrI!*I?8fde)3L0q`GC6f;b28O zv<0ckfY;K*!e$%9Q%o}@0V;6`Xu)|`mT^6x@&5n!7VoQ;gnRxdlhWPF2lW|k{#juo z>q7;wxMvIH5igx*stdqvpGZ*nt661I!+(2M%}Oc%HdyalWHNk$*XQe$m@3d=#Wud1EEpwxiMnY=4LhDicwYyj}6}8;B|WMs3V5rOA1t?Cdefg?-!2) z?OdT0PH+NhkI7Qqta96d7&?b86%d=8bqeZ;q#>Q1C_Yb zd^2Cr<4YOzLntrcL;KPD%) zq7$~((ajVsKqSbeE!v?@Kh=92^ zM3ge`_gu?kzi(=;5@crVN#>5?AoIo>noMkQLb-rM*MG3VDnMi>IPumvau zhYRmsry{p|~03_4Q8 zdaz#YKUHAlA4c_#poDii7y$+JxNX0KXCq;!k=zu_^{8)sthhsW04OzXXU$ zaSg;DZZ{l#3}bRpQACc5$0FrHh)a|Nr!YE8kp{z~7Qm5f{>WaK074vReCYq)Tg1YW zgorBMztQUQ+1&(Ns>zwQn6`hZ(HcLa;vQHY%_r#&vGHU*t((J0TtgRI-!9Z_T4!w%7l4BI9FpZEEsxcFRTiK7Q028X z5eWq2Hw!fl$~mVG>F|QbWU$+z1Qd*BXhCafDd7or^&Eop7g&kl1n&|*24&u~9K?-F zh(6dAdswrYzSK}MEio#oAUw*((jB{#g)O5#HjjDp&r+9fLU#y=IUk=XX4Pv8mEN)Y zaNxzTW~U2jW089JLE`E1qOq9lzOQyk9;$8GKlUKO^zc=|B;6bhi|UZaaRATBIQw8b zInO>d-b5%C0c&#IygDlW7`t)U0Ir7`n;me@%;E|ysR;|Ez-gu8h+_4cH|O=1+?_ds zN#T&4DrI;nw`iGlXXlfp|;JV z%!Rt@P;eJV4=`v-rTINCCWX<1oGuiA6sK3ig8RTw=WV(LaKep)N`3E25-FUy5FL; zaXfN3xn5UubK+o6<8qsib|PBQ1wFSKM(Ox&&^4D=UgqS!(?q-HvUF^!OL44!$hI@S zF#Sz&w-SmQABedS10x^3E@_`xJu#|ykXkC2H-_EfHc992F(VNX(v>np`19`hP|Yo? zH%X-*toHcU6A7lbUekG7J=xqG&o|+cZfp5%{!o~H-9AW#+{vUgI%whIjJ6_C56_lk zaJICLJu${~Yu@o`r9sDoWq#I{>zubEX%F5!Ry=5bFChCQbWTqu_@1d;_v64nTmF}R zeg2tpTbo{Isi@}d4}?B`1Ij3RxL>Z_pgPJt)bCsSlas0TC(0;%Hy4n#B=WIOBVpvB zn@CL=_?S&+svAr$iu=UzN2iZHWDKpE!LH8fspe_7KsC)x;LVK_i29joA(8?ZYARCpWBi z3=?WkcU3+lsw<;jhU%E<%tw!RBxB7AJ+6T9s8dHfMZ!pXNMnYEn+j@}IqbFGJ2xz~ z#8Go-RR* zTU3>9A=25=Fao*bPo91{pz{2SY{`}iAorGCmh1iUb;tHb3azauel?_skMrX&)K)aS zNb^15;z_ey5wPceYF3s=xKqR(ej8+xy&s!8v$g)Lkb|x4c6pJ*ShiI2s8HAWno;co zsrgRtDrdPLzQdR62?~K58&&sPUCcWHIeY*9HjSpH7IXjn!g)xDc91kn^>m@M_x5Li zcC@Ai0WNl3c?)n{$C*b{RoPWv{Pxk3N0Q_cV2wY za$IPg=%DC!*|HN}I@~m&L|kC98>Ret=o5j!a!BE0K z%&3YUzeN#L&7n>K_2?zDxDnpvyT2^z{#DTi>d2$cSvl~;jAb#Pa^`!45cC4axjd{^ zzO-~gb6+g*+@1D`{_w`Ub6v@g+<^FOuP1lg@Gmi7=4XP?kJiaKq`riHjVpS1oM#t3 zEO$|;t?EH8Y89jdEvn(MY4;miE%5Lnsifoyz9Visr7JVY#!iv0wBuQNri|j!pW!OZ zZ(WrytGYKIos?BB&I|R}EwWT7A9Bcst?L*$--LUfKy7JkZ^eg#11KT%MckIGGQq?4 z8<7_x$Wm54`xpAl55Sz&auBBi$|xCVqbq+LN4D?sbDkvt{Ea#qL-thThaA}B$4zX% z#oK4U3jWzJ#4Z;m;k)-BQ}%hIwEw1Q1ndgJfA$-cMS3j3=WHMI6!OHMF6)e@376IN z{LOrO{Wl=R0mnXfzvBn<_ClL5IlV{(25f@$sNKIA0bA!$|M?iSa~4Kk_WU^_6}0L! z`a)&zk~Fmrwqn_xPqMFW_u7MA6CfnGyDhWUZ(FCh)bAQuQjO%R;_-n}yeQ+2J-^If z&;*3~bY@;G|a^;tlWJs+Pe!hE$)lFjt{OU@mb@ z6x}T|MQU!H`vg)y>)&a;DFk)>w)QFbfFS5*u9rNlQiG@LeQ7GoEG=O4-z=Ca)xBA1 z( zT3spzdHPeNefO&O=9ax@t+X9vT<1l-_s^d8T$Kh>CyWoIOQVrSCT6DndsERoxn`Wi zHo4luZ;W(Ho{@hwvS@cNxD-)ag^T>Q?=wyagXQ_H5&HvVxHV(>v;<&rbESO- zLVDWhOU>6WRkfX3$fPf?3fTuiZ^Po_1ZWUyAg{f(Kn1lGMk;h)+uRM`ksztZ06C7& zhUGu0E8!^E|2+lfqHH?s!cwEGVou=0$wI?TJ{{p8I#IbxfZoMROh9Ixfbpx)b4cXjJZ^>VgJs0kSK zIN2r?dDa79K@*P!gy7qHB}=#a4TuK0W~i4)2H6=I>L?42UCe^KT*&yQ@p z#?@ZPHUnA5PuaiI89r+hlA|YgY9toq3`l#Og)-_bq{D)V%`!@{qnJ*YVbMF+WL@3nR7@N;}4e+&AEKTZsBE2y2;rCW&wEpnM6A9VTo)jfH7#|I^;Lz`vg?^oVI zIC?RPYcM{wHB8g}Pd1gPdGt7c z8tl+m1##-T-W6?71^d+(|LuIf+fwILtE(l0LU4Cg#@R|p2<=^*p@F&xE+>VHb2(!) zGRN+x+an(8Rh)|v0CBcr1ZwZUD4i2L7GShxlz5IWC+3#HS$QQ~-l_Wf6TzITk4QNy zotRBuDa0#Q*Fj&#XiDw174xmUAPbc zoj+XiT^R>`vg(q|T9EFzR>`zgL8q($mqId>LBj2)6OT({Uz6&sVjJSBguFjf&?3F? z4(|w92XeQ%PFuBar8mI96r{c;^l(#hh%1|V2#URq4c$&{d)H`m$1T0@Ycrvq4~=Iu zNRsEQCiLywZkYBbw&=JYc|3U`Q9GAOwcPy`(4us`IPUufqfbS zsa?qFlX4w;@0t0!^dp$7oWSnv-^CSIj=}s(_x{(m9O{-6@g6dfejuT&Qxyq8JzS}p z-`2{5RO!}E5w;2Ca2dUln}Klr;El5^qqQZI&fa~l2g)*1OF8xU9}#qfTb6sI0)mEH$uqO3pHHGx+cQC;k3r~=!q-IF^^4Ti818>;xHSXAbRJvf z8JVeVPa2RqB~213;@(GGf@s*3%6cMGncMxK2$&S_O-_)qtr|R=o)(gYZx8VE3m7^Y z=99oPl^cs0*#;flpWNN>L*44bH!}wpC5ovPJ6j@Q-bpd_xe&xz=|pMgD@>I+fOyQ* z7fW!KY!C_aU4z;e&hxrdbzRtQFFD>dx;dqCClXPcoqzdNB~o(h`^T)`m2?Z=NvUo% zyI4&7UVRj2Z`5V6iM23db7Mbp?sUC8-?}-ypRG%@33-hyoKT<$0t;!C=uXb3Uib|x+ED0?Z0cC3H?_%gWF`vkpJg2ASx#mCOCR`xB>$YEca zA1lGi@BYMh6)chLVO3alwbuES<(dXrnfuZ2A(Q$wdPh?q+}xR?5xM$ViO@D&l@3I& zJ;$+c*O&bD`CHQy`D!*9Q#Us`iJP?7GV#f_t>2A7{G!npv?b$zkV}n60>w7OUXGme z8GlX)gd;&fzw@N&c)k9LW}!uRIyc+Ml=&8?SDJ4osNBcN;RSA_apJh z+J_>V#vk2a*cyM?bdv?dQtnE@$9IZzU>zDbq|>RT$nlw1!jVpC_5RP+BoG|xOM$3I zH}k$7E2HifKHXb#Mg z?upl_#DRA3EgK&f(nik#G03ni_uyeq4I1c-os%02+wSuB ztFbV{gW7xrqzeumlo;&62^1qm0{Eft!QCX)7U1W2!3&@}3UIVEIfJ4&Ka{K?3H<0F zrn41-m?E+_dJEzB1hlwpcVR8q=wki!DW7zWvh>qqGILT@TO1oWe!j2;LZXi{M9oTO zcNbKNg6fGr01mPD9%j3^s!aJ0v|dnotEj#OP-c@YaavMc&dX;CAdSZdYDu?d7#&L7 z3fSv`1TOKn_!Mq``i-*au_E3k^V{k%p1A-IG5ln8yvP0s!e0gMorPEz3#v#Yy!qL^e-{ZF@_)Tc$4=Mzb04UO3~rey;)y7DI0 zh!HVbY8>7=diewVwYk)VZ43xZ7q(Z5F`7zxf9zb)ypBWcjXo&bNZ9j-3tFglPwWy9 zIl2jG{an}4l8zIc_|eob20RwFLDupwrzo(h%>NJ*Kf%^6@vDw`>K* ziLg)S?=WFUl&Gf~&VYAENnbPd#YDc{#(j%_KdX!yWSgc&ssHDMM4!#qLUEw zbI7Ek*Hg5hj{qfBXClQ?7}dnBfOPlQ_yG=Rh^I4AAsxqPA169+LKf_M1%Et7e!fyjY|Og z`GlRH7YCP+!YD!d`Yf6HCY)V=*KugJ(5J=;&o%AzQ%Mf2$@6G{20rh9G;sNXCMoP6 zHlaTeul9HsHUJJuZ)*yyiJ=ye4I4ty7x}BUxh?t06`6r$?fq&HwE!PA)eo z)1Wb7!kkBjX4m<37(fxpotMNDs0fK!Sl{^g%z6;d0~Jza9GiViKK`}7Sl^9 zqwL&i_%Qywqs|CG)j+XG3RjPFSKlCoACU>8hu;Zo&tp1>+4>h|0XqtA)|(5*jW~In zl`W$a;2kh5EiBZS$X%AFiY7rnW6q}p;qt0m3&2%bNUt|3Z#*H_!xVMJe{LQAW2OAh zP_s72SSK7?`bjhQmp4nmIdOE!O%iv679~kULzMCrA-(2~)Y!}T6A$$7P+~%F7d}pR z9zz?dR8BY}kG!9ui`Vcjr>*G&YX>sy9i+!x{Ua~nSD6HjlOyp)oeYs?U`af+#5lxu zW@<{dY3p9y(|r45@$nTv>0W54#%E9geNiLS(W%ab&N1YmD6 z`7%fD+two!7IRH;QUA(DDJNE_6FC^qv}10JY!H>zVDhH>@n# z(@X0htbL4c>3v7S$bQh`N)6}W->ct~Sx?_)vBo5bc7Hl^+WXD z|H{s)zG3b6BG-SL&!Q*zjc_X{q1c>O-paYo)xQAzqQPKJ%5P`R{bQzB^AK~u>%@Xd zN>3aA(`i#fAkiEKQS_jYe$(+d5|EvVjWioQ&{MXq;91QMMRyN=`?vS87Lj;|o9$Z! zwO)Q-IH7$kjF6l`6-Fjb<5%B#36jeXVJmG3RIUp3W%J~GIu^X}9IF4oXJ&fn^-y}L zYRma;wpBMHW(Huq8INj;X3QEY&nizf2F z^`y$GO3OvRbs#~Khgl8KBDZ-s1#~M6R?4ga0@sA$)~x_kD-|M9wM7ZZ`=R|fKpZJQ z7@hR&^Xg8F$f^~vD7jH~~N#U(FOAzCqSS0B&^1O(8AzDWK*k zB)wnXwQa0304IT~&o)HDir$%Mi`erEMmuRMcLK*%xkBL8>8ypBhLz(=&@bb=FHSM)e!NJds-C*1@zJ`8 za^XC8oWO5m!QmCsCB)QHAp8mAAxxLe+#=o&HU6lID`Oq*M11S37RYt$#2^E%P0;~_ zOsizYHkbpdwkvbc)t?k(hsGSzYfcnZVbzG}3F)&{UyhZH7lT+9@28 zlqc8!hgp?H&r8?E85Pd*H>T5Pe!=&O{09qgy;yr1a71E%LW9%Zr;o}){t<1_Wl;z! zy3A=JUfqO5fbxvRvuK6~5E=Yv6;*H?ihv6z3uA`)55d?CU0dX$$Uv-rTYUD(xNf=G zk$MT>z>|cKKj9|TW=e4kVuipDF#$AZ?RF<^ynLtY2Q4`YDCqd1?8dSyex8GsqYg{N z*Z}{*8G7~afJ3e_|J_r`(hIP3Rk{r-6jBkuud4kc`&m3sbKFe{lO7efRBdqcJ5?i8 znesyZO1%Lmr}_D@ofzE<370cq#iU}V%?&)d+b<+RGPn;^hAu!h1zHr(PR2DR0rAoJ znECvZme(nip$!rB8$Z7^IRf0$>@;0T zMZ)?8muUQbKaPx*_q1`d`>L|jLTgQ?em3Z$HG{@qRRR+MD^&tx$e+Vn@^P;1W}ZrN zTSp1@ON=KO^z=lZA3eSV-oxqi`F0P)xWGdN)LoSBP%1_#y`zvpSqC1fNnigtt^jHj zuaTfa9$3gKg$DiI))LqJEtOD8p=6h+tf(2lX9{l#9x%=~JF1-S%$B8D``_itPFDzd zJCdDT_wY6b(To&@!tX0)YnI@>vwfJ!I@lbw3%xGV0eC`#gQn>D@|4#20`- zXYj`V)F?S@EJvoQ`5~~#IWN!39ipjYST#^ErA=yH(3^U-O=@cWoQs@Ae$n_NP<4t| zFaLfu*1`9CcG%bvEYE%kEY4eiU76~U$tni=-5yhPr<6Uwk^`h80;PU2F;-ABJas4d zkDpp8ctKXPlNZs{JQMWx*{dYckoAy)zR=Ks;GLQ_gICNE`4NK-Y!SFiw9z1FvRS$J z;XA#5SM%`hJ6 z^qWg3^tm1xj8-H1=Ax=*8H7NPzyt zQw1B`Q1rNT=Q*5%7kHc79LxyE6G>HYl2|ILf9=2hU2AbPA$ahofiFf^WcYxeG01Dh zrrWwN0AraaJR*^`;+eJESkfB0}x4~0bhR0w8{+xucsRkuZz z#6dRc^izn#pttIwIc}CjJzuI4qsW_`+Ai6=$>|{)xSg2(E@+NVcOJ^@cdLMmR$Zoh zJLfNOyG&&&q;}Q2M8=H+ALV!&tl{Tci&wHhC`dRr;p!>nbcT+hJ1KB{(km>6M{)zC zGklQ7aJ6gTANVd70a@2GE0n(os}P+*MUm03N%Pj5B}fzD^I6~C?Ocly*euO*Gw8~^ z7)jFDYcD1nC$|1qF{ZXtm1-EhULa-Y=u-UfYP^OOTlKBxjXC0;jV|@Rq=E6F=P#R@ zK|#0u9ie)82xwO2xmZrvZg2$fN~|Z;PG19~U**69osQf22V7miLpX)ipCe@U$q%gU z7C`p5xKxmEd0X@lae*YOLR(mc$R&Y~P(xO9x{=Gw!;i4{^0%3yYAl-_^tDtMnTu-B zFz>>7;N5_-W8E&TA5X38HSg0qu@2ZRnPFSyT3df9rtX(*_SR{sQ|!&3qfsvL&1!b_ zF@1(MiW+TH``7Q_uuO5QN4S0EmL{Y7p`81G=^2QS6{o5p|n!qV~VtY|)23Y(34L?04S_)at(TDF>fpV;=^qFLLMQMyXA2HT3E z2E&*&+T6$$-2phztpxO+98#{B#32npB79r!kt{sYN@ntQ2ogXOLa5#^vuX%VT7BCv7I;T3}Kf$UjHE-7uN z0<~RygioO)=@TAB@TZ>M7CfLcM*>!hyq9aRJ$ksV3SE=nhYF@6{@VpXxc@}S@&T#^ zx&fm*2Fc~AQ8&tft3E>Re&U_JI)G_GNk#Wa6j(4OI|E1RJh+2_K|0=ckvTK3v~_E} z?5R-?VTKkVTVeT0)MEK*bR5iEsWI+9eXXsRL+Fw!$T~%Fk&e;LG{G8Hbv(r)$?N#W z!+*M2s?l#8zkr`=<;_IOEj&ysO)9z_HAzgYr&Q7S+)hEwvg)-HkG2>7>1YuwwC7EO zj@HS<^pH`g?}Oc#bvb=%XdAiy4hMP@0i}Sy6%V2%zj;co8xX9~7*>75iamyu2jE4n zBxIG9M>mfX`&uiT6Nr1{R0se?JZ0(tSlb||sjEtWARHr#xY=uUi)Mi95t+2W)3UQe zmDJjsNb7xy>Hjn*!;%asC1SSdew@L}rKtpF1&qraseV^Sf__%O5x&_4SEOJ(k3lqTPxY-E z&U3^Aqb9*3UOyfF5%+5{SjR^0O+^44Gf4_-s%F=Pnkm)0>v`_`{YtJwjIQmkqn{3b zhi{iMk=lw{<^ub|0M+UV=#3KqK1ZQ1lPj)=^5Cv)!R>ztWIDmSkF8|@9x#U0e-wXo zvf6ApiU5r=!w0g1x4B!^zpxrQ%834Oe8e9=K2y;gefU9TDtWkjgG4^5)?9$m zw*Egz#6{H>`pAm%FPf&|DT6e90QwK@$f;HNyXmU{Ry#<nNguV@DG!Rw4K7l`ug;qs41T(e(Fd zH7W4Bx4|-Cu!k~=IPB^BDsa9T*mp5<9(V9lQCUFijFyozWZq0IgMCnxt1@mH3;G82 zY{3!uGWM6%yyg#EnyIGMBecKSg6#KS^oHs}vHk=<`}`y5l1{{%&s)Nrh~Xl=?Ch4& z`c%_zH=H#EZND*=cmxzdis}Tb>sZmlcamz*!-v`bfi~!2C@;DlWp__{evg^ze%CE1 z0vJRbPsD0)*<04(Y=id?=tI&mL5pBDO-vp0X|;ekf+#owa77g4f$%tJ{IPu%465-1 zCKq*#$)#95s~R}F=|NRXlWg~m2#`Ac;%~sjGQ30e-!NQ4OWWb~-_%PldC4N8gUmS9 z7Kugoa4jRtH~!ws@1Qf^A*vBhXd!EQOFF<5n9Fqy@4ss#g)tRDfE>ha3-ewT?@ zx!x{`ew)oV(tnN|<0rl<#lhB#WaH41rLa&}B0#i5VjZ9ydo_xEIm8BdFPb?p6m?n^ zEu!_F8^;2juIo@dZLX$BHElP2B(orhcBrSsVDSH~BgY%<^gIVY+9`^XMGx3CzRK~o za47YTsJ5(gUn>HCPXQ3w8WRpTBTD~;&t4A!YtN0FgIZM5?tp$D!?C}&m2#g{#^ar;G(DvXhqKLRh)7RAqsu!_QI<5>B?RV zpUaPv7r@V&()j3Y&jmeEYxibhtOJ;13pRdl#8?D848F1~-c$Ob_G8)Itta@$J+u$+ z(DWRZb5LHbg8@*UA77)#?v#VJkvDp-CnUrl0IZ+|l2E4lV`J+M&yve$yYnq_PYQA^ zbN*LAR3~4~3d)F5V3M7DNq%z^_*4ZVb8dVufAHs5p!ZeoC;Mv9p3bZ^s!+xl^jb

G3&VsoCx$15+S-9WG^RQ34?3cTwjG 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 literal 0 HcmV?d00001 diff --git a/img/linear-strategy.png b/img/linear-strategy.png new file mode 100644 index 0000000000000000000000000000000000000000..059358c5835e61bba274842a1cb2b4afc05e86dc GIT binary patch literal 21359 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 Zj~rxsP8pXL2@*69Sz6ehtuS*Z{u^Rv?>Ybg literal 0 HcmV?d00001 diff --git a/img/linear-strategy.svg b/img/linear-strategy.svg new file mode 100644 index 0000000..52e8795 --- /dev/null +++ b/img/linear-strategy.svg @@ -0,0 +1,16 @@ + + + + + + + T2T2T2T3T3T3T1T1T1S1S2S3linear \ No newline at end of file diff --git a/img/logo.png b/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..09a3c6f484f5ec26f3abc857ddf209442e71ef2d GIT binary patch literal 18061 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 + + + + + + + + + + + + + + + diff --git a/img/mem-1.csv b/img/mem-1.csv new file mode 100644 index 0000000..b594c5d --- /dev/null +++ b/img/mem-1.csv @@ -0,0 +1,13 @@ +name,sake,pyinfra,ansible +1,15,57,54 +3,17,56,54 +5,17,58,54 +8,21,58,54 +10,21,59,54 +25,23,59,57 +50,23,60,57 +100,27,65,56 +200,33,69,60 +300,40,71,67 +400,46,76,67 +500,51,80,73 diff --git a/img/mem-1.png b/img/mem-1.png new file mode 100644 index 0000000000000000000000000000000000000000..44196cb69b1c78cc3a9bed34aac0c33631a2ecac GIT binary patch literal 45203 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 literal 0 HcmV?d00001 diff --git a/img/mem-2.png b/img/mem-2.png new file mode 100644 index 0000000000000000000000000000000000000000..ec0c44ea763d403df300d7935e437ed92a7dbee8 GIT binary patch literal 47289 zcmeFZWmME#8waY0q;z*lNr`kzi6DY>gMtXs&Cnf!fFLCxDWSAT4>h!uh)56JHRRCT z&58H;o^#%N*S+h0xF7CXESH1Mo@YOM?`Qww`Om8-DhfDQlvvlUUBgj)D5rky8VdB< zwd?yBXy7-~dKc5zuF+ppl#|wk7;n5shiJA_hBi%U>Z802ri_Kh#(k3)4ScAv%19%3 zU#`6QCW#vUlluhn#M0N*?$_NUW@o@`2+I5({00m4dymhHxf0BPFV3GU>W`N^ww$-k z2fiSs6yfjpGALyA_4O%rANr&I_0z0^NzMj!qtsK0{N=az2{RG959qHG{^zI68im-n z9+!tz0p<4n|NP7@O8oT#<@VDNR4^8eu}btWWBJ2+y#DJGOE8~6HbtRn)R=Fs{pTkn z3HjFx%s2i-llBK;LeYPHy^(S)WTd3Er#tqSD4sw(@O}vqb5!6uR;f`~(p#oqq5z4a{ZE#_ zU;h6@UHiY8s3bC!ps?t+i%k49al%_&rO)e*><)sqj|)~Tzd*j+h54 zf3(KMkTpNOJ9?fbBK_M=4+vy_ss*dx8!;NCqmFyoK_z6{-e*vKx6IE(SeU#oLyOIX zb^v#+l;eFF!hxNvNAjG!*r2kjYV*q%{?{~8m||`hq2rZS+2)Zi!XqPF^ZF(hASNC! z86VuD%3eQGj_P~gbS=a8g8B6WCQ1Pt|4&+XWV?{HQvLHsuiiR1I5htlF;48&byCfw z4;JF)rj2p+re6G-*j{JfB#8F_qg1z@m?K5XZj8Q(ZK~9}(uw3nUphxA2Pcd}*t>cQ z9V%&yw$$&7t|~bwfRtWH_~RTp&|R$!GV)2%R)ga#EaT>G@-h4(*dc~ih*t-8IzJ$W zfVFk6e6M99?M<~c4ykk76s6sU8(dtzX`Y^4SSVPJs&*N3>V1b~pxxbT?ZjR-*Xx#W z+9&ULoP3L6+r;+*Q(w?N&-(IkQdb(Il-)Rk+c><+R^NgUKGz=ES59{w<%&6pa8O0aoUtyQhmj)w1TTbo>X!sn>e>N&ddPl<^UyNh456p(i0hm%i_ zJ6+mV$jYpd>B`hPrDuduR1!`mRg6*3dVbRJE-sR(RXjLQ9=S!si{^E>*=gToBYW&* zzDJeHdC)e$sW9Oh5HuKGoZ6UF7>eXzK*yacc7HKnYk{8aKET?S6wX_!zemG;cFd}- zuC7)t;~@^uC3=A4huh!ZzqGDjwHm&=*fV#Igtxq|w&?zXnKO8uE9K_QrcgeEpdhLJ zKuXZe%nkL96g2*p*Dp(3hlXUU$`4NuAoG6qvxVjvb1{2l&$p~}b%nnsaz#3tDSRcV zx7RLhPvU6oI9Sh$8yr<*LOe(Ia(F1j&~#e#B+ei0ybX_z4o#{iOX0T)e>I;m&?aE! zH#BdD8)QcNqO{AuvHC=R+ZWa1{1&yDhtwsT?tu$KKaL>Yu^GupO5+=-9MA z-L7-@F?8xb3fauC9w|fQFpt*TOSLty`NHMKTSf<&)SBRaQud5hbB%h(i|XM#p;Ur) zG9#V!PkVmaB6fCKqBm1NJDi$#mhQfKRYS(As+8mx3%~t6O;T_F`gpaMa&-oipxkfQ z>ysml(IUiy2M@BAht3IzoY8NbN-@!4hK0hLg$kSGpKB5)@|+T1L{W8zH=cC36it*7 zsJd(hc0^Hr^~0W|3(oKzdv*Oh{c3royM{*!n?fFDkqf=JZ8cmJ!19qEU)`gX)c zP}tl|7LzdX7b<3@_%7#jbS>R;P#dRH`EtK0d-*7(j|=2VW9SiOXC$JXolI=2@5-Nq zKAgXsP(@R(6w9Umh3!OQ2pv!7Q*iNwUwqtnmXCoU6oRRGu==X5(%Q^q#@Bnl+v1*u zN4EmqR~FyVFE@FbL`h<*?AUcyQ0FIjc_GVFBKM|si!9S6+;LkoyhzzUvM2K)Wetz^ zc#h-ZX1F7|)V9PBBHc~fLk+W=n<0;qN}mp=R64l2CKUS0kn$all=~P2hbTNs$%)#F81~` z8Bf;4Tc4TL#aF(n$bh6IG8y~H=4t>Lwx;SwYc33qb7!UHyPO8{PkMgDt!kL`xxB+S z77`Mo=+QAw4IN1GzU^VonMXFIU3`S1dO($?2nkV+A>Tte+`y45rJc$Rj@*9*D#Xm_ z&fDa(Iml~DUQI#PM$IpFBNTn9H)odm40bQLwTs9lFBbTMsl{)1mU*etvqaNj+w-V$ zn$%*++;30vUq-{@uO6KL=zGuB0^#plpr4DyZPfU>w9%OPaub|b#doBO^{!NilP^Mx zHEl%bJH#K1-;W=eFWc`fJM@k8EJFAa$yl2rxF&qtR1N%^TN7yG=rGyIR3YgepM+jC zig2kS#duhL2Q~q(QEjv#`F+XkI)w7Z&pWE&?Q}?P)qR;??u846>w-3Sbmkik z8W=V9+t*@scZ}}(MH?Cs@f7u)KYdoBPD$%R^=i1zSQW3?ET$@sXL3bPdG2D26?ce+uzI{sU71zM zdwB0{nQWSJEOu>z7&_cI>h`42?$SnvreK?CC_HVe+`C-}vfO}NR6=aMmrBt|c)ZHM z6YXbQA``)AjAmHS3`vy!8?e(ZVWZ%e(M?bo#V2Cbci7zx{07 z1wt)k4{sa15#U74h}7%4d_-i@yb{~myJ$qrbr?9Y>2Y5{wA=P#h6gYGr zd35x}+}GcMokESJ*n7m7R={a&N94q*{S&!$qUXzwZ-Q!2xp~`cZ(_heE$?;d#&YkN z^9p?Zol7W1qfPkmggY_@95KSotU8FXnc4Xlvb|nVVk1NjsZueo|#SD%_K22GAw&}UhTBF9T1qU z2C)wgnSFO5yRbGLx~;RG(of%Eqg0Q-S0fa~SWh+6Q>tzHbHf9&=V#wVLwzr?A>edp z62ZHaV7K$`O*x0oWlMJsgM*vXJrizij`mNB=_Y=0Vlv5JlgW(ZdA37Px8efL3BP$c ztN5Mz2tA0Vh{^XvHWyV}B(JBot^0Ch`dZ(;nV3B&t=fz{n2uwNuH_}#fH#0)`@q$p*9%-)2+Re zGQy}W(r&f!$pNXdCunHeYr4UR94R#o0q}?RGEz&o zoKZi!&2N>`^tJWRl2%xZWJ@;Q@>d!KWti7?KTCPJW5IOIJHuS~@lc}j5Rnw$9ClmP zaUlR<622$B+kp}NbOdhmD4!6hHb=KqwRP{R^Lon?J zk|P>bk=^{bJ6{%>TUv#^=AIHZO%NMc<6UBGl-xUG|QCNeitty*Q%Cd~cR?G`*E!{erI zcpoBbK(Z`d(dk}E$70)kZ%NmaiKD}MS0{nN{n0T^Rp@i1uEtZxgjZ&5@-Fx8;1E#u1F z-e>qH@2#4le-1auXuM%A>aPfCSF$M0N4vjJ5|NL=c#tE%muESe+31lVE_RW}gTlg1 zQQF4_Wn9*O2z^-08)Z@iM?<@5HeF)!{7z>B2b88x{8eNUw>g#XPRM<4-n*Ek54pN( zA~^5p6c68r6V1W9bmeH>2eai7dsR`pHy@UJz3Lh!^{7RX4!uR+L6u*SO`YR4-;LDU zvd}QL>zE>Af%40DSYB??=&Ads(@9}UZ6A?Hd-)11hc7RsFAMyEjo-xjKgKFmeA9L( zze~&e34x#Y)Z}+(%l1#{UD6~X`HUlK#0(8X(~x24vYl^;&RKc-{#R?Kc8^uo$S11| zP1M`>-UdI1-py|aX^*^bii<4;p)(S7?JCC`y2-*I;B&o8SM3Aak&u1RYaeWiM-Lyy z$F%X0GC7?gRo)uVbwJIZ zN%!?AF_FjomBN8c*%m7ldB`f-r~?V9c|~%RgMOm|L3Om@fJ!HvyqYUe-aZ+TP*}Fv znX$8M;@2a}$Sk|inX+p@M~tE2#fppmPKWRD$O~fQ^1wS6e%*1*<-VIjHO|YpDOFZs z7X#wVLkeLOKKSHVcGXA9nDp=?`CLWmv*fZ$9$7eN{Ejc=i8!Za8rZ0jTU%gmnKhC zu4f^QW=U!b@(0pkF+H!kBq!f0d;C>`IqdjR_`vNRWPj@ySiK*z-)1OwcUR1RB<}a3B6(F*7?n-6)Qf&> zPBbv}s9>3%vXwJyshD?6Y=1SPX^XTsrK*5pl^*4A8J}29SK&w|3yEnoTC4ahFYnct zms<|jNe~hemAeh=;3=o#^YkD1zMvO%vt*@vmTF8|FVuej^(=DAi`RQ!*aIaQa{Vi1 zu+9Qrzsn6S10$ zZ&idn~GyG3lz3vFzC_vjU54DY%$5(*_4fIF`(lI=AhhPg)#i!x22LQcvfk ztJ7churlmpFZIQGBv3@+V=&BC+f66hL$PDY%xovhQzt*lbTzz8LQ@;~g7waE;Mp(- zbdI-rR8Mx}4uh%L9mXX0(Sl`jT)PhM=?&C%csyFU{T?hP>0vqlCI-Ve8OMw%vmx-QY(Dndi?XaTf?I4BJ45bgS1c4@yR3h zTX~!1!M7#hwK~6s`2MB9Zf|?NyoyXImK+6S>Y|$_9C>Bpu8dT&-s}>AF2I z*o%w`(bP4D76disV!d}v+ZVL0DPj~7?KgQYG#fK-3zJ8oot^cloYh%ZPbMMHk>AZ# ziHBhv9e1rZAR>YcRRCm4Q^V*Fo8C(yjc(Bi;2NjX?)syYV?oV2?VYH;OfDcII`_cw zlv7rIJ;{~*B8pP|uKUG_*xA{gE}HAgFl$1gSjz8}k64d?+@Eweo#1jT3+WnZbl;wQ z%}ZLCZhEjMeF_$e=^j?;OEI2upHcXpHX&StyGcK3madK#vCSu&y|IiRFk;}KF zxXu7IMUPAOIhpUD5>sz0)bu6SDKSh?#cONfwrm2>n2zJX?M-cdq;}@@;;XcwgL>UGm8ZvCG=Gtw#5tmz{wd zb_qzh2j7sQg(qBF#-_a=Te<~(;Tgun< zlX^F<*W{wNduB0_9XcEQ42Rs@v)U+4HWSJ0B=qx-tn)k>-)DQQyIFp(A!sx1$#asL zCCPZFsz31=CyB=>G_tUeSj`bX5+dEFxsPu=m^eD`*`ko~6q)X``$1r5?BaengQvdFDfK-_0g>pI9?~weI`RM`7x@kqE0(Sp?QOCfk zL#%QsLH=O*2O|9Zna7^T0=0ZW!q<1;jJV${r)$Nisc8^BPV*O{!R@R+&GfetnE5 zkjignMuc5Aua~PXF_L z8X+gEb2s(eLdx{6Q49Yj_WR!h3cdNtnBy}>dXxWl7U1l-cT#VZuSCa2yhF{sssp^|`rIlQF=gEJ+tM?i1&klz%88G~!$Cs&t z@i}mAg~|UB`W4Q~!~@E`B=s29fBXhuF(d$!L;vMdVa`7&&u=Qe518n|Y?t=0=0H(A zFb9-wv_AXQ99WU{hZW)}RQ;KutB-k_3o@)tNsjwpUJ4xu$Vd1y$*&*7!3>;>@68Un zf1tqMT$B_1OvGAiZ1Jz2gDyLWJOM4VmGPH3eha%6qC*#L^QTmK6>PH-uvLor?^2Wg zI!8$m(22uyj$c1UTL~Bgc-1}=sF%(~t=P26^h~E|TVv3+SG@q)Pk_*gwp)fs~3j>H1Gg~c-UIwBCAsH;md z-zyPTc&L(%__R0xzuFo1*#lCA>`hw&Z;n=4Cs8Y;3fkWFIdRD~BYeMn8}AMpM2MZ# zgw`MkG5T=q+{~BjTSsYaMAkz6K=XMEIj<+R3XRyC+^B_x=fa|*OZB$JB=ghWM2eU2 zFcsVJW^))Sxl-bf@uK#%WtRbU2p4I@shixaR!>I575(y8MU0wZgU!y*M`g*PnE^5C^X5FO%G`V8(p>LY1>2lH>gI$*DA1P@3Zs!< zgYYYtJ5^!3<{8<(4WbxWlK$??y{#F3=TYBteZC<-!mlJ|@~8(!9X?GD^0s~H@?it! zCo9Jt1ByGt`c;?v*e{wcuLIRTFk!mr!G2$=<<#e!@N{0176x=)x3k-*+vgC_|ocCUP4@cNMVxC^n+no6ZPnRfvApi_OEx z%S#|1QDZv3u7NoCWWOUf!hL0?6lkt8cz<3AvYF3zO~?C0CuD{ejCjG>KOh-t+LN^Y z-P{eS@79aDZ9P(AHlE7IEcGFGHdbM6{=Lv;*gbP^M|-8#9~AaxUl+)DFI!{qsmQ{r zVk$c(hVm8RNUZIz_CII#c$#EfT=?%ufMS}45Fh_ym)qk#_E7uE^CS7~04ku;dTo@8 z_r2JLH{f%4QAFx4T917;Pai+tte*aC=yw6jSQ*Tx1Hu&w0O;)iG2C@oATQG2x%o+} zIPmADXwJm^d)|v<-th3(ZEt3C2Deuw$tCy4lwqeM(_}l!mliu)y%g#ZOiVf7N;HI^ z=S%!9t3R%j@fgFtsSTLHYItk=70+2&h%;#P-mQz|eaM}$X$#>TA#>#nRLZ@xRQLXK zyASWD*-Xw6?3N6~cMfvEt&Dv<2u)45GF7JoRz@4SjMMJo7Eic!wCxPce}qdspUQAQ zi+ITg?`Sk?nrT2Tr-rzL3q;-O6zPLis``#Ix6m3X32}4q6BmNeA8z8ox(0cYbs%0+ zeE8M&oe#@P^m0TeCzYO4B~7aE=V`Fg^YT_FF^RA})b&nWrzm zs3r5cjLe-at}6$@-y&>&*;XpkaP0H$e0)5yu&{8~B@GEl6tHjJ?LC}MJEqOf&MAKi zx2Zr}?9Q|7uZm#=q3^fO1e`yYD0iw|M@k&MXus#2bY*S1ZNpPnr$67=vDlYaK6ZUk zqL<5ngZA8|1c@z|qMXmlL&)Z!i62djj;f;K<)Ffb=W)GzaHDEl7Z5Ob-c+CRb+59P6T=IZ9`NaEVk=A|lM-%<_|GW43d7a!B7(ws~MUE0Vsi*%*2wi}R$27p|S zCU_>EG#Z|qH$ELSJzjh+JSJHBXD`5d$2MR4%cB*?0-b!pju?{zyAecB_=jJ>&6)6dWztOUvu8Z>bXVKjq3{!{XLwRW`t$yBDR=y=z zVqj+HyS87wv+2J6d0(f>hT^Qz%&R1iHiDVt|;O@Bzf;*d*DMoA`56h~>~!N6L~ zavhD1f)2)NLv{Eu7O#YxL(_4NE0rLS0BeESfKGOCh8^R+9W^E97aW$4j-oFf3J85f zovW1vfpIx@>#J7jB{$;?!5iJ(?8$pu6G#$xl`U;Ma9DarEXb}lvIo+RUO)hBWVbcV z50lPxqCZMwvOScGJ-$UaxH%KTQfVqcxia0~BU1ZfsrJQgm>S3;ausRG`ZBfw>8h>!He!M_*PlfXYG$5=E#B*KI5&u4XVb6Uw&;6Bv zN0%2n2YY+1cv5HnXgHKBg6Oev^ucE9hZznM{KV@czCppkaxOlf@m@4Y&;T1$YYE=( zY0u4V(qKhuT(1AK*34^Ts+x{(`h>2|ZS{8Nev^;|Bd55Gu|@apM6S=#?h0iaOJ}#8 zwTD(~{L=iB-j4-K&oE|5Vp>&cF$Jp+rs{Bxp0;#iOg6|~7?kFt&+PQmsO5fbjT;`A z5PT8K&0s&z(il7+=EMViFXr~<_IPPZI{SYA>-sw&IZs-~4se(zt3kwE zHv<|6_G4eNa3%uyRe8(e@HYsg{dfi(Rwr*fiTyWr^U6v}9q;SidpYXncAC0YHwPOy zloIZQ)b5d)ZkJ^AogdJN6WiI@y_ofJU2~zHY@(Vjoo3VNZp%6UE-AUoT%%?E({x$Y zpx2d&kx?yM&atMZW<~GfpV{H$>W>S@%Gw& zk#vH#vFsQmcj8<3aun9Mq1opfbL!t5G%g$PqD0|gB7N+D?;sQN^TR}3%{%+25xa#N zjtk9=oG>r@Tk=XlT>~V%y=+jX&eut{5l|Nm0-ny}lSJ-sWIWiSzCR}=Lq<|^TFbA% zpg#@~eqhzy`tZ?04&Kfi6Ztcr9YQo*isYW+1d7{fY$e3O?fW2 zl8mMCbqrarY~<48MO=KQ&gdkbasj*ja4yrT6J8BSv7{U|{T0F-Ww{>P%0atqHwo3U zYgpLajPf1W=f$4fuZFkeaZRWdS*qQl9K|cv=Wnf$Rmf6&`(TCr14FB#^(+B_Q+2@3 z{DYp(ZqwAySA8BK*nY6SsmBp>z;1a`7gLnThB@m4tmtQ7dwAwrL+Ix1Y|^|ZOwvLV zcO1eUiHzgK^Z~(d+N|61dHMK}Qc$w2o1-4op)v90ox;?qw6;XIF*O zmM-_t>s;?19OyjB-EDmrjO@&CpJ@=;+c`@n&@Edbs+E1vK@P9y8q?v>nT;&fW@Hpc zG4DxiPgZOFR;b`^{(7Uq46C$3A$jljzMnEr6cHNzrz!rz%#Sy;TbKfhB|lJSCJ+EN zp?6>ScdiuxQ=kW%%xlrNa+eBtvx;_m9ZUh((-JTWYbF49J7Ow-r!Wui^95`K3keG1 zK2E7jjA@WcKILBfQIcCLC3qhtB|e}K#bf0#iRO8@HbvfLF{xc>|c z$z?;Z?ES}>00L*e0r8N2_IIxP3;I791>^?XpiRW&FDk8~~}EQ;$`_6*p<`H%jhRsZjYM@%_uRQ-$z?zt=>E@pX++m;CR{~v6g=cPEaxP3C8w<&UEu)j}8G9 zRb*9+#L4|x8UZ(P8C=?q%Tl$k&DJI-p|}GpnlsW7`7?&o%i0IE1m>=$HQ8 zO!*_hPuPNyk&7I>kf&0&XhcYH)<)_}5f}SCDj2oJ+Lc&$w2PiL=IO1wLFR{(1~+&5 zcj4g?5f6s6ZOc7c^)*YiNk$mwIXwr2MJQ)Cw*o&bXa-L!m03=2aK86fRn(DdzfCW~ z9QBYGpKV?ZHEYoEA(QXT{eaKu@|+i5s@-zMWg6%?*NwNhYY1rULvkuE8AQY=XSI7V z2_h~IBQhis&xqKFVb(A{$V%5K);6fT=671doGr|NQO>4QQbY`=diKn6ERV_U;>%4E zj(8yryOEDn9j_^)B^x|-&&Z@ggf4~ZjRn}Dp0T|qV#M$>$Du;f5FMU45Bo4!s{0Gi zpWrrB9NM*2gIw?RcE_EYo7?F(xNHRg$&M5bjPzzAiHwff83{JE>|ha5pA%fUPTbAA zPNUA@P}?yrZ3XljlaV4l!|ev{L|lHIWeV|}Td$KxZ-4mU^h&LLb4CqzQ_Y?5+MG@R zUAi4N>19Ly^K9+WkAqMBy&PHPIuY(o5eQ*Xa<$%JwXtdAor=ylKa>z-i0XUEAc7o| z=EJ`A*{!WaMaV#!MA%kCdTlAfZS5N^Qz(wKjJnmu>T*tA%e8R(cXN(*rk~AE`#T*r zKS#`D#K(PASc}MBpL;H6s(!~uhr#dBC0v7Ft8iWW?84pY?yh@Nb{9F0e^AhM#cr!c z+sJ7j&STam1tCHL0wfI?6yeVXa0HiEUKq~eVB;x7Du_MpJbaAUE40j;tTNMiO(lla z;OSzaBzv(fky_jxk5-vwrWB-qq5SemX9hdY+Gu3@yYHEaO+Vq-m5B5v1S{xyqGFz! z>Z5K~!9~ju|-!z82jf*3opkUW~oF;y=(t5ZpJWFli5~38B+m9V`=(ZOXNw%ok8#N&*0|rle=qYMBh(9l6?WQM8rVL840lAr z3KOM5bZGprN<|NejVm_}R;IQ>ko)f{AfMf*Ig8p#uuYL6UXMEB(RTEdl(66#P1n|c z+RJ&%py*fHaO0&T5Nz_Z!}GXbS_DDdK@YBYGpsQBYD_PzQvE~EeLEzq5dFi(_zaf4 zG*MOKAv@H{Oy-~eO=WiYVJxhWAQML$OdN+YM2G7ts0(6a*A4KNvGqZ*5FI9YtkMVm zg1YZvg@TqK+%tCQ+7ZZAIe3eCslqm~aTGB-l=AxcQ&+6g@k}+y@Gkh5d631=p;c{p;`tFgHiu2-v18-GPt+7VA(cL8qta_PZO!{eRs|xhWQc{_G1fHYjubK z%9oZkFnj47ppab64P-pj$Hs{>@%JAXh14=au8Mv7Liwp%K2+yM|L;f zsuS@p&rnZ>H^k=dkx{-@7>?885@f?F75qVLYv*R>zG$XJNpdhF<=Px? z=QH<&;@r*j{Ah(h+D4qf4`!ab^#91J$RV(6Ps4ZQkj@BLC?~+O0d(JntlFlJEXcFmMfNH z)Dm#&IA7MD#?%JPsk9Gv<@28P9OcODVAWbB!%{hvwY>;Jbj&Qldj0dn^wFHg|9GAj zE*kJ=ok5t!m69<;nixHZY0dLzLY%=+4h!`y+*)hIbRiqm17_|D!~lWN;D0;6@QpDO zs1HF)R*NvT8-`=I<5DC?tyW-@{WZ#C{=mW<>&6=oXZwUi3kx334wIw(>jAI$+>`)$ zq-581Kp)&5lFEZ{*^wifX0g=IqYyWgaKQxCg6#EI$}EJHoa*c_#y7M=P!!Oj{!`rS zMZFwP#$wBIFmQ2TA*p0X#_9B_O0qPjdM|mnx9oioMbC63I zCsf92U|Kxp&9|r2*kYlbBZ#I-)gcc|BS&#N0oo!3Emr*(kp^xeu;3_ja91c{_f0V-zdC2_R zc}KPl`aqc=$JIl5J_Ml(^S2roB7Tku>PGoT2IBa{#*s`Tm9ij{X`mopvn>+^*UGm) zua#G;YUlK`~WfB@c!qyM_`W!$^LUa z{0xZUP4?&SeNXg`7{ZeV<#;Jy7c8nhDp~q@P=JkY)?&+?XYqv%>iPYPw*kmT@4sb( z1IAH>hmDC7>$MbWA$(p04w$-A>d{e7frgqZ{oWW~h)~8%(Fd?e&FKH-tl9U0YzBg= zNZc|qq$6S$ZE&l|9*5~K8?#;Bi9)1=bI3)o}6 zH=mdb8Jnxw*G_+gMGlHq5f1U`6B|ps)NkYhJ8S7*c9sr1bS{L?5o+Is15O(=U+{N% zpM{-9)1;fRjZX(cVAd0Zxd^M*bl@JpPgSHct3|~RAi~uRf5ycHWq-*Iwn7Nn{PQ-& z@JpTu$ZS!pD$`LCu75^qhA2Jh);pN>vl&r$1+a?o|8g=C3E_qAC11~-@LmlK#BG1Y z6eFj8(o<1o*j>D-2U^cz+HBA&njIM?C}?(UGuVcPS)k)If54o-CvUqbWL@UHC{*Lf zbPLD~ok!(4-*TN5JRUna-Fq=`ehoD3%i9=IUv*daQVh+k8|eG0%vNsF`JcT`ho#b4 zoeXCo);jM=aj8M}WFKdU-eiaJD)M4rO8C!wJI83CvZ8fpC-lWY94O>`k5c_ z*B*Lu5A)b!UQbenToIJhSKPhbB%YW)X}*#jJf3}Xp4Xf1SbV->jRLXq;1Lo#=x-YSy<3oA4CNuC9y6Iyt}D0 zvTv{UYFt=|aX~J3g|nkAXi(!Z?>~-7331}!mB?w2N4qLV#WFllMa6FN z{YeMSLj9Y3c@4NZgURezjA8D(A9hv-8w^-5FC2T&G<|CbLq>LyMZ2^B(aPTcor8q> zIauP$*ag$G3((rBBbct1ACR$*$S+@NgPcQBV6e(VB1>prFtDQN1MN($~xr;57bBT-bDuOV|58 zN15kxU$EUTmxF`48(brKb>`tW{XV&7s55iM;lD2qPZ%U=7$aPORgoA@#zFPXY=G-21QNlZa(nV6{TIt)}4}r2`mD)~U0~g5^(fDsh zW`H;=ooWb`xZG&TqC#J6!GHO%ep?!UD7F9TiA+ma>*S4)k-#$SOb&1r75w8W!VdNG zRzks@683Ml4ybWjj@W2xneq2DKKKT;!A^TE{%X2(EAG;`Eepd_Ym*tY{FGAzSAJ2K zOimSyrTLFLcCF`f+rWSJzS_ZE&;oDFFc;H%Gm&a89f$5kJoaHOZqh_XrCWE)9q?qh?tK z@b(hzrK_*VaPr?TTl@Yqjo8o%HH)(Camlm@@t2a+uz^j6FKiPiI6joHv6VDuS)`%# zY#-k}BaSB!*Y=k?9RT;l{sY1J+`Tlw6v|yd3qhL&ki+AdT4%PU`Zyg|Ph}iW&P#S; zW3K%#BLFj46nwr3R{G>0E6uk@rf{16T0Fq#a-(%@rNU{zQ0)^SgiGcIa|ER)4tGV@ z2m#38Cf}RF2oaiIDar=>+YCG*`!~juw-@s|rWtPSMc^cX%BZRn4?y*dNe76eDmcF| zX)6GLr)Yjv9CgTEaL*byK=-!*3HvV?6QT#vP=D*lKUbh^P~)%{cq{li+7C9Z8(d#2 z@7oTD?v{lI-XVquGN#tMvHyN<0H=Hq`rpaDgt0+|Ok2y484|({OsM7E<@c^J8!2Uf zwAVN$OsRjQ7>NpS6hSZE2H;v(N)y;?n153QOmu^F(mdUSH_9f3kaL@ngmr^Y;|ptK zoU9=~f(g(f(Sl&sYLj}RS3I_~(jDYA@NapYs7Dw_*D^9A9Y&?w@^3#JH7TDh3QFX*hsnO3k>Sb|6Z4RHX~`KY!9+ zxjekWet_xeU+8;*U3wc3zh4halLY3rN%du1VXXoznvq>VE%1PAi9>_*>=AlqxEVoZ z+4ky{07QLNWh%i~!w=v5cNYB&$l#+NbWx`<60^OS4nu?4RV0Y=GXyw>s1qhRKHf}w z43whEEI@MpR|2vT5v}z73K>ZguV`X~L*u^?W0nJ0f6qM2akaz2(LoJckPOV-`TECe zJXr`*dP8jfE6(`t?{N?sJJQR2tEAtkZ^TpEi6u-c4C0_nvI|UH)XxUY&+6Gyx%-tK zu<8x;0E!A1cI zt1ORN(t=Gj1KM+IKspKqK{fqf`{WV!_CbBtsJkr>?j(k^p;KBKRJVo1Ye|r2=oU~~ zexQ^;1v>>B)~KrUe=-P$y{Q!=ZU{|Wn5$LpRlisc^wEI;JxzV>W7uk2I2IavS0y;EFQ+YhI2@o62zOOSbO0!|Nt<3e-Q{L%AFas~B8f zXo_s5|CIEJTgQ1FG8_`rBHj0N^NS7$tNDrv{aaW)ds2RT;-oZ2$+`33iF5K>Xf^fm za(vTymfBS!O11Hy&#qxB#$1Jy_ggr~f1%Hgr*>0lSI|}n278k0WDXuP#Y*9~V=Glu zQR&#IrF%M`R~#M{DLdrPk}P6I>Fx8VXMi25Z!bMj2*hhD@W{vFvHgiPk(=QK^P^7l zh(u}R9o6hS1uk;zS+%CbXw;CA?`?cYh2J+j;DoGACurOzjS-`xx?g-Y29;s;Qf#QL z3Nfuk)pOa~w6(qZ=0ZSNQAKl53jTinjO{bH>q zT$TX({W%tp$+6w;+e|5gKGnk)nSU$?1C-?<__l>!M(Ad}OJ~8X?2hI(?Tf@x)zp9w zAD%0NrzE*1T;dBGu1rEdIB0QuO(8(-<9PtkWy1~IvMa+0D1#?^WL`^XVYK(VqUeL+ zI0-c2#35F>R`i+5*Vep7I;X2II!ZX27~0=l3*#qq@hHIlnpBW^`E+#d253?phW~LB z8?S^^s|{% z@u9m={P;?k)b|z;i+4f4IAaQ1W4kwyy>DGKK7ImQZJPGVGj{eu1WV>T=b-rQqw#(} z_g5eN>^?{Dy96!BzAC&MM8?1%*@+b}F2!6t2$@19BCnphn!Z)~_krRai`H9z2B9OAI=J7^->Jr;+UMS~;9Nyj-?fr0}ru zk_gY{6LAX89GLU?7cSuyV<~{AQ;C8X(SE>l0xT^pQqnZClvcHlsBZ*it8QwBl+xo~}@VbDSR^jUM?X z#>HKUa&HK!Gv4@XbEWnKv9{4R5F6(rOFs9rL%)9OiDrlD9h=nosFf_x;)UI}3L}%*!%EU9iPzKEeS3LmLKRyus-ULwQHQOhZGC90}Wxpjn zHlzhM```ye-rkiPvQ>ufB!ThFads(FlU+~Cj`?SE{dL3F+;!-{)D`<~Ch_rJ#4#R9Lsa~)Lyph58qk>jv|rAF|d5xA<*?s$pMp|)MrW`~pYQ+f)?+^6u< zbn+TCy|OfJxh6)lL9ZNgfLs2F3ctV@p;_fZp#I?lgRgC~ZQ_cw$VK_7N)@ha;Q{R- z5*@G>$Au+a;P{S$%lh+3&J}Zs_yyx`=s~t`PqzcT;Hr-5Y#Ry$aT^pB_4e4#QTfkB z1GNZLPTXId>AyL>DGiwRPWty$bwl%ONWWn9+ zngy!wpVmCc2m3Dstb*)!fs+~e@aS0^Yko%B;!_7Xqk%dE&peN+6cPKapFoFRV+0QP zpO!RPI3mK#gXioxNjzcWuz143**z$xTIdsauIE9{!8ytZ3V@0s>9#*}5uc(`xv=du zy@>Nuz01)}p6|ckE20b%~Zi}8W z@I5r)jqiMoy*kuCJy-lf@Gh(X%{&7UuB=}TnC6MT3jQniUqm@BuPRy z;6$)!yFQk;p<_{%0xnZ}NxD_mQq~o5dAjP&ennh7Y@+M=7YLFQj7lOA5hPQ1WLzEJ z&3mrC-vNAY3sa_|+0oHa^Heda-Bh$Rc%Udy26tw}sGfj;;1xk@=+ilDGh5&GB$&0i z`22@&v|f5emQ!R>n>N1tMqIowYBx? zE5NS^0T07@d9?TQW^HTWs1hdw^Le;Qxoh&MjB?S!&z+(h=sC82uLjubh~a4%B$LZP z1TjlZO#~uH*7Jsu6-KB|$wd(O;u1#w{m`_RN&PfCKlg>cReI5Zn<>jG;f}ELR@#~O z!B?Kann9+Yk8Z~>j!#2pp6-1-w>Z1Kj(7R} zI@_A2CKoX_;KqQ0d({CHXT}5EwBLZWQLy`NywHO7a~^RiN^b7{qy0S|wF*yxwvP5^ zV-}IK8w8+zceW;NO?B;p%8;p54yfGEXuBHTSY19md2nJDCE1Vt;9=JlWw%y(zz-b_`k#=_ zT;{hz^`PrVoC~^s{La8Ye)XvCmjFT-X|01F_~Acd_?3X$Z!=#Er@hD!_eMtqs=li8 zy#w-D>yL3nDC_X(4u--QeXpcDQ#A0p8z@8DZ@_qiMj*SIB8I#X3@!%j5HUwljsWM4Cc&G%9WhJ~1*iaNCVa|MhKv%+S^({P`=%v^=JdRfhII8yd9YP^84WzR2vTwpTUjuukT5dkiS(|l;127;c|q_vb*&CZURBH%a9DVXMvl3Msd z3~lknUqoC5aWqq#|5&vNt!ug)AFoXuz#${2TAFGCsF~;S-wL77P`#a4Kc-WOV7U+% z&T}df?QeA?0JPHK&{3oR$q{qv$tSYOUoSg?3)eG#2cqbdcr*=8ub_$bro<)sUY^eH z>ZvRMB5Q6lH~ZYf)TTa=<@i+#9$x*eg!#H-abnLR+(84{DjO!kynkx{4brm?lEvFlx7@DHFk|4BLf>PZ&`s})b$@qBtmUc%EL)=VGZ8s*M zN`x6wf`Q()wAt+A)0tr}sb{u*j^B6J#XvoyFZC2=@!ot5+JFT(3Zg$EecpjQ6$Gf& zb|P|BeJ4RhuKeKbj1Gv4gS%ZyM;dANd7zVoJ9W>6pV?d;R?lZi-q7&1DgC+3$s0%U zqR_(0JAhX75V$Buxr*HxGU#lKNr0jTYLHNH*R#CGs6940Kt7V`=9@1?8#|MC2%B^| zzVy96SgZb~W&Q4SU)NkQ1j8=s+i?HwETqWA*QG4~8xAsHSg^Q>n6+7~QbRT5=XV^D z+j}<;cY``F{dwgTtn4Jp^j-5#g%u2=bf15UaBN4ygV^Vl?t^D3~ zQYN{PR;L^r0z?n2lm-fOQix7oDuD+Dm2oBEE z72O=PjFbhb@YA9z?4@_8sDrXJ7o3@>``LP~O=kRg6J?ph?W=9nfcUv1uLhRnpqiB2 zLA%gA(*KIf9rkNj9g#`~ZF%~mHDiUi_vgu%d;|7~42rpo`88eho0h5B6(0kV{NiU2 z0L-=iut)(~I}el7@mAtOT&C(MF`4SIf=%n)S+vsQEq>~GfCz!R*v;tZR(O$DrG zZyuor9|dx>JQGHFdU`74Dp656EH^XceBTJWu-3^|&p>o?v1OfBQ$=4Y+XZXOHIe4!^(;VO_VW2*dK)UNYZ4 zTDnQKY|d|-zNJlVE9;;TDfXswDBR&hunX*Z}$toD>^pu9Y`D4nnypx!BkhGFDYzn88a-uzh?G{L7gs(h*C$XV z+)jRC;|(H@_ag~6 z=`nB-dr!G$vdTNsTvzLy=6fb~rIk-4eRo8PUleib`8P@b6j3=l*7u0NGkp(U$GB-| zJqu2rYeNQEh&C~c3*Yj?A%#qS223VHPtcFG`@;TdMTgQ;Vteigwg|HjiOlnPz;%%& zKyeog+Za;8!uD{Z(9}TK01}dd#OD7fK>#p`TbYLW#n059l5XOZ%|6NFB=Z0Ox&SKJ zvF&rH@Ja=bNo#N{Ag>apA^QTQyboO3hx&udv|(J>vnn8f|KK88?yefB<3Y)HDW{aX z{23a5h98kUWal#x9}j7H;VD9sQN7D3jHR*a7}_kx146A&cK&|tA$xP!hKkE1ulsc~*xXYPhWe6+Sps<4Mzn zBEx5}eZ3&rw6ZDHQc9(~5T=M4q~YG>?*W{r>^j^YcoQ-{{5>-BW2oc z$mpO(`BJ0@{GfYaeaerCL#9AM-9a*TF94O^Ylp2M@et&&BLX{=4P& z#Cbwa@vRCUrtVHk->@CxHgwf*2BSpFeV)6Tl&Cdj`^KJ!vpuk})HQwSPlX*10XM3e z<8gBSQl!othj;nQj^`cI__dODxiNZlFOEJDP+GKKfo!e#;vR@hE6(H{X-SwrpQjKR zb+)#+Otn$oXA!ng=FpWewu98Dn{+!)-L_R%-}=1yogc2p@-8xUQ~@UJG?QA0lAdm{ z|5#r4BdVdpesGlSL*;Z6nmO-bALWp`{KwBJef$y1kza0Cnd3;a)q^$Rc5{&7tvVRy zqYY_&3a;zxV-Dhl?=Pq{X!#LK)FFp5JH_vmUFPTBQI8n?SS%29X8YxeTFN4Lar4hJ z6c=L!g`F-XD*DYEnKvXp8rpr%l6Nb==m>*tF{>(x+R=h_E_dCvE+8GcY+Q%)Q@-HQ zA@yvAuTG5q?nt2*zV)BqSswIIO(on|C69FTSZ-r=5WPQ9MNE-!))t9$KchVuQQ7-+ zLp(sQmrNKJF(Iz+=x-ca=byZ%U$ueZQ!no{z_89^3MQJ(*JIP%A^A_rRn^qGJ7SYe zFM8Cj=QrTEYmco~c|GvwoWMekLw2i&NPr2G5eNrAJQ8j9;(g}!I*A>;L)-93O; z|88;ZT;Y?B|INJ|B6juBMR&TT%84g^lUN@YK~MH;5%W6i)=Y3>qXZu^ceosX4h*It z!IRqwoW+iOT*(7!P`ItM=fO?Y+qh6`PYt=L&}t+*b|1NhMbGi2V-ln18LO{5iix4&>p4 zC^!w@{-F`JSlBT@|Ni}X=V<)_ANw^$O@~^{GGn$F5G@-Y)ZwX0qjvB}o|6pX(cuw% zGODVYqp2&`lY}+;vah6R_x@F#?Oa!9YiY*YzSsz+$2WVHvaXQW;vP4`LXV%#7bI~d z2RuV0%;^#prd9&t#%$|d4`OJL>92|1YqdVtvqMLLSF6Og1#E9KFgVvIvIarwoF8y< z(gM*<)JR;ei???t4WjieOQV;T`hMbM0h1sXx%;ADuSAlrqqy1muUvp$e7t46eCqG3 zWrtbgM>V??C|xr)HNOVZ{FVM1@=JHvM?}GySc!W4n@|}?#dOGcS)!Uze-By3t!K*%jSzS zspuSqGK=AsYhqsAVoEfR^wsU_0T2HmnGeTfWDYYG6hlMm|1+a=?oIuoNqwFds0RPu z90y?5>``E=;x+dZi6c(qqw7S+?B-V8>Y{tIwq>h7gZy%F!RLvRPMPCzhB6aQ-n$Vd zoOiMtgSVc`zq>z{hmDK88;P3}oD+P9T=U6qbA^_3FJkg{1YYgIDufS%9{z^P%8Dsl zUDggil#}gGaJM@Ax+-a3*(1`Fu|At3F+IILiwZ&!6@F}(RX`=uJjO*xTJ-;&#T_j!)pHj;J?OsFruc-iEVPwWa z^u_vnEgMcg!rtK~%$W&WxH|r9w)TY=cCp!6XFlJ4Sx#&BIgbFa^ZJKu4IpJb&vt>Ke3H(pBZP`n8iwSyiuOt{- zrB%2y2u^j&md0~gnw^@pigHUJlR*#u%oTJGn*bo*EH3>E^H}^rRp#r?(8)_C(Yha1 z&Pf(Ou~gA}5NAgAVo{{i1y-!OAMVvryEq!m7X$494zh(yf9IZymHqDN-&KB@w35C| zknAAA1n2siuN=x(bn`s3ZL&|$!+ou#PZ4ibu^o$HOFMJWuFOTOIJ4~EIy_nBRXW*@ z^Fk2>Z-VNEs>d+K@eeys-by5+jVGx>-L1`9W&%X~MjJBybmz0Nt`4$I>ZqYBipkxK z>KsuHxW+1c{pW{elburqWmUm<5`=9v6f8N0$bv&7oCYH!!;|@8gBbITUBedG&g37 z|EM<+m4w+_+0tlvv6nHz@<{e-lLU5gp_flbY3xq-*7%u^OSiIBk6~@PaG%|G(mH{9 z?+Ui&P9+YuKIZdvdiRGcYAMAI?XaIGDgE>dECuRXdHP1y!x`ESVJkC<{U1#V9=Y#3 z-|$o1=R}bgrlz>9>~)WReP9_zy+In0WX3frRLZq13&{yfUeOS#4o`frr3s^x89xLSM z*3Xr>a0kQMEWk5C$EcD)nEByMzPiYJ+?E&XWY7jcVo6%gOu_M&ZLYE_Q6(!uGJhG* z&)1=P_!Ir1QlauI(!nu)ZerK8bh)Bn4BsFz>Dk_I^ah}1?9o97;oe;vu2ZCKlrl0w z{jHPYP7Jo+;yU-j-gj&}5o=3BA#gnjmn4^f%!3gF^Hgxk`$_kFGw27q!FmdG=1y;Z z4Yy^JC4;Jl(w+r)l0jWbQk1PopPf0t=OSB>h3dMT$jh6&$O!(jF{A^&+`z~$uOv=@1$W~In9K;{UQ;=Hu=p7jpVq>Kq^I=Ef4f9evE(QyW&PV^6glx`gD*c)=Wxw0i{# z2248(cQPnVP=mf}IJWbq-?OLBI%fXRB<6;@ySXL2fO^L^k)qV7o2YQlt{VWcX-yNS zXTNn|b}aoz|*Qe1|4vgz4Q6P_no9xUCZ!x|wTJvvh3iYs$W163fJm z9|rMrErZTZaL$DzEEG*09ody+P)i=q@v<%}OMT` zU@rJ1@eAYXL1Aw$Sry+%AD4iIfche3XG;wyH($3Dtf5zbT5YtN?#v`excs15O%+}X z%gsUem|o!OFSvaJ5`y2a3XO=@(cB_zfw^b?34+d3v{r$ z{##_({l?yy8$BF^ zK}jRLrxULqBjKA_VasLMksDzqq7+bW(LdLuBK{ZOK#7AddEzNZ4h?C)QQ6_=o|z`W zO(SQS4Xh!v%P{(hgbi}{tJjW16TT9EY)abrb~BM))18B;gSgZvqj)$6TLmWQ6m^W<_2MY5~OyV;Ta&u(sr4*+Vk*Gr3wl=DzUcM%}U^+5906k zwu4pB;Z4c@0$=oe{II+T9jX1r(7T?%#Q&05tj4*}%fU%2vtygWtk~n-eWh~!?)d;K zaROs8n1WljoX1l$*)I@?})bd!KO4-nyHqcNEgD+WfR#5Tn|#wI0=I z^p-lU@LCmDtzC)4lcc~$!nG%RU89X^9f`Zx`Ff7s?B)7{Wiga+J*V&gFlb|K6nV%! z@8R1XyS{YHlE<%SDjO7l%|b$pzB2y8{Z;X6PTfJe8lP?R3vWVy+$`s(dt&4JZ`J>Z zF;&FpzAF3DxgGFg&bo^X>Qi}O*oN7TMH;tI$$>ZT`$CLpzB_7UKdLQ$Soh1@cQ@wq zRmmra&ukOJdm>{e1Pd>5Ke6pw8CQTYwYe-!*xG^KY}1d9v-z!Bpq5kk2~tM@WK3h< z;l?~`w%?(uUGYi*)Z=6vg+z=m!mAd;4U&gNel3^J>u1x}ewRkw<}_q4`%NI} z_hi1rj5Km}#Wgi^UL$8#t9>M7P^B@?SF^x}!)yk}64NUNo)g#kfQ@R*X6L0a-9KsR z_kxGsJ=3&lu41T=dE)O;9EdMAx&5~B($ldK2Ny8z1$e*&B5hGOrawgs=6-T!oqoB; zO}TJ&-kOMzN*&0f*s?0S+4?tXjukEjKys^rKe&SrH!wxzG zNu}I8l#O!X2|HMg7D_?ydlM0V3e}%@ZtZ{_!DA&Dtr-_pEtbH=VOf5y=8Vrxf+7~Lq@fpI<*W%;6;9aSwRL> zW2#sjujp_zok28Bp;B(RAUN6zRWj0U<4?s>O3{%0W|s%JO;2Yg3E*{{!j&ev zfxod`jIeheZ^Y2+>e{JY#XLz))Olo+i{N=1o%)%Ip1=b3kcb$^;ypIigZrk~v6>5V zy3FU|$lKh;aZtH5uf8kK5`USOTulIw-t8zbc=YdtC*W#j_=A8YJGH>4Sc2V|`>d?k zo12?c4fcd%+|Vu-Rf?6X?m$j&1DX#5$kVF=7X44!5FAJ^5M1#|m-5FndDYmZ;AC$gC{8Y~haK99Xci6wZ!fe6HpX0qW9%4E=9Vdr3%4d&#cO<8(=iM*y09Yx0hod}DYM=H} z@Fr|^&q_2W5I@QLM3hvY38_;d*u1`MzKv|$F|3+rI-f`YTUTh=$!Wh(aiGr z^omk8B_BuL;cvFA4VB2|$UbU0Ikf}B=J6+m4uTOcbZy{s)ErMQHu|u&V06mK9UQUc zYk)zMbg#h;XArKJUUN8Kr%J3H1UkWa=2M@OY?-39%a6{wBA^2Tw7EfGz zFfJOZUytgaIBt3HelN40v13HiMwDR@tEusEv^vd@nX3kK7z5usx&*+3!yMpBjV|r{ zCXHZ9Z|SWU#I`vYpKa9P@cGljABrvs|m?0@XW z?)pDCV+(G&*dd^8@R*wWKp-X$m*EPg_`hcXQ4Z1wm zqU!Ei)pcz0?zRv6@KxT*iEe0?u zyq>scvxAgHL;5Nbi$yvt0?k}s;0~ZhEDpl5i(mU*U}6fFq4|i}_fP5hQx!^ci*ZfR zFfx0ND_LtP<`z}%`&p4QV6(i2_}=#yU&=>{oLFw3Vj3*gld3+4Wnec2m)DR%9|4th zNk}taN$E(E1R1luFn@J_n&9d22R>S|{S+PWr?apoJs#;jg5o}9?&Lgw5Tkcj3KYO# zEd|*w0G;in#q~ZFHJ~HYP91;kA1?(mm<=!Z_YDk$w6$f%$2cs9UM+={Pd0V<+Y5m? z3QyMVtvyK70;k@TjX;iTSq}&eAWS~^&hxzy^HEX`S|vC$jv(`BG3YADzt3@$8lKym zg1q&@>t6Iolw?&mUrB_ca({U|85H?agvbE@erhWH?Yo4+mf{scok0&@#%v^Adya`( zWmIj<`&<=Zj0zK?yJGS%RYK#J*62g8p>My2WOR{~U@hN;X`!+B;lc?Vv@|49U+Cy7 z%ZVx30LnxZ@z&#b{=1HWzo1DwQ{UAAWCJ(q**$`;Y+NOk!*7Nus4QKsUy-F%&brgm zG)^j$k96$?hg=wg(tj8OhLw_?X9$=G*AmTRgikW*XH0yO3l)B_Lh_uDUcLv!cPYeZ zlb%N5d{(Y_4q8|3_}S3CS1G^mfJ`Q|#EXZOytgO1`wxDjh}g4KBoGN8E7Xbh2|QtY zRUl!bGd%`>%fzG?iZpP~(purWNBEZB<7>m9%T6hX4y5Ur<6sEl$%|3I)w?4>`Kh=8 zU2a%Sz@Hh2b8>Q$xBzwQ;HIqibPRifD*Ipo)smWEFbp4L2?!B66&_(Bnos-v`KM~{ zyZDd6Gnd&mdWCeDLUfkG25uMql35Hi5+v^RuEa|%q0g;Ps+9C2kvZ%yf=N6;_e(V z9Os2kvOnX7JatErV`(@nHr^Ao>l;km;#?+M;KnW2f~p&bw_I3Ko>8FaCp)BjHR zX}{D(B%c8nvHKwrgNo!RWz}nv;G6xjRK!)_A4`Y1+hJaGraL1zVTW{P8zaios@wc0 zH(|*twl*6R7$+q{(^6CunxOMpf}2^~PX^UFexU~_J<gHwWQING)&k(d}P}H800e zIp|xWcSQ9)UC5CiPw$$;h4U1#oucT=SBL*}MRh65FRtd6mY|6u zZdjJ7%sHgH$%KrY^9mC&D6LX;@Wkf-!NkSSpfE47Zg9i0}P?}a?IPX;s6?Pl`2)it+yNRXiI9t+hglUB=lzcI+VAI*O6QRK*nafX-}nd- zwU4bEH~>y_Uwn6+n=2IRh0igsfH)+-Ys-j=O8x>NA`T0nw`HNTkh$t?^CqJ-rP7xYl$eE+{<|K zv%l=}b3oRoPn?{tMo!;;@KPXV*b(Ls*>^%%Zvfk7$kY?~@vY;cA~lqboQA(~Il9=I zwffE=yl(h)J9Zgea1h2R)2Da_mj_NX?7uz=?lv`os|~P5bqh*ABl%~^V$1@}sJ<-| z)O?zk)h;ie)AuBT8hcHUH*hVumuM(s;)NZ!%}UHXtfLd_m}(`BobkMIizC2PC#wEA ztTxLA0Q=Ch>7p`al#Z&VG002B0;=r69+b_PgtQ4=y0&Tws28 zl7+iIlnJ}I4e$os#*KGWw8LUC$G`j%5ZiaYcV=+)?p=P-;(hyWbA1TLI@T&GztmvK27)@HXhL11t|WNEE({oe*hKrMvJqLHC! zWyO4caiQ5+_eyFZ@aj}8shBgtYZLucv$Y{Q2o(Q@gF~Vzqyw`*6i}|WbMPmVOcZ_A|u97YG}T2lcc@uB~cYz z&-*gnXKf;V{jlpNnET(pYcVikN#M$hw8O|nAUIZPMTBq>3i1S8Pf(t$3+yxiNdN@j z!~tm3bv49=xwa!j@UU>H!JxX*+vFC=P+b=#S6mk7R}##~C@9Pq?43==w>DSA=o6YFGqIm$HpVGN~Z9p06H<_=^%G%|zBSC8l8 zC4s=C4r865sm*-PZ{CzXcaFnZfb3P~Y};N}%<^pi+ybyJON7Ec0qOOyxB2>zv55EO ztuQ?Lh<8h-t7~iR!^0X;uJ#j(Ug`v=^L%8vO8C7ID7m-~^$a{iKI9CoM->OL46VD^81~?D4JvL+EV@;9MedATwjHb)R|=tLapi34N62BuX~vdH-Ne3bH-HG zybyI!wzA)zknb*YWC8E3nkBQDlyDGxt8X~4M0TbveY19UQozSuCzMM=7KHby5V+cm zl2o9R$u&%Q_CQr93=#|bnpqVTW%{0M( z+v0aCDc~^BW~w5H!eTNn8qs=5!O;o{LA0IcbsRPx?u4XhNWYQ6z+&Am%r}-#rf;dV zTAQ>Xrdf&aI5RuPifAKpZp}2)s$=3~z11|dr>pDE^TR}-O|KWhJu_(^9^)y_Bq>d2 z@-QG~?*2D9d&kZhbB6&}Y;@Aq%U0(0iBH5_fYpWgGgGZP?aV>X{_SqNq;r3;)+vbr z@mt3k^8E%bsQ_Ui0(3F7Xi0@12oDLw?K*^Y*t3Y>gAq)@*Pn85tZX%J4&N!Z<8DT@ z{A6y`KNX};6``MZ-iXw)*z4FzG~s~Upie!g+L|Z|f&W@>{9Kk6hVIhl*Q&O{newa$ z)6}y_VY)fS=Rt3FaUNH`{z!=X9A>&9dqAw5+f!vVL6XKAt3gOYlt41!OnmHc#^cP5&x`)g;D zrfWg9YwVMM2<48@-1X&V)}dH}UT9XyYM&KUCo9a2+9R3Il$RPM>4V!GmLMrO@*x$s zbBK<2XNsUR2K+UHW7J%g2m9}#y1R(uQka7HjTTfwGO zyLU?~Bfn5?a>aM44^>qscyW))-95;7Ij_q2bN<_dIy%~07EOmwz^i%g6AVNDG{?xrz`hKUvCC zBBnN^aX0yB>EzSnUd?!TKdO_zaej(Y+DSjUuMhjzS1r8!c3Q;d4th`*?s@l_%v<3jXH~|s*65HfiAm(89$E`l>ga8 z)b;E3Je(6%2<{GnnWu%7ytM6O``Ql5td2LF+ETi4j_pe8AxYU>k0q>bcbs>WW@-6H z0Yp|dIrY{>I~k+koyHKhk*LXBrQ%R?hmjkg=3;k)d@q-LORQGHBhNA~3ZGQY;@JFm zfcslADJt3k+lcE$gA2EkX&`mw#(%Yy89$rs)(e&df-b;s1_3sX6Pt^PK+qz}J3R7fm8DPlwnTx*7Y&R)e$0XPJSI&l2y5 zz&3bO6&07h*v0~)7`=+Lf`WoR`t&}hNhPk&$;rm(1?JikP9?r<0s0b#_I^duhSvZu zATy7)6YTj?O1d_MXXAVKx_%Pn2h+!2ARM8(5sr3z9$4Ds+2eF#w~ar((NRdwhWb#wNrjHHoU~|-sqF*G|Cm?a#D>jCWrUNM1dCp1^a(QrEnYsMTmR2 z(92RZwLjlabXeiQ5^>4UGR{~Dj|LdqG;X}@e-NNx7nl0Va$GmUNmn=jTMrdAE&ws9 za&yRw1O;OGl$|{AA|-z6iS_36Z{m6Og$NSwkC@uE`eHjupV*)AplH%pGUF zCJ;yJsHvGHw&ZHSwf8yxIfZ%NXpMmx*g}d5=UI}Se{$}lEkICX_b^74-^6_UXw$ww z(lv38e++doPKgHS_cE#^=kbcBfJ=o}cK_4x;oydm-u)n^`y>h$Jsl%u*6`hJx2kMa zVvd@EmKs@y!IE0?{1bMkZYnD5-21i6**d!RxjAMLG4RK%Pv84h3KAliGpdOBL6tZ6 z<;g#DUO-)wHk)+_;*0j(N1Ek!;J{BIxTG6tMr1Z8c_f>jXs%T(_)g0p&9zW;%p+|KbAFj4N~>(es?)*xYZvZ@tUY=A=6o zcY!?dzI_MN9GwM8kf;TP{j{{0@TbjZC^dDB9@M=f^S(*so7|}+MqRr<%5U@zTlX}0 z9f_Y#VHrEcNQ?ypur_96;Z?f6)~}4Vy(w31#htML4Bxpi0$_=5Kx=jlXwCMU1@3GR zzoizsv0S^FD(25zpq2@4!T^IssEKQw2W9RnHc>wNsn7X*!9ODUjc%7WxP=?g&te)! z8%-Up0T~{yjqR9y!GziKI+YjcgmF<$_e5rz^kpO_q1MgY#)|CHBPsCGJ zKVt||>#&s_a;K})_|F!HBQ(QnV5ETVK+o~Ez-gH7OyJ8LpYt#$$#++qe1kJ}G;gCp zH$dptw|XGN0rpHt3HEv*TLzeg1uX5p_DK_yV4s&(U#gO<+Z~)#+{C+`R1(a z*oB`c0V}(B3Qmn!F#-8Pd^Vk^8`*yAl{6Tw;ljhjI37v$mmw_-IPtWz)w5(OPqdV; zZ%DA z!wLf%o&w{FDVs9G+IKoK{R3sY`>Ln-L<&)hS(W`RTUZq?06iw^&U*0}ssO z)Pt6%EBkbk^krA%{=`yYWtQ-E~0y`8n}mBlk%2 zgBS7ii30EMdg9ahre}qLYilyenOLs0!BQtTLN{`t{$vY}hjx+p^`@T&ewiZOUBV`~PhE*;(NE36&e}9c@@|(h+w6R<-DtitX&C!VXFf zIJlMob$)Vp<9j8B;+&BDbP1%yO3m|u$IltQ&^jj7w!j(-J7rZWRyF()t~qMMEkjOpk_kiLk_Vyl`x};-;__tkeqOY(-71HK}*rc9h&x zVZ+yg>6rhoZ&Jc_MK^n;@MbA;QnJZAX`4}i6VH~CJK^jqliN5*${)){oDr#eeYcpO z1_3UjALte9-16uIdc_SuCHmUPYGd0(v>cqzoh;XM&q>|VUNPdp;qV`r-8XRuQtEh)jnji-zXNZ@*wtgQe2qJ}>L zH=l=9JufMp+Y`ds%MM`!`p%ZCLShAXW{}&jk=?-@m*QDggDT?3XTw~)6fYm})-ZL+ z**B-FzV6sIS};5U%`|18>q(MDqYFIGwE#Fc*Uko)%MTwj)jcXfF6+={LQY9+o6OBu z`Xe0Ekh>@bB2#T%y14Mkf}1aX0iSjB5s;D-ftuWZSX7%Bz+tpmh)p=_mUT*(BCqsL zNqp=rRt(uV>deK1^;;b03+T?z&tG5x@g?f)V$5q2l`;a5sP5hRFUu$a!_7J~V_-3D z!BautU2tnf*@7u*Iwnu?DTtm?*m1(4x& z8Ub$P#4i=_k`!s5sR`}Za>dP!g-c*B%bKqkBDt&Ah01kkj!#d;xWI^MVRm`NgX?o- z2V}B}flStaSa+K^xO3La#WXNS6#ql~wom|JRdsd$6Y^O?L!@1DxlEw4`a;zB$@&C z2_~O6m*!cy02YDKLH-wL1BL*UfeHTYR1sWV$Hg|nh3;wXULw*-%oTHYR8}efHcQ*W-JXu#h_P$(2-8_=jT4bKqBjEnTDDz=VMA+~0hRSmsq_ zuF}X=K{>1c>4&~x^zW|*PnAWpo;r23!d8E*CeRpNoZ=^7(9U>nyiA%4!>19wVwT7P z#Snm={$k*OQRzoA|8d~fe^Q1EJlxmliYHv`6gKg9srBOx0D&_28k?Jqg;Q1xW&JhK$l?ZGHU+A4E%#PRS$_i!h~ec^gHw zwPZLZ{uT!y4P@|Z85e=}<(v(;7;`<4SMUy&_Is@Z-wo5^_}RnnUr2SlDCkI|P!D=P z=s9;w89`&fZ+g-~>0+K^oV7{>bX`FgZ-~q}kZ9Gw?m>kn7~qD)$xts#($WK%B`Rzz_rwr+-JGOh{L+_I zKUo1QU&BoQmrdNLs;W*b!@Kf#q;EanvKMfgdrG_j)M#`35CH%vGn5ZC@d>WixRQa{ zEZ=@hD+NNH7iOaw@C0*NI^KVIAp$xw2vNC%9MLRc*CgdoW{H5vvOra2-gP9oWj9g7 z{%ILiJNNe6Cx+2m%Qvv0L*V93PwxE^zw0mu$bYyzucr}Tm7JXDe>DG=07Z-$#X{Z% z7Vj}~wsK{k3mXqlpqgIPiuUx+ZU|P-VtDD?t=h*GMXgYT+!K6fDm=y2Rmzzc$?#ex z#^ZHk?qF2$idwDhd|Vl<=W&VPMROe2kAUfKmUK8nn~qUrCQ7mNGAeF-&!5M~#lOlr z7QELR18$pc%NdxQ)Qg+h*r1Z{?P65)iPmW?*IWEO7c>J@8-=UfPNs8{awY$CD}T8Z zY<~A%@1=&P_!bN;oI)qi#Y5XF1?@}xU0s*dwu#_s-g%(83->mWy}uWTCU(%TzSZ6? zV;L&^*j73WkHI(_eYo$(zsC^B!&j975_%f-^UrL2R3~!1t)hs=q$Qq!=}%pvFY5N| zF(p>Q^)z4e@VRsQh3B6}u|f8=hhjCdYgk$dToHZ4d1Qv%7{(q)jBNG7j%5 zO~+wP0>AghvKHKsrow0bTjXuqzSA%I`7w@Wz{;1aqd8md#V-N3nV9{UYg#P(aE_M8 zV-O03P=Eg_C0sVZh3|&3?gXKB1Ga>5uY4$!v32Nc^9#0TaX+(ixUkJL%k;k_dQQ8I zrItSSwQHLjV-&6ee_VT|l3Fx#VV*gV62QhG5OV+eK)Er3pKd>*|G~~)3^U2^b0Z?2E-n48 z80CQd*k&Xv+vtNAP&}!{NLADUoP?m7YM}I(v1F8ISWB@GX|Cz38UiypCxZMpqV{HF zC9 zj0o(2AjI^HCM^d&gZ*q}83^tKi8X+jtFmPDKlzB-UDV*>H1+ zZwOPZ9LJmH? zpf7F5gZmtGZ>Op}extK->lh=nd)b@LF>Z~N8oYp;L&OE14GlJFBEX&i#fg7+?Ld42 zvONEcPwQA}eNIgiNgG#_4%)66<8-w8~R5cH+dp+_A7RfO7Oc!v-jq}Y%do4PU3n;IZw=2 zu7D?{9?Z908>x8CuBm!|qxC?jcdXZO1;NsCfpX(N3Kcq<8aXWON@+HkljMYY!ZT8} zE8}T$GP1W;gSq{ZycBdaSb>3oy#z?=>#B;<Cn_&Iu8cNVjA?0P&*=Rr*qC>G{71g?jRZ|Bx1g69L4@IRiv5E+t`_CDlcB{`F8 z=0R#gg+|<5zdH3@JUm$kBUJ?(_!MqCc@Ct|sYS)QyY+a!uMtL#$rI?YJMj(QI`)hZ zwB32auHn;v^`-un;vn`qIATWCq~5)pu};%(9tO&zB!MFN>ldGo2D%4M9^6{s zVk16)h+G>_BMa{So0Z;Y_lQ+=9p96@b7L&(!0MBanPEl||G21q+X6JnuTA^R`B{z#ik{oGq&rwUQjMO%r!vyi7?+W`8RW!gpl%#P1sh zV5H{BVCvX?B~)Sl{HV+O>bb4b8P zVq&T_=9UHDi3yxv07xYQ0V|H=7K-ac7u>1zSE6gI4Ey!#m)ePaRNegq+6hJ^>kE$) ztlNx2LPO2ZOP)FnOaL4zZdM*N7lmK^7j*kS2dohf#l_Cv(f<8qLg{z*qBaszmsPY( z#8G+ct5>fQ-uIS6Yoi@z@XQIz#ZAC^MnUNRKh{GB9CM4FGWTqJjR~kC7PNsF{lq)3&cRB4I`3ZeJj1gQZLLhl_Rp@tH|o#;_H-#x!? zt-J31>#ooB{t*@q@62RomuEkF@4a&tV*6)-sEk%0_iizSPQx*sZZBhFEsZ|%R$GHK zkya8^ffoEuQ^|F)J0HH-u6=^bpK8u}3d}}Oh~z(-Z^_A4Kt zuO(7uXTM?Hp)=AA&)6a#&|UwUPWH&slH-yHDZ7NNiV8(R0bkUsB>4AOTGR{VS7RRY z!osN7*sM+N=HxuVu_@vNxzCrgN_h24WtIh2&alvsI-WQOSpm1V*vv{*5dg=TRu6(e z^CvkE{mGt&?Z2GD-NAKp1YCN`Jp0^h@&lC_N&x_%y{QOFp~bepuslaaTc8*4-iQ}I zwOnQJDo=RH8u^xnODPe=PGwt~C9~(&pI!w*%xqWBI;p|t#mnh2faXNNZ-D~o@V7G+ z=0K!@It9fP6Bs7sWQ}858|^lR(kD09z8rp{zu4OFiX)j_XF{=Bp@s6vv$cBrE}eS5 zXSP0T1>E4UlhLxUh*ydAtRN#Gq$shwa5eojcoi4J3Mjj8BPiSl;OF1OpG`nM$E#^7-BvPh_VxD_`}_H|2Y-a-wHs6Q^L*9gxlWA3;rbPx!1cH} zJk$4p0M)q-qfki^4NNHZyKdcad$#`8KaA!dhwL|z5WMVURwbI`bl7+y>HhX=ee$RX zZ4dl0lca;gbv{16wjS>%L%j@#b&YLpS(P)dif{48w_Ryo(xw9EH3?)b{39BJztVsX zbl0e5oS%@L!?$b|=D=ssnVgF)y$zZ#KFnn=1&*+W(JN|eC{=mJeZof-lFG+Ok{%r;(f#yhVE0RQu%rz+K$1c)!FOA%=(qY5=*DVs1AeD5VUq59GPB1E5V2lC3vqPZFQDG|fec{P ze+K^13eMV-2Q!D_bsPN2YJ8B*vV1{0$SxUBB${6{S@DqXG@ieTN7Ro{yzok?XY{!? z@#^NsSNY%DRij1d%Ux4oi<%2t?$Ml|RPk@r`A=`WG+7)6Q$tjWRp zgqsg`Ld0@6j64WHB?2iGyK5B&FkTCUI|Vu8H|;SunMoF*A_-~2z^q(JsUWi+kwgEK zloUFrjG+=qpwry(aM3?+B*q$2Wz%n4*D-J0WV12%PB|-PAa9ZAD%6XrKL^X$)1$7l z*X0IWMI@xb??j>mi~^tmJc5EXy2Vt2&dVP^Cw|-peJ*BIfav_UWok=xbaqN1S*zFj z-vxkxQ5k)vfdRwa6%+FY6kJS zCs7n*tMPmx;gOv7(X^f2W#dLt-SU9@&0Ig2JD{;}$2AFw^Szd$k*_YPnzrtvKyuI- zdoduGBQrP$#$HN^D+BL_yi{j`e!NmHP!vWJ~dPnM-iuD+j$AuPw1r%STJ?5th_E_??-4nU%8^$@AO zq$JyFh5ZRrvv)U^%g@b54A6Q&Q{seAH3pL1*NK%!Hy86z^;N}dOp3)uMYa|2oji5Q zY-uyTt*5@K2mnfbF&S_;$f3FT!S)$RZi%R^Rd|C9Dep=NL zwUOiX*N-6PStTR)jbq0cq!pwkHGq67|9y?BZNDWAR6^b|x97WsHD}K!ORB3!{T1uB z8>0E89U&LFDtMVl^?6{w>*-0ua?f*Is#TPNm`WKT$?(`|;FGZ+%098;V-G;=;G_M& z>{GCkPXU)b0^ItYR3{rTV+}N_VxcW5iEp=Eb;e16=;OH{ln2u4d>!DA6B=wzaT3wK zMGF&WM|T49RdlLB5d#PZnLHvvecFfO)L1z{JyYX#5U7P=qmRJ%t%+w_%`-0`ra_VbUo z(KeUo1@Ry~>6vaYnO|@zdk}3ZT9^#3Ne-;BhM9r@V2w#}NI_MZ?@0pyu07n~k$Iee z*M~L{Qx`=*yNMwd+6Exwe{0Nv;g}UHso<#dgYu|IMt*>k?-NA3H7&&eGn1F<>G%D! zMlI{b--Lf8c~*lY&wrr5n$6A8&trM2xc#m0ia0uzSp%feUC7fCP@F8LeoYVpl4gFf zL7vvX18P~)u>?K$$he^*+IVu%GDSv9E9TkeRO$;9Y%~NNEdPJ>>cNI#)Q4-p($46} zM6pOv^;%Zd5%p^{e;ooAjE#cO2j&3e4FkAsbb}4=m6di7Mz*w848`a3mmBGVa7Wzj z2T;sslx*#riWsIFnAJ~Y@4|t8{&x~YQQ8%fT$XNwOi+r_{CeV*Di|*RTv{N-Wx=VO zQTK^q%{Lljr?d1yU=O^kXMM^V2w@OIcMkTBR{S2J79AxFR>{LZ)q_4`i_7z5SGX26 zL5EB}uCB92H@?jp!+ft$j@z>q>x>$%FY>L9Yo3$S2ey#y+jK9m!rw2o#oyssD;pcI z_!s3`&%olLxAmc3Kl3FdaGU&LM#mqImQ=Tin7HVJoAXBq*a9KIb7Q3qw8s7Ab(E{H zfIpz~g?h~IE|7J&cFBgzgUpW5ziN(h<2D=?hIPaYJ{pg(my&5E=Z1Iije?4V**R(w_kbvXQ zVCrr^fE$p8Jn=s%@AK=aGY2QwP~he)kt2#qDhlx9U4NK1-f}BH>?<}Qn+C2+BFOC^v z*}4+NLKiYGiwaI(G4N2ngsgc;&p6s+)^cM$dUwZin8IAe4b@MJ=!%ONVqwr5bJ!8k@atY*{w0jap z)lT~ztc0M@9U$cGf}RfS3l0nON#C_y^lAxDq6c`MOslrMw&F&8N(T_ zF9=A>-D;tEa9##L8_5Z4qQDMW+w;@DgNsO#1QgkZQOszh5Tw@U8qzRy2DH`OZh8Q9 zUT-Y(7-6BC$Gj^Y`y8TaljksI@H`;Ebj%Ow$hN{>qr3OfjA+#t4%uCJP<1dy)HVXy zO&|U`Pr94+h8RrZT)#c`xP5hgs*;q;a=&kEEdABB;?0_hp8OImpab=yoE=1nC|P6= z^@AoO$Gz`VSGYuv2>>>Cvd5(PlY;}x4el`ipvj(2SRqzqbJ2?<=Gs2tn^LTPBKMbrZ)8 zJ+|vRGEy{fOPo1HMWG8{(**mov5dT?X3Qc`O6!AfAA1=D)+uIVm*B8N8ys1|=I&kG z5|{5z;$TXmpWC=+bwl9)zUnMwPKzIs{$V@fO@~+#$SW6f$Fv~EbETlKnkiT%x2$kU zpU=Oh8H-2MKF7B2LRb2PEY3_JN=fOf13D#an>tI$)JtxMG2ZCF;=<-F-lnFWAvH=e z^qR1OX0TJ#;#NcnH1j(2Kyp+HG0N#K3FuC8NXYA1&5inxk* zJah3au$_-mpnPZb=wyHPu+QPa_@y?4{u-%?=0>F@#xC(Nf*kaJ1w;+0#eqO^+$L%E zj-5+DL#5~7{9`0A1T;@_Q;!9)K4Vs96y&~C^@=qqaEC>|XK{};PNK95y{f9xd`MRd z@9X{~BQm909;uR)LyYWZE9J&0;kr&Aq$|eX29$v&x*((xj0PdjZ}>t|bHiiEEYjp3 z`5MBOXuA|wHvJFY*ru%SF_80&4G8mc4OL1 zH4=|dDr9naPbZd6kYjPnn7Ogf-nq$MgcHc~$TY_^S!&tYctb7DDnt#M@2Qhhz5~u+ zLm380qqzOKN|M8xQnJsWa9sTgN6?#bdewuFKHq0f#_LLI6lFa5E|%LPyEs!FhDhxO;)pV*#sd7 zELmM&gMJ^7CL$KDFA(-?pPu8wo49bkanZv@ z!g~<*@RvKOXDJI7orQ_E3=R_D^3czesTc{Ygn5c->qXqA#}$5>P`a+hqD6iX)(F+M zFjlbTVCAct&&e-y>d0Io>WL7+?IJtlcCn#53yqjFNcIvX(iwXqHKtF&#v>{zLjKzG zRgZHn7MuzuA`;Bjedy>NDi&r6`6zGBbuH?yy1dWJ+041lCkzT(SKUX+tJTzWi0vVe zQ+2a`?OzbJVRd@NuWOg8x0~N+de^+Ht$4;!U|M85N+P)Z-OlduHx{?a3Y(nzSG}y7 z+Y4H^YI&=3?VPjWv_QjZaVB+liA`VPt`5fL#9lLONb5~*NJ^8tLK%p@uX;x`(c~t> zTI;;=6be=npE5xE(MmeuYE;aVCiGhdw|RyqjKI26IG>dQmBVPB6HRgJI0 zhP~x9czy&v=|RHIz|g`tZBO0Zf>d{KaKWuzO%ywEa%5z`P5iNA>DE%8jH|1_uE_OG zecTQyM&kB4h`0oSsp`8g1J@7YEpI&PhPP3ADsJe{plAn`|4#Z)*IJbMR1Sj7t`nJJ z8CGkges8~+)Ud75SChZ-RulJcnny$=Rc2v5*Xw0ifB3h-laM3U0Z%uXy%mXSJ>ei>0KKn7YuHq{Wao?@H!}jL^*Zr3>%#N zbKGs{UF>YbS1eu7#o()aQ?XT5SWi%J=r+_(D078$s865q%eZ(%fB*f0MNq8{bPtl& za-}mhGrjILWH#sTf3?$I5v4Ce5##g|;xBOdZGwRhtw34+MNqe@@Y{;=ImwJ?DeBZb zN&012f#2QX14m@$Sf0nx!7ZS!yl@Oe|I!h|>?LT~#Upg=d-9RNb@|{e5wq?6N8iE; zivQfBI97Rdu*3hKQ>^Peovl|*p(-jyqbRXY41}o8s-5`Wrc;O4)-7z=!&~ee93xme zAy53~qLqN}j|k&Ig@9e>IKda-)}5yww>QACFq)Mi>(*$2H+WOs!PaXb$~JPqmS%Ha zP=tcQay@Gmc2LyHA#gs!XhWlb{d~t9Y>jb=>2*pwLBH(s3xw;gezCsw#t< zcHbC4(z+iJ*9Z}a>TbrojC&q%=tb&*;kv*h!US;FmOM89>x=dKYp9YH1~=5fociwm z{zJqzT)q-Ym0Mjy43z4NqEIWqK#@eSj)IE3AHhcp=AU0QH^4(?zx}ZJd7fj^4y%Je zhEy!i?&q)OI(ZIJ&nQuYPV?6tz7XHE89K|KoDi6ocOARFY*EDrW5XA_#9OW)4lk?M%)@V=bZJD~R_aM1o%M~BqmlcoHHuMV9?hyvfbQLswojhPLhYyU-~;>O z9lp7mO;JXHC&AM#suSr#x$hjOWp&nZB4HdqeT#rtryqy@$tAUd2Ahu-tx`7NqOy*8 zqX@9r17VP7{>8v{=@nvP-q^db7G~QF8fA6kwTU3+BB1az8E{!09{-lDi?8Iz;f)d} ztM$4%2E!kVM5&ycNx&;oRl?ZXXfg>s@#y_~=B8YU9LR(14M=T6Xrb}vz*{`s*;br( zK=Cq*Iy0D|%2@C@wkk=&b-=eV$Y7sL60v6k27}Xxfb;de&D8+B)t#Dp$Hvaz{Kvu3 z1J_P#8ND$FzM`vCc`lTwg?tP<1VWh=1JnGl*k^+@8`?a#W3gzt&|SmZBxQBi2qZzi zDo7Co8F^H?`Ll1e;Ci$<=n%qM{c1WMZFqf&t%#bA#`EOS%7=6Os;O3w-lywN?*BM* zw!k6FvR!yK4SwoBlcx#5GBg#lyh6Rbgx%%dDxFi_&sIgB=wVnA1a0)@szX5~r?N#k zJPf}#Ho}BAJ3pdhG|t{XXfcNIxU55rn{y?ygH$pK=VEjM@dD-(%W)F>Vp1XdxRdgA zbSFwkP$j23F2+MlkgH+? zfE%R?rLQd{_Oh#>($&@7nJwjQBY2X|&llV`wulf7g6n(B>gZ(hmNpmsTou5`vXnEQ zrGcNe@vDLu$T#x(2Y~{y%={_Izyqw`!L_8U4Kdks2gxfXI-GY!4Th?_jRbh0Sc4`T z2{|rYIL015h>uJ7cneB>rxyoZ3HCP6Kd)>V^`$B$mzi!3@3EgaQ)G88NP$vW8YD(L zZhsf*Fy1jXsBl&oWaUT2^R`&3r?}k(>eN6sJeKJ)ppfY=BBaKO{R_bnz|RV8#( zOOQb3f4TB_pWa6foIrPH=rbq$X8wA>I*urj3&$e?C24B`y*~HHz4yV(GJ@YDzz0@P zy=u8wZ0IP$8nDgG50ML1bEE`zy$Fbi*de0Q3B3qX1Vy9^h=`Pc7<%t53BC8;LMYM+9TDju zz4zXPgV%N4&wloq=bSUM&pmr)&phuK-Z8BIZ>{zDtgpO+oT%8_K$2F1LjZt=maeC# zhk+RaVPU;>OGsWG#>X!pEiH5Z!9yNi-qzMub`H+Ey1Md;iv0Y1Z*QOY_=NY?)~^f< zl$4b}eR7D3j(+r5^xZp?4|aCu<`%B5ZW$RFFSN9th(9&Aum}zgeq&@579JjlM5=z{ zzM;q@{0;&ulFQ@MzaSHkMqU*i2S4IM%CYDJSc}3Ri4R%8P4mQ7>i?-diuNqVA zU_y{ET!ww8H8go3$3d}Bzj(C3v3nE#ZKlLoSvbCbr2^h-Tl;GVO(tU46s1#-`?$*0%NzTxVBzPj6rU!1uu)LqCT{M#sh{Ca0!<&CJftFDx!C zudJ@EZ)|RD@9ggF9~>SXpPZhZU;Mtj0^pOf!b(z_yohN94R9r?Eq-7g#ROPsT3Zm) zeXB`aX?jN(=aqCIt3p{uXOzH;JcG`%%$r7JP*%T{s zzNf<^0ft=EHdxf0}PK)m524QoLMZOrTU%Fjj8yW1PlSyKth~ZoVt= zmA}mktn>cbRCo0q`da1Rr0mL=lGzp>z#Z8hOzC__c)uVeq^4}K8+pGr;o?R4a({|+ z&}-b-^2(2#7x}N*svf8fm%QsvQgKDDk5qnIpYE-#&YmE3BjZr5!|Xn7BqA2=tE<^x z9?0i?!(NX)TpulOK^E259&gXof1*-rs5{-S>_~sp-%x+vvG%JwS?!zq_{q`!`mg@4 z0+*KnLV=kp1lMo*84qH9@YDjw`c%HP*y){=Bgk zbNt*%Idg$RRk|ZK!tDa{!4H-qj6+11a^^!dCvo#(5`;H-DW6eVE=0iiR-06dZmupw zsmtj}MrgjYT#Vt>3|NdcaI8s*eeHX533=e5u!OS68cRf3Rjn??x0I4BCpZkF&l$&8A6hZtC>hMY(ys7acwo5^dsq7 zPDU&`EGH`~Z!NE&YHcmQq+M{mpkml+y%4jMw_a3tw68$)&GI*^CLPx|t7m*~ZDAH--*45dWaV#RH>%dRYIoXiZPy(P zzu&GuS<2sTxHwwh{stft+GzwaTJJOw^B3$ilRw_rX#vX%?Y7eCSnswmnHTJ~vpQ|; zc5wO$?csRitoJ$vvJ3XQgsM08xkAESEgimI?8EsEy{rQVd=7Jw@oy>>J3!g4T>)4(yBF&3V zua**=woaE*{DjX|GU9B{R&%n8&ejU5x6al}I)u+RDn@M2H!;ga=Ua8hTj$%2M0YNB zS{XlF?Be)~FZOyKZ(r;W$lv*WFr@S0_u;5{@$aKar|sXzGk$k2PZr`nT%N9E7hj%j zRBvCN?{wU`x;Pm5aP|9Sx%lex;&}V&3P7F$Ac8gGL3R+t$|-n^xJE+gjt4k61;h_) zBGupVWa>#Fe2i zr!#fH+V7!vgDsTPAtSi|RVsaHf#CLbv|-UM!|3Q`o7q`}NsgTqAjwh@V1_F=QW!Q_i{<*V$tR-A4@sXFbSP z=(W<{N00VoJ*w{PwT17;&yZ(}b}01OqxTb5l(V5Doqdkj{luN%Z1H7Y?SOYT9;knCmB!SYBlo!Ldno)M zos%LLCa*Xci9W~>P{~!)=^Bj19%PDyi(WQq3XsycQ3NIE;nmY~Q}_fs57 zgB<3-RPr?Ax`wi#hq>w@d0N?uKlAht^K^UjURHPgEP@~A8&Txzb|?;)p$`izRPyyl zx`wN;hlO?_`3B31BiQl7BIn-x*T-EW4QGeN2#NwDBBjx0$We*EN`WzB_h>uxs5CsJ zz=U6EtV{o>4B1;?_PBej4}MghLQ!ZTuQWc0KB~x3DYVk*9v{XYRhEPlTAM3PjE^5x zVR{Q~ow_Hc&yK1aDT?g;lqTmO#~7SSk$qhEGqYyR`)ClyPkF%QI;b+l$Y+spY~X&mZL{{mLAof_S%J(CoC&3 zLnluAocqd?j(e7$ouBq2C@WHkR92)|&IbHdE3VQQdspP1oP7@ut;pb4Syg;>Hi+!2 z$a>tns_J_7BZaauM_y%3GyZHSN3}9fr+4jT?b*+g(8>aHm394zvtdkMWsy_w`s?$v zkw(g@55G(5i}Tl})Qx=i{S&RaMo!o3^g!6El?6m=2XK z`}p(871e6&Nbi~iys)u>6 zey{2FV~4Bz4vSoWuNzU-j&`UXmBs(wuu!WVAL%=)s{Orb7gjsDta^-{_`T)aUpsx= zcieFPdmBMjH$$X$(#&$Xo#N`pDzkbW9|9txV^0<+zVaHGHVvgnN z1gF-p7uSEW^yKPvAgtjaTkZGStE;oo{)VIK{@+`!SLZWS-%dKzF8AWEE>_gOosINg z9@Sp`-U<75v8;A=HgR=%(*N!9xc}?wfs6vTK6jd}{7c#1H2-4peC zpzHO>*-I4Z1;u!Yk9s{j@seQjmJ;=r(e;*d_J$$76*1n*qu#0~-s(&~nxZ~hx;`(R zeRPpN`WPRBQJ>c*K1NKw#-hF^y1r)4z7|MdD~zx8sITpbuN{-0y{MmquAif`pEJ@A zj`4FF^>aV*LooS!iu!x&`ujTj`y>4WG5*1${-G!S;YOaWY`%d>|!+R@+1tv9F8Xz4$=!J zbO|R$g_G8VlaGZ{o`!>&BWT1T==35OTq2lI5s;b)*0Bio(+E!HNN%x6UcE>@mq-Cr zq+m^?&{(AKX`~2q)IG7N2YOMDT%tr#QP7$w@v*39r%@8j(Nbd3GJ4T+F3~Vlv|>%P z@>sO$X|y`?Rg9)sjFw)^OP3g3RE&O2jKNsU>(dw`=2&B~SQEWiGnZHkRIF7^to2x| z?P;tXbDX_coP%DRqf4AKDh^%~=QbASej0~hMtX`Nz4eg3E=YeAGOz|2JcbNCMTRq@ zBE?YAdZ<_z6cU9(*Ps%{P)Vn#6lQdq7&=1_o#legL80?%&;?`YqEmDUb9|Xte1%?o zl}kJZ6_2fnuN#YRIE`;)PG}ZOXw^$-cS*pZ61r*f-hx8JMT@pu8 ziQ_eilVgd~r-?JnNpoUJ3wlXQE=en>q_vu)jj^Py)1)2d$JQ z^2J#4ei|P}PgQh>wPk#hY7e%K-vFYOD>Cev6B_J76 z&v*JqhVkSt?pmWh6r z89d7Zon?j1vL4T}J*x5wbNtac zf!Lhj@tn}JoN!2PBs4c#KQ|Vhi$v$5vAGH3xk+caDUiH0XkLbXXkHdPF9)5Mhs`S( z&nr61D}m&fLGvs0^Q++b7<4`sn_oAc-*A@S2q|cW7PRUYw8IN<=z=b6LC<(W-&w%` zq;L>gIHX@V3@;o-7mi~KC&vq?&kAQCMRU-i1^uEWc+m>FXboGmF zju+?`>n=HT_rkv!@jH=I#P{o!L3(Y2ei~LAP;&HdZ@^jmuhy!+cvybbRD=K-JOcsV zJ_>#TK^$tt(sXJB<{=us{y_i=H5ryPbbKa^Dk%`8MlVAOWURN8KrL(x7O+TpH*&S@6wG6)Ob7-HNxbNfYU4L<~7@*uZaQMvr>1E~dYlJ5`%!q@K z-Cy35G*bSJms>b3=$|U!1PEFFmsKG1R~0}kt}(<$WLKuuHHIixau4iZv93w5t?hWf z<@zklY4xPSFlpQC__e#IF5-m)gF3#Kj*q6aAq71<;Y)wBpqC*UVejeilw-A#D(Cij|&1jebChktL6c0YIZh(S;s@5w}!NC`|nVYyFgEn9qXQF*Zup$?b;@wH)2? zY$2w4y~v|8o9Lyj#wXC4VoY z8lBBodbICoYQkwGJWx{340I&^3+nL#cRsp0dJ9sUD}{g&cz4WiW|L7G{Y3E&dEU4c z0jg6siTTFGSU#=1RM|tMqv>U=dPjSc0e}|HRwlXo3^$W z1r@IV)L_WJp>6E7BO(83{z$bRSnZGyjnn9W`FF7eYjyYcN`50S6VJep?vN9Yj&(W< zUYmrS>@w^~3^cz6j&n z6LUQtS%|VX#S#^4o=OkJy>~1vm0H2Qki69GTV1@DIna703{zd z>tV6LMMGbcVFR{9#Dm#D2LNi-X92RGDBV(;h(31Sn1boMH_g&Lvp3gARhe>znxeEH zGRJ>0Q7~uwG_By52(M}~Paee4{9TfS05X8kx9gI$`G0tr{whhKsQ*-w7B>GXN!33k zS#@2Ket$|*5AniFkxL7#LaxQ6=%eYU5bz@CRcLsGdK8S6E`^RojYWkzHBBwdE6QcYVO3@r1#F#8L!*3ib*p@PSw%sFEgL&M1!)F^;N^f`XJw%U zXv~lWR9reU*wk#vrpW?M*I2Dtuixz6UZ|xZ3AD4Qc;ET>%ePhy9zRZnI(~*7dXqRi z{ZwhnBW5vw<#*I(e$SkFo4+fH1|gQfNV{3BoM0e3Ox(o{mSTg z1nQjSTB53@!k)dWHL=!>c1@Z-q|@fczx{I8-Y_S&SgedPRi?Gfanu-VOp18O%^f(+ zvo!ElvgFiW@xhZ@b}QA1yoO~VP@B)774Wbob$l?{Jw?u2yn6sequJ9+q>(f*oRCg@IPoFLI&npzWQ=T8$PPeG>e7!`S9bN78 zy^+@i|MmV50=NM}48VU+7qU<%_I&H@W9}DV6r|y+<{$Pt!ZalMnuP+RfH4U=Ny+Hc zbluF9v|N?^Y=00PB`GN-9lkOx2SpVHJ&iUEeS;b$2MvjAiJ2d5m6igC#ejlMk%-&` zB-0zHMx#zyIS(7sVEL&rDo^}f4cs;nFa0ZAgAq@PkY1fYo>L;AM2*&pcd1>oygW)x!58F)s}Q+CjTib_TRdhK*W}BbY$0J! z`%~#E;@({kyp!Ed=@dC5-|jKHv{+Q%8vUw8G|UevFF_L^s*3Mt3xhfCxm2uy^Gr}Q z)-{XLPXeXhjhR@2^YxR+vll#B0%D$E0Tl}M@wdIcxw$c(-nO#xC#da-ec5Ex$F5~( z*^kGNQfa+(gMB5C?Lcg2$sQbTr>$Pn^&~f4n_}475w_5E7a-W>tMGTJ{57!H{FkNjztF0@{8W52{eu+(HRQR}AR(S% z>Jd>f?r~;vk-+G9mqhJkbh>jUDm6DRQHtDEk(Ps!RPDN_fxZw@vy_oj(h$a9NgwlY-_Gn>rMd~@H~#Y7k9=z|Pq98GCJ~yCfI6iDh;6A8F|di}wpZDBJW$uLvnu34kLnMbF?m{ylTs>0ht%&o4iw86vJuWm0b7v*YwUNvpoU@v;yn zPGzD|J(1^^{L8Qq{Pl3OTvx-tr%Sl7s!?1IE`H7dW`9N&hlrr4=*U=GWN^H8Vp3pA ze0l~DrIMPKW08!`mZHrBuyIh3lCyBCFwn7(=YiSD;;LwxYpEFI3Z*N}AX;F21riQJ z+D}N5ZcRGbfr>!(KuWAS1V*?P2;K%x=GN^{YHUa^RBKQWNDI}~nt>8Z5#SVXPBv)@ z7;gr;K}LVOONyHz#ZH7qaz4WJY)}TyUFwgepfx{qaELdJT30y2mTMt)X?$*$(Ko(O)vBe;HKO#=Bc&cd zYx{BKd-9s#Yt1N>vg&ecF4|Ag?kN}WNEN~hYa@Y?b;JHM^Xm*ng~cDz1L-40Bz{Tq zu&uKd3@*=h0xE3WqnQ;<6B2G}*F6D!j3BFmZChKRY?nvh%0Ktk+#A3V-$O}V7R2AP zm3&4)6!O%-ez*CPsg=ZP6{bHnm!T4!JpWOKg5R)-0<| z)(3mRp*5VGgLTio@PPauwZRYR2xSRgnd%Vp(l~=XKt!xwV+8DWMgW_gdQZ~l5XWZT zw|-%6#B-e7p)2wMd`lk2fuF(ca{S?d+ zpfW7sgK%rXOohOZ0#h;_t8o!ud;rkiv0i>*Og}Buoj`!ogk{)IdJ$?(_QPMH>UNW@ zp zGD+FJ)GmM1r!o6XY)Wq0ur=rwq0Vp7>Ts%GI|V`pt&;|`xgUxj+1~EDDb88IC~D_1 zyVKPAnO1S|z6}Sd36r$^p3X%wFYep^%^5=jnhCOxWY6Yy!9YrOkVO+~L1nChfDIk8 z?=^8+lfGN(L(w1ZbXrCQIxh z;NcQ({s7`}-~O~V*?FHk;tHSJ+!oP@Fk|C-8YO7hr8N!4mt;n=YI2$Yh@{bfWbOM( zE*Vqy8&aGrTE!MW@(j=WntMoK0z)!gRYg;^Dn1s-pw>cVtTR3tdz}WX2S63wQ>XAp z){^*ud?Zru`F0~0yTk#2vTIqpu8^gGbpT*e|HxX~iq(-Ey=z%JfqIWowYrwI=W%aQ zG)V{Ss&yxLT=XVyfOrXWJ ztWCB1BWsPht#Wg+)^NFbRcnG=1r67-7T&6OEo-rF-(SmG!S zEo&K4t;+|v&#fzl2%p$gj?TetswNw-Hq|p}XEvAxcc^X6iYDCD7Q1l|Yg@ZRcxGF7 zFfR6?{^Ur{y5WKy{^1*d3j3iEG;;dkTGm4Cn#to`>{`Ijv39L=Z%*ynn2w!4wzJlj zeeB@;0R4#LB|H7tDd1&q-zD^+%)VRX)t-IN1NnQOdPVR5^X1V3xc+>3QvZHR?n6Sz(n6M1VOn0b3X5+q$se!pO)M@0e7sKO*BnM-ybwdQv z9${Z+iW(pS#K@X(-*`3!q+O**U@=g{2_T=DE^T?S#Tg6YxhciGN+}`hiVqM+9Xb`GK3E$R%X~)jHmx`S>Yy zT9Wf^$#-s03e%H4t=p0v@~vM&If)sfF;Yyn%9@e8=86fv3oMYFUr~6nl%|`Gr=QbD zrAllWGQMva^JY0!D=W(wEKLNxTp~2cNLr7T<#2$l(A?^H{FV**z;>=&EJpRRcYN}38q;f;Un6QV7+W3`5>c7#6u5XUXG{2BpH#aecw%@McEY=gF`@nQ&2*{U;OKz`XadEpcH8h^;4Su1PwXL8O_i}Z{k%yD`p}}3K1Y!sg1d_#VoNXqfMSyMKB372)CN?ep3rz$?%=*qq>5FW`(oC9Pb6UY{!1XJFH=%(K(`_|;p z!p+c*6M%?t5b}`xxdYNEAU!^R#$83ESzI_ue8EqWHW+ zRR&8varWWNNTV~cF@o@H5tmbccA*+s3)M(=*Cpw^(A*!QtBSHu$L~=ih&a_?-~f6- zI%_Q@2V;pKFe=2X_{KBuDRX5xFYT4- z@g-=qv@bCuslV$3W6nqz_JO20Op>p=C(MBIgUsh>=t_);n5 zU(Dfp0{nLUru=(W2fvqp7D>fGjgSbrF!jh-sc5sfNR)PbL}GFZN}4uCo=y!c2g$oe znB=77^aKvq^iqnErJMr75S3A!T~H)j2;?FweF6D|0a6gw_sOQVqT9PBxOyol%hYK< ze4nWS&#F_)i~_R8aUC+94aAw{gNvF~Qp7)jo^K&XMM!a5qC@%`fDapQ&$m1D=#>dz8+@3tdTF1_fTb@PN6*WF^PVwzt+tcDcCwi`uP2|Jg@Fh z8H$pJCKFuT!%01ImGN(Co+nPzYi@dU$_vovw4v#4&Ndn-v3EMnmZWhp@Vc59EVR7NH$HHHuN)R@59TwL~XgG_rhs_HOLGj)ia#bn~{{J_lTkKkqx1~NH}4B^k4m^dE_vlBKWA6D+Wvov7e z6}bSutn1amyZ|@H!P!8}bK{?7lLEm0hf@7}x{v~+8p~g<1m*{%4D$TbhN3)TOv8{U zw|F&068y|$=Tu;JzEo~OevvfwtAKQMV!%JJDocT|w*FfNhZ@A9RI2<%WR)5*ZM|09 zD+U~}%e)6bL_JGQpVX{=-L~phVMD7d>lEP5?$nWirN1!Mf*L_EuaVt*Vh;)p_Mr&} zwQeyUP&IR^dI=0)V6w;g7GCbl2AKSVXcl{oG-`(f?ASW!o<4$HUexZMZYi!Y8luDA zDMCXMeT9%C?Xi9|#N8*t65!y#)F{=H*2lz)GQYG* z-iC$Nu$##+n;Mt8$KY+5>kz*spjC>ym~`5mY>$e+_tLZ~>zg;(tN+K6qU<)rY_^y@c5dJH**U`8V=SG0I&g$9EA%2<^ zTKj&7eC??NmaZ%5`tK1liq1JG{8DlEwj`D!)1Z)c{THr;?su0OTdSWSC7u&Xw&Sl$g^C#)=nkH3}If(o&!>(-E!d;acO3m*y!-i<^*hgTP6BrYt zCmN$QLu?mNUCGZu3T8SrF~gx{qg9#$TG?U}g2>g~H+?5}z|eMR$g{U=!N# zwYEmEj$(JtB9w!g^X(tSuR4no19Cb40n^umLCf`j@~GV{?lLpGyK^+7sAVh$OTm}@M2p}%I zACb*Xdk^dnqyUHWGvtwjqicW_TDi>_@!~rC9s(Xvsz@;(eIn5fVmk7^7YWV3$eH^H zS%b2?xfkv*e46AJ8MRpnU?KC_*zKb4l=)`MOWY{pM~G$@_#gu#0}H>o`|)g!)!<`Z zGJr0|Krguc1DCuDwe?jhC*L5bUKJuL(lfWqo9H5_mLsj37E?rNXoZzT{g`N@rPb|A zkti1k3{_PqSs=OPg$?dco3161$O#D7(lJ8Jro@)8?S=kyg%A1qLG}8_gZ;BN`Q1eM zVxifUpyD{Ueush`NS8fq|*m((ClOZfh#tI1)s}D`6SzbB<$P<0wp@? zQ^SwU^}lJTX#U#lWDJ|xG2#$ZVr{(28UE0MPr)Ij|MJ`Gq;<=K^>=sd1WfKxj4zHz zIpIB%H1g0N^-Cf1AQeV(I#P0fmnZ%o`W%Ay?`aAD2_rdQf9Oh~rs1EiIkaM|8WI&1 zlaykQP6!Few9QVBm1lrZlBfV5lGJe;9s)NCp^~7+(rM}dsx3p~#~+D((KkMJ z4Y}}|3SLQ3;a@9^>$$>)#s8q(DSDYGU4}D3W{y^&HcO4T?xugGr)Hc~)t3{&XhFI{ zv&S{|yw+<ks($H-~9p#YJw+Uxwl#U0)*AdGyISxjX{CY4Mle~%sjnkzIhFVc0|ZeW^y zoI>evdT7wMx&3;o>htEsiK+JMkGd|$VM0&ZGFY74Q57V@L7PNmbnkv25K%MB>;j00 zSnWK(;?af6Z!`$?A8P{H*V0pHIyTdqf*=yajoM)$)ZB=TT{94Y$xx*XUXJW{rVxfo zfi^;F@AhOzytN#aaJPqClF-#&OynaP=M+;153pT$Y}3HUML|EXLKc@<5;F{Kwbx`8 z?^H*Uja=#-^Si#^zH64`K!aOmb-rXKeE1)O%3q*L>c2tNe}k(3237w%L6r&M&L2=^ z4|qqy3H+bWss0N@{C{|!=l>{!gCA zs3nl?pFB^VVRzL(c^;+eYef8ao~QWNH^em}{wvSpPkZb2-+3M$r6lFQ^E~gTdusm9 z^RTO6D^~x?^YmgX*GK-!^VC*tPGJ7d^KAcW^!hu`Gmne9_2%z9Pr6dF>fd>u_rLo7 z%JZ~bAFut%^Q`>H|0~aPyfgnN&(rYx{Nzub=i9gIGv4bw4+6ws@pqiZll;+NaUOwL zZ@RzYJZ3qwf8sn>vwobu0)OXu0tB*h{^WU9=B^R(pFGd7#r)rSo_`SWY6>Bcj=2$E zitic`hf!N*5Gu>vT#WuIrpFsuXQG~gujP1+h)oej3rLgL>tij_@KZ@zLKy%M-|}8b z6Fw`i9VkJL!N?ovY%A8t`jUODo4`E}w2ac=1WVw*fN|!~`6;Ffq-%->n$kt^VJ*_b zj3IzDb>*(rOm!2;T2_ps{#tfq8pJ2_g*O;LRH%8Dm#5|?^1d*=9h;w3CB?;;TXPAw zDp4a}zg5_HBnut*3M%-=*PwW3Wpd8oRoO1mF*NbSuk%m%Hh~>N3edR-;xzlQr@c zB>#`J#X5e4I#9=wzMIJ421BCvprQy{EvYP5dsLRu*|JvIs-6VHe)c3<>YjSMQl9>_ za!uQ7s&-C}n7wuNe=~Ja{U}1V@AJ|9nMM1@jrjNM zJ|uOI|BjTe<8D+zfG1hEUS>(+J+kv)sWT+PCzb%iMZ74<-vW7bpG><@d3^I?yWm5D zzO$+N%7#yo?N>t^BGf%et+@zkJ6dMGm%!=i-u%vTk~zKck$Ag%_A*eSmgQNvztGSd z+SA9+$rQJpUuC2nHXN5BfrZM{&g{%V4?Ge*!iaC*j|83UW=DXp0C-bg+~3G|Ja(EQ zv{=^x>>(*04=7W=0%jx#jvm0CdIpBMi!Tsu@On!~AtGf2G60mhg0lDSg-MCf^PV09 zl=W$%q*D-FJ^(SbrILu$$#)zImR&9+G%-@@i+p-xow5xy(E#wGP_AwlS3+cXzOu=k z=ee!&l>1?oq0W`5%>v$CGtb%w}fWljX$=?{rYZ@v%^m$uS?lX4|qK?oYzJWzul_sSA_3ixF{ zL6pq%Dw6$Mv!p+xHKJ^zv)C(hpWhH0^kaz0W;?w3tSMIwp^eDMeUPWDfgbW)+D|!W zT~l&8>o$Yzqm+NIDg1KL&W@$ad$_s8`xCCkQxTK%;dVYxf1YNls#XD@gx{c6P{T=3 zI|IIX^G&wk{T5BFB+DDk%Dwsu6=i$zA%$yiUtve&y9B< z&ib)%$NZFsEBcRL>BkYq7UUAFKbh|w$Kky!d7YVOYuT+zmSdlSZ_|k6v@=*3{vKjF z-ni}0=YeMj!2e*(<4{Sanii1lme>_dG{|eX@>&9OkKk(iY;O#)^N|cV_B#j8n4${| zX0)Q55pG+$HFmq^SYv0gNVUba_&hd__*A>m<^C;i&`woBkLzJNgWxg2Ks)*awyhy|}`P zHmKT~g!>to(%l!WXY<`bx%PMnU#)f9Jlpf_?3p|2b20erZXfM~Fu&6^+Q4*Fl8g#A z6DB*W(ljei9dUh|aj4fFvb2ysqHOBkUNcD4x0ihN9AfeCm)_vrEC&zT+vajVYGB-8 zNOn_#6!#<} zu`2gRAydkTryVZsSz8YaODU`^s3y92+DpS)Y&Jh;Kbe?$R+<#@d-F5D>oCSmBxmvI z68sH(C@O2)lDggoeBjzvv;okZ4DEox~ROfYna z)<0~p2)%Prr`{XYj0SYs52huG zALZEAPwN~N=d`Li$|XJBtTA&Pey$BDa%HQ7COt`7yX>kZQ`{68uAT4;9xLy9y5V)l z)jyg1R%^Fft^V=(503tv^~$NO=;vng!Y5T~ zWr%IW%&QL`8@_>G(;;wztg zon~7!&f1Tkip+=pUZiC&@b!CoFk$Gn7Bq7ddguAnjqS5vI_|$8eE6E!`t`KRoPFz% z$Zc);%Xf2!AJt!XmOmmpoX{g~lZ2b15 z{&2(UFL)N-*lA2Uak9eWTXy+}_y znVGz5s$F~dJ%A}+g)BTi8pFT%d3QbjDmm(nr|!Bf?=39g17Z65%)I_GBCNdkyp1Gxy^g z{qk_sCx{3k$lyQz*#9`r6SVB-FAz}2?;bPizorEbgZTwu0`BYje?auL^y|iWO_mm zc+b#j155FhNDl=CfT&O3(bjxnBnctP4CNdPK?p<`U_8bS{n$st_ydAEog(RLB8@~N zH!MI9<|sB75QnAjl|ZCDp0`3t6l=geLA|JNIu8{Cq7;pyQ;_08X$H4ZVRSAL<}#7e zfEa;Q&r|If<$xF&R0N+(w5C|}m66^f6|rb07m&di;OW@cHwv!>+agu3K48lh@0ci|VuU>WPGhh#B|d5Vmd%teZi zJqZo?>c@;qIF0ta8Kvxx`YeW4Kp}7Fp@MJ5J;+7LB9MnshFEL6F`hGRCm?Ubx1~nXz(1%L=(3#}Rhbm(Jx{{kP#~i<@m()R$ z+?kuauJARtCXpDOJS&!b6?7d`x)(zUj{#euOT>^D{yrQp-hN55y-$evk!k3W2AI<& z=tlaPFF1I6Kb3tEC8v{W;wi?^7$P?aAV#F;j~NY{8j_<4evPKpk0MQsKeM+x&3gsA zGC$Z8NYe$p7EK4O&fjAiNPv~4HCh0b^WxR1hZQHZD+p;a-yrQi@QM7VfO*YHB?5}5$t0)kY z$TN|HKs(Z056B}i{oE+0TxF)*H){b*Yq^hz^VBi9A&L+D&^eN4X*uH{&y2i@<~&t9 zk5|pPRj_O%+~LZ6KhM*O*H#hbGM-wwODVlrsihdVRvZK`Iv32xe^PK|RRRnufyEV&O%zNh=56w2*C-ZmTNPX7m3%)d zJjCXKTnih1l$(|1hq+8RQ2J^NsQ5gV4?dt z$o3Ydb-cPKuoOO#Cz_9$fmTyN!{k3ze-f-I{gHM<6cZbtk0gUfyWam;i%Fiyj*rKr zv($WbEzx!j2oI_bUWe~HV~BV%GlR0S&r5ESp=h3zLe^uycw(qoYMn)Eo7ORTt|I98 zV3V5K1ypree65m1?T{2EtGkZnRkhq1Hc_x%&86<=L`~azy+ds^_pOEtSOYUjLqd1` z_n`WZRt+j>Y>8`areK|$(zm!;><<WPD~YDSG$ zM-%ls>m~%&wHN1E-_9{->ott5Py6yMW${dvi0&Ear{h?ZRM#sFOR7Z9((LBS)gd9j z(zRb<&c_^FTV_3+7Ce>}GU zCl8JMGR~IPzJt~$x7sKky(YCLD1|-?zXcQ}eb1JXJ0I0_Pdej{L9Rc<(6SDsy;jnz z)Nbe2exTps+}!?7oX@nN1C`tEm{8C2MATEfWBC>CGsMtEsKa}*Bjc0XhQ3$;Yo|N3 z?tW12m$A~wg3dLsIy3Ulq=e27*v_Bhok16!WCor7%3b-^U6Ynssmf&tgIqVE?klRU z`+-)tK&I}Uz~nmCRG+hUBx_G?aMzqwkArxYbwaV&`))t$WUP2z*F{m!g=m~xS7mT^ zbwTa?3v9GouMIhl{Ylqs0j_hh*WpR8r0(an;M5kO-fz~OC3SsS*q-IO-o}j%+q(W^ z*1VsSIA`)c^7Fd)%FwSHeF=3PeFXzotT>=bFK(k7K+)fE(X%JoKQ8{gsOvt&rVrns z^rL-kjlp;R*!~V2AS8Y;xXy5LpWr=N9GgV?5(alypO4naz@(@1XJw0FK#S2#*}P6` z*NaqGo{UUKm+YwLBvXr4_%nsX;QO?>5hUT3cz0VzZMY`CLQ$y`UhKc4YrqS&xcC1>IhYP_NlaMyaGoZIo@=41lf zMBCGeubUGNB$HDX<53~LF;Ax|e@}F?O*%XsPqUnyG_3A29GhaA_I`>>{xaFQIkiF& zM|I|kvOzAejrZie8xBFQQH;gqj4l?&?ED_y|1x~}<<}6~L{Z|jtKqLJsAEU}m+@OJ z)0o?%l&{C%-<}oQuRHraMegp%NIBbDKl^laJZWU|_m>go>0wsextXb%Im6LgTVto2 za~!G@>niajg|lwG6I*Plw2u_H8cM1A<~G@8Bq-+xgy!WQ&pmzSSg(?Hvu~k=Vv4tD zN+YzwOLPHbxFCTV&AYv*xb`S{ZJ&3RbpPyJB7 zMkpxQ;HNOpFZgsb)GebjqsJrihsga)FB1Uzha0Oc+(czR~YKR)bs*^W6B7 zvti!6fxooT(zkifbmN!mX6M%WDCNcn!p*4$t(k_HK%{U1#dcNb7AR#)S94o1$NF@7 zV;QrF=rmrx+}@aWknrEG$=)VRUM9Y?xqfLy@$DA=o2_y8Eu!sh;T#l!-8K`Ivj@c* z3&+lP)i*y>5nNw)DqcCYHkya@MqOg6i>TrsNKWFNMLKLk-AlI$+o~JM`R_~v`KYsmP+{{<&N*uVECulN$r^-G`gbieb6UxC6d`29Zh70<(cU-#Kg`NBK#-mdTj znDv@J_M9K_u88_R-(##l?tg#x1<&!75BjPv`i=(mtuOhBzwaH-`!Ub(pXc}qIQ+1m z^r0WmjDPcwkNc+Y{JpQ^D5uhXjVl01OVZXSR6hAfsK(#Oa?ejKqVPzB9s3GX)%*@7zs~= z@hOo~16C#$x^=&W2ZdV52pEPOM#e~CMB|i*{5p&vHZ1Z29)=7)7zT>uJOqa9@Pzaz zxf%cg5Co4D5{nK=0YDZTA0vq-E9>qQ`8Ykp8U{xV5fvGQ93ds=U;!>?`#Kv#g$Q>~ zQ++>Kh3SfAGxv~fmj#+jc{5dir(K6FuTisWm2pK+hfI4{gNYIt9uh=t9V)chbKePx z7cLsGR*mlE6WAsWKYzctc_T6ql{jhi5PncNh2f_S{nkNj$4u9^XBG_>v8PerkPZ$* zeu`LfBDjhL$z@#7vLS?rGDmi!3C||9c?T`ATt$LJSWjl3#+EGDR+5*gF&*ferpYlhY3+ID&9R?T)hdu1DXh(yZ(RR%sgSnXt~KVQ3x z-B@sD!g?vg7Ac(eY~Qtk->y8f5MsY@k;+sJIxMqf!*x5q`4{i>;H|F%B`!8`O5~rv zTOU1MU{RDg19?|ID12^`&8e1$51@eOuZQVdFT4DnQ*5`>ic{~O0m&=Syy8OG46@}& z!>_^ZplbqxC(>{u0uB#o4a74P;LyZq&N0zMr{HL@A`bsI3WvijG~f_LLpY=WFc{ep zLzod=3^GUwo`{i%2CDGTi#MLoqO&B5&;!aQkhDn1Cb~$n#r~2oG8Zd}V8cZNeEjl@ zAJOR}iMzsl;>tIT{Ax^Y=O=76}2?6PA_fK6GR_vWYAVo)rwV9X~odhRAnvo zPdY*6;?qKN-N_A7du8-jTziX@S5FB=);L%(74?luZ*8?MXhS_z(>sr~wj*pI1((`b zYrVEsa?t~~Ky=YHSKV~g9aq3li0$`Tb?@yrU}xPWSlVHi zMYvvJTP>JjeFaX~-i04MxK(-g^%vhw8{W9vS|!dn~M^35isbWm0nXxI+-XF&9~!%r);<7vg3&lE(mdpEYqMM0>d## zJaG}^UZe3Am)4Qb4)H!*@yjDvn#Cfp*qrYmS|1#m(5Q{dVNa3xYPkA7}Ap!Qs3J=@tYd&8q0`Ygym>{O3?A>^R*a#zCj zi4cJ#1mOxJD5MGAFG%^5p$C=sChtuThl5jLDa5wIs;y{LYg8BQv?&i za5gE_D2XIuT93Mf7qvyJNlxS&k#L9xHmRwHeM`a^o)iGiElERrPu&m)K4TZ{Ot`3u)#1S&1c|&W4vY5)+<|&yeO=Iq^nY0|I zi!MmYSA|oXt)yl-Tjx%4igTC5Y^O4hNzV*|a%paSq945mhXnM~YJOuQm#*l=t_@F$ zR|sMo6KY1g!7-qd%Yq==2FB!xac)Z}q_ilC$Bp{aZ3*y09CaxofO6=LZ8D`j&Ztt4 z;&G*HGy)cQiP1P{O-Kb0oDBab5RffR;A52fX{3DmrlgMUsAF1cPkHLps2c65K{~)x zhYF{lDwU{Q)v8cO)zvFiRjO~YKmp(xnWj<|tl2}WS0zYRw!#&tZ5@_aq54&`zICj4 zEo)rUDha>N6|i(gs$HkY*Sr>%v5T$iUQ_BKxn{MoR>i7OA#2#bKDMt#oz_~z8dz6c z_OqGotY!)8Sk-RUvYvIrWZjBbDZsY0m6a@MUklpUqL#9p^(||Go7&)p)_oDZX-zMR zP8(UYBLvM`Mp+ugBhpcYj5BUSWwJ#luuZ4igBy;1cmbQ{3#1i&(Q`RsN}y_hYEaZ; z+i>bY%30%Q7#ME?)!YAF{qC2){q^sE0UTff517COHt>NFoL~hnn86Kp@Pi>7VF^!| z!WFjgg)y9A4R4sk9ro~t@k>(r)>H#QqT+ZBx)~Ey$88H$KpMYG-4a`|Y}h3eceC3? z1%q*6OKfD8W=dW(-pyDsp4^Q6`x4`7)NS$gZaXYcq)!_8nfV3rm9d;YL>S#)d%T;s$j+(Y7c)G=ZJoX2| zwru}hce;JX$c-Caj|pshu!7_u^h0hv!X#ZARF$Dtd?shighWlu3n z%kEN-CtCk+$i*#>Pfj~&1DoDvYB>`S3I?VKh>geYt4xk?H_^j`c?8pKZ&iUNN zxjO%FlvaxSa!#qRiv$4*@jkHrT<`^9Fa~9C25GPcZSV#;EcAfr{$_8!48Q}HsA@g{ ztZ)l1Ww5H{3rwWkH{Xcz2qn# znh&E2;P+myd(O@asL+biZ^UxSq}ovV>`L-*Fc0-`5BabU{qPR~F%Vzy03;v-4yX5A z%hB#k0Lm`|!f(P-@W7ZX4uX#ZaBjhtNa6$u0UFT*5Ye&>F>o$%z|JZZ6-y9NF%?yD z6ThMjS&WC@xr1E0zCf! ztqi~b((ptWOcz0n<&N*O#j(Q35uoahz^Jjpcnks_Km*j!0Ppc25i%haav>SAAsrIn^05|I zN)r?8A1Ms(*6<-Uaw9piBR%pXK{6!$aS`<~bFwbd8US#J?HtuQ!yvM>GeF99V7voH~8evoRg>F(ETDC37+w>HgPjIb#pg)vp0S7 zH-R%ag>yKGvp9|OIFU0sm2)|nvpJpfIiWK;rE@x|vpTKwIJHazN z#dAE#vpmi7Jkc{f)pI@BvpwDOJ>fGx<#Rsivp((fKJha@^>aV@vp@awKLIpA1$00O zv_K8?KoK-S6?8!vv_T#8K_N6kC3He5v_dWPLNPQ$HFQHcv_n1gLqY#EL`8H&Nwh>w z^h8lKMOAb~S+qr6^hIGbMrCwHX|zUd^hR+sM|E^Zd9+7;^hbd-NQHDriL^+K^hl93 zNtJX-nY2lr^hu#KN~Ls4skBP1^h&WbOSN=MxwK2Y^h?1sOvQ9e$+S$(^i0t-P1STw z*|bgF^iAP3PUUn?>9kJm^iJ_KPxW+9`Ls{{^iKgbPz7~R3AIoS^-vKtQ5AJj8MRRz z^-&=;QYCd#DYa59^-?i4Q?q9TIJHwbl?2Ev0Xj7@>(J)T?4%x%E5o2AwTy~V6~V9o z0=)>^dJ*%|(Dek&A~n@XNuUH$-~>7qRJ%)5JvCTEbyWFF&{qHC0Z6L}g%C}eC(=xn zYaW0MXHvFk&;q89rQ&5s;bC4jI>6c+J7 zCX+0HU$M?#N$C7~~5rL^;TMw&kM#bX>m{uNs_-J z$IN(+(uU}8+qPdr7b4d7Wb5c^t#61Hb^r{PA8NO2n{0>(_j6FTYF1Ly&ejN@b#CxB zases?rtNiwXOU_*ChIlfUTk8}YgYC4zYG`GG$(Cyv1kid#1J<`J5>c__EQ1Oals67 zleOn2AZV}F1FAQ6e@AnpBu8`!0T|75Z*bvek^`1#cLiy5l{UGAh#3(Ib@?yk;KGfT zO|0y9>L~Jy`c(qNgkcs~ShRHmikGG;;865TT%Z5X4f&UQc{hS5cvq@`fd%b>sVELv zcY4*<%GjV2Vh9?M;BDt-5jfbrI?7~IEraz-)96-oy|>jy4Z#L?zpnRtQqacKQjo{;y}*09#wD?flnm&dQEWHjaymqVfh0K6S0rUC~$oz zWlaq1;GloCH`)w1$DFxrRgM5KxqJk8SV38cNqNlHxxbDW^gOwg3)5Fk09b*Q1xf&R z1%L!p;GXlDiJQQQIdur^nNwF_pNBw|0l)};%NFDL@ z6c}%|Kz3t!nx7L5s7pj${djAld2i?Wf-P2EajU(G_X7HtH~OoHFR6sOrmgh1r^tDP z?KZun`3jjO_o7DB+ynV1-p$%A&DE zDu90GO_o8ea#h4bD!^iyl~NuXrzHSmKgX!kH3Bepivg;YvpNyt84R|SvONoGeV1DM z_hRYUE-AWY3mT#Ux@B1)5h+?OV%t+&UI`wvqL=!*{<YC-m`C2Oo9xU3`19VzOv5Ih7$ z8KVpw1^4Eci)*a~$-<{hlg0l~1sz<(kfa>|`;osHPQ;*Ohlh%Gm!poYv#(G^kXHrW zw!2U~oyE_^H}Su9RlQjXt4zDGg?GhPdoC5%1X5tPtJ?^o8@Dg|xM91X@maQm`vjhR z1pH*l!?(#Fy2=+I%8NC(f0(&Hhq;lQw~w4psGHn@7J{3ecrTzJrz|-}l!HAZR~g>nh05b(fv*+lzXchnT3*$Ib$+ zda5&>zchTnQP+L3aMBnkri~Y4BDt$E9HE-psUv*%1YEHnMzHy&)I8m;r!Ab*Cy?&1 z!Jn2PM!fO#xNS7;_Xz)8T$N3)u???6adAM{auNqi2EBF&6PaM z?;T=5ng~$&q^DcW`~9QSJmB42xrLzM!yMxKi_A@+1?-0fp18FK{uj{P%2hhfb*iPC z_NBF0%GxWXXqvtPdE|}j2g73rhZzC3&x9AD3ZCJy6Oenlh6zY6ENH%{mksA(;Z`Lb zs)u=$AYhFdC0#oXhuc=y+onn|9GwY#)Bz9~$E&ON_ZWHb+#3i0!XdF}JOUzti!E^Z@&Nw>Jm6I|=p9jY6etWvoguP?ZIOYgVZ902U7iVw0q#|K z7=2JR5lN69IV@3>RjjhjbsBM?7Cn06E-cCt5yUo@-R7DpE`f+rIUM;R@daG8zOHo$ zSO!!KXKQbBcYA+>hl`Jsmz$rXr>n2Cx4XZ?$G?xzFZ`9zkCYmv(3c>Q$ai92nSk|- z7}Qq;-@zmOM*KUHa3H&cB+%u%s4XGFj1K*gOOUS|g<8E-Nkfu>3V|}k0<;vcYu~O2 zL!4<~*T5K{SUH!O>{X=z8(~C@3h{~LK+u~*+oj=EE-D6YD4`WWslZ;52~E3oKnka( z6JP%+ZCQcCX{$LmFrAdVHcCsFv`E(C`Dso>C3J2H{#i?=NTxwzosbn**subkMy%{& zuwiF1z+DgDW&FVM5I3PZe^nWpTa{`>%szX}^YW6InFHu8vr<#mvtC%HJJtG#htWeY zbDP@$XkWE-oJ@hu%`0tr&Ywe%E`2)n>ejDg&#rwt_jDfNtN;m+UW)PJ*PJ*%gavti z>KC?`_c#B6Ul!5o8}T#-~tsIjsRWfijV zl5463z|3itvEhnxtT8YPZb-yJO&lPyBcd|rP!*hQ0HB1;io1nFOM*qsh{jioeRltb zj;Xcf8z&d;GbCF`C~3uDQx#X_jzZdHga#X2V2A()BDmxkNGb+F23m4i+J}Uw=*3(k zGU?k9P4?)NR8LH4Vx8Jh5u1JjPjcLsh$a3o)wByMxSvAo)OV{m ztD0c|2_+C)Y_3AIYD5dNh9eR^Br$e}mW49dCXSf$k{AHkvN=RuRc<3(4jJ9tklV$>o$=mj>qI<17l$Mw`bO3zAKuYhf6oExLnt3>F~|NHFaYE@B{d>#c(+w1;c#g6Pyk z|0%}n`|gfG>@4F9m89X^E2LJ2K`lhUhx7sRoxw<*J6&4&8~wJw5QzWzfN-_EXNe?! zu%Up-6Fh>j+-2e|Hag2`DjG{6ONP;5^jFvGyIVKLlcVt5^h3S)xy8V4T10C+LXhTi7F z7s4=xGMpg|YbZnZJnjT6=o|nhlEcSkZwQf7m3naKf{y&~he14?Ct`&H6Ff1BbPE>a zzEMRXqAH1yGh7du)iW-3LqI#3!^SE=0oCCTghOzUYjgxL(eRy35L9+MJ;nx zING5Cb1_p$>RWo`V+Uuku|pn`d|j)T$82^6aYgHY=4#k)niu~LiQ(d4ugMFIiq;J5 z<;F;HwA0WYhA>NxgD)Suq|O$_$Xw_qcnn0DI$}8iv`pz~XN4l{G$)TY<0rN&oq~{*4*bRd&Kp?~dOy7nmgk??c zKl=<&E2)J}B6zcsY=M-7R;kHtSYU-z5P@ZmH^>z7Y#PN0Ljor`&2pmVqOw8WA5{5D zD3!BMd1xUg$hSNWrRf_n%cS+TbtN)ZfEfcb0A%V{s4aNwLvQW}^09V`-t#Y#9V zAb}P7w5ZKUDeWxjjK;1j8`Rw#O-W|HZxnC3aT3v3J4wmg1=Sl1Bx+9CV1e!4q^s+| zMJ#Q!VR^rs1imzgx#BJGwq{o|XSKTeXIML#PQzPSYmEwJaclc}+Sk z_?f6GC7=5sdVtkfDNSicr~Hpe>NF^_xf z<9EjGa=P6c99oNnCqm?*zDg*OSqsK;%oZa{wpEf{6;ORr1c|4d>G80m%q54C$TpYqeuA#EB$SL_184VE6ujT{MtFH+KC$L;&5KH}>EvA9_O4;5 zaX<-x`)G80`S{o;u7mr5TzCy0oZ<`-*56 zR}PsXM8!|Lw%3>nwR-_>@4pPYVr{$^l@$L>aIn?bE?bA%oSyzMvzzVgXG1&M)QMXV zX*CQZyNYu8NVBzbf$iTSbQ|3+0~o12+9N=8%U_)6`M{iLF(-CUFG(PXMa{eugX4pd zzQCSOD`t^ZgQbc!SgQHk$W2h1XkM%JpnqFPfl2pW31{@ODSD^Dz9gui^z)?wK`%|q zHWC%bb2d>bY;}AvLi?5Y#|+59gtN6;4?rZ%yL3o<11;9$owzYRxan5!)yyT=hNT}p z3_wF1-{{r(i9fE^@7)aQtFzv9SuO3WV?FCy-#XWM**7CzyUQsY$!fvwZLx!U$`TnC zZOs71DwDg(NWQXZdn-}glH<#EEO`IC>v|1M%J<%a#$YzNMQD1P+_ z{E0k_g_P@jk@7*(FUpyo&*O|yY}o*w?A6+(^qCc;g`QfTP;)*oi#uKh6+f88H@+f;CR43cy!Ns0I{Vx2{`bTGZ9}xk+1U*qe>^0%x@voA(avSH8-Zxu zUqrc`uS`U4VVQZT+cyAL-sKt0z}t3p$;iY~OKH^LIh^P@ohLBfAH>B6SXzg{SS~zZ zH*DacDc{q%8?4>T!o>;ac>@3Ru~*QgP~edc@cGf~QIOHC9P#a7q*c}`p@w}$p9t!d z5po<4j$Way(l8;RI27BAHD9F-j0|=S5eDHCI+zlw7}McSEd5$!h~B^v0}QH^^*PMY ziOUL_As`f8=PU>~@m{N~5?j5P;?>t-4WU%UUmyD69|B?^t^*0E0RJJ`0s=s2!QDcv z0RMfPmUWpTI$|R#pgTkv90;J30bqD+kw5@O-FXoM`X4NnpjsVP--XkR)P<##l{R2W zKG_&Xjnw+}!j*{E_5lyWfe!)}h%K5NF6vnm;!+9*pS#VQPPyRbjp1^X;zub65Ox59 z0HFZn-Y~3*7MkME+2H>cme>a|01noL%Kai1K9UX^pD;q;om69pXuvf&h|*1xBY}e% zxnnGx7rzzW#+lO@cq2N6Be`&)-w6`~5<+gFh7qm_Iu;YoP0YBwi39Lq#Uxb?CLH%s zSJ4H@E&iH`WmU2H$w;N+cfEu-vRnhu;+Toq9tz@0vSdrT#5t-dp&Q7A<12T;m zo{s0l%Zxa~#bf~3BqT#Fp8|kj0a!o*WB`IlSV*EJ2OPiwaKM!$+`pm79h@XwqRw2_ zC8KF1I(Q>j(og?hf}Zq9Mg>3s1Vkk3iDe>;&*jBq(j8tmq{zEXla+|&z8ufdydz09$GW^&w_o=8HWVp-PrvX-0u3ewbWCHv&%N;bpn6PDS&L-#~hyy^7aay1k zgl7h%CRI_R_g6Ks_MCu)&S+TQ>Ipb$`K5?LtT5hXwoVBLfpQhK6${E!MT8Qt+# zC>9V_x}pD8PE+!rA|1k%AO)6XX3m{ap*RI*R)XZDM9XM)U^xsXV>qWq5u?k5<`8%! z9_XiJIwl4R9XaY=3c5w+Xr^YE6K{$p3Py`OrXn6}z+RrCVIZMwPM97Y={eTtq;V-q zzNvXd(hBY24qJ~V>blTXDX;Q0ArJy$)c&Lf-#1aB35pW z+yJzpkBVuX<LT zC`KrG@LvL=!)qC30cI$=wJMng*>_N7mi5Qo1y@hRs8@!bpm2amUCc(J9tmkDpTKTq-H?RZ(2ogjVAqQdrwbqFQ=xMYD(ZUF<=FlsIMO>w#UdNgY z2n-Lyh`}u_lWax6%2G?ma4Yaosj52d(?V_3jtbiq%g-SM-jJzuP;IVy*4A=I1HO}i z=!*oDDp+v9SdQhSVieeT0NL`QbkJqkfWXFXme{6kcSJ>rHA}v1>f))5-MX!Noo)Z$ z;-HSjE$@`=FAC=1_U+rc1lwK^ab9eJFmB}T#bJUJ2(<0TULaXjZsFDs+g^a*p2HW6 zE#1n);i_$=p)Kcb16#Ij;!5r8(r)eA?(O34*j;Gu@~){^jGgvwJiQg${4RC`Q^-0; z(IIc}GH>%b@AE=$^x7`iN^kXQhluQ}^|F&QEN`e_?{#E?Sz5;wYH#_P@A;x{`l@f; zy6F14Z@E_F`@T^4z74jp=5z2tn@Y!;ysG^A@BadD01NN{qXYF4@Bx!z(;~3hn8p33 zO9Mf?f z+wmRaaUSdO9`kV@`|%$Gav%%xAQN&S8}cC|aw048A~SL$JMtq#awJRgBvW!FTk<7i zawco?CUbHpd-5lPawv=PD3fw2oAN26aw@CxDzkDcyYef;ax6!!cR8tCa2{vc1L~@J*G=Ld7u6k`vWJGX-Tr(78^JyIw6nuc} zeiaCavqENrYI-Lz^fNq*0Lg9=#hL;AcE>SSujx`NKywE$Cu!!sb398x*0LNyw*xbu zO#yts7&*XX(d*GRm6(j&0VFg>&+|UPVNu}Dj?`` zNSETKtVKL4raUXLfP`v*XwC4QjVf&{%obSEJ~dY30Yewi%qlRG0yS4F>ASKLBZP!k z-*cF5bwfI|zoFqfT&DCg)wv!VIj6OPM0GIZLd*?9x>Ny45#Ik-quNk&N;)rTH^}Uq zYW1FWDqYv~Cv&KjB_#t_bNb|_Sby|eLzYo%c0}uS4uk7BTjOVIO7Uu6Im6Xo9}jDW z<-_{4k@DkZXSM?@CYsEnnT7;x-=bn?DR1KsW{)p?_Aa+LXM`b&v-21cn}tNFCI3Mw=GS3w<6QcC7Q#CddhjPxQ?IJ z9DD%wakFzn;fQm`OGvR=5=f@$H`>UzkdU4;y3}5Bd4UWpXMmX0A+~Gio4Dk% zh#wjX1=EXD>S6LZkq*pAM)!FYo2i?iXlJ;pZ<7CQ3i}@vtra&qPN}O#Z92;tJD*8= zZ74&pjYEsqkbt05iubv!Bm3?-`Kmi8tG-=&$m$u;J5b^~faE(A#a0zL(NPLOzng$e z?)yX$y#9$|d1u3jLbKf{yg+$@6X{UIubYr9H%Qw_D7?9h&{>UydJ?*uxu>`uSu2w! zG)wd2ON&X617yGcVoW?MI|uAapS&(G!3QQyViS6flyqb;b+GsgQe~tyM92aByh0<2 zzyY}XgcPH(cUY|k(yw%$%bvzxcesac1vJ%3m+kWKG8sJp=n59K;}QxD$%Va{rO!Hg zEAZO?LB=H=u-`V;$ETAsyN8(=*~^1t4;TN}F9J-nM)m(k}2 zppLx)K0J?oqD4aGV=BGId!xKmcEkK;Go8z4DK(h8+efUzZu5-{NA<8b&qtF$n{?%u zPq_?uVB@+h4xNHoePne4>+ifO^!zfgJW9)ZJl*P(9lw7Rgnra}CWe;sADIC*zpF0) zK21Nvf7+|(0zyGlWLp`QQ9rT<_RUvU&~&_)MJq<@t;@x{T0d$tgtl}2#5Oe@T;vx`eh>lB+XL*DarBf=Euz6+!oa*qSWp;;*qXeJ<2NuK3Qe1&r zzv+n>&?ukN1AD)#MDi1SJHqh0N`wC*sB$Pnz|$%ioFtlXqZ}*jy11lrI-;})!yN1w zty0LFG(9>%RVDR<3yj5N-KrIZC3=Xf_1f(tm?$Xz^`jg7Ov@;;3=n}_BA8u@n3|r; zst`D?YD;tw;~)e3;H$EberoddPCtDoN=?j?(yd=QCk!x@_dL?zm#zmx8bSL90AD_Y zQ*PN46A(g{c2A;EA(w_*!UWbBxl8z}VX1iVg2|Ie5uCm%+RTN78B^v=nl)|S#FEc5Y3Odh|Dm%{1D zB9bRZmn?#SHYIuO`b@?MC~%`ke)eS>kGekGVoxsgEl}-vMDHpB1qsm z>xZUUH24L#1PpTUm<0cbgvLlKsFbj_lyX~cNEkJ=Z7gp9Y^93ha&oV}1iL6OrW&~{ zbFnYMJdr0m@5D1tJ@@3ZPoCiNi7hFRx~{6b4Cv~tF0=xauBui*YY3(Uox%maieMB` zBD@+Q(kU1X^e+-1?aM8jOtk~95;QG>1yl`9ic+An&%PmYeYscgBJg;M#~7#?$~v)&a;7$8 zxjD<2W012a<&qq6n)5ZoVKS8>t-`Lhy9pW{a{M0Pid{|QZZcZ zw2Dcms9nqz=3%lGg_$CV1H;V)IEd9Vi7oe@ zkVl?E;Nt(&X4%Wni@?of1$$23=k}>6)(X=fhrHf-K0gS1$aIl+#E%l(dmj-D_z@kU z&1pzVo!fM@z~FGkA;^;l#I|#~E*KzP|H)ivY*s!uC2$axTbl_TwXvFbZzlo!a-(=Q5KOkk3m$$ zvjWQWki167jP>0OpjvEVAE>#}dI(N&<$TdN6B4xxsg!+?;Uhx3IU;M?%aW5wQ96}4 zx;Ap~M%l!Xy5P9PGoqAd0$rm~`OzID4fUJ@U;=tJ5S#TKMsm2>WZxp{5I>HGomT%0 z>M|xO)W;o?A&I<%e#*BrlfuMlNfW6{D~ghBeboRCnnO5CxmUjS)vteLoiMvV%th($ zmbMhb?*?VqVKNq0wri#IbfQaM%0#e}IA$^_g59r&eY{>&j1seCOkmIUq zwPag^7HWev6_7tEv97qWr;ssGAIY?Phl5q8isQJcLPHu_{t1k0(+Y~0Wgi7t=dC?2g&uI&rH0@?RBm9f-4kD8$BP9QM32Yut z4ppbwgey!!Fy4D^m&7AR4@1)VTDHM&L)9GOg|&*{qIUF;&>XH5ttn&o5zwspkQX&q zGAz-BI3a57Bz)5wTO%JsxTk~9PXRk+Dp%RcSC$!_G5tt9_xSY=gz$b#5P3yXE z(c;oC8$xyy-3o%H&TOH|XlkJ+^Ut_(g>9ghDw3=|=vi*O?WhOr-JEF9Ca69HX{a<| zf|fWWE4k!&5oE*;N7T=h-W;E!E5!e3rUA>mZ!Ko=X#3JQn|kd-HL(B5k7>7#rL(OM zHp8dM{T+#u^^FBSSZ#;RiSCX~lycpi?F|%9xDPS4Fo_2OBRgV|hc3j~APtvnn;Q7W zoQ(8%%Kd46WO1~7m@7v(yo!eQA>mSrV_*Va)3=ol%NNIZ#x>4YPE94uQaH<;MNa3< zb{T_TcBP%I7V?pcT+0AZIZi0+=3NeA%w|bSmSP@D$YM{=>>eXYVJvj=Ww@SwhEJ`< zfaFGts{yBhIt46)+%Edc#w`;xh$|V=Xmz?->Xe7*7AJutnp5g$k2(gZe)M?0kHuwo z+|$vX_O&P2POknZfs37wKl?L@7WhUfk}>T#jq0q_FoCz4Y#i}jbmH|vlDh1Ffm$@&@TKck{pRnHc|knf$<~y( zp|xc7jhEm2=STle0A&^zOq6;!Hwy-LzU7(|fp;#5Ed1l2J4{##1t$2v0Q@-Ld9#qX zti#g3I(s><=(#*AxoDa+gbIKU!Z$8LEeK4qy3;UuxrH^0t)_Fap_(wUTfM7Oucyki zP`f?>(=g)tBb^FAutO1z;60szBX`@tNysbAsXP;52ps=}CuKOcAsj+zGpFmaxRzpw zYCF7ho4^SnI0*waEnt!$#JrEXy^vBQ0&^PH!Z*Eoo3{D~Ndh(%)VErAtKRFpKKVQu zLP0xth6r*JZZk9i!?k}f01pWW4WKPLDkH8~8au?oaKk-i^S$N51Xu%|gn|TY(iUdp zG34X7>svnXYraKtD&+#2_F=0?TBr` z1YE$MI||+LrT3%7>WQrWgFjXhh6DTuVU(1!s4NA1Kmcd~Tl|I0g2E<>p&TQ+gjf&; zayLbanDyWm?bE}0a5n=mJTPbg7JP?FLn7(IHf#UXF&Ml=O&kdJnmo?CL0j9pr&2@> zo5umGK`~f{ZP39%Y!ctQnGL`p3DHJop-0QnK9Rt|d(Z&D*$qoF4?D8FlrkzH+QRi& zILZUJnDD*tx$G&F`C~xON=BW8 z##5LZL;|fB(x=txCo%jkC2YvuqK?*JHjw`cy~c#YPn4reWXiAWr@k-_5Fxgb49K5c z%+`9sxOq&$BTZrP$3eh5C6PA>gP^#HG()4Vf2gn^N=@&;%mlbfaSEyYnx`WnN>TH~ zoLs!E%EN#lM~3n}4P2czbf3!PK{+Hn$S^&a#FcO)D(IqwK*X?KgCpLY#6&zq+cZzT zTcM$gpsji z&<1r-E9*G6gi5;%%)wfoMj=36{H3+@xx5@k4{faaQ!~x-vbtzMj^I$l(KN!$L>HVs z#B>=zV3OQSM`}AV%}7cLGp_91#?$|VD2=*L&df}A#5(2L!R#wOP}>J^n9L*9ynW2O z$vmo{bi=&~O}2w5`8+|^YCH8bwS@9b=G+PI!v`@e)AqDT4;;;o{GlcLk>)%{FYTw= zw6;)l8j;*5>-14pD78&9lQ`-N(@@O)yv_Gq&+%NktTZt>odWudw!zE9Z?jU30nIYf z(qC}Y9<9Wuiown_O*kbdIO0H+(S{x}!+IJ}0$t5>JWv_*4jg^ZSe4aT{iIX^IS?(Z z3PrgQu$`9+(E;p(xKthyr3qs6Ra-^CDp*Ed1xypzw~PF{2Oz>}jg4bV(oN(iB-2s^ zA*&>;(UdB*`8-cm9Z7WTf(ZZYKm;8OAYI5~s3`deS5jO}{lpDbOf*1!rvg2O*E~*d zm7!|uA|~{^Q{6qy+f5r~pnMzC??KnU)39+vJt8e2fOOaN+9APowzvDFOzpRyjKdIY z$*HUCx zXgb-V8mgfMLXw@r9IY-w)Yp^RN>^1)n5EUKwc4xgPz@adKsnJt5Z0c!inP4HTKL+m z1=dfAv$JJOwe{8iD>=VRQA}_~Qw3OIF~K3Yz{6Cbx9O*nF4LN?^GEZ;!?=e zTvffi!>HSgMAN2%L(NTHP?J(Cpk3BgO@(EUzm>qdwHnQ3+J7k8*xkC(+f$LHpVCDp z>`J$I>!ikO)29`LgY434b+#O00h1X_`ZT2FNx_dL!wWU|>MhT0{m;gv^F z9nKPJRq|Cs!o}MA#ozp;gqUly4b{~Q9hCy?UrZ=l2PD=O$_txgMq#3!zN|%Nm5I4^ ztA*OgDTImR0N6(4$JR_LC=Ee2B?StaUh4eR%BXGRHmrhyp;bOuq)*}IxWqZuwI{}S}loJ)HOts{Yc6&SMb5B2BHTL29Y8*KF&o> zt%H^(R#AMFp%>=Cc72E9jabIKj*sQMkj2g?D6OfyiE8Y&1InCbxZM+U-&){d&ZXje z1!Gc3%r7S6Go(EGWi+TIBOx7+k4(-(``amA+I-dG`gBGRbSm&!3t&mASvTMu3O(E$mHHDA1c* z5CMuL0B-+QN1otUCG6b&NzJ99Q!MgfodMz@Ez~R|VJ%5b2{5S+(2bsYWHr3vVN1$T zT%pJ9PI>g^eqvO|b4}@u)7E`vUf{!hiYO8O(mnRsKUV0r$^b{Cq%)>N)2t~ZJHBJc zfQcS97cJ<3i5D%&u!Wf8!j0tpOlUXk*$#@wTPq2Ch*t(GX=;pU%fu&Yjxv?ufR_GV z>2jKxHK^1KW=CcK_EnQQme)y+GJ9p_OxEhHrqxVI+l~vf?BP{8OIzMKV3U*E{iDCJ z{%W%Z<+%NYv(!IZUb(w$mAD8B2R6pC~sZ@}e{a4!lV1^~-xG059J+XfCe zfTRCTY?{4i2~>az(16R<0Qnp;q&a}cen@=H4i6{>(wGhNwdZn`*y&^d)_ZIbnr5rU zXy5t(5NJKpkOrpSY{&L&J<>-$&H_AD+{)H%1+~2xZe~j@I4|zC75NOf3Ju-HM%Sgy zgdANT-QX14gI!*nvuh1^t4Zue55mpiUrq$08!f{o?Zgf(&YplR>|91}S@1@WdVVG^ zhUfAQRc~}U%a#mj<(N2?YBx;nfq*?5NxK?+2>u%FaP(*&p2L?SB$^_Fr`zqImSH47 z?+&1jgRZA4S#WKDD>?2j(nztGUR1uDF$~9SrtVm1SaA}LC`UL>s1CkFHV^viE!{&f$=;;nmO%L;?AN1#ZFm0cN?QaV0 z(UySH=ISsPa|hK$S+r1EeCz*}OIKcN{Y&#vwu!l%Ydc_bK>$Fto+X@9fd%kuQEpb7 z7&0x`0NmZ>R$S4-rtK=Aa4X#19fCt}cXxNU;F91D!QI{6-QC^Y0|W?~1gf+5+nM3p9)wd=eCN*~hH}x^qO|zrmY} zD6TAl-ooy#qJ}~^56Ar`z;mL}O{!dM^jP%N)AL{yMs@tbXDRXuW;4O6NuuChPxuKH zk|b*t{LG-L_jSTkeLXnl%*5kc@S$G>8^}v~%{8pW-2kr1W9NaS?j>}<^V1eh5)%vW z?*_yD=MG)3NX)GPlH z;S!fU7dCJ8XL==)#*Z+~WT{xs`#K6hDq$Q^@OR?uH&*o@T@ouY1 zv+mxVe>0_t7V5W2^4vnC|M&tnB9y(uY3)*gz8m?k$I>6a{@C#~KOchm#P*X<^B4Z4 zepMlGnC}5hq8kX?1|wG-3zTspTz~`ezQO<=ND}|{6Tp&?3v5gpJhM({qJO)|{}8-) z_>K1K>E-L|DJEGU3>0SIeWU_Ix;zvuCQuP$rs^qr)-HK2MtTWs)2)hX7Nb%Yw(kWQ82;L8D`-0(hX7ZkJ3mEqi*kgil~U8GyhOFz|Q1 z;K)j5RUzv6bRvpyKk>^8Y}=`3S9ecu|M-JM#3ZO-;Nahr&ejyWlQYKVekiDD=ok=} zJG*vRdGn6)Q3WRcs({y^+qaH?SNVZT$||gE?EG)aUf(~zFWeD3WX$4;sDdCE$yO+4 zh8<3%1J79ob7hGqHzDm``sQ!+6_y zRX6R_U0n9Vspnx%-nrmzEylU;_iguS@81t3mjS5bi=P9qq_HkT*f=#V!>Gc@?Ec8o z3a+C##tyq9c+NAf<0SQE_u~}b72GC$!W`VD80u#plAe#F3z#bzfHc+4H!prmV;{sj404v8Ko4|FmZG4TZbcFhbFD z)5=TWdDE_bwyn{&9mQ+Mt^fD)j^}vsza<8QpefG} zD0Z`+mV=*Q;5#@J*GEx=H$k`OU`&Q0;O4yBC=d%TFvS3n=r#>tR%!hq(J>>xTSbc> z&QX;2jx|G!byjP0XudP0CLX^PjM8f%&1L_E4=`wiszfzrh&XT9L-~z}OBrhu0Ck!b+nQJb;m^0bfpmWEaD} z(^wce6P9jLr%XjRb(drYHUYeSYfIPEkAj^x-9*GIQOU8K?<_2&XF$b3D&hiE?QfmkgPQgG@6qw;Dm!Tzww~*f=ju%)5M5(3=DxYyF?a4puZThyBw?A+zqyg%NSMfr}6~ZW2xlZzhcFNpl_}j^S)2ctV1tc-HfRhuv`#x zpaE`X(}f-$(3?X4{(8VCWpqUVP(ET2wa-qB#j$Wy&D(Xn1aDX)+U1_A* zpo(W8_9SOBXrk!kr!_@R%VyC;?l42yLhu!6=gf zV|5%j$u{gug$_tESb?37w#;yoRis#hRpZRDL%LAwvZ#6R+}w5a$isttUZlZL57u7?*9%&Zoo{FPVf|j%E?fvbi}(p0L^*MopU75VvIj< z;L~JS$QkSLiY6VJJCrq-c&RbjOoS1=AS;qBKn!35wq}`{IWRx+lsv0y+88tjCH&gBv z`twj!2H7}*l%ms_0U`W}KxXeMG0Gha3d(e4rbmYmh*o5Z7Dr_mljBmDIQpJa(I%Uo z+Dh8t$>k)k-XiNl3%ml+@RmF8QaRVEy-+HZiYxsJYHSmKX>5`A2YfjgKDAoeR1_`h zPWm2&qeff>C^?c9%7Bv;Yb{xgF?EI2$qo9hH^2zcTUh;*+6v<$7i7Iw!9-s4%PvkZ zRhARJIC4nC3JMe`qq#L8S6J1^)s?cLAiJqirXi0cvwX6k%{(C)%O-jR2&=HbQ8P`; zt)iy`C%I46**qlO*q6VubYpCynq~ni96wDm`MDtB15#uKEDP&lH z{;4bJx7rHkqeI}vRG3Qw1_T`MIw*}l8WKloT`BL^ikW(wtToSe8X`(%J8husA~$!s z)B;%(jeQ`Cs+L&pp{K5%!IwJDc6RrxkisicjPhv5tW}2WE4yH<3KNj#BLh)C=)`$e zlR>nN(ePbnLomE`Lec6$D#;-c2T!eDXQ5iJx@Wxi9o%3aSDlK9X?ctK3xT8e#+{hY zY^Bd84|&!hhNqb-iQQHf>|!}Wld8Aj>8>Ha3T1L!INMGGyU zR{V~TKINYg-6&%>Ln%Rm|MCAV)B8D<^Pl0JpZ-YGlSE!|M zm)a#4S?M5$#wx_&b%5ax#4hb&JNPO5j&jE8Aj-obj-I$sExm)S5RvUUA=j0?<~8`N z)Tk|Uf#HT|mMX2Ryu_|npBT+@n&zPO6+oz1_dqa1iPxOsmoo-TIEa>TVI%_mIN^Xc zypVc5PW$8SUSW(it-bJou*VMp;GCe;czpu+9O+0meT!0+63Cv*b?itoKTIKHOTCk; zQnAeb?UmrsTFDq2xPYu5GJU;nwcBfzW*$u|+mbaC<#FWAYU0Q*xwzD(V(cBQ?3Xsr zy#JmDliHG?uEpHL-5N0YA`z$%fYyeN*P@dUa60qbU9iOxMOPQ2@SX;F*^0eo#8@1p6C1`RP$f-p@w$3{-uAq&G$r(+ zpb{xLnX{cKSmF2yxGf!FaK{bq+*v^SW}1rp@k=rT@G-S#*3YKfz&zg;QoSIa5gJMuK1{IeSE1iZcml`6C0=j=!{Ppz@Ij2{Is_LIxE&Zt z+I)fKlEKzrj)FX_G_71LJuNK3jBHvdCmx~^Fml>3nj`Ltv=-t#Py#lt=b|B+1=`LU zFn%Ras@C8rN>9mkP=tgJ=b(N#Hh$N!r?vFICw_@9 zfi{DjOn8fz$ZVQJnV5RX@aOuopn?!XoYiod0=rZW9y@WbwX<-C45e_q$PEjF7HikO zmLLjm`v5hgaQA@fG^Mz5>#TGi3au|oG!oa^)CL_U1!csYltQ2bR+kCT>N#x=Z*+*X zV7eQ{rDm{LLL>xRM+c0>drywQF#H~}_6U0iXVGVv6lJgICWwP& zZSNw|trPq^-0e{Yrf-NdEy)e_Jbn>A(i6{w_$V$nP4jy>zYu~MnkTLu8K2Um3z9TQ zG)#6_G6h0qlORNuTJ9nKNY0@no}f#n?DkB0zNSA$OF@rgZ6S#X=_#9spC7x)Z^ zQf?_zP$ro+7l+Z&Z4niDKbM3iuWq2*H=wY>Zp_E@il)CiXuhK4bGHbe? zH0ZF6Q0%bW4xi19KIwv}pyS|Lm=7>*-z(Yrb-XC)V50C1QuzcJ;WBH#i04OUA!f$H zbwxwy;R~P<0q5=sHah8236a)1g`nU1ZhU@YvIeD>!j>|u5LxetOT+v(l0`4M$qJY_ zxMWv4iF;X^xN-n?RxuMe#*i(>QY3xu0*of3JOsmmdq;t7msaQl6Yc|x8=b{o#g=#a z)#5_z3P-r)!kK)^Aj(~W3o8{W7YL^1~g4NToGP5efnI1K^EDX(kjRY^tZ_QjMp4HgZ_!FVh zaeQlGFXF{8HKaDd^J!y4SboH3$6L;Zl4p$z1#+CuUDK^3mO`u zvvzq>X$YG(S4uSp%IxVYJ2i{?`JETXjrn=96$r|V&`SYXW_xl>xt|YLg$d z=ky?*@w|Z2>eWtni?Qm)mv4VdRo1O)Hl(5%*h`mAnJoG2_qC1iXkGkVi&#TaP43#a zG=m9XqQwU!g!N6Cegs@Vby`*tdueqbt3?7cGU&59`8^gQmM{KHH zyY@8gpw&7*5b1wuIrn39z~d;)>O>!Gkj`yUUojZosj~;s6s_@Oek*E*c`eUGs`t1n zw9hC6(clvMm7cZN6Vt_LU;sgZRfzf>c;^+Kk)1FcMb2I=%rQBF)ulD|Aa~!UZ)S|i z>CK`iWx+pslJ!f9pE<&-$y)6F7qLuBJIbDz#*1WuJ z?;IUPQ6gGMkgaf_$G6r6;wWX<>af%8RrG_=N2tgo=zN#U1i3ZgRjKXSbh-L`CTma@ z%OThH58M)WHdAoU&m2g4?PGd2RnRLz6&y6h8&KIPSwu9^si_4ElpIX>kxwwz79~S# z`TJ+~yl&f4Fph@w)U`ty{_beZesbA}$%5uL!9DFlDF9M>i)_-t5LAD1>N>$QG~vRy8PxHZV+Z@irQSA~>Cj zA#3M0>uwH*rp_X?1NzMGKNf@P$G++chZ5vCc5Fw$(=3vRAyn_tMJeq!ajbV}N zrbgApO+l{JZDDHr$MVnX#xG@CPnI$WL>D8x{r4dte%A&3PXSZ!-|;hRpxGbsY*xYAqh%w*a~ z{0D-dxLz3AnvI*zb)EnVPQmsFX4)tez=m#U6y}l1^9Dl{)L)D7-KV|uhX&5gbXr`t zsq^`e?kO?}ZqtQ%-rNx^_`_A13TWawwzz`XlN@!G@#ARHWb8DJy|VtCuG04i!MRBR zgB_$j+84l{Jc1~+-@Dqt_Utli2Q8#Hj88TGm!x}wN3Aex)4Q>LEN#C;sI`-Rc!po zYeo~V*1~aBw0gtk2G91>`w7J1gph;;Lby50$O0mdKT`0j64^M~d&_$(oK+6gCPh7- z5}MnmuO6A>y0sgyWgjzFAT{gRZjaUf#xeL!e)T8a=oZQWrQ_B*q7ZfWmaxxoEy}6r zmasI|EvUHXVaebH?q^XaY(rkx-fZP9(^At-T|P1hNIIT6SMn|daM(KwUp=w5(d+3R zN8g@?)elCPpGQXfju+FP!o0_%b+PFuF2WE+4|lQmw8Hev&;ONDp83Z(|Lx<(>21tq zw!~H9*M}L&n-!;<$MDV?q63)DTT!G`_`c)v>rR~N0qFVt@SdLo3Ria;or?ZHEKNgr zwL!ge3~Gx%wrrmg4Ug%rrKRtmAkqti&SLH6FY6q8Pe#ShP#Zt5X)nGYzsP(SU88mj z*e*T0os!+Ji?BOCoolM=oNx)y|2utU@S&T(7xnx;$5rgR{gLvO(-nQBZ}3d zm14f$Rn9ib=kSMr?QxwsC%f_1N8RDFcGg|+20_N8mCwF>et;n3~e9f0_e^%Gx&wQic>$ul|6-tWoL15C_kVMc25@bmhm{zvzJeJUugF8HA#2kc~F&}XpyE()cqPxg%E9X<`80+)L zyc1*_Fc)Lob9AVeG)HIfM@tieRKK9^A`v4Fpr|mL{46G`{sAfpV2(ikX{j!2aTB4 z{+$cY`m!aCu(ugwI2*`>wbcLAPxTk}8mK0vVAL-xenF16gtGTpI5{#93BXQ98i1_3 z7-dokUlab!yHi<_NSdMk1Jw|ZTWx1B9%PwXq2=*F)G7Luq6Al_PTmvGs-e+u8JfUkeE=Mdr+4<9~;FjD?27BI3wh&zU+ELK${L z8x4@4GVg`!P(u*7x!9}vF|?}Jtm71R zMx--vhopmCMyWaOj)AUxWIXelE_^Tp3_zR=lMv#LxuIGL=8MOOY^LflSSP{8$);au>4El_>ps?07SBj(qlnE&01tkIA%;m2ORvIiB>A_rf zEx*Tn@v3~KJH}fxDEzpC^*7!3O%DVB7yy@$FOpx+&B+Dr$ZnxZG`qxWSU>#uOi1pY z_nACH2fnoPob^D#leCxXuB4~pTung!?%)U`#x{TL-oxf;?T-Nf~grq?8fJp(h8gz||->a$0q(?N_8_F$Nl3kWUPkV*EbVodUx7wmlhPabZJ z-x$AmQW9L0Hzdw!*o(EX+3W5buJ*6q)#~U|J*wC&B!iJdB#6+U$ zn#GkF9o=1tsi{N>eWxqf#h?a4iyYVxfng`V2w1t)16v*lNTgV!tSwH|Aq=|Gd6!^6 z!y1Bxv(5!yA?iwphD+QGSXc!X*U}9PGqzjGkp|t`$^n;RO;NpY2TM?*n|~Sx?v+B} z3W~HZMd{swNl*}ffrLortjMYjMGwPKVuCnh8U_5gl>(MMjiWJ4(o|sLnOeoZi>($z zc|@0|?Z0|hHAYyL=#1n*k6?4ju29ECQT zb{wwR`n$Jj?dLHXm2w`ld>MAxr<5#q|5g&m7|A+x@c+saE-E}VD_5fr#Pn>ug3*R6woxskQ4BnQIv=!2d-(z{N%u-r$tA$yX!6y0fttZqIr z#;+eOJU_6L0wyUq?vvK!P$lm^&&iuKyOuzc;kjrdH`IWT*5zP5L&7%+w{C*T6@`T=lSb5271m_mF|>u-9z?$ zj`ioCneze9zw&|avUXK(cHYR3h2UAsZzNocabxExa|YIw_O>JOZI7kwxweYVSguGC=I)8j_tcb@tr3nCs+GpS>%R z>Dhfq?Cdiyu#2}!{r}3_p3pAu#Q&AIowQi{(eqqFp*s#xyk3XNNnE3pbB?gvUq{*V zT;rT`j)`Ah$3;op62Ir1Q1iS^s^+<+*5{lud%aDYl6=XW&pG32f17p9`;z-J=lt`_ z+k6m-`~M+t|6VR6@u*bJz0z#|z1o=PQR|%h!|>(z`XGsCf7LfXY)&%RA9QsGvW$sQT%S8R1 zQA2Kg!f?l>X0!{4(*yyc z-|+Bpj&;eFr!X|;Qw->VXV*b)3N zDA3sGOr8Hp-T#%hAEuVjax0n`^plt%v+Q^f4wmW%`d%;qUIoFBk`{ju)H;dqNew?e zL3N>t`G4eXS>y*AcKmX1c8}{@4UnN;EfZP&s|c$AAUUvTk(d>XeXhpg+(b(JKl1jc z%rO^)nkE*WWhAUS=CUNt;{;T;Wk#x|H##-=+Xqg?W+w15rT+hxx0z@xD}&WPNq-_= zr{K;{274xxh%b|T`vNd)=DSRSXX9WWR1-}9A9)*-C#HZ{`jB5*1yJ{kyYP#O@2`C} zOAgszRF%zanRfyL9H0LqZ?6azs*%=yVSU3u{=SIXoQzkj-dV-YQLHAIi-!J=NfPr# z`MqLU&^9N4R5jj$p=jvU@ql2dTye z>Ho;v8gSWaVljcp0?YEEm{N;aEV?P0CreDHKS=~sD1F@M-PaV& zOE(pg%46=KoBO0&$ffrsQLwaCul7kVYE`GvL%*|C-w^wths$8p!(g)2VD`yik;`z^ z!*H|JaQDgZkjv=Q!|1Zr=;q1jp3C^j!}zWB-wDwe$ZZ1hG(l)HL3uU-bDLs&n&P*a z5|DBLTR01o)}xGXxGo6-kyw=sFB;P1wh7;M9;saLGVQc+O}@ zceGiL)_2)VMDPZHuw`9S$f+XZ&IAMrH-=k>efNrLuT!vYR(J!GBR%Dwd=mcdqSELZecl)#5Z>Fx>Ui~*PoE(8WOWFjhlA!A< zzBbZg5E1yy(EZo}Ew|y-Se6!E5LOIV0&h|u9ZBnu;cF6w{Po7xVB))zPNdgVyta&W zW=Cr699&R?Kp`7~vJ1yZlPWhlVZ;K`pe??0Cu$>`#%!9R^`^~oqm~AQP5hMYdnIm)8L67}3pWiVUbifU~Q$X_PLv^jE%&*^!dM@be!ES7aX=>iBZ z1mbz!W+K236b+`yLS;lw!}Q&D7UBDLvH6zJwkN3L6>vore9rPPd@GHLXWeTMc6LQQ zO%O$5AA7vR{Nd^=zF3y*8gofePI`-wg$;{OYo@+#D)?aIg` zg!h>@e+?kZOIeIRpcq}aboXM{_&cPLzq=!=Sb?dgwJ5NADCZnv5o%hcP+|#9b0Tq& zHp42X%MxB?@=5H&M1V{?Tjny{P!E>9nUEQ#Wg&Zh#wy?g$&VQ zNRZELIhM4|I<4B)4I%#RlThxEFzqo;1~izly7GgM_EH$oimo$l4C!#(tY}1sD^a0q zMe)BfQ%~PjC1zgnF@2DkZ;|zT>r5aZb$uD!M8KZ~OIt&}%=}9bQ&|P16FTKbX5-uS zE&|J&Z0hMdzaRe&yyt~!iVT0#ugQY%2}xVxMQFJlMo2uWa{Zev9TfaUv=7c+j#t0U zV8wf8jDU{28tO+cSHdr)Tzk+;9FcdLbmIqGzNh_YY4^ zTKKWT1VTnm>PXEOt*(3hrA+@x(y0a3@qWjsWp#r@;n=16zbhW`t*I>?p35TRCbK^L z1jJ49tSP$lIjD7D4N$5S$|u3pz}c6Y8MgP7`+7V)2yu0)V94oK-|FjFx}VBHSIUP! zkkg^CT(FanK9zw&iOvQ)oVcqNarv;sp{KW{k8nydpwHgH3+J9*5#jU{8ZTmXY{5_ETnO+V6t=`egF~y%UViU)+1|Nj`h;A+ zkQ>Eeg~I|0KO5OS{atP8w37QvjWu}e3U?AEe)2B3TYMTwjUOW>v<=!CR1zIv<;$m+ z8x6^cS0ti4lMNUZSUC=WuK|4t6kN_Xus7U0*%W~myI!`Z7$;_m58c~u{5UUYQO!_e z!NeXmUSX@^#qTv@705woN`h~FU?k6LH(iP9hkP6bsbzK_+3&pF?()?XLH~UFYWezc zdy`ApG9Ga5kna!~2|M}IW2BDpQvNSb*vDS(WZC7n6R-Nn@<)S#cAsraGsXDb6YPNV z-HETcOm}`E4Qw5@MgH~)d%NXdpFjon%1Fuz5NmNdOMcr6kHK4r(NB7+2SSnkFu2#| zzE`gs=J%xQ(+B)qcT)4+%3h%=a2_&=CT^I%0QDX;VH2L$^7>^S<2A}+xt*oLB&_Y2 zZ4_D4Of+v&-*Ww0hw^0=djI<_Y3d~DbGGT`e75ry0z6fa2(*cyU3e-bW&^%d))GB7 z8I_j!R2BQD9hbQIfiQk#_Y7GkO~QrHW5~+%tAAc7_&n^dYq$$Ka)qO%=%lRfHMvqG z-A1nJNar9q2R$l^TP?OO_oC>YkFQuLNq8oz?nx;}Y~Rq>)ZEhA*51+C)$POc?+++A zBs5IifBi*TM%GU}0>a|b^8ExU6|pAf$()H6_CucL-wC`4!?G1}ktV9X8(9yh>m=DD zSvyBGrpQxYleWs!2Qi1$e?1E3<46tCtl|Z$F$1LnLtx=N`N3wG)AQM?*h+lB%*8>b zcr+;wo6}3vG7~2930oDJsW4WxHGdn*q%~}9erSoBBS~}Y7>k(-0^5q>dMGTpO|XWf zvV6Egsecdu>9CSS29&>f3VAgh3naxP07W3IR9P1xXRWdd$*v--Lap}bfor9u?<)if zmC2j!a3~y&QmMw9{dg>%^b_XA{KSbWfH|`Q?!XX9% zq_5%EH6f~;`Ew~blM^(7E%vU{98ojF6eg_4{;9s0-ec$*W8MR+Uv{yIX*-dr@yBd3 z=`Fjl(kayKMe7V%!*Hj%yPRsy6U#}%4wW-%QdP#vGlg-0zfLTn@d^T=F?0%o;3zT* zf{{2j3PON#+)y6UB~ks^;lsG?+VzF zuTQq6ZYJ*|9eGtvDVc_dhaqWtJr=lda5J6K|IS=ZFZoPlIY9t5Y14Ki)t>?D`e_Oh zV#pd>PaVdX3ceAPm}k4^QQsTTY1g%W>T+3NYHWtfYne=Cl;`<9Y?kMLgCeLX2*J>+ zU`p+6Cg;U-J4A_LjxV5#j2$Y=BBC;pqp(Z!^<2QBYrx44PaU(VV77^dJEyW*^P2D` z(h#Lg6>hT2$xv6|464H8zQXl`L(29-}`cuUEep=;u&8SDL6HX zxK%Q68BL>fWy?-EhY9)Dvv;yPv@2xrcPS~kB~;h`RcViqkf(rtTI*o@V{&{H-d0xG zWERsB4|IJ_Dkalkcg@V$|HguO8{9~oCTpodi+RkqiPUI0SGCtIA^#_P&Npt`eq2r! zvlV*^Ho|F4|I2Rcx^W0`+lF~^ZxJE{5pXrC){&?gfx4KtM(wC6D`Z_7!_~sLippj!nqBqdlevv_NCu1p@1NYzC@ynnT+6)&&C=z5JR`dfM zzyw54@oBjOzNpj55>EGHDIEF4H0JDsyMjv)t7ZcLRp~mY6L8oUY>fyP>WWBj8;06i zCRXj3oXs?iPv%3eWD%$2E*B#4V`L#3Ucp*{t2lQmWh441>?Fr><{XcBo%d6C5fFak zaCUBTqIBgnyNnJXx{SR*b+zmthh;5HJca9X)ogPZ3Qw%>s-hFjSwcZfFDq}iluR&4 z(54{DeV*12+XQd~ED#OYdwd?Fs0_<3mH} z>g!YiBv@WDe=6?t9^loo?BhBbMCcUn0Yxl7p(O;Azli~X^tmD~umG%hv(;3BhkVtYqB$5LSpayi6Kp=hF)nE>#D2Zl<0GEn=Y<(&Lv zM&3$*?$gh#@UQ%|x4!MYd49|RKkWDa9P#x3-mU)~IK2;{fv$^P1fKvL4xlM{A%loU zY$1f#2e5B1gBVuH4e07620n5m&YT&6V~r|G$_eSFOF{5B+2Q^{zdxdcT_=#?S&ALl6?~#u&$I zb4r2DDI@OxN8Vn_<8G~Xyse6jMw4)1Nm&jl| zi0T(-LeC`w8fjJ$?)={4;kdHy-|QMq19&NP`WzkCZ-pr^}bDmx9Lkj()y`hRG?qmJ{wh_TjI~Ite?-t>r!K&9U0I9p~`>C=EaKE4?w7!CX*zedYI%BG+H zv+KQAjP?h7vNZDKn^9OBhH2wPUb4o^hZ}P#)Kw6vP%09h`|!Eg1)cO?UW&xMzAd54 zy%ot5X!Z97kG;Z8(%jXU#5Z$^>W&|515*R-^*7laLlbNRv%-v1iQe~02J6WiiLN+i zJs(D=ZqtA9#Q~yv)7zqGSNNh(uW*{!Nc>U6&>}{q+8^M37}U)OHU&1ieRFif z)xd$j#3E>yQAf@1yLy8)UFhgJ>a*$eKs-el@?tGv_0Sj4@AN$_p zX)rosI9?qFm|_T-W%wnGnEx|g&*zYMz>;@~%EaaYW+b5Cl?(|H?db+{M|C&Pe{_m( zWn7k=kmYb@f`nJr1e?Rm7=Be`BxN&m7#QQDPU-T9Hb%|Ash6%J(mT|~$ovT>ifZ99a`6mf$WKdoTS z8cZb)YtHv^1nW;leyi{mJ&?#udt^~2gO76QBAJ!x=%+g+{bqs1YB&MH@ibitflr_S z1GG>c=F;Zzj~Y?e&x6n`O6E@#igQtLoK&EaJrmcHuO*AkIns}RN!kLbu_}iI?Naam zs*ZvI*7xxl zg~QYyQi-rp^v-nhDq4gjItuzh2%Hg2uo2z)lVzON;6W(>w5fb92~V{(kBD5xzwsSF zUZ$2CD9^02Fe=XIgNH6iTWo*U z(1TkpNm0V1w}z@i%f07lvh55*X^M)bXaEvuU|DRT-BR*6c@hrMM^2L}sOl^uv7f6! zK4fMb{*zL{5~xrqm>+YHt@og+=Y`wPh1GQo#T2x4zv+qZ^Db5BI?ia^d=gb7Gbsku zLmtef3vI2)u{zemxe0=9u~j>{p-sW|xVd)icxvT)MpocB1;MM+lbNO|hK#0)GZE9Y zf|I;78>!nI0)vAuO!WzXjbtim>=oRL@`?iZ99wEHE&2t z6xD0asHkiezwQ9inWhT7v%sVhp^`PNLkySwN{iJ~LE|ohajm5v5ZLBUj@_~f%Z4#7 zY2Iul`{+*ECPRU5rXk>Cb4XGb8ea?Cvj6m&I3gFbn79P$5N$E4h0?M9wK=$BrPd^I z1E@3Uvzs=YEAM2vy=yvLkKWnu$_x3L41JQQEjN!TW&OVZa6pg0Ba{<9<}Jtl>A%LF z5zL*^CMdVi$rV{{OSRQrUgO`a4i6s?j&*T1P5Wn?nbZn#wDb14Ej^`%1Ru< zNdpS8btEc(ZcEGj;k1mdgpSq?yIl!?m0(Qk<6@1rh-THTslFzF`@+ySbuZ}-sJ`BC zK8dSa(yti%urZ)h1RNt=UhL^9PZzjrTdGO#W+*=dv4gS&wn`ybuHIKfs{75bs@z{$ zL2m+f%oxA$jaqCDPlFkHjH-}{<1Qg1b@6Nd!icWv${B!M0OCmWps zAYG^qTWm!gkN-y$LQkk;An9tV4kp?vr~C2{4~r?Y%7R|)uNS9E^`eeksGEmIF#tF( z@C9x;U)~cUA;;`q5i?E=TWFY$0?}-9&Wvsvlf}}$FZXee_Aue<1foLUs>mAh#vzM; zuox-}kqDg*VZq^B@&{#+aX_C(?1Bt%OtkE3-%|K9AI@t+hctCAo?I1%B_l%kf`pH7 zMAOM%?li^UX)@7B1smG$>@Y>ewz5a(lO&6W4Qj90n@m?|ay?~2IJMKU-bxzp zs$uuF)I9bQK2JxAv}HpBP4@vNuPtInH=NA#D}XW`t-@YZs!$igT74`v({sjpkGC*! zK4}YKKlM}Zs01Z*0sCcbqxEA3a0442i=6c^h;=UrumDs;ZT*}t$2ZazLpa4W?1;&D z0Cr&xmnPx1>*(u-QqdbP$>2i8XxcD2vu7jAt6!_m3YYbQYf*z&tpG+(!*Y1ldRA8Y z)Bk`HH$87Q6n76;3Gon3FhBc+DaSJ{ko3!pOna~;DYU1CkM?k@<=bISI}6SKVfe1j z@4_HUZ6Hq;bB-UCaV;o0JIA%oc!7GEe2R@F%31O?dwhJl`t1v__gDIP- z1z1GpVDzyjgaVA&2MKY3BD-x;C)M)!56N%27c6!2wT^2za<7wtW>yAxVJAJH_yLBs zXLt>(IEqVq_?YUt<7KSh!gY6eY%+lPaQFMUu4;bwpp&Sf*YQ5Z1q8SecsF_#3awxs za8G@DGk41UXf+1|J!tNf0)xuZXGSp>ps2$_DmJDq#gKh3u&RUlM-T2SgkDy((^>^+ zk$3inj-ADVmR^rLJ5~8D2zT0kxd4FugpT-dmN?_6{gj0K5m0*za=bd<#s5Cy9BMaq zUqJGP%YobJ6KonaBJ5+a11>@%B#{?+M&TS@j}k@0$&r(-5=kz!o|d*k?}cV4Z#wdW zyFIRx=-HDM{zZH~J-F!CW>zUSh-TK>U-sMIs>hQvlCXsNApSY!xGbPNn9pm=Bm51* zhvS>~65r}U(QQBl{LVW%14Zf76Vv=9eM)%{Em^;*N7E}%|CHu8FQj5TXbONI0cNH( zGna~pw647a04Kd$2P^~^=f%rD`^JgA`R;q>xq2qh;GON_K+l`s zB%+5Cf&g6NpoA8Rue}9(`lvc462OhT5JBln9N9daZ>0S`$Oy$QC^$*L2WOWRt$%uCY_kMQKy+ig^yDbiGK0X=lRk0;u#c86mMPD1s6l zEkus=5`qv-H-YZ}D`3Tt))wg-BUWw-iN;qt{d<%q8Ha_yl~6}h&@xaBy-`<8F;P|8 z)&|%vxeO1P)}{`>U87kLM7tE%ba$<_Qz1mU#Uy!|`3&A4_vN==fBywIV1WlFxJV+e zoKmYWukcbX0m4N1ETt|r$4<`Lgg?%6&fJ;F5rm3S5QrjT)2;I6fXuy z^*iTFIDNCGJq?=8KAm4)1?=CnZORh0VIz*w>HLo1ufI8ze27^&+B{du?}qMPdiArn z=MoEsJ$Bh=r@eODZx@)zw7%>`OGB9QeTdyhp4j9l&3bYui3Ksi;wFt(0!iq*1k!^(9TAzd%2$T4UN=Cro))3#d zzc`!_F*bQ&o;Gp0{_&4b0Uh$bnLq;)|w8=!EvMmDz5jcrGS(u;r}F}6e89P@hnLyQA42gR@(;9$#+$9fdHJFi;%^% zQ`ztmp@UQzf8k2e0g514)0XQZBEbnN6K7T{4lq-Q#AT4Ja#(mrgXBXURceQTZ-b={ z@AZ#&%#3W?9AL}9XE!NWt$c-Bm8N3kt^-C2j6FMLRt%%YnGj@z-2CS~ZRw634%3-_ z>t#$9cR0vd2vI~hn*|F8&76#Jfb_Z46Tv6UdGS+yS6H1HL30u`GVW$_L}^M@x>A<5 zw523a!61boJ>m(kkV;vBOdW|mgT;knOX4FDV5-wbLh+`FR3sMah!#eErE-n5-b_k{ zf|^WnpfZ_9Y{Vx{7LHO=_y7DS0XAUEVsUUo=kmu|5@b?R3St9HH01{U#LBpN)Mozc zXbxk+(OZ%OuCP&nJ{Pb{R#~w>hFFOMWN6SErjb8xr~rkoqg4lCA(_gG2{W6CSA#ZS zp!O);UKOCx_B8gD=_5~MPlQV&N`sZ)MCUD3O9T+r!DivSEoRN6*SrSStp?InTB|7q zR1(9s^-|z5HAOZD*fIeRXccmCX;pDnK!yld>lG#&-IM`yqw2h6v??SX9Cqrhf@O_w zdB|G$+4ZnYaH!@mBvH+I_CxNurV70)v~Mx@LvtuDrw%Gz(hf^SXN2fCAcO&zHrT-r zhH!)>{Gh71KzTz!SpQIm&^(cq;KIAp@FIDNiLa~xsd4eJ@<2?~NCE_?UnwymlZplD zbyx+6^wGdq>5GZ)E7g?2t1P(9(0q0CX72rSz3UD?Ch>#X}>7Xc?yCG zBca~)<^sq$19UcQeE&xZ3^cmYc-erX9lhSj-npc4{lW+Ctl;@X;yh%2fSWo!jU_7= znNlb)JBf_jIt}=?P(EP<<_V!B#~L61JZ5;ZT!02?Rni!U#(5;ITV^~Y47vU^uS2bl zOX+&bbFeF#Q~&LfTq`*Of@|}FdtJ(PpxM())(rul+-E8~uDoObH59rk-T!VV%wq<3 zISHESO;`1j6@UP~p(!|6Gr66af`A8{7$flx8qS7BbZZj|ldNjm(&Lphq<>9cMK2At z#znW5XH9J)7kmg~LN_SddTw1`;K_Y~HX}S9(iIIWh~jdDdEKl1HZlWUXyH$CfU4@DQpuLtydzanKny88cchw8$ zFCDm>DgU-R${dNCBpn`rxYhDi)kH3#FN&Btb3jmxS_sA6(~AlUtsNEJ1F00ii*nLC zei1RZ>$xod5(S;ZQY?u`ulP>uv%d_hZRqQX z_?MWzu(UnAdWGgci=&=e4)HudVib2bfLCI*{QyscJD!_&SF$ zG7U_G-wT2M`?&F&f;R9w?IJ-kfG;mo0>SA&1Q4)?ATQG}Gbs=~5JLz z5C1X*v3fld1T@Q=ER{&POUkzHGpr~gGLQIx+H*Al=(T4lfE-kSX~-`D5IIkvJRTCg z%X5g#W1n%lFY~)3POFD-lROdB4=mI|wL3ieDv&U810UqGEd;>@074KVre$D59pshI z5VNb|1o$uj3y{L*+J_+&nhtD&7*s;^GN3CfFanUoNpv9$AVEFLEGgu+wrfKaL__b| zvLqD5-zq4X=sUro8z%(6DtahcWQIUY!%!4Iu=|iqgaBMbLdBamb67dJX*uT$A}4S~ zZ*W8WW5#B5#%F}anozMFD-sW*MjlZy0JOhs+?T1M9U$a2fm=q}k^m3L0BYcs3I70r z2slTIdI@l2$L8Zf4#)s@L`T{ofO2fdilRq*bVrs-0Cn_71ALKl+(MKy#FjF&fqaa9 zR5F6>f(u~C9TCWV6ohb;fPZv7jeJLme8_Ccg$~e1U%W4Kyho(b$B(QDcvQ)6T*re8 z!isE3({RX>%q3QHsfC0|9SO*Kv`LzbM~Hk70|>{UjK-rx%A{1vrDV#cya{idoj!ER zsiaCyAeyQ?%B##ty2479vZ$^E%diy7u_Vi~G|RJ`5vY_MsYJ`RgvN6jN4JEqs(CO( zluMO58@ap7z2wWj^vk~l%)neDwTvCM6wJeHDcukt#7rs0gfP2gOxszBzyFHN%CyYO z#LUdp%)=y1*fGq_1kHk3iMR|++8NEqSW9ew9f3*&h6yR z?)1*@1kdmk&#AP=@ifo#M9=h8&-G-__H@tpgwObt&-tX!`n1pc#LxWH&;8`j{`AlP z1keB#&;cdT0yWSBMbHFQ&;@1C26fN}h0q9<&#(=rXu zp=dg7^aU-IKrY43s{B%)=#@39MBFK>ocKa!L`EZMfCrGrunV?V z2?r<(zY8j;9)yBE{nLmrR6(W851F#@n>NTQr%0RBd5f)&lQlkN)V&DQOuJNOz(K)@ zxl2>4Q^iSD4AnxNq*AR334j26sIm;GIdZJkRK1P-Fwnl^tiTk%j6S_To)yU50J77TGz%HQJ<|$W#04LRa)SfWQ;^tHVV& z$a-Z2NKCz55xE9{S%xFBR5d$$JwD*ugq08=h^@WBd5M{|1yQVptIEWKC8MN$2xzk} zF)=?-RJcF;w~GYEixu0krO&B~sg2QCfZ$kdeN&%+){t#fgu5B#<{8Q8C_t4M&ZXt>!t%oR4=!N>`;OaD@jam`nm{Q?R&0K$#I&!V@C zsyt(`0}C*@;Nv_IbiyYpx2C<^%>7%#1)oUcwh1^?$BQ%$NGxBVOSAG2aXBfMm?U-j zI;PcK22h36{VfHdL$X!ggTiR^fo_O2Roxkdx9k^6ks_oXhbuf9uw0&I% z+nqMv6+JogJl?%o23WjX@j0Bp82UqZFozz1_L;;H(vDChiT9SM?qUxnT>oH|oQ<{@t$JJEBLy zpd2Pt=Zsm>8Ji^56iA~zV-g6hyqYIozHboX5cc9P4hjbp85Wp8E)ZiBu&@SHi!(0c zhlx6?dpadxV~sTuHlAC%8DX_8VWDyXqcee+I>@as060E@oO;UX-6y{wfEw)6%pn^N zggb|5UrsoP!WFaepxi7>HQAE_cs(Bur~o&i;3e2RRgFRb8`M?l-#hJs#PU<}p;{ms zJTBzZ{lQgr(?eKofPh;p>G)IlSj@xq#FpcfUVH>yT|L^mhe)LWy`bUIVB|>4uaw={ z!0izd0XUt=V8;?!&U;);`zQV~z}ux;uK(O$aOSx15fE&~iEXxoNm9k)vYcl5x=RbU zCXQALnk-_g=N;ZNrtM;1sAP?}ev1p&mXh_;)J!U{Tb~+ui0<4I@qo}Ev@Yr-#1E;NyM;69NHit=0 zWXVu0G;F@|Kt9J}5mS3C`8h~@y`)rwVN|){WZZ*wap2col?JYedt$-FV+8M$L);=~ zS(ON@W&^RNSdk*TX9j@hqFF40+~@R>`Gq?kjtGH@CL#_do_0=G?T%krVW6?Yk8)}x zmKVkSVVt0AhMMXI*2`dWRdyDqQU3;8d9h%^79(KVh5ZTWUU-F+`)N9mM6ggk7enI&>}V0& z7~IBayFg_B^;7vX0Ct4#Vh}dyb_|h}G{AsvM$nCS9X4xeP>}Biy~^3ZU4F7h zF@W0Jfc_q>w$oyG5*TW#Lm!3-$5uGG#^3Yq>IsLLqkimiCgOAUoB*ibdG1|wuAz1g zA=XsvR08p@W|?Jb92U%{7XLRZQOa!Ywjb|~gj!|1D#JHy8|@>1QlJ{7Aeq~qqo<9L zZQEXwHioGv#~3DnPf}{)gb!vHn{z+r|pKRBZH|$6pd1yZzMfqf>5ymq?bR&g)zrG;!OrR!0WW@7Y^q)rbdWvu z*xOLXT)`5sOZGzhKB;>6I7;J^yCFND@5gZ2MoPg^M7wDid z*|yF$vH?~iVogsMDM|3RAdi3)$G&@6;z_42uSuCtG=L2mknHBsX^;HS-O-K#$o%h;HKH!6QXNn3$ezHgD~l0a+h>!kAS7GkE~|Jrw8z3V|Jm@%Y4mx zRj75%J1$UGCk-cit*7Dw0g#|KpN0o{{r28-$NL|B(~a4}Jt}U1m+Ro6qcHLNFE9A9 z8F*hveA>3{zyHT=GB5Ku-}6gyRBk2MHQqhX0-i zbhu}hw}$oSdOmFmxz7#FCoIP(Wgq|uhs2`sh)gP%%%<}RjY_A~s`ZM^YPa03_X`e- z$Ky+@_z32wxKS2V|GH)x~>m}amSN9Kr?uqRNYIAU<* zg6K8*R{!=8AX*^BFdBs*8n6(iA$l=nph`sXs$?;{xSxq?bC256v!L%N3ntO1BPF`YM6_Kr|2 zC{ks|X-V*HJE`h}kYFEE4us->W)l%YFBZM0DuWP&Isx!>phaATod<B*)M zD*ucKencb^;=zd$r?~`KjN~UII+ytT+47LXnmBdwgaTAXC;uj(TFu3*)~{AM280dv zXgk}te>ypE!2^Zi6e{Qjpsnd_fJ6weM4}Dqk_@csq zeq`NwBJSYHiy0nSKIF%T@I1?&;{mLIjcg2HQ341Zz}0$UT=1Mx;Jh~9HM|Y+Uw{J^ zILv_wu5=1?2YG;0g9P1#gmb?HHDY~RQ1@UG=2i3rQQA?H;yh%Lc+*o+u>&3iwfQDu zjWZs#pMlFo5L-dlakQdq{VmC4lTJPfWt37*NoAE*Uirlxd!W!lW=Awa2WVn~CjS}{ zq&ab>5NV>dhYMqlS>|YQmI0q8kz&$T*8? zP%YRByeh#4zu4Yb)Fwfj) zWt2H#0-l1{X(r7x$J_JGF6T@%3ORdu%VwJ4d5F((t|_yiU=ZpPrBPIqC@WxO+>J-# zLi(^v9y%3ckMA(yE3BPLv#M7Wb}ZAirXm4vs@idlEdcwdeNZN+^7lk?OQ`oY*@~hg zq{HmCOYXgVCnRihabx}D-e-46E*EbC{o7ZdXtsZn_Z;XD&p9 zf5UDu?S^mrg?gEyovy$5&i*ynw%;o{7Q`p8o$ppGv$8^ZDMy5B~=LfwhUWr~T=D zj2V>uinKczwGKmy@jd8THPmH3|1o$-I0wIfCeVWCpe(@?+5IPu%PzScf zJ#aY+pupwO5;s|)#0h2_T&{eBI1xvIz8lyo{Uc&viFo1l1% zkrgL|%7z(AV($LIL@F-OQCl2g7rj_Se~b+PQc)rnf!4&fwdi0(n4;Z=H^zOLh$6}h zhde|iJtI6X3oG2w^5*d>)roJAge;^X4~fV`Dzan-C;|TB2Q?@}GHH~F!4R>;rV0_#N zHxlk>ZlY2^6rZRpRt&KM z#FPTCDr)YGQuwA5zeJ)2II{^SWWqU-;4xihfpxabmO6{@EMH=;QmYg~8Icggbr$qm zwo`(OipPX{Ms$n`wE`W1brcfylN&=EC#U$R%sk4np%#$9t2npFl&W;4EN!VvUkXzp zXvvhBT*4)jU^F}7uLYz`WhZ?)hB_hb3!ogrV^I#hRCj7c-9mQgmzAa{X^K+Q6Na{fH- z6=Hc6l4^BqU1jA!LsA_ZAtAoo!s~}Hz_!|!*0L#dYolPuSzi%&N76eUHDw#j!riN! zIUKO?*jhaS$Cg8w6(?{xD@PtOQ>0pGEo39BK>r3T*r5x(orWMx1rx`(cXdqX7w@HB z#vZeciR~;K8H&R4{Mc|gX6TMf2;$EQ4?he(@@4^~l?F(8%2YncltHS-u1z(xpna~F zzYOLui+Ri~jT8QwDuwO-4-=Q%S!1YMQ>T8{Or8lN6Nom{B4D?adqcL3UciTjn5lG zd4xJ-6_ZQLR$F;<8iAcsF@*VyBF|$2ye6&iAlgn^4#08PL|2k|jO$erpx3^}9I%zF z=`7lHwhRP;*kHVD4)FTK0nx2SU@K%8ga6UkYP2A>nY~^Qx$~91y|#vzjKI}uxuGAr zFPZO+?|kcf-~4_O)Gz~Q^x_l_o75ls^2`J^cVf~_d}pS~kI&XUFQB2?;}O9Wk1qv}3Op=js&*ceR_Tw#|suir`f)gHcD0}IO9tHhgU9cpt?L*WqcK3pZz+|sG8q|BK2C}TiNf#D}M2e zZ@l9zp@KZ;6wO0;WS+@)`8cyLlmC{_!wNDlaEl{+%{*%Y!0*RrQ%*8ae=d}+9er%B zLebEKH?Z)&?x>N=9(HwOO@m$sVRzD~*t=IFcto6WnjhM54mh&zNa)~Uah7y>y(V8< z?L`dFs+bjUG9cZ4J7NC?JokO97LVI@+&^L6>L0e%6D{+>t5L@Ibyxa*jChUBUiw{3 zx)c*iS+vA;x8QB(0B&)|M*f#U07xU=w|4Cpb&`cCU8FCu(11^HPaP+39~V3hU~fD0 zD~~6FBS?ZJXo4_x32x#UGgW$#u_d438KMMwqa+1<5^-U&f-+cfWbjBnLwSzjaM!gZ zDWC#GD1m_Tf+WyLXGU@5_5Xug@Kj<@Yf{i7=kWjF0 za8P2kfQB+**SA~ep=}&chz9|8hgC4z1{@j?PxW_zO%i~@pl!$3L3GnMGl5tiI4(f+ zHP1I53ZYq1MMHP^0in1MX>?r2*NO*$iY}6if&_>zQ~(!1Bd)b*Qm`T$AORDQHr952 z?T3qxV{bpyi^)J>tTTsD?y=3Y;?LCQFcn)VUhc!yf(O2st=@Wl}y zBZ$EmY5;bE^=Oaxi2slISTdJDGX@8QPBVj4ut^d}di}VAV=$0DxCAd)gD~S{dcr4- z(S&|xajr)NH@AHc#wpB*1@35kz%_=o)M!2zd$$BD5wH>OmU8qq522=uDd}#9XL}hD zNICIoI`?X@wtILdDtd)Oy5TsGGl_8bbbJGIEEIoILJ}J(f42yd@-a@R!-|iCQn^Hh zcgU2s=!{QUe2*h_-pDBoBRm9B3VcJ9Tey{{Xp-ZEa@Giq&*XG$11o3wfK5=7CS;Zy zCwNV?fra;uKty-?D42sun1yMWXy8aQlL9EWd3(YF2&ZtH2Z2fzk&vl*Sdfr3^Mo!) zkah(0NA;I zT4{fyM+Oa!|6P#F?K{;xSL!rYNI0&b6n_)u(w1sDUb|gG#7{YN%20rfbSNc*++K zF;}JXrE;2ahdL!Dr(BkrT$oC!o64!3>ZzX!s`{3wY8s3v`4`veq}|paK^m7(f-H<0 zs@_Q)&V>NQ!j3LgtHlzjvC6Bx>Z`vBtif6(qbjUs5IP@#g*2j)#mcPB>a5QStaE`juHh=KjYq8GiV7J}0mImv=E|<^>aOn!ukk9c^Qsr+ zO8>9-im&;qulvfc{pzpo3L5_^umel51#7Sei?9i+unWtu4ePKE3$YO^u@g(N6>G5< zi?JE2u^Y>=9qX|l3$h_AvLj2fC2O)Li?S)JvMbB7E$gx`3$rmRvolMxHEXjsi?cba zvpdVPJ?pbS3$#Hiv_ng@MQgN2i?m6rv`fphP3yEz3$;-zwNp#ARcp0Zi?vy+wOh-z zUF)@93$|e^wqr}SWox!)i?(U2wrk6_ZR@sg3%7AAw{uIkb!)eGi??~Jw|mRCee1V> z3%G$RxPwc$g=@Hni@1raxQolUjqA9N3%QXixsyw|m20_|i@BMrxtq(mo$I-u3;(*I zE4rggx}|Hnr;EC&tGcVpx~=QFuM4}eE4#BxyR~b(w~M>EtGm0)yS?kXzYDyNfH;ltMtiwCZ!#(W7KMce{EW|@h#6@hxM~uWttpCJI%*0LX z#7_*xQ7pw%OvP1vNXJK)R;7K)_F*hK9&1<= z9`FC~K$;*<;^pVSsyvuSl%#~csLUzoboXligp_<&xPQVPyyc@q9%Ck(( zTw%>ohRuR@psL!(f=q?oIseMu+|Kk2%=SFb z?_5Ih{LR-K&AzZfRpZmW7qNM4zDS#kb5-XDqMs z{2Ki1LH>NtAT3KGJ`yT* z%{qM>Jgw1Uv(6rk(>^^q|J=_??Ryv<(oQYVG%eNXcFsLr(E1G1SuM^tEz@1S)yAyV zU`Nu^Jk~$W)#mKgMq$!noz!4W)<8YgaqZG#)Y4Q9*BFS;@Lbh|EY}IW)(fa!Yz$nC z8DG-GV&f!275#{L{1(8M#TyOC+=|p_jn``J*6UZ)#GKh|egD?zLeynu`z1@=hVZub8DPo*&$vo7m00B;hg?tSma12Nt zgw)@uOOj~d+R*_C?mAz?$ZfPy0A9_pFyH`HI0rrt7)~l0ZYUH^r4Rt&hZEu)Ug3*F zFA9$0g`?ul!Quf9NE7}5G2SsIP75{8;rEc^A1>fL?*HTP0OXP~hFFc@E^%Kr&Ey20 z56XYB|YTo0_jsO={jENCywW#9_N})>YQ%sFW%~)4(O|1%A4-#M!w^IF6I{K zWEC*#Urywme&o7N;xjJltPbp@j_Zg%>${HWJAvk_)#u8d=)*4JRbJw~{_6rT?XJG; zux{tYzU0u3>;us4y#DG)Ugz2V=~?dR?EdZQ{{QFCjvPYX>}XNs)t>9uzU~+9=9fO` z)XvVguIkPX@3!vl9iHz4Z|wyy?*A_9&(7=-4({)==JS5=v7YcyUhd}}?iN1o^`7eY zPVTiH>mM)UA9`7NpB2HZy;L#^ztC| z$?$9>!i?UH^yH%SWM3g>zd&8j@Lz9kMgJAwDfS;B_xOJH&=5swk1uQ=_HaM;S`gYhQI4?A8Uy(>vS(djn5E|KmP&ZaAcD2_I8h8ORxC51vo;lP~(& zMEaM%_#`O;yTbXZ5B99z_pguDm@oUZzbc&{`2(}}tsnQLzd)xi%&G5Jc2D@gR{XGU z`r!2Yw!cGB?>Bhw`hV~A$1nZZQvlST4QMa*YJc_GPcYnn9-BY?%P;=dPyW%Q`(+>d z$X4XF&;B6N{N-=_nZJKP@Ck4LfI=gnNEDfmq-IcL1Pn!wBCugH9mz|PkOeyf&@Cq^ zA{2#0BT~5qXr58Pvzweqe41WXJdfy|5vEenBdsAvLiY0JV*J>BeAg}HJYx)P znZox)W5Iq8Uq+N^T)A`U*0p;VZ(hA=t4jE4;J}|lTe$jDDFF)u4`sK?3qb&2 zH#$7!^!juG#)KV1-Zj~?Q_FY)Lse&33}TVjN+Xu+PjzCytOe&G z-1&2C-J@xTiaY@Igxtb-yEXtE^lYCMOy8!wB|_-kR9Yil={dUStF=$##>4>*1p#dc zh)81r(^2rkrN^XxJh@Z!b=W(GzZE+A#B0pB@~%T9y#e|=<~P`2k!?WI3bf88IrdXP zy%2_Ik3r5H)I&SsrmK!4^DInoK}F&V@WBwrn-Dn@&p>fQ|CST2ISe-&5kDCb6QA^*Iy%K{DlzybmaI!Mds4dB>-f`-jPMQ1)B@T&;a&ca5k_&kt@fsCb& zN@$p>#<&8k?~)H`MzKx9;~z23;$NrOrW;VR1cE#4xXqmVWVP)^t?j&{+8ar}c{U3( zrUKuZaJ;G8x&pp0UOcMC@oqqHR4<-$?6>=#HS?1<@7&Z8EC>}J&}5z3*H&yxkCY=tLtrFo{AGVG|R0yup2Na$6i@7t07mQuU318RX(wbmvC+)liI8 z)ZP=ba72{6E){inn7Q^al?t>aF- zr2zyO%gP8dfdZJ4WhOCzB(s76y5}6exKGK$D+Y1?BIkY>%G6HCs1TLon zC;toJ?3GD~3I%G}N@3Q4m{^3%bFxCGwS+7yr)t?KgQ=Fn7_yPhi zq3&8JKi6W;jHty?KBT8F?f1{MV4#+s)F(kJ%1wYSN2B7T=r*1C(U7j?oB8zTLtpAm zd0LaD9>r))<5|scR*I$(h3QRyYR~B{Gm#DusSssq#g^_=s5bTKKv&8ap7u;=P2Ffa zJs8!V3REroG-N-gc2%a%Q>9~dsaLT&)v%HjswqX4R?}KfgI=_)U{xnWOZwEj(*O07 z90lqKy9!u2h}EtR#p_vdy4R|@^PVw9YhbzRRGnIttrJyiPNhjIxgHj-~4 z?!$1hR_g_HvOukGTouaH#)|f`n7!{@|Etfj>{r13Eo_15c;J(wRKYLgC4()ySa(Jk zzZ5>4BrBWD0rxAvRSL0ZF-+h^Q1zh3`tZC)d<_n745^<5v4-o4$r!JgQ2#CViD321 z7HTF|Ogy%yY3HZomtUpPUZzoFOdVD66Hk0#-#;dO;XV zLfI)Q<7A4%+*?Z?vU|ymqOGA)TV5wxHWJfIu)Bu1Rwwf$cIzZ#eoV@T5#&j<)+c!_ z1OeUdx}Cf<^*%IIC@@?>SB|oovb**%lI3{Y$UxE*lo|;HfIBaU_5bo&28klGQf zw4-9{T~Z5*TH_Z~WN03jD4VS6E^I-fUgd zd&L>G_*_3e@Q^n%#7~knpCx|lq85DQATD{vPyTO*vv}tGu6ZpxzTL!hQ?sC4>c;IV z+0X#J!WjQ$%yE8nJ|n!3s^<93bDZ;hLwx9WW_s0sPFyBH-Omth_+PCa@PvbF;c{Gg zq-z=RiGNt-W$#W>vu^SRp4I7W7dn7dcK0os_2Or5dEY7dce(33>X1zgr^UYYtGB)G zG-o`@4~}@ZyIksa1$*RmK6#t#UFiy6eBu2)_MnFz?W50p%Kv@dS&KSaC`f^6!A~;Q!tw07Gd2ha@sg<@x|d0aNM#kI4c3hlM1dcOIZS$cX)5C$P|2IB@bu!#fRhy*hbtu`Q4sr(6kpMXt}qn~F%#wJF<3Ac!H@{kNELfg1-S+n_oxYHaSvJ17DE9T zl~ESOaFYh{7^9I1r!k1!a0{=o6oc^zv=M?BF$F^i896ag#4!es@EPk69d*$JtI-nK z2ysXT8`p3c$FUO2u^62(fU=PdEv*x+(Q~}8VE^Ee8Vish2d3vF5SvS}j|W)-Gl*BB}OifOn~DcCIX&dyAgpv(T=2ouDQX3UQ*bg3;`$NOGV z`93E7)T=%OgS}c(4q8zX5-OE`$sH=;d43X5R3cHfBE617#hT|GcmgT23nQ$FDa8!I zR_PO-B`SC2C{=+DaSb1^auBrADZ3ILszNF=DlDrYDX(%3vNF9~Of6x8Ey1!R;<7B~ z@+|98EbcN8_3|(5Ln+NN9nkW(1oJByuok9JE_0$O7jrEuQ~3(uQ%Io;Ba<#?qyH)s zFB2p9FjE@P5HkBxG8NM~-SH}j3&(l9C047@Tgl`AtF!8q?ITB?%!l#?WwQ#1jvIAilT_wsYnQaIrZH=mOT zqO&!r6G^~xIc;-1OA|V|( zln7Hp5A#I-Gcdz5JG-+m`O`!@lr|Z&IpecNOSCs%R5fF?K3kJVTU0hBG)HAr`P@=I z)ALAC)H-`&I2$xM4|FyYG)l4aKL0_KL4OoS7b`f2bV@07J9*Pa5EM9T(>`z0{c`82 zSP7s$G`=WuPKRyThSJHFPq4y^GKFol8s$!-g4n#ykw{XOq=|)YhEIUwYPQ3Cpb_grrsW6Q} zRxJTlfq|KVZxsBl13g4UFcaxW6+LPdM06!pV`No%l^@!J0e*E>6K`7#3Lv3{@!Zjh3 z05d?$zcpLS6<&Q~UT*~gr1f6eHDCY7bzhscUt0cT4u(%aLh)iicmRToOXfIY|jn-k1 z_GF=wX}4BuTUKYKmR1{9GC9^}$F^vrR#tnKSV>kN)V67dwr<__Xk*q?8NgU^l}*={ zW!u&{VRmZK_E>WjWEV8w%9e1?_FGF9aRFCx$(CzN!f`WYUFnt!epX`fwrlm)aQ)U= zIX7%eHE#>|aUC{tTY+@7)pP$X*K|`iZ6DSTT9;o3_jE58elnMKS(j!z7k57wbukxm z|JHYx)^#QKX-9S<57&4%_jgMd8=Mwrp_X`6_j%dCb`h2#DfJ@*Ab23kQ90({aMIQS z)=q87LO`h_nJ>9IW-}%!YCWk9>=kyf* zSL_*pna1EUKq~+53$gA~HE4qBp}i+v=Oo^kS$>5qjg>Yv#- ztZ-RSJDQY}c%yd;gj>s&q1i2%7^O2hYD?OL;Ubw;x-DF_CZ8FTi%+CYlZ0VffKD3K za9X2v8mLK2s9&0>;ewNgxu$pes8e{Qow}Ma8l9zDrc;`Ki~5IwvKcbAXnKzu+Uj^MPu1?7 z1L?c;Eq(ualVVd_PrthPItP9;a-Ge0d@ovo7aD(f6FdcypZ&LcWpdFZ64u1mw5f{> zLZGREff@*SDwA8e?^(A2yRS97v59XhI$N5vo3kt1v*Y@^8T+!eJF_8MynC9wZ+fqf zb+p}Ltwo!oKl>-!o4aiozRh}Wy?V3N8@u!SuP3{RNjtP98?)VbCiR=If4aZR`@h*c zz4=?M72LXe=DxeQrN?{11N^}EJHa_zt|`005vju!nZP5wl+oL}{aeKyoW&*F#UUBI zyF12H9L7o9yi4}IA^gR6{KGZ8#f{o~zd5-DnGhldOI7;!gA8)U@+?B&z%C&N*`i z+M6ceS6zi?U2kxm&u?AO zuR_J<+W_+qZrHlKrrn zz0kG&(ZOBP#l737eE_^2+PU4*V}0Gn9o^Oa8mb-M+1 zz1smE*_WN)3E<%QJ>iF4_uAC7$=&q6VxA*r##@Gcw;Q%YQnuYWfg3fR&w8Heyq|g7 z_P!jrKORHJyeJKT1B~Y4g}bc(*;7%1d&F%nIzr@gUgvk7=X>7ge;(+AUg(FO=!@Ry zj~?lhUg?*f>6_l^pC0O?Uh1cw>Z{)BuO91{zTxTAw26BTstM$atz(E)wxHa$QC{QE zV#>q**yOiQvpg???WDf5g|zvmWsiU-1{8 z@f+XqA0P4~U-BoP@+;r+FCX(WU-LJg^C^GpG2Spf<1>idx%CgZyIi&r!o1o^9y;==t72f7$1p#P|2j)w#rVqVt>I`JW&9 zqhI=`pZcrc`mZ1RvtRqSpZmMN=|A7Ih9>o6w|nn-{0HBAn>$Y*ykeH$UY)x8nARH0{Mg(zqFb0xN`~E**px_{3q2VE7qT(WBqvIoFq~s)JrR61NrsgJRr{^bV zsOTtZsp)AS1QT3V7gL*4+UpR+F!iY604+0zqJHu$Yjp5$ylm>J^sADo9eW z0ntRT!$CrULol0G71ID>ctC?dtkL<_h58s1l2d_r5(WI1)#FB(Qw04U9cOp09r-uK z-w{3mQ88@j@FB#A5+_ouXz?P(j2bs`?C9|$$dDpOT2jR%gPT>=`Zc5F&B2B>)Qb2@ zVxU((K*e6-Ts4jtn4mpZ3FQ)Pn#v^myfOWjrCbwoEv=m#lfm7Vd-4nlNg(vVkOU^p z2>Re5!9lG^&Be2Mbpf+FdxVmm;#FN+Tp89)SxE9P;J|_h6E1A{Fyh3D7c*|`_%URU zV_8kmC=p;JS*xJtCjDi+26W3`bGL`>ZISB_Q$TEkn z9){q-p@oS+LHJ5u3AY-lOtiw%2Cg&^G6^9_XlSi5af*= z4>s7`5X8~;*>cv?5cB$QD~IVF`ZjDyyx!`YNok$~r5pwc1Lbc*b#%s3vc2 zmca!R{8q|qmZ+%21s-rvC@D)xa0mhu0IMvTcUa(_aRo5T$+gpJdn>r%iaRd3<(hjg zy6LLBE`<`7;v!2vVRGz5I5EmDzWM6AFTefz`!B!&3krdY_C9zGU#(sd;K-oVT(OB z*=3s@w7V&f2NO=xVZBMjYD2IF1CzpgHr{#by*J-|`~5fIfeSu3;e{K1IO2&bzBuEJ zJN`K2kxM=~<&|50Ip&#bzB%Wed;U4-p^H8`>7|=~I_jybzB=ozyZ$=tvCBR??X}x} zJMOvbzB})|`~Exd!3#e;@x>c|Jo3pazdZBJJO4cN(Mvx)_0?N{J@(mazdiTed;dN7 z;fp^$`Q@8`KKkjazdrlzyZ=7?@ykCy{q@^_KmPgazd!%|`~N=x11P`&60m>uz?PIAOs^Q!3k2Zf)>0W1~aI^4RZgmgC6`K2tz2s5t6WkCOjbuQ>elfvap3N zd?5^DD8m`ju!c6gAr5n>!yWRlhd%rv5Q8YhAri5OMm!=Clc>ZcGO>wHd?FO1D8(sK zv5Ho_A{Mi##VvBNi(dR97{e&WF_N*2W;`Pr)2PNZvayYBd?Os=D91U{v5t1UBOddp z$360~kAD0kAOk7LK@zf%hCC!96RF5WGP045d?X|zDalDvvXYj(BqlSd$xU*ylb-w} zC_^dAQIfKhraUDoQ>n^Tva*$~d?hSnDa%>XvX-{IB`$NR%U$xam%jWZFoP+~VG^^L z#ylo6lc~&QGP9Y^d?qxbDa~n8vzq_bye2lYsm*P2vzy-hCOE?>&T*2noaQ_yI@77n zbt<9`n zo^tA^h*0c%vtWVvqDBtag>Pq&b_R!~Hwzfutdk<~hO#b3CL5?g2qG*jM}*c83l{Af zPfCN7rgjoBfbA$gTw8z&R1vx@?^Da$w%^M4MZ--3ZX`97s(K>1SwUzcpbH8eYXl)Q zK2eLd5Z?yGWEDSVS%d!=9EePfcfLVpjuqxM6+&oNvxGQogRE>5y70HU01%CY;yWQN zV0l>g-4J8Fqnb3=mooYt%7evh<~M(K3dN|vBEYPw9Jg#SOXzbXDqM&QV;GO9t*3`U zED90Flq0t_F?=QvU+fZLV&)BN*IW!`1z-SP1n}|5D7}`qE`rCNFf~TrFj1GB>a2iT zlNCH&rkt{DMcNQT2Qo!!fT%+Q9Egrpj9jg{;>ZbDRzO)hAQM^(;;}x}vLNol0AH^J z0lIygQXB-Q8eOQ^T}jW2LFMK%k6R8+eE>tRH7Wn%xI@|oKrY^$lH&SI3v1Z*A3CaF zKqtn=ln^wP`Cb2LIUbr4j4s8&Nla5nJ1X1uh4HHwvWiZ+&m13%jFe;ldCi0?@j==<;`sOhxdI9YXE$85|3%sKc4lLHreaR+zaZ+Tg^<* zE!8~^p~e5`xg@#w15S3nb|luI#0hY%KVzWPDZCV=r&TEcWKaViB>xOHz`+gtSp(CK zA>ns8K5cWb10Aed5Z<@y^>;v8@#A2_S)K%UeSVz>UpuMRa=II{iErL%;-~!a%wlUsLIG(%pT$7MoBCG)pZhJ0&Zbyp!=As7@Ez$w9CRTgBO*M{nZ*(gUkVi;P07Fuyp;PfA0l#C zJ&9O9HJTF`V)OBU2TsBHbzk$rfb!X2++{)dz0?iV9}&Rc!tuaM>A)h~Uw8qEE}&fO zEnd9s9sq0}k_q1;sE7fCloWoMk0@8P+(_Pag)Yj*6Y3NTvKoCQ!r-}Ae?b{9zC<7j zLhtE;2fP~1i5n8O83jrn(KTKkxL}iHo-+PkLdjbX8l%-w-gbT9{xx0xabpoG73;N` z+3_Dcz7>nW!Y(o)7)oOlY+W!YS<>}m>3JcYd|g;DnN+P_Q~ci`a6?l-KqU&n8>0VL zA#e>_)FE&sBhT&O6skr8w4orH5FwhPKg~e)(UT|+L2I!VJtZO*9NJ8YUrLtP6i6aH zp=3TSpCxvpCqAP3(Gw~n!X;+GNuA^;I$siyA`bA=4Fu&&DuOB+!cB=+ z;TwcoZ=hU?oZ%V9SG`$-aTv!nPN2}pnO)4rgZaq>7NZG!PVN&fQl9{GI*1@QhNW8S8of*VOoh6%?g>GViEeii*n9-(z zr~?FuK$$pXpwOjkOr!+pW_0u>c3jGHbXBNWnqEp76J%p@rA^{rXHw*=y&^*ngGT+F8%~~7EF=Mxi4VpLWTIm)u$fr22>^`6k?jO> zzKEBx;|63L2+$(o9b*^dVM4Y^w=n>%?N-|)R=1FCNRl``Rj|xRu-Bh0P<7TEEc;Q-nLEBZNp*kYn zTKuL?S(XwM*F=uz2od7jnWs>Wfrc@ne70v)E@i3OXMMh(Pe$UWIsqx7=OF}WBx=F> z*?_CwC#k+urUt@PwpZO{r#+t6FtCV~$ta#2>W0q9&C#QB$)W&^A(UC%4wfa0?&)D( z$Y=h+;I-(9h6->2s);72krF1#QE8fFYj(|J3nHmWd{sr_96P4rr{M&f-KCe_B|?H8 z6#ycvHQYIdiIWZi9UNw51{ib7phC{$g@EOys@%$y1F{xFv6BBD7Oa7!F4=P0MC}c% z0;-mJp}fsuMURsHSQb z9NJJOK~M&*dammF;Z{#}>Q5>y5rnEC$SNK%<*oW`(^73A^eQ2&W?0Z$utJ%Ou+^{_ z>s?T0SUyaak}MS}0yF zA;j&G!hwTzneDwqXbyt0K0?1?ZOBI?T9zD|ZoT79ijj0vi63E+Ry(utFi%MGEFlYGa|=8$4k-FsvXr z6z<0E7BsIR@XY>}u-vBL@AWr z=*_J1Dp&vRpaOGiWi+R797hAcW4{tCAlwz%&M)RwKyu_rixFlgr=AZFq?&B$%)T&S zE~@4YZy;>lo}n;jPRIlo9a~f>#d;$o^eLbb7?cI?+oEzma2Z^tYc>vXr`WC^a3KXd ztG^lK0?*<+a`WNZEYG+yBm8S^3h5~)f&inVmqrV)Ojq#9>=%a>2=r!JiZKb1F&4C{ zQBrTNrtcf0@6W2_9K*2_%w!(tu}La)9h3iWB${LBzjdFT?vbpK-kg(%pBEkme0WLG_kQJFS zNZn3cY~ivi7Z>jV>*%o1Y2oz&7Xw`dqjZUWG!bL)SZ zNXZfI*kBKnsB#13-diBCc*UL{BYf@A0boF(-1goI*j} zprMTtrXj0^#1`bS#sX^(@d_IDke>gw|N5ftDsykuZOHi)ok;Oq93ye72DlPw+4;0D ztD_X3E*oSrx?Tu0bL0Oaaq@m8bO(2KcWyd1LRh2RQlPTA3NKU8p29{oEe8O_E#1YI z!(07y7TjQ%07<7@9X9tZdQhFk2H|3(<2~E5ZyDRWqRJp>EzeyrVP~y|q8%eq-yti?I4H8BNHa_)r=Y5`G*j+(Bd^H}-94rtQj2pqZ=~mfHz+?Tr*#0uQ8!N; zNRg+4T!XPGSH=c@icf*%vqArY;nJ;p=QxH!*^~p{bMrwisx5t2sbDqW2bf5>0yiYc zw>skP)`{dkYr&j%g|?L$o4U2%9>N1>s0u2%F`q#_zaX7(UI8}6fa3uKE; z9d^}UOQ>+GtBPKL)h%~m4?%M) zdKjxY-_|lPr)N^v@=crj4z?(6>%mlmb)EKbxc}gh%N`59vm`_*GI|*mV^&txTZHcn1dQS$N53VV&K_WQ~(NLOftoWtWT-Kt@GyBK?< zT=&R#F-vP~;_YL|VL2s8-N$}=mV;=rmMo3~xa|pGpdo2cyE~u5b1#Q?B9!PcUwm*Q zYfrbCZm087X|e4sZ!veXlkT%;!3bV!{D5hw6xQyO;+vCK_}jX@9>gNr1A?UmqEBTj zr<*jOEIkGXuhh?DX}&$le-Nj}WQj*MdlEs6U$m!g_N=eGRUx$7jdtl<^v;|1jPv}8 z6MJE+J8XBYn?C>U(J{H?;~3IwIki7MP+@#bbMm&c-q;2sn}ECKUu4&d`|}S!adkO` zA?f3jy%YOAyDh$=8avd_Y$iXu%4~TxFVWYP0@jrRIi*V zgB|8@n~I(cvPU*;w61o6Z_X3cJ9JvN3ET zs{kk@47LBfv$;Zvs|+SGqz0q1mcB5 zLO@(GLY81-Y-eoeFe)uXVk7;j*- z_SKHXZs8>oAvDVS2jlWT=0Q)Mk5&T2J@NV|$-uDS65=JKhM3AhA>^{piTX6^1C2cp zBub~|;)0GsCk!x%Ev!DYM7sx;0Kq&Ghv<(ArFI+Qx$07kK#zGeRK&so{hDh$niLQ~ zkCIvh5j;WU+V2o=@MDR+3f1F^!YFN$(MqV^V=BV+E-a!uCb_#pip%q|bgE>^&#o5CFitrBxeBg6A9 zN_M6Q%vLI*EXzd)CP;`dXIG^XfTJ3)5l4%##b?Mwob*rCAt=ZqSDp~;)ejG&HlO!$mloE3BJt0DuQOF|0vPBV{5V!52MACCBH-T2 z*0(MH(d1bO16Bk<_njnoARtN!!Nq!~6qQ}95oh{9@5ExX;hiHO8BqVq##**H0Vqu% zB$L%1N;d_vXwG{A(H+!Mh_nfQ;Q<;TqW+@x4GzeT12S_EMyQv>4lJ=q3rGa*Kp;Ei z@Q-QSQbd#>kVFog@FPSE7!srCq>t!jdjTY(2GnS>3@XtcVocVVzII1E=24G(6xI!kfY=L@Q*)*fb)rjFWl^RuQJOfF@Q3`Hhm?Wo!gUJ{$@{x`d zLaCm?865)1hW_&&y`Trj5c-UjL}N|{{r5_0vC?zJ0v-?nAiOQ&FKF%RTmz+Y6jo~Q zX2xt!%7Riq?;$9B=cC#nX7n*#f`cLXgP<<8SgUVVFquqZ12e4XbAa7}!^YgYNxP93WWNSMY{rZc5!O>KJ9#|TmY zz^T;b%!bKC&Mk181c2N$S;!sysWyc4q!T*XQ>6+jlfhsrDjWGJV2Dx(mHQ-dPD#o` zC}&cuoTn@Mr_G;$kSgQ4)jLWWf`R7JogNFGstTaioV`k|Y-N@(4OSv%USzLn&1cWd z$0YPIR19;iAwWa=yNjkXh~Wh1I0^eXBTC4g3J8JC+9Ch7&$@H6f9;@Fm65}n~1eM z^ZN?p=iE}03lQL7y}?}=0NrdPe|Wp8`k3rL*`!54NLD`d`Q z1Hwt7aA=c3TodDqP)1cUnWHHIIBuhh3uMkwC&A zt%XF0bsHuhFQUaRE|HC=8wnM2K(>rA@`78Yx(ojV`4I_#F_Yi>WiW?X%wr~VnR9AY zGrO0-X_i)b+00&yH1^GNrbHQeY_&K~CNRUCOnm74XFvy9(1Rv)p$#pW_#RrOYgROM z+^lGre#FX1%2IYhVXk*uy5asa0LS{>t$uZ^XI<-C=X%$@{&ld2UF>5gd)du?cC@Em?Q3Uy+ui=Uwl6=X>A%{&&C!Uhsn_eBlj$c*G}O@r!4C;~oEa$RE8=0!|H4 z7g1G1o;j#nb2uLPLeDpYV53}gFR#)E!?u1-P1c_Kv@^37uYX1LbM}TlBY*pvPyQ5^ zR|Muuqj{uqzTb{0zyn%XGd|!pScw0qE}&_<>-L&nXsBm!`_h!_7lU`w@-Qv~s5j*s z8NWau$m}6NEdGCo@k9`4SUaNReGEXG5XP%ObZl>k;#aEyB0z^87HE*~yRG*lKZQbx zTeA%58#n{gJ~@#+lbQtP(!U2dGS%WI#i+fjF@OlDmI^4p`MUu01D*DxgAm*Rmp zt(t^QoQ!?Enogv#BKZJn12O8kttjBej)Fu-Fh{HTwCxkKA!I^?#J`EmgboX>2AH3p z0TPEn0AwPqTx6I7$cg_BV4Y4InJ93hYM}$-IT9|T1|lOJ1+bV7hz^h>g!7<)p+QC@ zauou|A`p0s>X0XxDWjPjE7_Yzd1;->@GpCFG}QUSJe;vwiO3YI$Uabt4Un=22_tb_ zBc;Hxa>Pojj1!|&7mZ;@20$IQGOz+-0%Vdi0_dUd$Vds0%6rU9%E(7a=*P(L%c}WD zLC~F>><~F9A+l_OQgp?{w2YznwpOeJV5*F-=%5!AzaRMdi3jY$;55S~?KJ%*Na;Q0qZ&bgfZj zz8mVE1~?Y6G{67gq!SBit=uff5;>6(dqsUxigYM|*!Yi!)JyP048Dv6zmD=^G+e7$_KdurzDasdrkEuirqXt{>&o~m6%D8Jsfifj}ZbE zJ;@)^%$+QC&=k z?hC+n=|2DX^G}p?Cya?GS5ZG9A~NUrfc2Bj1;_(6WyZ3QfHl~i?@T^%U?DlkNI|_H z>zh81C`kY`)aj@{1Drq};YJPAKQ@ieiNsLFR2BW~0zwP~c2OQ&^cNsJ(-A^MCq$U! z{H28`Bip2xj8RBU_>YHLQVh@?yBx7gtH}%M88N-d?CP0Bg})A^F+6>uKIFg=q|<%y zK7`Q5RC-i&nJaFQ(?+boM`)=+yr2>F!nq2{l{!NaIfw(mgSDh018CNS@Bm^AREhK@ zvAWf)_}3*EispQn5(R{0-HCc-*2A>WUm{eR*uQM0QLqyv!KoW3;L+W?g{J~5vuTvg z+##_^PDk-P^dkc z`k5GK>cZ^$L~=BQ&4R=PAX_8>1dDY|y*YhXTSZ*e zOG7=SN2F!f<)ase-CX<8TwrlqfCAS(kc-4MC|`P7Zlc(Vot(Y_SwB$)w$Yo7&4K^f zuu|iNSthvH86X=+kCmx75?chJ`zwT1{Z;;CTf^PmxiznP zfjt=>#iO9meoEaU^Adsml40uGrM=tEoZSQ6S{{i2+$l{0t#E>wHP$0G z)(W=ZxLuwKKCPr7V5RA%XFLzgeb3P41qA+&66)UslLsY!WByI!O6^7kHCq3cjF8ej z;5W7qDD>Z8++#Rx-w8g*{o~9fD5w2XVU2>sT+Q6W(xM+8$1@IuDW0Aru;P{ao+uXH z;mte%$PG|o6g)X3tO^{EwX#dfQl6?LE!Bfn-VL5Y-$OvzA?Se|eJ&X=-%+@#>K&?) zO$3=`MRAy&DvkgXRuA|sgemr{1F%lB>P&a_P(45%AMwbbz)pPeO5&qF1WDFO$-iyN zTJgb`dHF<}>5f?a7Gnb5Ikwco1&Cehi0CNG&aoKfuuOAJNG_=uMkph}mE^RIk6hSv4m5Jo=RLRd|h<|2pQPl8rw1#lIoc1VN0+fn6IPOdtp3gw@2 z20j_($`b`#R$fmTI3@XeV-l8U!kZoq)4TYF=0G5Zw+q;d2IYKQRCR-pn4;KdM00C`y z(x2%Z>vle2;|!tZLj?Q0VE$}uA?O?ftrfE-1SXyuV`jq!sMP;sd4O>#uIqq=6P*M@ zM$tjFO`Ij2UY!mDj#n271KLT^mkO@z?g-MVE^J||k|0eV?8fn~rOl3>%g$$~tHyC8-Q=dMCXrPYrBW7?g5oNX+$_IcU%@dd$AsM;Vvrsq z(2baU;EN}JPVFd6(%D5DR3a+_4HS zmY&=~H*_zDE()q*F)s|;W$FsLb%5T3@0Nrm1l@l=8r!rpOI0H>DND+J+nZ<(%#}SW zi+G7Sb;9ssOVw{{;_rY5U96CL_X!`0|BB5V`*T(|mN7v$C<`dArZ z)!$Yrgx+D#d2&m`!V%BR_Z<3skcc;;d227b&F}$ZNcSRlYafMyNLeJy({`r zcK3tIClvg$K}QmD9&d5u$Z=&Z@CdKaBG7kAj*T z5lh@MqDn>}+--|Pw|M9ie2!Fg!+MX`>24wk?qIE^NM_Zv|N9)V--t5e)D-^G7nzcf zViExYLSQ%n7>~%La;c0A0|TaXDpe4vO2l)4O?I~3aPt|cC=-lfV;rD(pB=H>s2y_W zfXYxa)m)8eLr4@u7e@tPOipEZOG1T44o?x0YK>TF4OV1DifL*Nns8x@P?vpwMwUl} zZAp5mr?RuOwYImoxw^Z&y}rM|!NSADOEoYsI8w+kHdH;xGE_9oOfkqaN!Q3yGR)G} z*jP5qG3Mv!JmR|B*M)!Fs{w_NAVZ3xge1_D3K!xCn_!TNfl*k#46#rJ5};K^`~(1jq~$4u z6$vJ!asj6@}^&uV)PLo^&C2A*BDI6vV>*`?2 z*ar}|T0C1|EP`K~_z2U;l*vUi021igKp>}2peHwM+Tajy#iujHLQlVY%G z&MgU-|Mo5i;Z7q`?=t%!kSSi4UUkN0Xm9RwTa{vfnVa5_0HgnCKjZmBtkIFsuqj%Z;BAghH4A;ec zyDed3X21cU6Hhhb_S0lYR7Mn*w;WmCXHs&BBW*%X_2rOgw#5~gVzf7fU})C2BLMTk zwU(IODS0LXP1)fjp0=zA-Y07M#U?9Gr8Oi3aLO1JWC9lD9Tr`2cNqsz4icYdj#had zEeCAkqXQBx(Ic8bvS$COYkKmM!JM@`DTi@$glZ)&^I4Wek~enONpO(fLMtB0p|YMT z@IBWGs-p%MPg18|&Bi@UR4KE@=O}`@Qi$sb1oQO=kyOi?l z8>u){YM*A((wG#hu@P;UopKzq5@RtdXp0n-MMeYF=C-XPu+o|1CvMy{DWb;$kTZLD zYB`0tC)>>DnYyAJrOE|JAr;Xs&%BG#U*%lD1EP3Tn5`?9xhgQ$Nippm+yd$K4#dpNY!^{&nfB{uru{CEOL7o>+Rtrp*(rrv%IY8UT@!y^RsC;^xo2j zF^(RmKZ0nY^VPV16(9>#!`R=JJ<#PN$_U>X(1IVrNZBFut=TRAO(eYodhn>G^{b;5~X++ zQO#~_z3YV$YlFlif(}`;lgmqZheqXyD~(MHo#&2FEGbMWQ$UN|B2q}HG#bTnaiQYX zCX+i;NfBsgyu=-S20WDM&X1CCo;~jn0O2xmC%Sm+W-NlAC!v0z1WUzsW_Z zBmd$dFU&*Dpke%fMna;RhBURT-AnRbYN6zSa1 zc}*21MI*q47S*EKCMV|fyA#B!f)~u-20OTUTD9I0E`){ZiE^v?+FlaMA=k5>4_~0H z*eMJ^gA&s~4%lmJ`gRqf40qT>xvc9gm}s}l5V#97ENpf!hgmFiG_v$da3i$kUR)VB zu>k<`j8Z0CMn2K9v_MRNb;ezwJv1W|$TAM6@K`Ie%BcdD0t7L^MqqSw%fkOeZSbhI zOwNWu%M;jde8b7wV}EUr7h1&_5^kW&uby}cDPCnKkDv~> zjun@VAS;D(Am>Jy@|0po>xIkB?G~%=taFXAv{OCMU)xw0Gd!p$_y;oA3SblDqMMsf zagj%McAqK+K(qZ zD?hYi!OJ+_2$L~my~FMq2Am3mEjEqJBfv;kdC?AtJAW%6dju#jTON>oj~zR-F+XrI z>{gGC9X`cwzD#}wCnxJH?pZd~$bEfcLopOEk{Rd zn}TwdmUlyzdrPo;Bo`I82Qtw_JPsITbmw+#CI>xoWkKX=Gj+GbmUFR%sS-ddr1yez$~7*o02# zJb;%BCRQS>Bm?MR4vBY#*LDQiR!ff8FE$_pR+s~Whj@v{1868>-?lJfNCPp@ZI(A~ zao7de(*rYrhCHB#o(G0)xC0jhSMUaIxqu=@LOEHM0b~EMeKn?fq;UWoFfG`Xgi0uL zAD{u2r~x`AcM5QT2jGa0LqwJ00R_+jjyNrfI8zAcdtFdls4f-d>g1!qf#Fe zFhk080ag)1(g6}hM?4@HU8+WL>PIS`C@M&p1){P65YUVVB^6~L0i!qp5ukCrw@~6W zGmj(*4)6dVAv2(69xkH*9EW5XNM+e*1!#sy#z>7&^+-V2j**0eg`-%rLj|zNWpEUM z0wIbZa4I2%6euwrym(|N69UT@fIop1S1~ON`4Oh|M$y=b31kJvWQ2CNfQDF#B{+f; zNs1C+iuJgO96*r@#3fM0iRcw|xsZ-D=W1jC0p9;;iRDO(Cvl0GXj0zcj0lMfNivc1 zs5!!si2`_&mzaqU`H35l8ZMZTp8}02F^$zYi^T{6?)Z|Q`Gpt~16?>_ff#s$SZ%$)gTX=(t%!XPPGXBYv}9Xp()nAPQ2~ zab~$_aWaz4DQ5I&MlQ3Ck~k#6MkmJkBQ^hNllC|{;q{Wju~L+QDg!s3vpHzzSd3z{ zo6yOfwMl%50Bk^$ztd9@mkX>3IlU%xt*J8H>2r29Ib>;k#WP&bNd@RhmHas*BKUQ_ zAW39!nlE>cW=DKknVT(%D*b1U__q+ul6=uOUY8S{`cs4wMLMVwiFTQyD!QU9dJECR zFY=&=fVr5Z)Odm^Fp4=3J0M}xke7)EnHX{dI>4iq_n6q`qh|%98ggQ6GW1TL+HCA&s+vy!SxtSXOg+ClhIlU&1E|ZKJYE9LdgKO%jfZ(7f zVSV5PD_Y7d;TdZQs+yTPrcy(t{U`>u(tZFqezQ=gsB)(SAf~8sJS}K;i6a1vssI5B zDzF-tuWDUO8CktLGg0H8qbH(0bdoW-Bp>jlZR!YsN(RM2pPN{8>a?EXw5(nPp&bRD z6N+>qH7bRg3(F!9l)5WbN1gSr75%A6fq3$yyB&{2@J*rNEFull;LT@XFz zV*>zdr2S!q=0F3~dJAF*uwm$#`{9NFOQhu@rEtRm8Q=l+A+Y{Iv8Ca5zS%$$V6hkL zW7IeuA*)FURIw$SL^$!57JIPE(*Ym)Y%CBaYweO0r zKYO$VptG{TvR&&~Hruvdi?(X(E@vyVG%L1vo40zqcmBG!b1AX>nrwc{JV1e}gZpUk zcesjswO7h)wYqGAySS1&xs+SEmV3FF>xG&7Y=En;Z-ktiTU-hmx|^efq)WL=)uNB9 zY+-x4uKT*M8@sYQyU?SVv};wJYqpAZy9{NJysM9r_PcqzMjcC+tNUQBE4<3Pyv*CY z&P%&}`@EKvyD6F;70@yaE4_vPIJ(*k06j9d-CLIqL9{A*ybcz*;hVndyT0t(zE~B# z?&~hpYnNbCe(?)&+Q!D(!FdV}&Ji|2X zFa=w~IGn>eytvcY!#@1OI~>G9Jj6s?#72C?NSwqwF`JjNjmOg=)!Xq?7syvA(Y#%}z^hik^r3deL@$98tzJjjIqT*!tz#C~kLhrGy)+{ljn$dDY#&6~(0C&`w4$(WqUn!L%J z47rKi$)Fs{qCCo^T*{{0$De%4s=Ugq+{&)}%A8EeusqAOT+6n6%edUdvYgAj+{?cF z%fKAWAiT@MT+GIN%*dR~%6z%Ryv)x0%+MUo(mc%!*38s=&DfmH+Pux&JPg*{&EOo) z;yljejLqL%&gh)Z>b%bEe9GtC&hQ-1@;uM3&=4Kb5+{&%Y$GzOn{oK%9%gr6# z)Lq@y4az|Q001HR1Os;jK6uCK7Mva__cwzs&sy1Tr+zQ4f1!o$SH z#>dFX%FE2n&d<=%($mz{*4NnC+S}aS-rwNi;^XAy=I7|?>g(+7?(gvN^7EBBFfcMp zH1|1YIQ#%-;Q0sep1?^q?$sFeAV7c*#w0*s;Lu?OTp1j02s5GLnF|#|1`xEtp#gfa z8U{E3C}V()C^LZ+=@3{2hZTxfhlK*}8{1r4fa43v}k`lsLXlB9#2NW4>(9i*j zhmc-0SS$vW=EYks9dPt$W+u%*5^yphz(GSvvP?QWjXIS8leS$IH2AAz zW6U6W=X?0^M=2>~I>tg?n$y=FhPy>2L^TNLM^gE+LWutnVAR?d9o1w;ZWJW=fqr~M zg&h(FM)zP6#6{=a5d`oTT>}MtLI1&gBEfW*Wi(j;A&Js`5t~SuWrbpjN+9rFa7si- z(*}z%=1_cmgyk9l5=B?h0~2}?m2bZ>cti&YDyZZa1}rG#S53Sp9F$KoL0pTB*cg%n zw2`4C25!3Ri0FOl&Q2#EiuK?ALoOJ48CsL8DC+(2B3ILmEcWIPm6XrM6VOJ$hZRZiT+l6p2y(h0GAn>t#Uddkx_cz-T2~cF;IqnG zfpKIM+gdE?!e8VC0kdrRmzux1hS7BuM>8UG+Cl&=@EC^WMJBmBPLbCJWPeueD(xzrf3**t}64`G~D)@iIQNzPz*v*^H;-!W4NaVZYuzl_gaL~^I{9WlFG6p(Ff zOqdqts6-30oA$hah zf`LKoRFuTw^Ic3I3dS%G54ZKXVn6!tR+(Q*;PBpAXS|Tut8f5HOChBJj)N*XZU-Ym zxdP<49De#I2k0Q_3D@%4=f(#-?kP|Rd07q^K+`lM$nAAndQyePpffe_CtDCi*$~tu z!4U3`2o!7`1C@6OUfm6P=zA6c8<@E*%!f!-P}u|`1j1O&&=NN+lL&oS9Q6$WI=g#9 z=y2GG8supK;xR}Jq;>?OyeSFnlK~EVz>^u+pb0kEqCsll6DAZ+iQ>6|6e0G7Gp;3! zJJ8Mza6p3+7XLv9Po#kuUqDAP$PWxEsUQ?6=bZeJENi-v4I9PB06U&fbheol2x8j$a+Cq4+qbxeaj*1F=!gE_ZYQ(BLpi6bU8@5lJ-+w2eTSEM;5*niwiV z=qVQ2UG&O;J1ZD$RtUjZc4_pG{KxsTKM6y$pG{TA+I#2r5lbEByr(TYi zu4IO0ng3Ye;SuDP(JE-KGvWk5Hq)oh`p{E*_beGpPnj20+D-^gT#5`{V3aZ%!HP~j zLg4IF5PBJ5saE8HL66`@Gg=V_Qw&2_lYky8-XN+&z~kWvQV<=C;g4TfmKEyemu_ky zU>|^%Na|-!xrUN>5`}0;o2RA{DM3SC(H}(rCInlVWB?-_A^|M*B~A`$uUF|M?KI*% z_@vC8it>ouGi9T)jd=!9oFZHOa;t+gC@$d^s;EHZ;^Bcrv}dw; zehI)UY%FC}n^d;$U*?9`I6iQ?wk{Mv%4(9bllj`%icHqDG}|(XdZ^|$N0cFgIR%&upG4$k$>K4x;l+4JuDz6m^du2p3F0H?YL!Vf2(Yl~J|(zm!$fdbPliBn7+8#{3r}l~m%!p1 zD+FQzuVWiyjD#NSzy@rTL5h>5)-Dd*O*;?+T)o_y35WnXb)gRAhTO>qr+H!I9NN8= zyH%ZcTEb>NE=m0+1_8>)zY3sA%L?kA)vQK?H~QJm`nNFtxw#gEn+` z5cOxObLw1GbOAnwdjET_*SzY>Z~Vhbdajnfk_Z96fgK6{#U&gZ#6c+n`;9K_XgcU1#sw>PV74`0G(l!ZTKWmSsREgC(VRv=kB;2Wld?S!maTJ;z8%U>UeYXse_mt;S$x@)F6#E4kzsKXYo{bb)3teXJ)U zpMg%@WPQ7WY_?W;XNZRPrG|2YPU+$~cr^r{1{&d$YDEHh3|Myw$ZbRQ7PCY;n?i%; zvRDhIhE#xiM|XEg5QL`IUZ_KL+{Pk@H9nuFE!HLk-ePW$GlvX>has3}cGq5v=UZ{Z z7ZGTKIRq>8HYpXbhmeS0lLus*NIZ1-NPcEc1xSFom~!qHasODve@8%K{Wk;|h65e< zL`V>TyC`G6sDS?m1!`rC{@08{@L^!EfIjsQsWu~lS9UEpfmYCVeRp~bM2<^#AEc&; zQkQ6un1-vzFeONXi|0s7c6#(NUM=ESplAhHgO0&fb)Qv)M#zOa^-Xkfeesn+0yzL$ zcvqLmZ3@&;E9irMvXEPVd=}Xi8Gvr#vLsVTh3Z9+V^A@aw{F~q88k%%F!x$!*MpK~ zk(Q^12iaPhGCeAxG+{Q5wdIdZ5EXvbjyFeUiL;K5h>GL~fmwivGIxa4)>E(2Dd?Dj zd`2QX*_3P|k<65YOOT1f_H&}=Xa|Im`$&{mQX!&9l>cO+CcXiP(bq_5>2sm7O#g#5 z4M15{2{1KzkEQ05P7rYP$UXyjMM&T-8&+ZI!Vel}1WBcSNuZ3?NR7D|R$btjPM{Q4 z1pp;?j9|rM?D04QCWl6IcKZm9cnOe%XMx%UcQMjG5z>UX7f6f}kA}#XWoVju>2wd7 zPCMsX(@$Qb_zgkwWW8d#6Z_#j*V4V z8lh`2B$D&?NN9K2mb90ST3KNBHi!_)ME|^k7#d?Y`t>@M*gX%4kCx_!^ch3( z#+BR&p&$}1Z&sYVNc~v&hVNq0Y`DdlXsEd<{jGkF<(DII;aXh5CpYT?1 zT&aYsIcY#MmaK;^W-)}|bc818hR)fWCU`&}YGAoZe)_p+waKOfNsn=uo{OY`|H6p) zsh?@rl2}k_Q{Y-%S))K_8PUl|NLiiOB#(UPfm7Kl-e)AS$OWE=H*@2B5E(N`Lj*H1 zlee`8?-{9*mPDbmqmBlsce;k5il!{tdH=GtEemRRFnN3NS*Hz3XozQap+k;9GlUsR zMCxd0x45XrHLFC3PXAM=>x6p0W1;n!Iztq1q4aO$CxfoDoO&vOrB-ZBV3?ZOenEaB80d`(G_6$_%5R-Z%}s4SVFLqIIP#Hl7p zgv+&ppUQmCnprGpXUzsr?a7{-20KofZ~tbpCus&N+H?n6kgTN{N>c!Nk+W?Xts1Ii z<(4LTK^rNVo|>d?!r7{NAhTQ9p8qU61bNzm+_i-D#x_)W0nv$PKI^e|wrER>w!;dn zZYc&0sj~SD+RB1JhmLG1tkY?M%BitU zGogiQ7StK5EnBTcKz03LHT0OTJyu0p`c#m4fGRePTD6R!1f&MLq|^9SRkd*kYjL`Z zrO}843Co#b(1u-?pEtKi5{sj#=%=@XK8Omqm{*juWV&BMr+QYhd3&+Q+laWivMPJC zz?nJ3F|^qlT9vb=|lIkspBg8!Iyu6rx3FSNGa+k#lPAsBp$?ZqV!d<1=akw}ZVzbd`b zv!|!ktLF0-NOrhdkhoZ4f)=>qo2Ef5wjdn7jtc-N%)N$Ivt9R_shGa& zUx{k)cw7CU)ydlfA$=RVaJj3Ec$(VbK0(=D~XfZ#0yxB6UDJ#PfY{ZUi=w>&328?5>&#ulpLi z^J>NRoN%@)yIuvxegv?^_?P({yusMITfnecfXc(^XpXqSWGcr5shY!u%y*1|#fKN9 zmAw<}x4Ad07Mr%&JiZ&vdgfb>Gy93_n~v1!zGaugBN?;+EYhEx26r3NiORj&H3Z0+ z%IN%dtQ^CzJf7Fqwt>i$Y(TcT#i;U`&FtLE&quQj@&qJaH3dA|Rg_mlq z%v`iZiH;`<&HpW;#H?tEM(xy^D8Y_PDlScVMX8cjt;+iAh=cSzV|~Fz%+!a60D(c3 zv>VU8IK^T7F1K6Q_S&RX?9W=<29!x~*g94OeZ~f@1qfYy!o`Di+|UhMcR7WThk`2F zX@P%h!7t3M8m&b63&^@=J|K;^L4C=o3xkV%(l?mWH|?NuH_{*Rkfm%tN)3Cd8wNG4 ztIcVs9!=LetlUwM01-6F*vhK?iL%GrBWS32>CD$ohuwpGqlk;zbqvZZsmQ3xKv*4K zedpcZS;D0K)z5m~Yx0)fTh>JxtWr?XFN@8v?YJ2AlHBadD=3~*P+WHX1PX1j|H<4T zsLm7I)c+%rzkWHSgiQqR2Pa8-&sIus2v@K6y2T8)&)%W0WSphItAA+ByPXNDY)m|J zQI{_6;w4&_-B_AI?W+#TV72_WNL{tI&CyLjXIBlrY@N`;h2SWSzPv51(#`db3zVon{EER*ShW)6u(0#EhoP_WcGsUftQf-@IJM zN#4d}@XN+cla)htcv|0g34P3g%ctY*yC=S|YRtdKJ;`0lNoJuyKDpYFtg$02X$+jfNxpsl*}qwS z=O|O^&dhDt8-vt+wIIxVj$V?Efaj z$aik8OI$p+4==J4M0^R+F1KlkdblapA4+Y)(iqiW(<_l^I&aL>VGzI87-%t2crEI-kc z&w6W~1+V(bR6pr(4*Q#Zm(U^JT+r}szuadJ=nyBhwiqnTl9mvT5KiWi^*Kq_|dNF2gOi5psSjO=m-am)mZAvK;1crDjD zHM&@;WEFFWq=$S!QIv_r7zy`qr}&7aegGZ26_ z2p$S_fKF}sMt%gp?g24|LVDxuk0NId!qMBE*O!n$yn?*LfC2+Ch%SNy8N}v_i1Y&o zEh0vVa3TjlIH0&#Mxv?{2tfSc2#z`uP`w1-8;eA^vLI)S7ys2-YYFWlXl1C0T+ET2 zw&18L3ld?>@c@+4$iO}hBj~XcBa6c)rEbE(M8_ao%;$=nf_zad@0OfEg5yLKP6P(2 z`)4NUG=b2};4Wa$Kn2~zz`-KEYa=`kwzz=8p^Ad4JS~a)62G0yLr*d|KJv-Bz0L2Mq0;))1(dlqhWplBzhz}WraMDc6c}@ajiM6P#{`T|}0AMZH z&{+WF@~pAW9Aj~a)X;4=HzHC?%r?f{os193(8Xa2&j0E)jWmBX!M8Jgji9aG7%aQt zHsA`LtGD(_dl%E1Ds?mq<(Avx0~^O!Q#fz?$&$a1D|LX5iY*Q>=V>~~tw}zOtB$#xW@QU6+vhui zvzop=nvr@Lw;!z-Z6ZJi+qZ3pXuZF&Ut0{f&i@{Ux8HVo_-!tEB{m{rba#+J6B*9k z>tAX%`V;#(fF|TJ@t3 zSZ#OAkXFaEGLHk!ZhipRhs|iw6@AE#0R{X9@E$c5oH@{JADB}S{N}s8rR-P!A=2U? zw}G$$;sJhR#pGrLxQPh_O;z$4H|PXI4+`aS1*DekmLY09HNRi;l< z5~?FLQ6>=ks7+;tQy&VACLEp_(8?i{qaAIBL<8zkky<2N%G_r>Ki4L1j+7Y$T_!(q z0)hpFl%)bF=}Z?I7lzf8r##;0Q=v{F7J3@$QIV>W4JMVT9qm<9p&C`GB9a`n zoT@(~GR3P}<)B;j>Q})URTaUmy1^0MwhzPwQgwvJKC;Zm%H6Pt#ZAK+UD{$vJF^dc+)yv^sbk^ z?UibFX=ecW&X>ORl`m3>>;GQ;?w7y)<>`3+8(;wsn7{?T?ALroHv=!2!3}nBQvHIk z2Tz#76}Iq&HPaU1ExB}SFmA3SyF`efgB)T$;wq>N}RZ~!}^{kZ6^r=yu zYE^$3(J)Z7^gP|bVE_JQH#6wztYc7Xx|({`z4rC5fnDTHySmfi6Q^WsJyc}xdZ)p5 z_OqcKZ6K@foXb{rsM*SEX>Xg`-S&2fseNp0`_$TS)%Lg1oo;on+r8oLX1RX~@3fwq z-SxKjz3~lI#U>!vn|=&7k5I2yJ1+;jticWZZG%1700#)qfe-9*uW<{s->T7pZpR|; zFfiO;5C05a$pxIeI2;E9Uk<(VopP10oaN0dE5F)BnSi^?muIo3CH!^SJ;-(=Hvzy4fvv`qQBvb(NHrd@$I-40cZJBMb&#GT+x^E`z+U53J)> z_sP~Leg#KEy#MS=pV-i6ewp3aYkfF4*A2?9^|v=&>V5b7-}_!J)yxUz3Nu5iYrf0Q z7=4#Jk7m_#;B|Zv>Xa`+tIdk`=C{xE!%loLO&_qPA8+7v#}vw=AAkAJpU;Btx#Ep|1@jmC z`a=LO01UwWfNy(#4hsaZ2+rkwcB1!yYT}NE=#KCGP>=p7umUYm&dOy6{^tOV0P*GX0-px{F0ck|@CLW+|G3X#vS9zf2Lw|v z)k@CU`Od58cv zUJnj95DJx0{G2f7m@f^X#|g_&r=pMtg)jWD@DA}X4~Ji_H?bCN@fJZW1NAQy+<5fR62dIFIybV~(=v7S^A=4MOd zaQ_1qhd>JJ5ErF!8mY0u1g{kTu>W8!{|1qLZto2VP@sqr86EK#g+SL}$r;a)4kb_( ztMMJ-F&?jL`YzA!)b-QY8zqM)gxURa7x2I6#$6Z{}A^wOG;5HXDdl{r^c>rOH(kwN2f$ zo^Exhrq#5d^{{refr9l~!(dT;V_QkeTa9&G8xUEA&QKAGS)-UERxF&q-c=%3z<2UY`bEeTrfA)mZnf;y!N|V~!)o zC*c;Z;b;@+loSpmb_XslSfFMsHDukTR)b+F@(C);UTtW{(4I>GomW^W|c59KDd{K5#adQU{?p#&%Wd&i3WX5%tCRU{bmEd&~D#eG?h-E?mjOIsa;eeDDfol)| zOPGkEss)B<77@T!Xda<|QfUaB*AUhgSnG##+`@?}OLMrShxk@LLWevAS9iL%XoaYb zZWnU-lj_nk>r8hr&Jq9;rWLO)b^9V;@{U_sb?q>Nel4+MS5fX_GdY{_?(z<0KXY|U z3mMd;dQoU8wh3d-Bs-G7a^3YT$!%au!v!2PSr1LhtgPFf|xaDvhfdYM4b@ zfpGt)R}Y74mEtKp#vyoNPC6i29#=+k6)VOC0iuH>Ey8zewgM#J>~g|m><2CqX}kD9 zS~g%S%4?3lHf%|T0RqYblI8w#xrAuhdnd&>JpYL~sMuElmrmM<87w((iS`7yyvCrX#wfgXgECc^NR852PZ4ssC9NrrDsTWr{b)rHetbDte{a7(%?-7kGG^ z>sp{BNTk79nr~W}Nvf^4cbSPwR{3DCiCRGIPpRXQVrz5Y{x1MeRTqoU9X~MvsakZK z@*o|KpPBEeUke%Z8fw%Up_?eOnPqoVre)Yzc|~}y=}2lZ<*oN1BCz(13i@q=142;n zDLVH~X}GL4hPOW^nFU*|eHyX>A{L_dtI^szrr9QtmTS0Gu~#@J!l-RVxw7dKw9W8T znX)z)GhP6-C&zR#Pfwr+fR|pdH~aA=37NH73mJ@P3%pdeseo_R0h4ytu1fl?cfq=K z`?smv9ta9jCHqmL_JQ82Soj7Re*cRrUK^+Bx`u4`wpp5sN?g2sVR)6vP4WxG345^t z@Ojt3vBTNHGl_*2Ah6M!Jv;GzlX@F_U@`G!zUBK3k8m)Ou(bE!VvCM7wJ&r7+P_ze z!xbBiDjBQ^HmHe~95Bf%8kMBad&INiRuCkvAw0~%f`XdEp z<6LC`rL3=DK46?efLCvJ+L&$JD~@HTwfnoHH4E@M7oN*;b$Q2&dIDQbwTE(@QMJem zaRb@=6xVPe>pP!k^L(7#BT;h+T~>b@Ak|ep)#V(RSAEqZ;nh|BqfY`EdRtNGTz0=a z8)9Uny*zKwyo6Ea!;?0`E&sS;gzK<5D6xN{!uzARH8|H#h}TD%vYjGkq?@s6=g)oJ z&0o5%2|&#?0F?t>N>;e9UmQjtAlr*w)|28#+&$I%A>QR2((SFs*|BmhT`@7;^gMgt zIla@XuYVnLW7X;@T{7SMTbEo(u3y+iOW#_}vxL|| z8qF_x-RTIvv^%FF>$shLt;3wTuViQ{9Nm$im{~}U7M0HjJ&hrSk)mc)!dp0YKFzZv zu*!F=th zUggw#kF(wr*<0(2{1`F27{8wD`?VY?m+bknGgC3`Pm9V!oU1QBf-`<*Mp-HjfA1At z^lj;_dz#t>KkgBG?nhq1SX%Lgp7F)}${`=mmEDqt{_c;U+YfzMH=hhY3$|QEo~4^DHu}e1yGz_*fq>m7Kt!c4F-Hr zEpjsbAxItkSu%Z?l8Rb6YFJIEGEpQFN!kT6#oA4rx^?^&R7p&tIb<%CA}7nj-8!5) zLQ^3$zdc9(MfnZ^^KyfSi;t6+o1dentFN=SyT8N7%g@u-!`D2?NX0w}Smhwmm!{u7 zF+`r2kwPZRlr2cmyg-t$k^vePX}ocGu9xd6Vz@IKwjat(2S*gH;qNChA06J6Y5DtGHgtBv#8Ln`tq%N&m#T6`>Lui32 zQf=9gtN#Hwv|~WFh|>U+&NNsHAgNpc283QKW(gZLylyh+t-=y#IJIY0B2KJ$G2_OL zA485Tc{1h7Ztx*Nx#ULXGd%)oA;^SZk};x-IFVQ-H4K3RA6jw3RhIytpcRL88=TTy6~V~CS1a<(;a_kQQdpsd z7h;&9h8uF&VG)>t_F+Drg-At51(mi0Yd9>C0B5sx!J8BjC1Jo6Hh^T}6GFV` zQrzUgHhB#?j0VLe_mUNrUi#O4K~2J`bRev!Xig9=*{Z9r!Wyfrv(j2?I3Q+r5gBVR zAwvwm0vjZ-zhZO+jK(q~A{I8t;445S>KG&s)0Wi4jyN#eYz#c$*6WVip&?rm)ACR) z0AVbv?GBmohOUvDsb&svwvscPTNxZs09bTRfPudhM35)I|6*{!s#IP=7IX?cQ2!JM z5d1OCqYOY$F$BE;`V@Ry85bST1Q_z>yBo&;Tr>eeaT!01o8VU)YZhfGE)SI^W$sOui=Lyp19(R zGu}8inAL#8j@N=agt(Jq&V!2G4nbP7%3>bx8Kh1AE^0J}&bhJ2qCq<4M}{85iOQPZ zo9HGf>9`z&wUr-(i~1h?G78!kGMmDe+Q!)V{cRlb`AMwEnaT^FL8s3bK>wG`M7+#u z99KOso$vKs|Ge)%XGMO-<0oL4Fol`lvh>26YRmHt5WwEi!26CW$nopxVCJJ6c7nIH z|HY4h_A_7exEDW1=`Mm2oFD}&Xu%8KqciB*k-03Fy3lEEbvODE0MszK9oU8=l6&0< zLw5rmq|jTc)4~daGzDBOtAy?Pk#FRcI}G*(7lj(10?PuxHlYnlH}O((#D~PcEH7k3 zAb`vw=7IW2fjertMii~r#Na_-6xu7#RGzdGs}MpbPpsMzud|Ck9ZY>qj0?pM_=zG` z;sQ~$A{V)X6)^q;zRPkQ9K-(TC zaX=t~GL)hmB`HfuE65SSBFw6sVti$TY_UUIzltGZRB4gP%*&Kh^hpOK-~kDgaWM(# z7tY?djsy7emj)9~GC6=n#Eh{i8Nk;jCnKSiaA2AlaHcve0D@u;@-YS|z&F7u8D#zv zn1;+|F`uc+cfvEC@|-6<$AQXvmNJ*R91JA!SwZk&2xk5qC_xKq(1Rj0p%}WSLMftZ*%+TAX9yX)QWf;YV49WQyyTaRbaQiSZNAqy45ULw5; zuL&idZ7n*hwB{8$)tt<6GF8{!xhg=uTc%6xYhGP`rvEjeVcj|2OTv0MI7RVoYB}^Z zwgyz7fr+DUZ4Df&5WEL5-Xz*5c*atQdap74jb2I&5P?dOjFqAUaH|BI9|E`3!x~!a z@{HMsMq(Jp!+-!Cc@|D;fFK0yq1e}oi{1t2pdB0RAeJIH5->1; zU2~)q2jBO4=TwMpUWh{0s;?A|eI$ei0%Tc(J{I7NVcf!kIb-p{ii(`tfB z`wY=`#>bc)VtW)#opy;Elt%hC{`MOlr+#+YfC82V+%4gUTXs8|otApbbKkqmcZLF# zVS<-z&zYX0Suo(7Oyv3CV5VkMu=JMamfIcacFfF|wC*{ykEmvZqpAyzj(gi%Ko`}x zn(r*=f7ip$2e*r%#{Jh7t3)RR+?S6RA;-AsTybw?Gt$?Q=$tOvR@}r7#*6oIB8=_e zZ}dQvoW@C~3t&1LUT)0WQFF%Jy+b!INB^FLTr0Y8o8Iwe4A47DbXrS%9c@lb@BSQE zV5j60sTRP_h_H)bB7zT&qc}ULU5>9uHSCD<#^5c#MzZ$_RKSxlngjn?R3>0spV>hN zi;%ibdk{7tUs5MXb}8dsblZt zI9}-+q!&Fd0^%yILN7_YhbFh713-2_!+^$=H$pHu>wjzcP_3yzVwnEmSl9#C6c`V{ znEnw;z$_X2`4}151qGap(nS*fWth_y1AP?8n=Jyap`9af%@I+O5w#KAHN^w8RaUi# z0$Lpf*vR4~#`J(gApyXpwbcO*;Qxi0U>g{k`PI?Vg-r;OAM)@T5wuU)6#?L_2?&N? zhIvPyJrV#w9-i^XzT`sOxQWr38Y}RbS_IjT*`NWs!MnNK2Z$i&Fee$mo-ZNa0&G2VVq?k00pBH z4UjP+kEQq@{h%OXNZ#jtgBs-r$H80wWW)1#(H9AiJ)TGK^xy@`j{^e>Y? zAwA@YjBy{vT;6|(06OqfCTU)B9An}o;5;q?=M4rzwg@ybz#yKXGMa-LD&b3ZofNQS zLxv&tqy!$qV>|ke1tt+Euuqpk;(X9#qwQZJ7EeeDkhJv<9;x8dRLNCc%^5BoPa@kV zvI`a<-;Uhku#CuSynqSqz$zk%6oldnbR~RI!7MHTd-=e$a8Qy+*gss0TzVyn;J^yq zKw0J`U22A#Ddk>F0{@I5k|Va@_*Bfdof!8G4I+&jhxN|n<>Vd^*|ia(-AJC@m{ajs z=4A@q8@QNe?g3{O3>6TZy$BMaK+gt%rhIfs#2}IsQXcdWjiF>+Yo6h2`W-vrOwo`Z zXLgBcj$hs6jNF;#(g})X`jI;c8)#BYO*TeU`hx!W<3CD=Z62n3XlGO|rvkWUb2guF z`dU*!2=}Sow}mHm)?X(+1xYGi!ab4IX-8Ykon0j59>}KExZc8O+iRBRH_GM)6b||j zieZFVHFoD{AQDq%XT*rWYic7AY};*~o;-LHX+8pR!j0VkCm+72 zw_#X`+2n#s=l?GbW)XboBL=0Ew4>`)!2*KJiYn%Z5*&rPoK~_WYvf23JegP~forIu z>Y&ajl1}XuNgHes4ulGbjG{QW9~j|D zBB-n>faCdLWVXa)QUR8*9id=}@;PM4WuVU0VAjD;_rcB3WWq2pAbwm-KiO%wEkg4I zVvIIioQ|La?2mdZ=TTgR+c6qY%32w64Q)EAsuV!wnH_;4z;^yxj8^A+piC~P-x-D5 z9@LLc`01>=1>G1L#gtD=vW2OJiH({`>7`Xlk}9w!%%R$es9qdS8ivh09L`+D*AwLgMKrrK={Z1gM(SFSo&`P(Wlk!f zf`Fwi#^E8wi{dJ$jE$qvS&e>xsTdlo=4bZ>h@d8By-h22P^%UEpq1!oWgbvP=42Q# zQpCa)h!kn=ou!dN3tE25=8WtqDxBR_TU=4JF6hGDT|;mU?gV#thhV`&fZ&9rpm28z zclW~GwQ%>4KyY_WHCs;i>E7q6f5Ey~-?P>;#vF6JEAd6n7y(mXlF8d2fH)E55#i3G z_Wu(760$CmV-){bxJyK3~2Y>n8T&+C8 zA*`sZzc}4lq9N^a0PC%$-W-H45&e`xoI+s;iGBezLZ`PZvI^<4slRS&WZlu*8 z2%OyX(4AV2%$ixUmtsV~blLpCu+f#u;k|b%w2{E=gOpmQJu}lc=H?i6Qh8uzzm7jm zLBs?N=GeByY{-E>+^jgHii~2&f7{AW_rpReqda4hd}nLyM!&kMfEwGj+Q6t>x|J3{ z>Cm`Rb(&oJ@NcvMagVK;{bA}vD$98~^y~47=1b^j;Rkjh(;^|UFZr}CqEl2{{eeL@ z=XHJ+l9L)&wp#XAJ4@{>=X=aXvZ~v}I0I90UMtJx+{f)1v+M1@GR89C*K1_dsj0g^ z5E!4b*yE8a4#4W3s1E+YS6Tt5`K#(;YwlP%v6t&m5rzitDt7ThjVv&4jLmbY{*Wqf z(fpz0$w+|zb!Z&VQKE?EQT64EG9G*jvv$7=dgYl6wyik4>*ou$TfQ~MR;t-qxzQ$9 z_*{gmtsMq_vpaXkA>ulMx9VO$f}2W_VUm3Kjwn1i-`R)jsiq1_d^IVM0=H2+9?3w? zer57T9@?tW42 z7%CUWxTeQ*7Q7EJ(X=%~`Dlm=)tCQxG8`0un1A}SFT|27h1pkWpxjAJUvh&6mIF|* z@xM?G!TIlgu!*i`PrJt|15Xw#2*9CVA zOZh2*Uvx=UFBK%u8SHWKC#9b150K$?b$7}lKF3b{l*O;l@RB&czm^vxcxZm17H;Ga ze8)|-Cd^wDQqVW9wGSV`3f-`~>vo42S2hS=2qb>My1MCo;L~QZA?Ca#3lAgv%9}g- z?QKq(?N_5F(2`ehco6;@gO8Iz@;>T(8HH*!zd>zayNmC}kx4D&TDc5o9<*cp6*ubl zc=$G~2~vSOC#8!pp{u|#oey^bDPvU3(h}v>`F2*Nd|e+{hnB>1LGNkh!*3vZm#ylo zNlfooz^oYtqWD#(taie{;z4_W_^9f5Z(bv+R&ce1D1&{jb5R~IeTYjFY(@>iyahmc zAn(t;QZr`im|IS|YB{TJ&H`psKDpCdv#7PW8DDb-ThMM~6^ag{St*nI^NzubB`SJGe8$f-8n=UPDBsEfSQpT4E)Y4Jz+7LhWUKjb3wOb#986+1bc z=B`6SY3*_Lj_HR33O-<#y7FT*=&%U@6n#KAK3KmvwAKd(_YQHTaPg+|UHprFsOYGD?vr2OZ>M^gPRL~=XJApm7Hj7{F|{2qlsPu~t6lvx6r zuVcs)V2{hyiN`{_=6)2T^Kp-)l8D4NX#ApZ18dcT8*MfWA$rN-}QMH;NQYhv##nQUu}5hZ;ZpDs`u~=9>{mJT^tUH`DQUx3%z(V>~I2n#uv#@+v<& z)9wTK1AAOt(RmXm)0rJegp!|<0ExcpQ_G@l5!@%?$hEo|M6WMNW-n-8izw+AFParJ zTQV{W%(2IEo-MLkgjMH#v!~k_R3XkAKrGjCkB+bGA}yZF(5YnM4E?)2H?Q8=fWw|_5@idUO_xSHb>_8p2Pc@QB{*0uru-BrFe#nsp z*d>=}rV!CO1w4K^BZ?dJi+^b`{rRqYJx3vo#qDRKt2ep(&ZFwP&c{$(MTNrw1V&Nt zt;=)2+^;gSQ{Gko7XU1_`j3!t<;$V=U$s)P;!L}85TxgbbmBM@aa6T}<*}&DX??!M z`{w&8D8Pm-6t@;s`LnrJ=7Ay@G9~zWclKlMlf*J)K|~uSOw%kgRS#IY=#~7~@zE2~ z@bBbYsGI=#zNvewRDpf^oByY0W(@g}2}WF0fQ*$}c>H%#INNQ@VEJO5Bf?oz-D0AW z&cynrkv@^P0NVwqz6$_I8wT1Vli3}GZLgeJ!~kv#RX0>+EP0tN>sq$sI9HH4!v8Mj81vgHcm$r2}w2x6Ldi% zAL1h90;x+J7YdVM^ozKfQ=9P7ny3~wa_bQO$}rbH@Z>-LMuaO)#_99lJG9adObtn3+1 zmo2n&%aNVXia1lOPO>xCl7oxK2-`Fugg*QfepHpDfR)NGGx)#BBxMQXw0RFF5W}iI zUKro`)>>gdN%#4TP)MsYJR8$$Oh%OFro?%$UBbk&3qB}glZLn5*?&9w*lOR;+1&31 zuZjW3Kjs2BCS{G!Mxg49Vgumq9#;<$QbJ>SZlJ;C*hFXtZ7dl(d$NRj(g}>7HIJHQ zSVN9eMK>=|m_f=1ZBQBbSz$_VKC28z z02e9`8-G=-GXL^K6|c6js2br?N$)AYVKk|n5VIFXXsU+${GX7|0!lz+85K8B8j07V zP9&l{Tw}|xNK|$9KU@g=@;%Ax80hdL4US!`#X3_#?tq&Sl@nS@cRAH@P^eTu>O|oX zXEAgpoCl$7~I_bp*^V2Cd)D*18?S69AuKG)GXS_!hOR(dafP(Jtxr&NL`lA;62( zYE)GaGGPfM(b{^(OcP61Vbki-=YVfn;Z}g%pp_iQ>wK)GpPc;_-xx&yISr@x1I0w3 z3Y@8&c~95R=Qs^X;DJCaoz27#*8ABW{dPD67R*OWYhvkhntE(~&wV^)5!hSIX5F~U z6t~QdINja}e!Jj5aYm`!@MQcP>HY{Xi_Y8K_Z3vvIg|u*SwmLP;3-?dnw_?vFn+iB z`Os~~*=R<`-1IeE?ZOPL?Os$gRO75fErGmO7GK#qsiNj#enDARl<`EJ6+nEHJXxP0 zEv0nvC^9G)i9u&&J()gKPU4!#(%`B;^R`{#quUoi=y?a#$8c}NfZtBv$m1za&KOZ= zr21IVTQd|C~6cN*}m{`htYW8_zjcDTWY~m%0!#K=Jm6i=~;vh??mkh z?Tg!g$CfSFF+lhsgy^1+W5e(}w#>A-5W6+O(4tCOmw6;@!*@JB3SlN8r$tzaQj-0~ zWYw+HK)Q#zv`zob3!QsABN z?F}zJvLbY98asoV4hi70BAbS1e?5>hmENEOJcTRzxJ_FFr9O87WbsABPdpFWKTQrN z!C{rTCmXzc5?K@u0YEM^J1pN5+TbGiEj0w*7BLlYfqU^%C!K2R0x|bk$`EMl0>vcD za!7qN0_SLuRg&_=6#As9)I5+;{{~8KE3$5suppv04YR6aU}BL_ONZ59G? zO!EN5qYf;Uhq`4?3#Nmx##}F#484k@c=y~tDaTcjix{M_AFXU`jc0*y?eai)zEw%H zQ)8JGt)@!zrl^L0etqn>f$$Omg9N!L!l=IL!5+%T6LS>1=E4s)3PjcaD-F!j38d zl}#Ha+`R|{04bQ-r^oH&VGUKi@n1q%qWZ;C0_n%Hmm;D%pr7iE&P}dv(M)z0P4(%?LES#2VYf&z*O-ZrgTEWf7w92~_9VmGJkyK~BRhaK%He=z ziX_{7_K6x+ZO17P+q<9aPMnt@hdoVV*@(^lS&mF!!hePPPcZ3qZ?=_Ji~rE8N4@<0 zqczF852})UMidP;Y$bmvt5-_%yXR|>vQ;KMAtYX?^= zf7HfDxJ54!sJ&wCaJIpl$|X zT!P9y*1(f+f!7@pZoZOORfrGIl0BMIk1CRwHj<9xJx?Zmkp~il^r_YUQucK~0_R>& z7;PH-V4s}7q{!7fgbT^a#6BvUegU*r#^8SDD#Y_7>Gq_4w!wb(1?dTka*msR&Rya} zgaM_UUT(et5;6Q%(E)y&0Rayg0>6I2WSN}&e&)u0;lTm11sQ;8(Z`zsPieMWrvcoT z0cqLz-`s<;x`T3v34g5z6+9q5vx&deK-$<5=e4Sk}T=_U>5D%~&qtcpl++KK*zB-*}(NL!>72LokZs$&JS+Slns1Cijk3A3t<|>sS1YA@O z8`B-=+h#$H7bBgRPy>fL5s+kMzOC}>QH~5%7^UUTM3^9;%`}!4Ae~$mqr;mKMbe$b z+Nm>R4M!4gNZc_eQ;0*51e`9VN83t(qKHyjv%UIw6W1B{z8Mnj<|$*m2TkjWKs zzduQyPg7x5P)7EN>^{0RDM?R}37H!?X8I}4PN}+H9G#dQO`PkNAsxq_Xp$WUt*c3&>AnlZsX5%UB zg;x{TO^~`(J*_3|G z;e5=t&QqW3iy-D>Yp<8PhM}O6|d_WZqwjB)FR9_Up(^GhFx1lIrtFr!sA$_0L@<*yxYE0g($`DWOn zSCQv)FyLk$Ou`)$?pmM#+iMVJ!27{L-mzynd)~sP%~-5YtaJ*K&>N zBl-0cZ?okf#$}7Eo<Iyt77xOf023>u4EiMmUG?yYl})I@QOhbRTaD6yS5xv`Gn|>H-?3- zb#0!|W)gr>2OmFZRj3M2VJb2{Nlj`_3fX+eTsV#FFsc}0n>s51n#AjZcSNW8mj4x{ zJgD|MDyi!%Mi2Oq+;-`_UM*j&8W5;`l7BTm-DU{an_t~DOZ5DN)+bVeq`8Si@ws(j_#Or3u0nMYm1edL%zI~qz%dhGIQs?pwvHV^ zvup5uV{e8r%DR!+^_=GbMTd@n0i2d8U37t5+v8%IWCH|;MN@M<4*K+|5sUw7F_!DE zz}&V*BYpe7x#^Rw=PKz7TLKZy1D|&Lf z3%(*Zo@-B8!SWw+8s)Zkf^-8ttzJ2@OZZs@aM!dZS~1*$7`#MKu$x~oErU(DgVqAc z>@B->X}+Ul5$Y!!O^2HdA0Y$ZjAw1^Sf#UqDp#wnCns!A? ztvh&Rlq#JO{@Qc*atJX_VE({J?;CPr=4X4ldc)9|gH`SQJ)w0lFDC`uk^!-0R0dh; zY#wg=MhD055(i7N8}9G)`Qac-0gG1h8FF~zd~#x>RyAs3b(&CwklHOMjx1%{#m%}i)?1bA`2iJG+-K7x2+hnW@^=(Bxvm^QcbuXu` zR)_Lb|bDY%qB#l z;ljaHS{!DZmA|?~Hw8o9wju8s&eaqSi}SYmi0>t^TK#ndc_h^_9+Ra85y}0gZap@| zhBVl;Lcgqj;lGHXwH>Oy@aLuJwh`+2059iou2QyFXnp|vO>(5~T>rLv0HMqLr1fnF)IlgT6p?Ea@PYRI_Qqc8;Zq4BP4zC_)m1UEn^WJJ zK#w}o1;cx0dQypZtcLEKFt_2esWIiXIetUZO#=MA%J!+P`!>a)Jk_NVV zzKfJ!$|cPO0XP-gkSVZ9oCxp!S|K~rhSM~st^=c76a9D)Nh0)?gQ&W>&ulIfo!_^D z%e&jY79G2&bA`!^s8()Yj$#YaHLgf51QV88h370&_SM+Rc)aGtLJt;u(bAtvz80jM zkKi(7D~8hwUxfvvy21?R|D_5)tI;p94aAxbAwrj8j~|h`PKNxx2YlqYmhNYX6q)Rp~7#9 zOd1oHP+Xkc!82zs*p5w4)FYqBZLqI0aw*@k2iTm%5Kr6v(`7;REGVz0uyBo{7%g}1 z+Wps+M}9qSuk?_DO*wR&Kcvt25Omgdb~bT?ZJ^%LvX6}NM_%Nkdi8Z7@Pf&qBB?k?LzREIZNBXe9?Fj$ z(0hRc6KA)|t$8Xyt(Pnf4~r#jwq&JQL4+p(UapW5#{mbp;w_X*7bK%84DRt#ijm#r zE*fR+Q1fAeK6;8CsrK^mHa<6Xd)!SzHx}i}#fL1Y7h}YZDaMwFDCBtZfAjT8Bz@b* zR1}{F`7++?S+0GL$*%62+&q27qW&PRZtn6y4*>yWU5srFN-6vRlG`ySDx9KAsdZu4 znF6N#;DxNXlF;S%hlt%o(6uTcV|x>_Y=H{~-lvcqS;dRCo>2nyJABO0LjdG$7Wm*Y zMET=Is9=4?K_u|6%p?yLP4(j{5S<(e0eg~<=VmI|lA*RJ!wZQ}Ra+s}@BQO76P$#G zIHt02QB(?=XpDz;62uEHbO8QROQ*w9 zzzUd5@u`#trD||XeC5l+0|9NR$!f}JNeMdI{7adfSLKt|tnl~b8kMJyoAHH~m%7BQ zns{b{L9E91%p!e;rdj@YhlZ-UVj^-y^M~bBDP^C>%3?UgK@NVIwn%*$&Qr=x)y-Rx zPBmAT3I#u@kWfJ&819PYPX(GX3l1#oUCTfU7kf(~WbCpjEP*25;E!m;&?)(z$dxJ~ z{S3arJg9{-b)^!ILkLm{@(xi^4z*pDbAk=v9Y9f^JJPs-D37^WA<=QEQ~Tm?fxYU4 zk`zjcu0nKIp2#02=dVPap9y5bap|*fGTJE8KaWH0wA(Y_F!C^X>F`tGz@jio$_(rY z7BJ%ciA^ptPK#_N1%@7Xu}F_8lK!|Bp5c_f>i5EmNyOp$nNAwCcRI2NW@j@voTrWU z|B#g{WGrMBEA|FG2jkj&>tix=F^sP&{APUYo>j(2K^%2R` zK|lX&7s%r^TLbpm$%_rShKYbg97)2W_0!m;;UOj~HWp$P8(f_3abD&FrbIGuFKOhq z?M4kGs_T?uW4zP$7q`<2IL9eHJ$^UkC?{wTHSkXf{->`v{jZDV-Z-$-J>ytTiG3=} zZ}~JPfJ=`Luvw^eCB*F1Mi&FERSoD}nTNy~-iHZ^kO(X6hmaZw_5ba4eW6(*Y(zrX zq+)hc_1Op@Hb4TF$%F(|61P-vwZ+w4Q0$<~MQU2HHkMZ^qVMAG3wY9A&eDHpJI+bF zTS$x_n)WY2_*4KdG_>~JCH!Y(!Ga<(972w|zA4$3brB-QPcxvLZ$2^{3o;pvCxYMI z4wb+kE-~q$Qi4x~+<|0H0*TVTja-i2N7(l}h|5{nFl-S+4k+Yg*dlm6VAp zoWvN9+*@Qa5@hZA@O(q-d*cC&)P%TXX^-u8<)LvVPYqCGRWXd?YlI5h#r{{_c*|ds z4=Q+x@o3$I`e?6x?9f{tnmf1rUsV9w(M8R4`BBTiexGBhv87|tOnd*DD#_lqVvD_y zXZI}-s{~NR>8b{1Qo#@6mV!A zbQRxH)8Ch?(c#UBg^t7?&lloDB}Y~#Z#>txZ8&t(dvk_v;>C7pJ1|q`OqD$=bgLaV zyc{M*-xGk1KIWqtl&J=9yM{5d4`AHMe7vf-e9Y3qul3oj!=_l&ZoD2Tt{t3mpDywWnp!eo{go3c)WoF~wC zbNKT>+vKLPXZKBYZ9q@GiZf5XoA>5dRO<)5>N5l?IAkE_@Av&R#B3-@dk%p zR#5Tp^iBIj3|JQQj74uk1@802-i^1Uq$08-A~es#`pKg+4dUr7KSy$-z4a{HVpJ|P zosfft^#C6?-<5_obWs=Hr+juc9-f2rl8l`5U{E2F7)DDW5aQcqn;v;fb&3z4eQQhD zwD)IFkrj^KOLK=2r~Vmv_JZU$yV(XZ^GEz;V897Xc#FA_M$M;HJ*x z>WF7(@z+Py52T{rl;?8i5%sRk&&qyt$u*7oFbqJIZ9WS4jy0J3!~x0x?CC_bZ3KFP zl&Fh3IEoaL)J`YaTImMnJL`ZWH43 zez1iJ5#AJ|YgYIKjjVk$@*3$ACn|kftydC&p*PA?b8IX`H^^mIz)S0?=6-r2rFGI2 z`w+6*F*e~y|9v~4t3$WTBfqUE#vsR*dYy5{_1t5>V_TEzThcIlj-V5Jmk_TGJYVI8 zG%vn3=}OgN_j(4za;Uf+%Y``6)-C$l{x@#aOfNELyd{(XbFh>6MTp~-xcv_O0yCg05sTV5Rs?{?ra3lq|(n!+Rf zI4PL|YA=J96k0RbQ;@f$7sZL>yXG9m>bN>f*XT0YcATX}xBwq6zBTtS-t`<&;#L&& z8940L)!6m)J9uV4O(~Icb`6mhJZH+vsjxYR2oDVuk=j7@JZhnU()FQ^emHDX(WZwx z9t6NSTqh0ams+w6vIXZ3zwTa#_k5g-~F%1;VGB-DF2kKbRB>0GgN03=*w2i`wA0j^>sz~axJk?b)uXMA|~ zz2iyX-jjay(iAxwwihNLm_hN!Df zyWy9=v#&;Qw!w$9Z)Kv}qcp+`ke9|sYw@cs@}ZMr#GNsK6zQ^u1b!*J`l32GNd}N1Dt+YfSzHdK<^8dX!tk5l?ZEm+VPnLh0%Jm%>vBBb)rc)ET_6_2 zM};Qy&s=15ppZygyGDYKV0%Og!=IrL=JkdN0m?lUCtV-}E3)RIa7+M(7;H@x+td8nWAvzKh z$LOwnQoH?}9{+uWD^BeK26rTA1vZGr2o|?SfoJEc2=OZ_Wn!N@KO?e-1&{ZKpCw`d^rWZwf^S$speSD$TufWyrUf9GEGX93G@C3a+de}g7^^@jLoXzwqD}cz1W%zuX0v-n zl~KB{5d~#DTy;4eetKrZDo>R|dY4WnrDjGfb5{9dR+v)uqC>WUX4-Oj_5#Fd-B<8^ ziIgCmZKje6<4*Rz(K}Uc*R}F*$G+Jb!8w;Kxz|d$w+Y#|<=LSTxlBns^DKfw^0H?U zQrm60NbOjfjJc@fx#*60SVws{%DMMw`L5-;1mlc2ig1lSY;oIJc0m6FZj!en9FO0* z#OV2vctJ>!1#yhIY>ow-=!M+v(fF)|oQC=QB59c4!k-P2g^mg((2Jy4i)56G@Qg#` zl=TE7i#QKpqII;+D(J;pZ$k>o#d?m#Pf~>jkt)g+#rKN>iiZHK)naSqlFyDMc9A6x z6(ueG#ZE^hU(rk5SWCYtmwGyuHbByhyi5JZO9PHdf1;NKvX+6A%R(H>!XnEeD$1h9 z%VLho;?TW20Kot;9wgz6627Z+iZl?y} z$_7zr16O3jM~p@(wniD1YzdV{g{VfQ%Ep_|jVi~D8W>Gl{yb`IO?pmE22o8rl1)a? zCbQ$FPyRq6jAm<<=FiY36Q^c}%4R2(2774pSBw_7^?Db!7Eh-ZZ>Ktss1|={OTcrD z-*HPITPw)F8mQ757S$SoQ59O*8gtwl=Ts4m(Uzprmcmw^=+u^8*_KIB28OofV)(S@ zca-F?wHG_Jmr@iLMYUH#+pC`oDvsOh**Y4ca_Uq%TB17IGNYR-JGzcLdZ3L~7@Y$u zokJKNolc!&m7P!(woz#3G)C7fv~HBOYtgA|x$@mqRM$GRYg46u;kfHJTlY=}%9={| zepGjxQ}-dX`?RB7>c21^0s!KFU_AH$Ycx$n00IoPed8b3xRQ0N(6Lzu%z`=A#v#YP zqQ?2S`$A#RvW@lQ_tA7qb9 zvVZCShwL#Z_iO)ec>2G{p5{&y=>Jak6igbe|8HbZ?mw|DoYh;Y_sC*(xP z7ig4^lgiycv{ JD(KKu|Xwq=6Je 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 TY>y9{&j19Tu6{1-oD!M<5lpK+ literal 0 HcmV?d00001 diff --git a/img/time-2-short.png b/img/time-2-short.png new file mode 100644 index 0000000000000000000000000000000000000000..7c3bc0c3336272928c59988fd803c610bbf32d94 GIT binary patch literal 42032 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 z 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 YJtmDoY%810OW;2_DaA)c68f+IAEELJu>b%7 literal 0 HcmV?d00001 diff --git a/img/undraw_docusaurus_mountain.svg b/img/undraw_docusaurus_mountain.svg new file mode 100644 index 0000000..431cef2 --- /dev/null +++ b/img/undraw_docusaurus_mountain.svg @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/undraw_docusaurus_react.svg b/img/undraw_docusaurus_react.svg new file mode 100644 index 0000000..e417050 --- /dev/null +++ b/img/undraw_docusaurus_react.svg @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/undraw_docusaurus_tree.svg b/img/undraw_docusaurus_tree.svg new file mode 100644 index 0000000..a05cc03 --- /dev/null +++ b/img/undraw_docusaurus_tree.svg @@ -0,0 +1 @@ +Codestin Search App \ No newline at end of file diff --git a/test/Dockerfile b/test/Dockerfile index b0e329a..0b761e4 100644 --- a/test/Dockerfile +++ b/test/Dockerfile @@ -14,6 +14,9 @@ COPY --chown=test:test ./authorized_keys /home/test/.ssh/authorized_keys USER root +RUN usermod -aG sudo test +RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers + RUN service ssh start WORKDIR /home/test diff --git a/test/benchmark.sh b/test/benchmark.sh index d4c1b8c..05528be 100755 --- a/test/benchmark.sh +++ b/test/benchmark.sh @@ -21,21 +21,20 @@ function parse_options() { function __main__() { parse_options $@ - # --style=basic if [[ "$SAVE" ]]; then - hyperfine -N --runs 5 '../dist/sake run ping -s server-9' > ./profiles/ping-no-key - hyperfine -N --runs 5 '../dist/sake run ping -t reachable' > ./profiles/ping - hyperfine -N --runs 5 '../dist/sake run ping -p -t reachable' > ./profiles/ping-parallel - hyperfine -N --runs 5 '../dist/sake run d -t reachable' > ./profiles/nested - hyperfine -N --runs 5 '../dist/sake run d -p -t reachable' > ./profiles/nested-parallel - hyperfine -N --runs 5 '../dist/sake list servers' > ./profiles/list-servers + hyperfine -N --runs 10 '../dist/sake run ping -s server-9' > ./profiles/ping-no-key + hyperfine -N --runs 10 '../dist/sake run ping --forks=1 -t reachable' > ./profiles/ping + hyperfine -N --runs 10 '../dist/sake run ping --strategy=free -t reachable' > ./profiles/ping-parallel + hyperfine -N --runs 10 '../dist/sake run d --forks=1 -t reachable' > ./profiles/nested + hyperfine -N --runs 10 '../dist/sake run d --strategy=free -t reachable' > ./profiles/nested-parallel + hyperfine -N --runs 10 '../dist/sake list servers' > ./profiles/list-servers else - hyperfine -N --runs 5 '../dist/sake run ping -s server-9' - hyperfine -N --runs 5 '../dist/sake run ping -t reachable' - hyperfine -N --runs 5 '../dist/sake run ping -p -t reachable' - hyperfine -N --runs 5 '../dist/sake run d -t reachable' - hyperfine -N --runs 5 '../dist/sake run d -p -t reachable' - hyperfine -N --runs 5 '../dist/sake list servers' + hyperfine -N --runs 10 '../dist/sake run ping -s server-9' + hyperfine -N --runs 10 '../dist/sake run ping --forks=1 -t reachable' + hyperfine -N --runs 10 '../dist/sake run ping --strategy=free -t reachable' + hyperfine -N --runs 10 '../dist/sake run d --forks=1 -t reachable' + hyperfine -N --runs 10 '../dist/sake run d --strategy=free -t reachable' + hyperfine -N --runs 10 '../dist/sake list servers' fi } diff --git a/test/benchmarks/list-servers b/test/benchmarks/list-servers deleted file mode 100644 index d728d7b..0000000 --- a/test/benchmarks/list-servers +++ /dev/null @@ -1,4 +0,0 @@ -Benchmark 1: ../dist/sake list servers - Time (mean ± σ): 3.7 ms ± 0.3 ms [User: 2.6 ms, System: 1.7 ms] - Range (min … max): 3.3 ms … 4.0 ms 5 runs - diff --git a/test/benchmarks/nested b/test/benchmarks/nested deleted file mode 100644 index 3db111a..0000000 --- a/test/benchmarks/nested +++ /dev/null @@ -1,4 +0,0 @@ -Benchmark 1: ../dist/sake run d -t reachable - Time (mean ± σ): 640.1 ms ± 71.6 ms [User: 359.4 ms, System: 48.4 ms] - Range (min … max): 571.2 ms … 724.9 ms 5 runs - diff --git a/test/benchmarks/nested-parallel b/test/benchmarks/nested-parallel deleted file mode 100644 index 5eb3de4..0000000 --- a/test/benchmarks/nested-parallel +++ /dev/null @@ -1,4 +0,0 @@ -Benchmark 1: ../dist/sake run d -p -t reachable - Time (mean ± σ): 476.8 ms ± 21.6 ms [User: 340.1 ms, System: 26.6 ms] - Range (min … max): 447.0 ms … 499.0 ms 5 runs - diff --git a/test/benchmarks/ping b/test/benchmarks/ping deleted file mode 100644 index 92e48fc..0000000 --- a/test/benchmarks/ping +++ /dev/null @@ -1,4 +0,0 @@ -Benchmark 1: ../dist/sake run ping -t reachable - Time (mean ± σ): 463.0 ms ± 43.0 ms [User: 316.7 ms, System: 20.1 ms] - Range (min … max): 426.9 ms … 516.7 ms 5 runs - diff --git a/test/benchmarks/ping-parallel b/test/benchmarks/ping-parallel deleted file mode 100644 index 45f8147..0000000 --- a/test/benchmarks/ping-parallel +++ /dev/null @@ -1,4 +0,0 @@ -Benchmark 1: ../dist/sake run ping -p -t reachable - Time (mean ± σ): 475.6 ms ± 20.3 ms [User: 334.4 ms, System: 24.6 ms] - Range (min … max): 451.4 ms … 503.0 ms 5 runs - diff --git a/test/integration/golden/golden-1.stdout b/test/integration/golden/golden-1.stdout index ea7e4be..ab81272 100755 --- a/test/integration/golden/golden-1.stdout +++ b/test/integration/golden/golden-1.stdout @@ -27,6 +27,8 @@ WantErr: false work-dir-1 | work-dir-2 | work-dir-3 | + register-1 | + register-2 | fatal | fatal-true | errors | diff --git a/test/integration/golden/golden-10.stdout b/test/integration/golden/golden-10.stdout index 32e62b9..b2b17b6 100755 --- a/test/integration/golden/golden-10.stdout +++ b/test/integration/golden/golden-10.stdout @@ -8,10 +8,13 @@ WantErr: false name: ping desc: ping server theme: default +spec: + strategy: linear + batch: 1 + output: table + report: recap target: all: true -spec: - output: text cmd: echo pong @@ -21,12 +24,15 @@ name: real-ping desc: ping server theme: default local: true +spec: + strategy: linear + batch: 1 + output: table + report: recap target: all: true -spec: - output: text cmd: - ping $SAKE_SERVER_HOST -c 2 + ping $S_HOST -c 2 -- @@ -34,15 +40,16 @@ task: print-host name: Host desc: print host theme: default -target: - all: true spec: + strategy: free output: table - parallel: true ignore_errors: true ignore_unreachable: true + report: recap +target: + all: true cmd: - echo $SAKE_SERVER_HOST + echo $S_HOST -- @@ -50,13 +57,14 @@ task: print-hostname name: Hostname desc: print hostname theme: default -target: - all: true spec: + strategy: free output: table - parallel: true ignore_errors: true ignore_unreachable: true + report: recap +target: + all: true cmd: hostname @@ -66,13 +74,14 @@ task: print-os name: OS desc: print OS theme: default -target: - all: true spec: + strategy: free output: table - parallel: true ignore_errors: true ignore_unreachable: true + report: recap +target: + all: true cmd: echo OS @@ -82,13 +91,14 @@ task: print-kernel name: Kernel desc: Print kernel version theme: default -target: - all: true spec: + strategy: free output: table - parallel: true ignore_errors: true ignore_unreachable: true + report: recap +target: + all: true cmd: echo kernel @@ -97,13 +107,14 @@ cmd: name: info desc: get remote info theme: default -target: - all: true spec: + strategy: free output: table - parallel: true ignore_errors: true ignore_unreachable: true + report: recap +target: + all: true tasks: - OS: print OS - Kernel: Print kernel version @@ -112,10 +123,11 @@ tasks: name: env theme: default -target: - all: true spec: output: table + report: recap +target: + all: true env: foo: xyz task: local @@ -130,10 +142,11 @@ cmd: name: env-ref theme: default -target: - all: true spec: output: table + report: recap +target: + all: true env: task: 123 xyz: xyz @@ -149,10 +162,11 @@ cmd: name: env-complex theme: default -target: - all: true spec: output: table + report: recap +target: + all: true env: foo: xyz task: local @@ -164,37 +178,27 @@ tasks: name: env-default theme: default -target: - all: true spec: output: table + report: recap +target: + all: true cmd: echo "# SERVER" - echo "SAKE_SERVER_NAME $SAKE_SERVER_NAME" - echo "SAKE_SERVER_DESC $SAKE_SERVER_DESC" - echo "SAKE_SERVER_TAGS $SAKE_SERVER_TAGS" - echo "SAKE_SERVER_HOST $SAKE_SERVER_HOST" - echo "SAKE_SERVER_USER $SAKE_SERVER_USER" - echo "SAKE_SERVER_PORT $SAKE_SERVER_PORT" - echo "SAKE_SERVER_LOCAL $SAKE_SERVER_LOCAL" - - echo - echo "# TASK" - echo "SAKE_TASK_ID $SAKE_TASK_ID" - echo "SAKE_TASK_NAME $SAKE_TASK_NAME" - echo "SAKE_TASK_DESC $SAKE_TASK_DESC" - echo "SAKE_TASK_LOCAL $SAKE_TASK_LOCAL" - - echo - echo "# CONFIG" - echo "SAKE_KNOWN_HOSTS_FILE $SAKE_KNOWN_HOSTS_FILE" + echo "S_TAGS $S_TAGS" + echo "S_HOST $S_HOST" + echo "S_USER $S_USER" + echo "S_PORT $S_PORT" -- name: a theme: default spec: - output: text + strategy: linear + batch: 1 + output: table + report: recap tasks: - ping: ping server @@ -203,7 +207,10 @@ tasks: name: b theme: default spec: - output: text + strategy: linear + batch: 1 + output: table + report: recap tasks: - ping: ping server - ping: ping server @@ -213,7 +220,10 @@ tasks: name: c theme: default spec: - output: text + strategy: linear + batch: 1 + output: table + report: recap tasks: - ping: ping server - ping: ping server @@ -223,10 +233,11 @@ tasks: name: d theme: default -target: - all: true spec: output: table + report: recap +target: + all: true tasks: - ping: ping server - ping: ping server @@ -242,7 +253,10 @@ name: ref theme: default work_dir: /usr spec: - output: text + strategy: linear + batch: 1 + output: table + report: recap cmd: pwd @@ -252,7 +266,10 @@ task: work-nested name: nested theme: default spec: - output: text + strategy: linear + batch: 1 + output: table + report: recap tasks: - ref @@ -261,10 +278,11 @@ tasks: name: work-dir-1 theme: default work_dir: /home -target: - all: true spec: output: table + report: recap +target: + all: true tasks: - ref - Override inline ref @@ -275,10 +293,11 @@ tasks: name: work-dir-2 theme: default -target: - all: true spec: output: table + report: recap +target: + all: true tasks: - ref - Override inline ref @@ -289,22 +308,51 @@ tasks: name: work-dir-3 theme: default -target: - all: true spec: output: table + report: recap +target: + all: true tasks: - ref - ref -- +name: register-1 +theme: default +spec: + strategy: linear + batch: 1 + output: table + report: recap +tasks: + - task-0 + +-- + +name: register-2 +theme: default +spec: + strategy: linear + batch: 1 + output: table + report: recap +tasks: + - task-0 + - task-1 + - task-2 + - task-3 + +-- + name: fatal theme: default -target: - tags: reachable spec: output: table + report: recap +target: + tags: reachable cmd: exit 1 @@ -312,11 +360,12 @@ cmd: name: fatal-true theme: default -target: - tags: reachable spec: output: table any_errors_fatal: true + report: recap +target: + tags: reachable cmd: exit 1 @@ -324,33 +373,37 @@ cmd: name: errors theme: default -target: - tags: reachable spec: output: table + report: recap +target: + tags: reachable tasks: - - cmd - - cmd - - cmd + - task-0 + - task-1 + - task-2 -- name: errors-true theme: default -target: - tags: reachable spec: output: table ignore_errors: true + report: recap +target: + tags: reachable tasks: - - cmd - - cmd - - cmd + - task-0 + - task-1 + - task-2 -- name: unreachable theme: default +spec: + report: recap target: all: true cmd: @@ -360,10 +413,11 @@ cmd: name: unreachable-true theme: default -target: - all: true spec: ignore_unreachable: true + report: recap +target: + all: true cmd: echo 123 @@ -371,10 +425,11 @@ cmd: name: empty theme: default -target: - tags: reachable spec: output: table + report: recap +target: + tags: reachable cmd: if [[ -d ".ssh" ]] then @@ -385,11 +440,12 @@ cmd: name: empty-true theme: default -target: - tags: reachable spec: output: table - omit_empty: true + omit_empty_rows: true + report: recap +target: + tags: reachable cmd: if [[ -d ".ssh" ]] then @@ -402,8 +458,9 @@ name: output theme: default spec: output: table + report: recap tasks: - - cmd - - cmd - - cmd + - task-0 + - task-1 + - task-2 diff --git a/test/integration/golden/golden-11.stdout b/test/integration/golden/golden-11.stdout index a84d023..2496527 100755 --- a/test/integration/golden/golden-11.stdout +++ b/test/integration/golden/golden-11.stdout @@ -1,70 +1,47 @@ Index: 11 Name: Ping all servers -Cmd: go run ../../main.go run ping -S -t reachable +Cmd: go run ../../main.go run ping -q -t reachable WantErr: false --- -TASK [ping: ping server] +TASKS + + host | ping +--------------------+------ + localhost | pong + 172.24.2.2 | pong + 172.24.2.4 | pong + 172.24.2.2 | pong + 172.24.2.4 | pong + 172.24.2.2 | pong + 172.24.2.4 | pong + 172.24.2.2 | pong + 172.24.2.3 | pong + 172.24.2.4 | pong + 172.24.2.5 | pong + 172.24.2.6 | pong + 172.24.2.7 | pong + 172.24.2.8 | pong + 172.24.2.9 | pong + 2001:3984:3989::10 | pong + + localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 -localhost | pong - -TASK [ping: ping server] - -172.24.2.2 | pong - -TASK [ping: ping server] - -172.24.2.4 | pong - -TASK [ping: ping server] - -172.24.2.2 | pong - -TASK [ping: ping server] - -172.24.2.4 | pong - -TASK [ping: ping server] - -172.24.2.2 | pong - -TASK [ping: ping server] - -172.24.2.4 | pong - -TASK [ping: ping server] - -172.24.2.2 | pong - -TASK [ping: ping server] - -172.24.2.3 | pong - -TASK [ping: ping server] - -172.24.2.4 | pong - -TASK [ping: ping server] - -172.24.2.5 | pong - -TASK [ping: ping server] - -172.24.2.6 | pong - -TASK [ping: ping server] - -172.24.2.7 | pong - -TASK [ping: ping server] - -172.24.2.8 | pong - -TASK [ping: ping server] - -172.24.2.9 | pong - -TASK [ping: ping server] - -2001:3984:3989::10 | pong diff --git a/test/integration/golden/golden-12.stdout b/test/integration/golden/golden-12.stdout index e194b51..78fe929 100755 --- a/test/integration/golden/golden-12.stdout +++ b/test/integration/golden/golden-12.stdout @@ -1,18 +1,31 @@ Index: 12 Name: Multiple commands -Cmd: go run ../../main.go run info -S -t prod +Cmd: go run ../../main.go run info -q -t prod WantErr: false --- - server | OS | Kernel -----------+----+-------- - list-0 | OS | kernel - list-1 | OS | kernel - range-0 | OS | kernel - range-1 | OS | kernel - inv-0 | OS | kernel - inv-1 | OS | kernel - server-1 | OS | kernel - server-2 | OS | kernel +TASKS + + host | OS | Kernel +------------+----+-------- + 172.24.2.2 | OS | kernel + 172.24.2.4 | OS | kernel + 172.24.2.2 | OS | kernel + 172.24.2.4 | OS | kernel + 172.24.2.2 | OS | kernel + 172.24.2.4 | OS | kernel + 172.24.2.2 | OS | kernel + 172.24.2.3 | OS | kernel + + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 +---------------------------------------------------------------------- + Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-13.stdout b/test/integration/golden/golden-13.stdout index dbdb034..be01ea5 100755 --- a/test/integration/golden/golden-13.stdout +++ b/test/integration/golden/golden-13.stdout @@ -1,11 +1,15 @@ Index: 13 Name: Filter by hosts server using server name -Cmd: go run ../../main.go run info -S -s list-1 +Cmd: go run ../../main.go run info -q -s list-1 WantErr: false --- - server | OS | Kernel ---------+----+-------- - list-1 | OS | kernel +TASKS + + host | OS | Kernel +------------+----+-------- + 172.24.2.4 | OS | kernel + + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-14.stdout b/test/integration/golden/golden-14.stdout index ac71d3f..df07caf 100755 --- a/test/integration/golden/golden-14.stdout +++ b/test/integration/golden/golden-14.stdout @@ -1,11 +1,15 @@ Index: 14 Name: Filter by hosts server using range index -Cmd: go run ../../main.go run info -S -s 'list[0]' +Cmd: go run ../../main.go run info -q -s 'list[0]' WantErr: false --- - server | OS | Kernel ---------+----+-------- - list-0 | OS | kernel +TASKS + + host | OS | Kernel +------------+----+-------- + 172.24.2.2 | OS | kernel + + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-15.stdout b/test/integration/golden/golden-15.stdout index b4aef38..2f68711 100755 --- a/test/integration/golden/golden-15.stdout +++ b/test/integration/golden/golden-15.stdout @@ -1,12 +1,19 @@ Index: 15 Name: Filter by hosts server -Cmd: go run ../../main.go run info -S -s 'list[0:2]' +Cmd: go run ../../main.go run info -q -s 'list[0:2]' WantErr: false --- - server | OS | Kernel ---------+----+-------- - list-0 | OS | kernel - list-1 | OS | kernel +TASKS + + host | OS | Kernel +------------+----+-------- + 172.24.2.2 | OS | kernel + 172.24.2.4 | OS | kernel + + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 +--------------------------------------------------------------------- + Total ok=4 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-16.stdout b/test/integration/golden/golden-16.stdout index 3d8c5aa..1ada0c3 100755 --- a/test/integration/golden/golden-16.stdout +++ b/test/integration/golden/golden-16.stdout @@ -1,18 +1,31 @@ Index: 16 Name: Filter by host regex -Cmd: go run ../../main.go run info -S -r '172.24.2.(2|4)' +Cmd: go run ../../main.go run info -q -r '172.24.2.(2|4)' WantErr: false --- - server | OS | Kernel -----------+----+-------- - list-0 | OS | kernel - list-1 | OS | kernel - range-0 | OS | kernel - range-1 | OS | kernel - inv-0 | OS | kernel - inv-1 | OS | kernel - server-1 | OS | kernel - server-3 | OS | kernel +TASKS + + host | OS | Kernel +------------+----+-------- + 172.24.2.2 | OS | kernel + 172.24.2.4 | OS | kernel + 172.24.2.2 | OS | kernel + 172.24.2.4 | OS | kernel + 172.24.2.2 | OS | kernel + 172.24.2.4 | OS | kernel + 172.24.2.2 | OS | kernel + 172.24.2.4 | OS | kernel + + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 +---------------------------------------------------------------------- + Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-17.stdout b/test/integration/golden/golden-17.stdout index f8152b9..7c16ddf 100755 --- a/test/integration/golden/golden-17.stdout +++ b/test/integration/golden/golden-17.stdout @@ -1,14 +1,19 @@ Index: 17 Name: Limit to 2 servers -Cmd: go run ../../main.go run ping -S -t reachable -l 2 +Cmd: go run ../../main.go run ping -q -t reachable -l 2 WantErr: false --- -TASK [ping: ping server] +TASKS -localhost | pong + host | ping +------------+------ + localhost | pong + 172.24.2.2 | pong + + localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 +--------------------------------------------------------------------- + Total ok=2 unreachable=0 ignored=0 failed=0 skipped=0 -TASK [ping: ping server] - -172.24.2.2 | pong diff --git a/test/integration/golden/golden-18.stdout b/test/integration/golden/golden-18.stdout index 384324b..cd0b2c0 100755 --- a/test/integration/golden/golden-18.stdout +++ b/test/integration/golden/golden-18.stdout @@ -1,38 +1,31 @@ Index: 18 Name: Limit to 50 percent servers -Cmd: go run ../../main.go run ping -S -t reachable -L 50 +Cmd: go run ../../main.go run ping -q -t reachable -L 50 WantErr: false --- -TASK [ping: ping server] +TASKS + + host | ping +------------+------ + localhost | pong + 172.24.2.2 | pong + 172.24.2.4 | pong + 172.24.2.2 | pong + 172.24.2.4 | pong + 172.24.2.2 | pong + 172.24.2.4 | pong + 172.24.2.2 | pong + + localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 +--------------------------------------------------------------------- + Total ok=8 unreachable=0 ignored=0 failed=0 skipped=0 -localhost | pong - -TASK [ping: ping server] - -172.24.2.2 | pong - -TASK [ping: ping server] - -172.24.2.4 | pong - -TASK [ping: ping server] - -172.24.2.2 | pong - -TASK [ping: ping server] - -172.24.2.4 | pong - -TASK [ping: ping server] - -172.24.2.2 | pong - -TASK [ping: ping server] - -172.24.2.4 | pong - -TASK [ping: ping server] - -172.24.2.2 | pong diff --git a/test/integration/golden/golden-19.stdout b/test/integration/golden/golden-19.stdout index ef6a7e2..264884e 100755 --- a/test/integration/golden/golden-19.stdout +++ b/test/integration/golden/golden-19.stdout @@ -1,70 +1,47 @@ Index: 19 Name: Filter by inverting on tag unreachable -Cmd: go run ../../main.go run ping -S -t unreachable -v +Cmd: go run ../../main.go run ping -q -t unreachable -v WantErr: false --- -TASK [ping: ping server] +TASKS + + host | ping +--------------------+------ + localhost | pong + 172.24.2.2 | pong + 172.24.2.4 | pong + 172.24.2.2 | pong + 172.24.2.4 | pong + 172.24.2.2 | pong + 172.24.2.4 | pong + 172.24.2.2 | pong + 172.24.2.3 | pong + 172.24.2.4 | pong + 172.24.2.5 | pong + 172.24.2.6 | pong + 172.24.2.7 | pong + 172.24.2.8 | pong + 172.24.2.9 | pong + 2001:3984:3989::10 | pong + + localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 -localhost | pong - -TASK [ping: ping server] - -172.24.2.2 | pong - -TASK [ping: ping server] - -172.24.2.4 | pong - -TASK [ping: ping server] - -172.24.2.2 | pong - -TASK [ping: ping server] - -172.24.2.4 | pong - -TASK [ping: ping server] - -172.24.2.2 | pong - -TASK [ping: ping server] - -172.24.2.4 | pong - -TASK [ping: ping server] - -172.24.2.2 | pong - -TASK [ping: ping server] - -172.24.2.3 | pong - -TASK [ping: ping server] - -172.24.2.4 | pong - -TASK [ping: ping server] - -172.24.2.5 | pong - -TASK [ping: ping server] - -172.24.2.6 | pong - -TASK [ping: ping server] - -172.24.2.7 | pong - -TASK [ping: ping server] - -172.24.2.8 | pong - -TASK [ping: ping server] - -172.24.2.9 | pong - -TASK [ping: ping server] - -2001:3984:3989::10 | pong diff --git a/test/integration/golden/golden-2.stdout b/test/integration/golden/golden-2.stdout index a3cfafb..fc62ca4 100755 --- a/test/integration/golden/golden-2.stdout +++ b/test/integration/golden/golden-2.stdout @@ -5,23 +5,23 @@ WantErr: false --- - server | host | desc --------------+--------------------+---------------------------- - localhost | localhost | localhost - unreachable | unreachable.lan | - list-0 | 172.24.2.2 | many hosts using list - list-1 | 172.24.2.4 | many hosts using list - range-0 | 172.24.2.2 | many hosts using range - range-1 | 172.24.2.4 | many hosts using range - inv-0 | 172.24.2.2 | many hosts using inventory - inv-1 | 172.24.2.4 | many hosts using inventory - server-1 | 172.24.2.2 | server-1 - server-2 | 172.24.2.3 | server-2 - server-3 | 172.24.2.4 | server-3 - server-4 | 172.24.2.5 | server-4 - server-5 | 172.24.2.6 | server-5 - server-6 | 172.24.2.7 | server-6 - server-7 | 172.24.2.8 | server-7 - server-8 | 172.24.2.9 | server-8 - server-9 | 2001:3984:3989::10 | server-9 + server | host | tags | desc +-------------+--------------------+-----------------------------+---------------------------- + localhost | localhost | local,reachable | localhost + unreachable | unreachable.lan | unreachable | + list-0 | 172.24.2.2 | remote,prod,list,reachable | many hosts using list + list-1 | 172.24.2.4 | remote,prod,list,reachable | many hosts using list + range-0 | 172.24.2.2 | remote,prod,range,reachable | many hosts using range + range-1 | 172.24.2.4 | remote,prod,range,reachable | many hosts using range + inv-0 | 172.24.2.2 | remote,prod,inv,reachable | many hosts using inventory + inv-1 | 172.24.2.4 | remote,prod,inv,reachable | many hosts using inventory + server-1 | 172.24.2.2 | remote,prod,reachable | server-1 + server-2 | 172.24.2.3 | remote,prod,reachable | server-2 + server-3 | 172.24.2.4 | remote,demo,reachable | server-3 + server-4 | 172.24.2.5 | remote,demo,reachable | server-4 + server-5 | 172.24.2.6 | remote,sandbox,reachable | server-5 + server-6 | 172.24.2.7 | remote,sandbox,reachable | server-6 + server-7 | 172.24.2.8 | remote,demo,reachable | server-7 + server-8 | 172.24.2.9 | remote,demo,reachable | server-8 + server-9 | 2001:3984:3989::10 | remote,demo,reachable | server-9 diff --git a/test/integration/golden/golden-20.stdout b/test/integration/golden/golden-20.stdout index 81eaa68..5b0da66 100755 --- a/test/integration/golden/golden-20.stdout +++ b/test/integration/golden/golden-20.stdout @@ -1,90 +1,111 @@ Index: 20 Name: Simple Envs -Cmd: go run ../../main.go run env -S -t reachable +Cmd: go run ../../main.go run env -q -t reachable WantErr: false --- - server | env ------------+------------- - localhost | foo xyz - | hello - | cookie - | release - | task local - list-0 | foo xyz - | hello world - | cookie - | release - | task local - list-1 | foo xyz - | hello world - | cookie - | release - | task local - range-0 | foo xyz - | hello world - | cookie - | release - | task local - range-1 | foo xyz - | hello world - | cookie - | release - | task local - inv-0 | foo xyz - | hello world - | cookie - | release - | task local - inv-1 | foo xyz - | hello world - | cookie - | release - | task local - server-1 | foo xyz - | hello - | cookie - | release - | task local - server-2 | foo xyz - | hello - | cookie - | release - | task local - server-3 | foo xyz - | hello - | cookie - | release - | task local - server-4 | foo xyz - | hello - | cookie - | release - | task local - server-5 | foo xyz - | hello - | cookie - | release - | task local - server-6 | foo xyz - | hello - | cookie - | release - | task local - server-7 | foo xyz - | hello - | cookie - | release - | task local - server-8 | foo xyz - | hello - | cookie - | release - | task local - server-9 | foo xyz - | hello - | cookie - | release - | task local +TASKS + + host | env +--------------------+------------- + localhost | foo xyz + | hello + | cookie + | release + | task local + 172.24.2.2 | foo xyz + | hello world + | cookie + | release + | task local + 172.24.2.4 | foo xyz + | hello world + | cookie + | release + | task local + 172.24.2.2 | foo xyz + | hello world + | cookie + | release + | task local + 172.24.2.4 | foo xyz + | hello world + | cookie + | release + | task local + 172.24.2.2 | foo xyz + | hello world + | cookie + | release + | task local + 172.24.2.4 | foo xyz + | hello world + | cookie + | release + | task local + 172.24.2.2 | foo xyz + | hello + | cookie + | release + | task local + 172.24.2.3 | foo xyz + | hello + | cookie + | release + | task local + 172.24.2.4 | foo xyz + | hello + | cookie + | release + | task local + 172.24.2.5 | foo xyz + | hello + | cookie + | release + | task local + 172.24.2.6 | foo xyz + | hello + | cookie + | release + | task local + 172.24.2.7 | foo xyz + | hello + | cookie + | release + | task local + 172.24.2.8 | foo xyz + | hello + | cookie + | release + | task local + 172.24.2.9 | foo xyz + | hello + | cookie + | release + | task local + 2001:3984:3989::10 | foo xyz + | hello + | cookie + | release + | task local + + localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-21.stdout b/test/integration/golden/golden-21.stdout index 6a834db..1bd951c 100755 --- a/test/integration/golden/golden-21.stdout +++ b/test/integration/golden/golden-21.stdout @@ -1,106 +1,127 @@ Index: 21 Name: Reference Envs -Cmd: go run ../../main.go run env-complex -S -t reachable +Cmd: go run ../../main.go run env-complex -q -t reachable WantErr: false --- - server | env-ref | env-ref ------------+-------------+------------- - localhost | foo xyz | foo xyz - | hello | hello - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - list-0 | foo xyz | foo xyz - | hello world | hello world - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - list-1 | foo xyz | foo xyz - | hello world | hello world - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - range-0 | foo xyz | foo xyz - | hello world | hello world - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - range-1 | foo xyz | foo xyz - | hello world | hello world - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - inv-0 | foo xyz | foo xyz - | hello world | hello world - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - inv-1 | foo xyz | foo xyz - | hello world | hello world - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - server-1 | foo xyz | foo xyz - | hello | hello - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - server-2 | foo xyz | foo xyz - | hello | hello - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - server-3 | foo xyz | foo xyz - | hello | hello - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - server-4 | foo xyz | foo xyz - | hello | hello - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - server-5 | foo xyz | foo xyz - | hello | hello - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - server-6 | foo xyz | foo xyz - | hello | hello - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - server-7 | foo xyz | foo xyz - | hello | hello - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - server-8 | foo xyz | foo xyz - | hello | hello - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - server-9 | foo xyz | foo xyz - | hello | hello - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz +TASKS + + host | env-ref | env-ref +--------------------+-------------+------------- + localhost | foo xyz | foo xyz + | hello | hello + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.2 | foo xyz | foo xyz + | hello world | hello world + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.4 | foo xyz | foo xyz + | hello world | hello world + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.2 | foo xyz | foo xyz + | hello world | hello world + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.4 | foo xyz | foo xyz + | hello world | hello world + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.2 | foo xyz | foo xyz + | hello world | hello world + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.4 | foo xyz | foo xyz + | hello world | hello world + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.2 | foo xyz | foo xyz + | hello | hello + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.3 | foo xyz | foo xyz + | hello | hello + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.4 | foo xyz | foo xyz + | hello | hello + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.5 | foo xyz | foo xyz + | hello | hello + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.6 | foo xyz | foo xyz + | hello | hello + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.7 | foo xyz | foo xyz + | hello | hello + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.8 | foo xyz | foo xyz + | hello | hello + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.9 | foo xyz | foo xyz + | hello | hello + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 2001:3984:3989::10 | foo xyz | foo xyz + | hello | hello + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + + localhost ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=32 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-22.stdout b/test/integration/golden/golden-22.stdout index 3f6a131..131cd4e 100755 --- a/test/integration/golden/golden-22.stdout +++ b/test/integration/golden/golden-22.stdout @@ -1,282 +1,111 @@ Index: 22 Name: Default Envs -Cmd: go run ../../main.go run env-default -S -t reachable +Cmd: go run ../../main.go run env-default -q -t reachable WantErr: false --- - server | env-default ------------+---------------------------------------------- - localhost | # SERVER - | SAKE_SERVER_NAME localhost - | SAKE_SERVER_DESC localhost - | SAKE_SERVER_TAGS local,reachable - | SAKE_SERVER_HOST localhost - | SAKE_SERVER_USER test - | SAKE_SERVER_PORT 22 - | SAKE_SERVER_LOCAL true - | - | # TASK - | SAKE_TASK_ID env-default - | SAKE_TASK_NAME - | SAKE_TASK_DESC - | SAKE_TASK_LOCAL - | - | # CONFIG - | SAKE_KNOWN_HOSTS_FILE - list-0 | # SERVER - | SAKE_SERVER_NAME list-0 - | SAKE_SERVER_DESC many hosts using list - | SAKE_SERVER_TAGS remote,prod,list,reachable - | SAKE_SERVER_HOST 172.24.2.2 - | SAKE_SERVER_USER test - | SAKE_SERVER_PORT 22 - | SAKE_SERVER_LOCAL - | - | # TASK - | SAKE_TASK_ID env-default - | SAKE_TASK_NAME - | SAKE_TASK_DESC - | SAKE_TASK_LOCAL - | - | # CONFIG - | SAKE_KNOWN_HOSTS_FILE - list-1 | # SERVER - | SAKE_SERVER_NAME list-1 - | SAKE_SERVER_DESC many hosts using list - | SAKE_SERVER_TAGS remote,prod,list,reachable - | SAKE_SERVER_HOST 172.24.2.4 - | SAKE_SERVER_USER test - | SAKE_SERVER_PORT 22 - | SAKE_SERVER_LOCAL - | - | # TASK - | SAKE_TASK_ID env-default - | SAKE_TASK_NAME - | SAKE_TASK_DESC - | SAKE_TASK_LOCAL - | - | # CONFIG - | SAKE_KNOWN_HOSTS_FILE - range-0 | # SERVER - | SAKE_SERVER_NAME range-0 - | SAKE_SERVER_DESC many hosts using range - | SAKE_SERVER_TAGS remote,prod,range,reachable - | SAKE_SERVER_HOST 172.24.2.2 - | SAKE_SERVER_USER test - | SAKE_SERVER_PORT 22 - | SAKE_SERVER_LOCAL - | - | # TASK - | SAKE_TASK_ID env-default - | SAKE_TASK_NAME - | SAKE_TASK_DESC - | SAKE_TASK_LOCAL - | - | # CONFIG - | SAKE_KNOWN_HOSTS_FILE - range-1 | # SERVER - | SAKE_SERVER_NAME range-1 - | SAKE_SERVER_DESC many hosts using range - | SAKE_SERVER_TAGS remote,prod,range,reachable - | SAKE_SERVER_HOST 172.24.2.4 - | SAKE_SERVER_USER test - | SAKE_SERVER_PORT 22 - | SAKE_SERVER_LOCAL - | - | # TASK - | SAKE_TASK_ID env-default - | SAKE_TASK_NAME - | SAKE_TASK_DESC - | SAKE_TASK_LOCAL - | - | # CONFIG - | SAKE_KNOWN_HOSTS_FILE - inv-0 | # SERVER - | SAKE_SERVER_NAME inv-0 - | SAKE_SERVER_DESC many hosts using inventory - | SAKE_SERVER_TAGS remote,prod,inv,reachable - | SAKE_SERVER_HOST 172.24.2.2 - | SAKE_SERVER_USER test - | SAKE_SERVER_PORT 22 - | SAKE_SERVER_LOCAL - | - | # TASK - | SAKE_TASK_ID env-default - | SAKE_TASK_NAME - | SAKE_TASK_DESC - | SAKE_TASK_LOCAL - | - | # CONFIG - | SAKE_KNOWN_HOSTS_FILE - inv-1 | # SERVER - | SAKE_SERVER_NAME inv-1 - | SAKE_SERVER_DESC many hosts using inventory - | SAKE_SERVER_TAGS remote,prod,inv,reachable - | SAKE_SERVER_HOST 172.24.2.4 - | SAKE_SERVER_USER test - | SAKE_SERVER_PORT 22 - | SAKE_SERVER_LOCAL - | - | # TASK - | SAKE_TASK_ID env-default - | SAKE_TASK_NAME - | SAKE_TASK_DESC - | SAKE_TASK_LOCAL - | - | # CONFIG - | SAKE_KNOWN_HOSTS_FILE - server-1 | # SERVER - | SAKE_SERVER_NAME server-1 - | SAKE_SERVER_DESC server-1 - | SAKE_SERVER_TAGS remote,prod,reachable - | SAKE_SERVER_HOST 172.24.2.2 - | SAKE_SERVER_USER test - | SAKE_SERVER_PORT 22 - | SAKE_SERVER_LOCAL - | - | # TASK - | SAKE_TASK_ID env-default - | SAKE_TASK_NAME - | SAKE_TASK_DESC - | SAKE_TASK_LOCAL - | - | # CONFIG - | SAKE_KNOWN_HOSTS_FILE - server-2 | # SERVER - | SAKE_SERVER_NAME server-2 - | SAKE_SERVER_DESC server-2 - | SAKE_SERVER_TAGS remote,prod,reachable - | SAKE_SERVER_HOST 172.24.2.3 - | SAKE_SERVER_USER test - | SAKE_SERVER_PORT 33 - | SAKE_SERVER_LOCAL - | - | # TASK - | SAKE_TASK_ID env-default - | SAKE_TASK_NAME - | SAKE_TASK_DESC - | SAKE_TASK_LOCAL - | - | # CONFIG - | SAKE_KNOWN_HOSTS_FILE - server-3 | # SERVER - | SAKE_SERVER_NAME server-3 - | SAKE_SERVER_DESC server-3 - | SAKE_SERVER_TAGS remote,demo,reachable - | SAKE_SERVER_HOST 172.24.2.4 - | SAKE_SERVER_USER test - | SAKE_SERVER_PORT 22 - | SAKE_SERVER_LOCAL - | - | # TASK - | SAKE_TASK_ID env-default - | SAKE_TASK_NAME - | SAKE_TASK_DESC - | SAKE_TASK_LOCAL - | - | # CONFIG - | SAKE_KNOWN_HOSTS_FILE - server-4 | # SERVER - | SAKE_SERVER_NAME server-4 - | SAKE_SERVER_DESC server-4 - | SAKE_SERVER_TAGS remote,demo,reachable - | SAKE_SERVER_HOST 172.24.2.5 - | SAKE_SERVER_USER test - | SAKE_SERVER_PORT 22 - | SAKE_SERVER_LOCAL - | - | # TASK - | SAKE_TASK_ID env-default - | SAKE_TASK_NAME - | SAKE_TASK_DESC - | SAKE_TASK_LOCAL - | - | # CONFIG - | SAKE_KNOWN_HOSTS_FILE - server-5 | # SERVER - | SAKE_SERVER_NAME server-5 - | SAKE_SERVER_DESC server-5 - | SAKE_SERVER_TAGS remote,sandbox,reachable - | SAKE_SERVER_HOST 172.24.2.6 - | SAKE_SERVER_USER test - | SAKE_SERVER_PORT 22 - | SAKE_SERVER_LOCAL - | - | # TASK - | SAKE_TASK_ID env-default - | SAKE_TASK_NAME - | SAKE_TASK_DESC - | SAKE_TASK_LOCAL - | - | # CONFIG - | SAKE_KNOWN_HOSTS_FILE - server-6 | # SERVER - | SAKE_SERVER_NAME server-6 - | SAKE_SERVER_DESC server-6 - | SAKE_SERVER_TAGS remote,sandbox,reachable - | SAKE_SERVER_HOST 172.24.2.7 - | SAKE_SERVER_USER test - | SAKE_SERVER_PORT 22 - | SAKE_SERVER_LOCAL - | - | # TASK - | SAKE_TASK_ID env-default - | SAKE_TASK_NAME - | SAKE_TASK_DESC - | SAKE_TASK_LOCAL - | - | # CONFIG - | SAKE_KNOWN_HOSTS_FILE - server-7 | # SERVER - | SAKE_SERVER_NAME server-7 - | SAKE_SERVER_DESC server-7 - | SAKE_SERVER_TAGS remote,demo,reachable - | SAKE_SERVER_HOST 172.24.2.8 - | SAKE_SERVER_USER test - | SAKE_SERVER_PORT 22 - | SAKE_SERVER_LOCAL - | - | # TASK - | SAKE_TASK_ID env-default - | SAKE_TASK_NAME - | SAKE_TASK_DESC - | SAKE_TASK_LOCAL - | - | # CONFIG - | SAKE_KNOWN_HOSTS_FILE - server-8 | # SERVER - | SAKE_SERVER_NAME server-8 - | SAKE_SERVER_DESC server-8 - | SAKE_SERVER_TAGS remote,demo,reachable - | SAKE_SERVER_HOST 172.24.2.9 - | SAKE_SERVER_USER test - | SAKE_SERVER_PORT 22 - | SAKE_SERVER_LOCAL - | - | # TASK - | SAKE_TASK_ID env-default - | SAKE_TASK_NAME - | SAKE_TASK_DESC - | SAKE_TASK_LOCAL - | - | # CONFIG - | SAKE_KNOWN_HOSTS_FILE - server-9 | # SERVER - | SAKE_SERVER_NAME server-9 - | SAKE_SERVER_DESC server-9 - | SAKE_SERVER_TAGS remote,demo,reachable - | SAKE_SERVER_HOST 2001:3984:3989::10 - | SAKE_SERVER_USER test - | SAKE_SERVER_PORT 22 - | SAKE_SERVER_LOCAL - | - | # TASK - | SAKE_TASK_ID env-default - | SAKE_TASK_NAME - | SAKE_TASK_DESC - | SAKE_TASK_LOCAL - | - | # CONFIG - | SAKE_KNOWN_HOSTS_FILE +TASKS + + host | env-default +--------------------+------------------------------------ + localhost | # SERVER + | S_TAGS local,reachable + | S_HOST localhost + | S_USER test + | S_PORT 22 + 172.24.2.2 | # SERVER + | S_TAGS remote,prod,list,reachable + | S_HOST 172.24.2.2 + | S_USER test + | S_PORT 22 + 172.24.2.4 | # SERVER + | S_TAGS remote,prod,list,reachable + | S_HOST 172.24.2.4 + | S_USER test + | S_PORT 22 + 172.24.2.2 | # SERVER + | S_TAGS remote,prod,range,reachable + | S_HOST 172.24.2.2 + | S_USER test + | S_PORT 22 + 172.24.2.4 | # SERVER + | S_TAGS remote,prod,range,reachable + | S_HOST 172.24.2.4 + | S_USER test + | S_PORT 22 + 172.24.2.2 | # SERVER + | S_TAGS remote,prod,inv,reachable + | S_HOST 172.24.2.2 + | S_USER test + | S_PORT 22 + 172.24.2.4 | # SERVER + | S_TAGS remote,prod,inv,reachable + | S_HOST 172.24.2.4 + | S_USER test + | S_PORT 22 + 172.24.2.2 | # SERVER + | S_TAGS remote,prod,reachable + | S_HOST 172.24.2.2 + | S_USER test + | S_PORT 22 + 172.24.2.3 | # SERVER + | S_TAGS remote,prod,reachable + | S_HOST 172.24.2.3 + | S_USER test + | S_PORT 33 + 172.24.2.4 | # SERVER + | S_TAGS remote,demo,reachable + | S_HOST 172.24.2.4 + | S_USER test + | S_PORT 22 + 172.24.2.5 | # SERVER + | S_TAGS remote,demo,reachable + | S_HOST 172.24.2.5 + | S_USER test + | S_PORT 22 + 172.24.2.6 | # SERVER + | S_TAGS remote,sandbox,reachable + | S_HOST 172.24.2.6 + | S_USER test + | S_PORT 22 + 172.24.2.7 | # SERVER + | S_TAGS remote,sandbox,reachable + | S_HOST 172.24.2.7 + | S_USER test + | S_PORT 22 + 172.24.2.8 | # SERVER + | S_TAGS remote,demo,reachable + | S_HOST 172.24.2.8 + | S_USER test + | S_PORT 22 + 172.24.2.9 | # SERVER + | S_TAGS remote,demo,reachable + | S_HOST 172.24.2.9 + | S_USER test + | S_PORT 22 + 2001:3984:3989::10 | # SERVER + | S_TAGS remote,demo,reachable + | S_HOST 2001:3984:3989::10 + | S_USER test + | S_PORT 22 + + localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-23.stdout b/test/integration/golden/golden-23.stdout index da36852..69334fc 100755 --- a/test/integration/golden/golden-23.stdout +++ b/test/integration/golden/golden-23.stdout @@ -1,26 +1,47 @@ Index: 23 Name: Nested tasks -Cmd: go run ../../main.go run d -S -t reachable +Cmd: go run ../../main.go run d -q -t reachable WantErr: false --- - server | ping | ping | ping | ping | ping | ping ------------+------+------+------+------+------+------ - localhost | pong | pong | pong | pong | pong | pong - list-0 | pong | pong | pong | pong | pong | pong - list-1 | pong | pong | pong | pong | pong | pong - range-0 | pong | pong | pong | pong | pong | pong - range-1 | pong | pong | pong | pong | pong | pong - inv-0 | pong | pong | pong | pong | pong | pong - inv-1 | pong | pong | pong | pong | pong | pong - server-1 | pong | pong | pong | pong | pong | pong - server-2 | pong | pong | pong | pong | pong | pong - server-3 | pong | pong | pong | pong | pong | pong - server-4 | pong | pong | pong | pong | pong | pong - server-5 | pong | pong | pong | pong | pong | pong - server-6 | pong | pong | pong | pong | pong | pong - server-7 | pong | pong | pong | pong | pong | pong - server-8 | pong | pong | pong | pong | pong | pong - server-9 | pong | pong | pong | pong | pong | pong +TASKS + + host | ping | ping | ping | ping | ping | ping +--------------------+------+------+------+------+------+------ + localhost | pong | pong | pong | pong | pong | pong + 172.24.2.2 | pong | pong | pong | pong | pong | pong + 172.24.2.4 | pong | pong | pong | pong | pong | pong + 172.24.2.2 | pong | pong | pong | pong | pong | pong + 172.24.2.4 | pong | pong | pong | pong | pong | pong + 172.24.2.2 | pong | pong | pong | pong | pong | pong + 172.24.2.4 | pong | pong | pong | pong | pong | pong + 172.24.2.2 | pong | pong | pong | pong | pong | pong + 172.24.2.3 | pong | pong | pong | pong | pong | pong + 172.24.2.4 | pong | pong | pong | pong | pong | pong + 172.24.2.5 | pong | pong | pong | pong | pong | pong + 172.24.2.6 | pong | pong | pong | pong | pong | pong + 172.24.2.7 | pong | pong | pong | pong | pong | pong + 172.24.2.8 | pong | pong | pong | pong | pong | pong + 172.24.2.9 | pong | pong | pong | pong | pong | pong + 2001:3984:3989::10 | pong | pong | pong | pong | pong | pong + + localhost ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=96 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-24.stdout b/test/integration/golden/golden-24.stdout index e22406b..7d5cb06 100755 --- a/test/integration/golden/golden-24.stdout +++ b/test/integration/golden/golden-24.stdout @@ -1,26 +1,47 @@ Index: 24 Name: Work Dir 1 -Cmd: go run ../../main.go run work-dir-1 -S -t reachable +Cmd: go run ../../main.go run work-dir-1 -q -t reachable WantErr: false --- - server | ref | Override inline ref | Inline | Override inline ------------+-------+---------------------+--------+----------------- - localhost | /home | /opt | /home | / - list-0 | /home | /opt | /home | / - list-1 | /home | /opt | /home | / - range-0 | /home | /opt | /home | / - range-1 | /home | /opt | /home | / - inv-0 | /home | /opt | /home | / - inv-1 | /home | /opt | /home | / - server-1 | /home | /opt | /home | / - server-2 | /home | /opt | /home | / - server-3 | /home | /opt | /home | / - server-4 | /home | /opt | /home | / - server-5 | /home | /opt | /home | / - server-6 | /home | /opt | /home | / - server-7 | /home | /opt | /home | / - server-8 | /home | /opt | /home | / - server-9 | /home | /opt | /home | / +TASKS + + host | ref | Override inline ref | Inline | Override inline +--------------------+-------+---------------------+--------+----------------- + localhost | /home | /opt | /home | / + 172.24.2.2 | /home | /opt | /home | / + 172.24.2.4 | /home | /opt | /home | / + 172.24.2.2 | /home | /opt | /home | / + 172.24.2.4 | /home | /opt | /home | / + 172.24.2.2 | /home | /opt | /home | / + 172.24.2.4 | /home | /opt | /home | / + 172.24.2.2 | /home | /opt | /home | / + 172.24.2.3 | /home | /opt | /home | / + 172.24.2.4 | /home | /opt | /home | / + 172.24.2.5 | /home | /opt | /home | / + 172.24.2.6 | /home | /opt | /home | / + 172.24.2.7 | /home | /opt | /home | / + 172.24.2.8 | /home | /opt | /home | / + 172.24.2.9 | /home | /opt | /home | / + 2001:3984:3989::10 | /home | /opt | /home | / + + localhost ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=64 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-25.stdout b/test/integration/golden/golden-25.stdout index d537bfb..7bf7182 100755 --- a/test/integration/golden/golden-25.stdout +++ b/test/integration/golden/golden-25.stdout @@ -1,26 +1,47 @@ Index: 25 Name: Work Dir 2 -Cmd: go run ../../main.go run work-dir-2 -S -t reachable +Cmd: go run ../../main.go run work-dir-2 -q -t reachable WantErr: false --- - server | ref | Override inline ref | Inline | Override inline ------------+------+---------------------+------------+----------------- - localhost | /usr | /opt | /tmp | / - list-0 | /usr | /opt | /home/test | / - list-1 | /usr | /opt | /home/test | / - range-0 | /usr | /opt | /home/test | / - range-1 | /usr | /opt | /home/test | / - inv-0 | /usr | /opt | /home/test | / - inv-1 | /usr | /opt | /home/test | / - server-1 | /usr | /opt | /home/test | / - server-2 | /usr | /opt | /home/test | / - server-3 | /usr | /opt | /home/test | / - server-4 | /usr | /opt | /home/test | / - server-5 | /usr | /opt | /home/test | / - server-6 | /usr | /opt | /home/test | / - server-7 | /usr | /opt | /home/test | / - server-8 | /usr | /opt | /home/test | / - server-9 | /usr | /opt | /home/test | / +TASKS + + host | ref | Override inline ref | Inline | Override inline +--------------------+------+---------------------+------------+----------------- + localhost | /usr | /opt | /tmp | / + 172.24.2.2 | /usr | /opt | /home/test | / + 172.24.2.4 | /usr | /opt | /home/test | / + 172.24.2.2 | /usr | /opt | /home/test | / + 172.24.2.4 | /usr | /opt | /home/test | / + 172.24.2.2 | /usr | /opt | /home/test | / + 172.24.2.4 | /usr | /opt | /home/test | / + 172.24.2.2 | /usr | /opt | /home/test | / + 172.24.2.3 | /usr | /opt | /home/test | / + 172.24.2.4 | /usr | /opt | /home/test | / + 172.24.2.5 | /usr | /opt | /home/test | / + 172.24.2.6 | /usr | /opt | /home/test | / + 172.24.2.7 | /usr | /opt | /home/test | / + 172.24.2.8 | /usr | /opt | /home/test | / + 172.24.2.9 | /usr | /opt | /home/test | / + 2001:3984:3989::10 | /usr | /opt | /home/test | / + + localhost ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=64 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-26.stdout b/test/integration/golden/golden-26.stdout index c81d28a..e6e9d57 100755 --- a/test/integration/golden/golden-26.stdout +++ b/test/integration/golden/golden-26.stdout @@ -1,26 +1,47 @@ Index: 26 Name: Work Dir 3 -Cmd: go run ../../main.go run work-dir-3 -S -t reachable +Cmd: go run ../../main.go run work-dir-3 -q -t reachable WantErr: false --- - server | ref | ref ------------+------+------ - localhost | /usr | /etc - list-0 | /usr | /etc - list-1 | /usr | /etc - range-0 | /usr | /etc - range-1 | /usr | /etc - inv-0 | /usr | /etc - inv-1 | /usr | /etc - server-1 | /usr | /etc - server-2 | /usr | /etc - server-3 | /usr | /etc - server-4 | /usr | /etc - server-5 | /usr | /etc - server-6 | /usr | /etc - server-7 | /usr | /etc - server-8 | /usr | /etc - server-9 | /usr | /etc +TASKS + + host | ref | ref +--------------------+------+------ + localhost | /usr | /etc + 172.24.2.2 | /usr | /etc + 172.24.2.4 | /usr | /etc + 172.24.2.2 | /usr | /etc + 172.24.2.4 | /usr | /etc + 172.24.2.2 | /usr | /etc + 172.24.2.4 | /usr | /etc + 172.24.2.2 | /usr | /etc + 172.24.2.3 | /usr | /etc + 172.24.2.4 | /usr | /etc + 172.24.2.5 | /usr | /etc + 172.24.2.6 | /usr | /etc + 172.24.2.7 | /usr | /etc + 172.24.2.8 | /usr | /etc + 172.24.2.9 | /usr | /etc + 2001:3984:3989::10 | /usr | /etc + + localhost ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=32 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-27.stdout b/test/integration/golden/golden-27.stdout index 0fec558..bb18184 100755 --- a/test/integration/golden/golden-27.stdout +++ b/test/integration/golden/golden-27.stdout @@ -1,42 +1,47 @@ Index: 27 -Name: fatal false -Cmd: go run ../../main.go run fatal -S -t reachable +Name: Register 1 +Cmd: go run ../../main.go run register-1 -q -t reachable WantErr: false --- - server | fatal ------------+------------------------------ - localhost | - | exit status 1 - list-0 | - | Process exited with status 1 - list-1 | - | Process exited with status 1 - range-0 | - | Process exited with status 1 - range-1 | - | Process exited with status 1 - inv-0 | - | Process exited with status 1 - inv-1 | - | Process exited with status 1 - server-1 | - | Process exited with status 1 - server-2 | - | Process exited with status 1 - server-3 | - | Process exited with status 1 - server-4 | - | Process exited with status 1 - server-5 | - | Process exited with status 1 - server-6 | - | Process exited with status 1 - server-7 | - | Process exited with status 1 - server-8 | - | Process exited with status 1 - server-9 | - | Process exited with status 1 +TASKS + + host | task-0 +--------------------+-------- + localhost | foo + 172.24.2.2 | foo + 172.24.2.4 | foo + 172.24.2.2 | foo + 172.24.2.4 | foo + 172.24.2.2 | foo + 172.24.2.4 | foo + 172.24.2.2 | foo + 172.24.2.3 | foo + 172.24.2.4 | foo + 172.24.2.5 | foo + 172.24.2.6 | foo + 172.24.2.7 | foo + 172.24.2.8 | foo + 172.24.2.9 | foo + 2001:3984:3989::10 | foo + + localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-28.stdout b/test/integration/golden/golden-28.stdout index c7e6d39..5d7142b 100755 --- a/test/integration/golden/golden-28.stdout +++ b/test/integration/golden/golden-28.stdout @@ -1,28 +1,207 @@ Index: 28 -Name: fatal true -Cmd: go run ../../main.go run fatal-true -S -t reachable -WantErr: true +Name: Register 2 +Cmd: go run ../../main.go run register-2 -q -t reachable +WantErr: false --- - server | fatal-true ------------+--------------- - localhost | - | exit status 1 - list-0 | - list-1 | - range-0 | - range-1 | - inv-0 | - inv-1 | - server-1 | - server-2 | - server-3 | - server-4 | - server-5 | - server-6 | - server-7 | - server-8 | - server-9 | +TASKS + + host | task-0 | task-1 | task-2 | task-3 +--------------------+--------+---------------+---------+----------------- + localhost | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.2 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.4 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.2 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.4 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.2 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.4 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.2 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.3 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.4 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.5 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.6 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.7 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.8 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.9 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 2001:3984:3989::10 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + + localhost ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=64 unreachable=0 ignored=0 failed=0 skipped=0 -exit status 1 diff --git a/test/integration/golden/golden-29.stdout b/test/integration/golden/golden-29.stdout index aa7a2be..f64b4de 100755 --- a/test/integration/golden/golden-29.stdout +++ b/test/integration/golden/golden-29.stdout @@ -1,42 +1,64 @@ Index: 29 -Name: ignore_errors false -Cmd: go run ../../main.go run errors -S -t reachable -WantErr: false +Name: fatal false +Cmd: go run ../../main.go run fatal -q -t reachable +WantErr: true --- - server | output | output | output ------------+--------+-------------------------------+-------- - localhost | 123 | | - | | exit status 65 | - list-0 | 123 | | - | | Process exited with status 65 | - list-1 | 123 | | - | | Process exited with status 65 | - range-0 | 123 | | - | | Process exited with status 65 | - range-1 | 123 | | - | | Process exited with status 65 | - inv-0 | 123 | | - | | Process exited with status 65 | - inv-1 | 123 | | - | | Process exited with status 65 | - server-1 | 123 | | - | | Process exited with status 65 | - server-2 | 123 | | - | | Process exited with status 65 | - server-3 | 123 | | - | | Process exited with status 65 | - server-4 | 123 | | - | | Process exited with status 65 | - server-5 | 123 | | - | | Process exited with status 65 | - server-6 | 123 | | - | | Process exited with status 65 | - server-7 | 123 | | - | | Process exited with status 65 | - server-8 | 123 | | - | | Process exited with status 65 | - server-9 | 123 | | - | | Process exited with status 65 | +TASKS + host | fatal +--------------------+------------------------------ + localhost | + | exit status 1 + 172.24.2.2 | + | Process exited with status 1 + 172.24.2.4 | + | Process exited with status 1 + 172.24.2.2 | + | Process exited with status 1 + 172.24.2.4 | + | Process exited with status 1 + 172.24.2.2 | + | Process exited with status 1 + 172.24.2.4 | + | Process exited with status 1 + 172.24.2.2 | + | Process exited with status 1 + 172.24.2.3 | + | Process exited with status 1 + 172.24.2.4 | + | Process exited with status 1 + 172.24.2.5 | + | Process exited with status 1 + 172.24.2.6 | + | Process exited with status 1 + 172.24.2.7 | + | Process exited with status 1 + 172.24.2.8 | + | Process exited with status 1 + 172.24.2.9 | + | Process exited with status 1 + 2001:3984:3989::10 | + | Process exited with status 1 + + localhost ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.3 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.5 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.6 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.7 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.8 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.9 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 2001:3984:3989::10 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 +------------------------------------------------------------------------------ + Total ok=0 unreachable=0 ignored=0 failed=16 skipped=0 + +exit status 1 diff --git a/test/integration/golden/golden-3.stdout b/test/integration/golden/golden-3.stdout index fc5f895..77792f1 100755 --- a/test/integration/golden/golden-3.stdout +++ b/test/integration/golden/golden-3.stdout @@ -5,8 +5,8 @@ WantErr: false --- - server | host | desc ---------+------------+----------------------- - list-0 | 172.24.2.2 | many hosts using list - list-1 | 172.24.2.4 | many hosts using list + server | host | tags | desc +--------+------------+----------------------------+----------------------- + list-0 | 172.24.2.2 | remote,prod,list,reachable | many hosts using list + list-1 | 172.24.2.4 | remote,prod,list,reachable | many hosts using list diff --git a/test/integration/golden/golden-30.stdout b/test/integration/golden/golden-30.stdout index 3945d66..1af6aa3 100755 --- a/test/integration/golden/golden-30.stdout +++ b/test/integration/golden/golden-30.stdout @@ -1,42 +1,64 @@ Index: 30 -Name: ignore_errors true -Cmd: go run ../../main.go run errors-true -S -t reachable -WantErr: false +Name: fatal true +Cmd: go run ../../main.go run fatal-true -q -t reachable +WantErr: true --- - server | output | output | output ------------+--------+-------------------------------+-------- - localhost | 123 | | 321 - | | exit status 65 | - list-0 | 123 | | 321 - | | Process exited with status 65 | - list-1 | 123 | | 321 - | | Process exited with status 65 | - range-0 | 123 | | 321 - | | Process exited with status 65 | - range-1 | 123 | | 321 - | | Process exited with status 65 | - inv-0 | 123 | | 321 - | | Process exited with status 65 | - inv-1 | 123 | | 321 - | | Process exited with status 65 | - server-1 | 123 | | 321 - | | Process exited with status 65 | - server-2 | 123 | | 321 - | | Process exited with status 65 | - server-3 | 123 | | 321 - | | Process exited with status 65 | - server-4 | 123 | | 321 - | | Process exited with status 65 | - server-5 | 123 | | 321 - | | Process exited with status 65 | - server-6 | 123 | | 321 - | | Process exited with status 65 | - server-7 | 123 | | 321 - | | Process exited with status 65 | - server-8 | 123 | | 321 - | | Process exited with status 65 | - server-9 | 123 | | 321 - | | Process exited with status 65 | +TASKS + host | fatal-true +--------------------+------------------------------ + localhost | + | exit status 1 + 172.24.2.2 | + | Process exited with status 1 + 172.24.2.4 | + | Process exited with status 1 + 172.24.2.2 | + | Process exited with status 1 + 172.24.2.4 | + | Process exited with status 1 + 172.24.2.2 | + | Process exited with status 1 + 172.24.2.4 | + | Process exited with status 1 + 172.24.2.2 | + | Process exited with status 1 + 172.24.2.3 | + | Process exited with status 1 + 172.24.2.4 | + | Process exited with status 1 + 172.24.2.5 | + | Process exited with status 1 + 172.24.2.6 | + | Process exited with status 1 + 172.24.2.7 | + | Process exited with status 1 + 172.24.2.8 | + | Process exited with status 1 + 172.24.2.9 | + | Process exited with status 1 + 2001:3984:3989::10 | + | Process exited with status 1 + + localhost ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.3 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.5 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.6 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.7 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.8 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.9 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 2001:3984:3989::10 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 +------------------------------------------------------------------------------ + Total ok=0 unreachable=0 ignored=0 failed=16 skipped=0 + +exit status 1 diff --git a/test/integration/golden/golden-31.stdout b/test/integration/golden/golden-31.stdout index a55b488..61949c6 100755 --- a/test/integration/golden/golden-31.stdout +++ b/test/integration/golden/golden-31.stdout @@ -1,15 +1,64 @@ Index: 31 -Name: unreachable false -Cmd: go run ../../main.go run unreachable -S -a +Name: ignore_errors false +Cmd: go run ../../main.go run errors -q -t reachable WantErr: true --- - - Unreachable Hosts - - server | host | user | port | error --------------+-----------------+------+------+------------------------------------------------ - unreachable | unreachable.lan | test | 22 | dial tcp: lookup unreachable.lan: no such host +TASKS -exit status 4 + host | task-0 | task-1 | task-2 +--------------------+--------+-------------------------------+-------- + localhost | 123 | | + | | exit status 65 | + 172.24.2.2 | 123 | | + | | Process exited with status 65 | + 172.24.2.4 | 123 | | + | | Process exited with status 65 | + 172.24.2.2 | 123 | | + | | Process exited with status 65 | + 172.24.2.4 | 123 | | + | | Process exited with status 65 | + 172.24.2.2 | 123 | | + | | Process exited with status 65 | + 172.24.2.4 | 123 | | + | | Process exited with status 65 | + 172.24.2.2 | 123 | | + | | Process exited with status 65 | + 172.24.2.3 | 123 | | + | | Process exited with status 65 | + 172.24.2.4 | 123 | | + | | Process exited with status 65 | + 172.24.2.5 | 123 | | + | | Process exited with status 65 | + 172.24.2.6 | 123 | | + | | Process exited with status 65 | + 172.24.2.7 | 123 | | + | | Process exited with status 65 | + 172.24.2.8 | 123 | | + | | Process exited with status 65 | + 172.24.2.9 | 123 | | + | | Process exited with status 65 | + 2001:3984:3989::10 | 123 | | + | | Process exited with status 65 | + + localhost ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 +-------------------------------------------------------------------------------- + Total ok=16 unreachable=0 ignored=0 failed=16 skipped=16 + +exit status 65 diff --git a/test/integration/golden/golden-32.stdout b/test/integration/golden/golden-32.stdout index 202c678..60cca28 100755 --- a/test/integration/golden/golden-32.stdout +++ b/test/integration/golden/golden-32.stdout @@ -1,78 +1,63 @@ Index: 32 -Name: unreachable true -Cmd: go run ../../main.go run unreachable-true -S -a +Name: ignore_errors true +Cmd: go run ../../main.go run errors-true -q -t reachable WantErr: false --- - - Unreachable Hosts - - server | host | user | port | error --------------+-----------------+------+------+------------------------------------------------ - unreachable | unreachable.lan | test | 22 | dial tcp: lookup unreachable.lan: no such host +TASKS + + host | task-0 | task-1 | task-2 +--------------------+--------+-------------------------------+-------- + localhost | 123 | | 321 + | | exit status 65 | + 172.24.2.2 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.4 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.2 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.4 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.2 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.4 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.2 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.3 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.4 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.5 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.6 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.7 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.8 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.9 | 123 | | 321 + | | Process exited with status 65 | + 2001:3984:3989::10 | 123 | | 321 + | | Process exited with status 65 | + + localhost ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.3 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.5 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.6 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.7 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.8 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.9 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 2001:3984:3989::10 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 +------------------------------------------------------------------------------- + Total ok=32 unreachable=0 ignored=16 failed=0 skipped=0 - -TASK [unreachable-true] - -localhost | 123 - -TASK [unreachable-true] - -172.24.2.2 | 123 - -TASK [unreachable-true] - -172.24.2.4 | 123 - -TASK [unreachable-true] - -172.24.2.2 | 123 - -TASK [unreachable-true] - -172.24.2.4 | 123 - -TASK [unreachable-true] - -172.24.2.2 | 123 - -TASK [unreachable-true] - -172.24.2.4 | 123 - -TASK [unreachable-true] - -172.24.2.2 | 123 - -TASK [unreachable-true] - -172.24.2.3 | 123 - -TASK [unreachable-true] - -172.24.2.4 | 123 - -TASK [unreachable-true] - -172.24.2.5 | 123 - -TASK [unreachable-true] - -172.24.2.6 | 123 - -TASK [unreachable-true] - -172.24.2.7 | 123 - -TASK [unreachable-true] - -172.24.2.8 | 123 - -TASK [unreachable-true] - -172.24.2.9 | 123 - -TASK [unreachable-true] - -2001:3984:3989::10 | 123 diff --git a/test/integration/golden/golden-33.stdout b/test/integration/golden/golden-33.stdout index c57636c..c2e0655 100755 --- a/test/integration/golden/golden-33.stdout +++ b/test/integration/golden/golden-33.stdout @@ -1,26 +1,15 @@ Index: 33 -Name: omit_empty false -Cmd: go run ../../main.go run empty -S -t reachable -WantErr: false +Name: unreachable false +Cmd: go run ../../main.go run unreachable -q -a +WantErr: true --- - server | empty ------------+-------- - localhost | - list-0 | Exists - list-1 | Exists - range-0 | Exists - range-1 | Exists - inv-0 | Exists - inv-1 | Exists - server-1 | Exists - server-2 | Exists - server-3 | Exists - server-4 | Exists - server-5 | Exists - server-6 | Exists - server-7 | Exists - server-8 | Exists - server-9 | Exists + + Unreachable Hosts + + server | host | user | port | error +-------------+-----------------+------+------+------------------------------------------------ + unreachable | unreachable.lan | test | 22 | dial tcp: lookup unreachable.lan: no such host +exit status 4 diff --git a/test/integration/golden/golden-34.stdout b/test/integration/golden/golden-34.stdout index 40618c2..a453097 100755 --- a/test/integration/golden/golden-34.stdout +++ b/test/integration/golden/golden-34.stdout @@ -1,25 +1,56 @@ Index: 34 -Name: omit_empty true -Cmd: go run ../../main.go run empty-true -S -t reachable +Name: unreachable true +Cmd: go run ../../main.go run unreachable-true -o table -q -a WantErr: false --- - server | empty-true -----------+------------ - list-0 | Exists - list-1 | Exists - range-0 | Exists - range-1 | Exists - inv-0 | Exists - inv-1 | Exists - server-1 | Exists - server-2 | Exists - server-3 | Exists - server-4 | Exists - server-5 | Exists - server-6 | Exists - server-7 | Exists - server-8 | Exists - server-9 | Exists + + Unreachable Hosts + + server | host | user | port | error +-------------+-----------------+------+------+------------------------------------------------ + unreachable | unreachable.lan | test | 22 | dial tcp: lookup unreachable.lan: no such host + + +TASKS + + host | unreachable-true +--------------------+------------------ + localhost | 123 + 172.24.2.2 | 123 + 172.24.2.4 | 123 + 172.24.2.2 | 123 + 172.24.2.4 | 123 + 172.24.2.2 | 123 + 172.24.2.4 | 123 + 172.24.2.2 | 123 + 172.24.2.3 | 123 + 172.24.2.4 | 123 + 172.24.2.5 | 123 + 172.24.2.6 | 123 + 172.24.2.7 | 123 + 172.24.2.8 | 123 + 172.24.2.9 | 123 + 2001:3984:3989::10 | 123 + + localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + unreachable.lan ok=0 unreachable=1 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=16 unreachable=1 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-35.stdout b/test/integration/golden/golden-35.stdout index b0b337d..ed180cc 100755 --- a/test/integration/golden/golden-35.stdout +++ b/test/integration/golden/golden-35.stdout @@ -1,26 +1,47 @@ Index: 35 -Name: output -Cmd: go run ../../main.go run output -S -t reachable +Name: omit_empty false +Cmd: go run ../../main.go run empty -q -t reachable WantErr: false --- - server | output | output | output ------------+-------------+-----------+------------------- - localhost | Hello world | Bye world | Hello again world - list-0 | Hello world | Bye world | Hello again world - list-1 | Hello world | Bye world | Hello again world - range-0 | Hello world | Bye world | Hello again world - range-1 | Hello world | Bye world | Hello again world - inv-0 | Hello world | Bye world | Hello again world - inv-1 | Hello world | Bye world | Hello again world - server-1 | Hello world | Bye world | Hello again world - server-2 | Hello world | Bye world | Hello again world - server-3 | Hello world | Bye world | Hello again world - server-4 | Hello world | Bye world | Hello again world - server-5 | Hello world | Bye world | Hello again world - server-6 | Hello world | Bye world | Hello again world - server-7 | Hello world | Bye world | Hello again world - server-8 | Hello world | Bye world | Hello again world - server-9 | Hello world | Bye world | Hello again world +TASKS + + host | empty +--------------------+-------- + localhost | + 172.24.2.2 | Exists + 172.24.2.4 | Exists + 172.24.2.2 | Exists + 172.24.2.4 | Exists + 172.24.2.2 | Exists + 172.24.2.4 | Exists + 172.24.2.2 | Exists + 172.24.2.3 | Exists + 172.24.2.4 | Exists + 172.24.2.5 | Exists + 172.24.2.6 | Exists + 172.24.2.7 | Exists + 172.24.2.8 | Exists + 172.24.2.9 | Exists + 2001:3984:3989::10 | Exists + + localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-36.stdout b/test/integration/golden/golden-36.stdout index 9de8f97..d3a1ddb 100755 --- a/test/integration/golden/golden-36.stdout +++ b/test/integration/golden/golden-36.stdout @@ -1,70 +1,46 @@ Index: 36 -Name: Run exec command -Cmd: go run ../../main.go exec 'echo 123' -S -t reachable +Name: omit_empty true +Cmd: go run ../../main.go run empty-true -q -t reachable WantErr: false --- -TASK +TASKS + + host | empty-true +--------------------+------------ + 172.24.2.2 | Exists + 172.24.2.4 | Exists + 172.24.2.2 | Exists + 172.24.2.4 | Exists + 172.24.2.2 | Exists + 172.24.2.4 | Exists + 172.24.2.2 | Exists + 172.24.2.3 | Exists + 172.24.2.4 | Exists + 172.24.2.5 | Exists + 172.24.2.6 | Exists + 172.24.2.7 | Exists + 172.24.2.8 | Exists + 172.24.2.9 | Exists + 2001:3984:3989::10 | Exists + + localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 -localhost | 123 - -TASK - -172.24.2.2 | 123 - -TASK - -172.24.2.4 | 123 - -TASK - -172.24.2.2 | 123 - -TASK - -172.24.2.4 | 123 - -TASK - -172.24.2.2 | 123 - -TASK - -172.24.2.4 | 123 - -TASK - -172.24.2.2 | 123 - -TASK - -172.24.2.3 | 123 - -TASK - -172.24.2.4 | 123 - -TASK - -172.24.2.5 | 123 - -TASK - -172.24.2.6 | 123 - -TASK - -172.24.2.7 | 123 - -TASK - -172.24.2.8 | 123 - -TASK - -172.24.2.9 | 123 - -TASK - -2001:3984:3989::10 | 123 diff --git a/test/integration/golden/golden-37.stdout b/test/integration/golden/golden-37.stdout new file mode 100755 index 0000000..41554e9 --- /dev/null +++ b/test/integration/golden/golden-37.stdout @@ -0,0 +1,47 @@ +Index: 37 +Name: output +Cmd: go run ../../main.go run output -q -t reachable +WantErr: false + +--- + +TASKS + + host | task-0 | task-1 | task-2 +--------------------+-------------+-----------+------------------- + localhost | Hello world | Bye world | Hello again world + 172.24.2.2 | Hello world | Bye world | Hello again world + 172.24.2.4 | Hello world | Bye world | Hello again world + 172.24.2.2 | Hello world | Bye world | Hello again world + 172.24.2.4 | Hello world | Bye world | Hello again world + 172.24.2.2 | Hello world | Bye world | Hello again world + 172.24.2.4 | Hello world | Bye world | Hello again world + 172.24.2.2 | Hello world | Bye world | Hello again world + 172.24.2.3 | Hello world | Bye world | Hello again world + 172.24.2.4 | Hello world | Bye world | Hello again world + 172.24.2.5 | Hello world | Bye world | Hello again world + 172.24.2.6 | Hello world | Bye world | Hello again world + 172.24.2.7 | Hello world | Bye world | Hello again world + 172.24.2.8 | Hello world | Bye world | Hello again world + 172.24.2.9 | Hello world | Bye world | Hello again world + 2001:3984:3989::10 | Hello world | Bye world | Hello again world + + localhost ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=48 unreachable=0 ignored=0 failed=0 skipped=0 + diff --git a/test/integration/golden/golden-38.stdout b/test/integration/golden/golden-38.stdout new file mode 100755 index 0000000..227cca1 --- /dev/null +++ b/test/integration/golden/golden-38.stdout @@ -0,0 +1,47 @@ +Index: 38 +Name: Run exec command +Cmd: go run ../../main.go exec 'echo 123' -q -t reachable +WantErr: false + +--- + +TASKS + + host | task-0 +--------------------+-------- + localhost | 123 + 172.24.2.2 | 123 + 172.24.2.4 | 123 + 172.24.2.2 | 123 + 172.24.2.4 | 123 + 172.24.2.2 | 123 + 172.24.2.4 | 123 + 172.24.2.2 | 123 + 172.24.2.3 | 123 + 172.24.2.4 | 123 + 172.24.2.5 | 123 + 172.24.2.6 | 123 + 172.24.2.7 | 123 + 172.24.2.8 | 123 + 172.24.2.9 | 123 + 2001:3984:3989::10 | 123 + + localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 + diff --git a/test/integration/golden/golden-4.stdout b/test/integration/golden/golden-4.stdout index 1035c90..151fa83 100755 --- a/test/integration/golden/golden-4.stdout +++ b/test/integration/golden/golden-4.stdout @@ -5,8 +5,8 @@ WantErr: false --- - server | host | desc ----------+------------+------------------------ - range-0 | 172.24.2.2 | many hosts using range - range-1 | 172.24.2.4 | many hosts using range + server | host | tags | desc +---------+------------+-----------------------------+------------------------ + range-0 | 172.24.2.2 | remote,prod,range,reachable | many hosts using range + range-1 | 172.24.2.4 | remote,prod,range,reachable | many hosts using range diff --git a/test/integration/golden/golden-5.stdout b/test/integration/golden/golden-5.stdout index d2786c7..dff04f3 100755 --- a/test/integration/golden/golden-5.stdout +++ b/test/integration/golden/golden-5.stdout @@ -5,8 +5,8 @@ WantErr: false --- - server | host | desc ---------+------------+---------------------------- - inv-0 | 172.24.2.2 | many hosts using inventory - inv-1 | 172.24.2.4 | many hosts using inventory + server | host | tags | desc +--------+------------+---------------------------+---------------------------- + inv-0 | 172.24.2.2 | remote,prod,inv,reachable | many hosts using inventory + inv-1 | 172.24.2.4 | remote,prod,inv,reachable | many hosts using inventory diff --git a/test/integration/main_test.go b/test/integration/main_test.go index 6e4b332..ee15931 100644 --- a/test/integration/main_test.go +++ b/test/integration/main_test.go @@ -6,7 +6,6 @@ import ( "log" "strings" - "io/ioutil" "os" "os/exec" "path" @@ -51,7 +50,7 @@ func clearGolden(file string) { } func clearTmp() { - files, _ := ioutil.ReadDir(".") + files, _ := os.ReadDir(".") for _, f := range files { filepath := path.Join(tmpDir, f.Name()) os.Remove(filepath) @@ -115,7 +114,7 @@ func Run(t *testing.T, tt TemplateTest) { } // Write output to tmp file which will be used to compare with golden files - err = ioutil.WriteFile(tt.Golden, tt.GoldenOutput(output), 0644) + err = os.WriteFile(tt.Golden, tt.GoldenOutput(output), 0644) if err != nil { t.Fatalf("could not write %s: %v", tt.Golden, err) } @@ -125,17 +124,17 @@ func Run(t *testing.T, tt TemplateTest) { clearGolden(goldenFilePath) // Write stdout of test command to golden file - err = ioutil.WriteFile(goldenFilePath, tt.GoldenOutput(output), os.ModePerm) + err = os.WriteFile(goldenFilePath, tt.GoldenOutput(output), os.ModePerm) if err != nil { t.Fatalf("could not write %s: %v", goldenFilePath, err) } } else { - actual, err := ioutil.ReadFile(tt.Golden) + actual, err := os.ReadFile(tt.Golden) if err != nil { t.Fatalf("Error: %v", err) } - expected, err := ioutil.ReadFile(goldenFilePath) + expected, err := os.ReadFile(goldenFilePath) if err != nil { t.Fatalf("Error: %v", err) } diff --git a/test/integration/run_test.go b/test/integration/run_test.go index 402eeea..64ab02a 100644 --- a/test/integration/run_test.go +++ b/test/integration/run_test.go @@ -74,142 +74,154 @@ var cases = []TemplateTest{ // run basic tasks { TestName: "Ping all servers", - TestCmd: "go run ../../main.go run ping -S -t reachable", + TestCmd: "go run ../../main.go run ping -q -t reachable", WantErr: false, }, { TestName: "Multiple commands", - TestCmd: "go run ../../main.go run info -S -t prod", + TestCmd: "go run ../../main.go run info -q -t prod", WantErr: false, }, { TestName: "Filter by hosts server using server name", - TestCmd: "go run ../../main.go run info -S -s list-1", + TestCmd: "go run ../../main.go run info -q -s list-1", WantErr: false, }, { TestName: "Filter by hosts server using range index", - TestCmd: "go run ../../main.go run info -S -s 'list[0]'", + TestCmd: "go run ../../main.go run info -q -s 'list[0]'", WantErr: false, }, { TestName: "Filter by hosts server", - TestCmd: "go run ../../main.go run info -S -s 'list[0:2]'", + TestCmd: "go run ../../main.go run info -q -s 'list[0:2]'", WantErr: false, }, { TestName: "Filter by host regex", - TestCmd: "go run ../../main.go run info -S -r '172.24.2.(2|4)'", + TestCmd: "go run ../../main.go run info -q -r '172.24.2.(2|4)'", WantErr: false, }, { TestName: "Limit to 2 servers", - TestCmd: "go run ../../main.go run ping -S -t reachable -l 2", + TestCmd: "go run ../../main.go run ping -q -t reachable -l 2", WantErr: false, }, { TestName: "Limit to 50 percent servers", - TestCmd: "go run ../../main.go run ping -S -t reachable -L 50", + TestCmd: "go run ../../main.go run ping -q -t reachable -L 50", WantErr: false, }, { TestName: "Filter by inverting on tag unreachable", - TestCmd: "go run ../../main.go run ping -S -t unreachable -v", + TestCmd: "go run ../../main.go run ping -q -t unreachable -v", WantErr: false, }, // run tasks and display env { TestName: "Simple Envs", - TestCmd: "go run ../../main.go run env -S -t reachable", + TestCmd: "go run ../../main.go run env -q -t reachable", WantErr: false, }, { TestName: "Reference Envs", - TestCmd: "go run ../../main.go run env-complex -S -t reachable", + TestCmd: "go run ../../main.go run env-complex -q -t reachable", WantErr: false, }, { TestName: "Default Envs", - TestCmd: "go run ../../main.go run env-default -S -t reachable", + TestCmd: "go run ../../main.go run env-default -q -t reachable", WantErr: false, }, // run nested tasks { TestName: "Nested tasks", - TestCmd: "go run ../../main.go run d -S -t reachable", + TestCmd: "go run ../../main.go run d -q -t reachable", WantErr: false, }, // run tasks and modify work dir { TestName: "Work Dir 1", - TestCmd: "go run ../../main.go run work-dir-1 -S -t reachable", + TestCmd: "go run ../../main.go run work-dir-1 -q -t reachable", WantErr: false, }, { TestName: "Work Dir 2", - TestCmd: "go run ../../main.go run work-dir-2 -S -t reachable", + TestCmd: "go run ../../main.go run work-dir-2 -q -t reachable", WantErr: false, }, { TestName: "Work Dir 3", - TestCmd: "go run ../../main.go run work-dir-3 -S -t reachable", + TestCmd: "go run ../../main.go run work-dir-3 -q -t reachable", + WantErr: false, + }, + + // run tasks and register variables + { + TestName: "Register 1", + TestCmd: "go run ../../main.go run register-1 -q -t reachable", + WantErr: false, + }, + { + TestName: "Register 2", + TestCmd: "go run ../../main.go run register-2 -q -t reachable", WantErr: false, }, // Tests for running tasks with various specs { TestName: "fatal false", - TestCmd: "go run ../../main.go run fatal -S -t reachable", - WantErr: false, + TestCmd: "go run ../../main.go run fatal -q -t reachable", + WantErr: true, }, { TestName: "fatal true", - TestCmd: "go run ../../main.go run fatal-true -S -t reachable", + TestCmd: "go run ../../main.go run fatal-true -q -t reachable", WantErr: true, }, { TestName: "ignore_errors false", - TestCmd: "go run ../../main.go run errors -S -t reachable", - WantErr: false, + TestCmd: "go run ../../main.go run errors -q -t reachable", + WantErr: true, }, { TestName: "ignore_errors true", - TestCmd: "go run ../../main.go run errors-true -S -t reachable", + TestCmd: "go run ../../main.go run errors-true -q -t reachable", WantErr: false, }, { TestName: "unreachable false", - TestCmd: "go run ../../main.go run unreachable -S -a", + TestCmd: "go run ../../main.go run unreachable -q -a", WantErr: true, }, { TestName: "unreachable true", - TestCmd: "go run ../../main.go run unreachable-true -S -a", + TestCmd: "go run ../../main.go run unreachable-true -o table -q -a", WantErr: false, }, { TestName: "omit_empty false", - TestCmd: "go run ../../main.go run empty -S -t reachable", + TestCmd: "go run ../../main.go run empty -q -t reachable", WantErr: false, }, { TestName: "omit_empty true", - TestCmd: "go run ../../main.go run empty-true -S -t reachable", + TestCmd: "go run ../../main.go run empty-true -q -t reachable", WantErr: false, }, { TestName: "output", - TestCmd: "go run ../../main.go run output -S -t reachable", + TestCmd: "go run ../../main.go run output -q -t reachable", WantErr: false, }, // exec { TestName: "Run exec command", - TestCmd: "go run ../../main.go exec 'echo 123' -S -t reachable", + TestCmd: "go run ../../main.go exec 'echo 123' -q -t reachable", WantErr: false, }, } diff --git a/test/playground/env/sake.yaml b/test/playground/env/sake.yaml index 7356f9d..ca708a2 100644 --- a/test/playground/env/sake.yaml +++ b/test/playground/env/sake.yaml @@ -74,20 +74,13 @@ tasks: all: true cmd: | echo "# SERVER" - echo "SAKE_SERVER_NAME $SAKE_SERVER_NAME" - echo "SAKE_SERVER_DESC $SAKE_SERVER_DESC" - echo "SAKE_SERVER_TAGS $SAKE_SERVER_TAGS" - echo "SAKE_SERVER_HOST $SAKE_SERVER_HOST" - echo "SAKE_SERVER_USER $SAKE_SERVER_USER" - echo "SAKE_SERVER_PORT $SAKE_SERVER_PORT" - echo "SAKE_SERVER_LOCAL $SAKE_SERVER_LOCAL" + echo "S_TAGS $S_TAGS" + echo "S_HOST $S_HOST" + echo "S_USER $S_USER" + echo "S_PORT $S_PORT" echo echo "# TASK" - echo "SAKE_TASK_ID $SAKE_TASK_ID" - echo "SAKE_TASK_NAME $SAKE_TASK_NAME" - echo "SAKE_TASK_DESC $SAKE_TASK_DESC" - echo "SAKE_TASK_LOCAL $SAKE_TASK_LOCAL" echo echo "# CONFIG" diff --git a/test/playground/performance/sake.yaml b/test/playground/performance/sake.yaml index d4d0cc0..c89511e 100644 --- a/test/playground/performance/sake.yaml +++ b/test/playground/performance/sake.yaml @@ -721,7 +721,6 @@ specs: info: output: table - parallel: true ignore_errors: true ignore_unreachable: true any_errors_fatal: false diff --git a/test/playground/sake.yaml b/test/playground/sake.yaml index a01e306..a9add69 100644 --- a/test/playground/sake.yaml +++ b/test/playground/sake.yaml @@ -1,5 +1,5 @@ import: - - ./tasks.yaml + - ./tasks/tasks.yaml known_hosts_file: known_hosts disable_verify_host: true @@ -7,20 +7,30 @@ disable_verify_host: true servers: localhost: desc: localhost - host: localhost desc + host: 0.0.0.0 local: true tags: [local] + # performance: + # user: test + # identity_file: ../keys/id_ed25519_pem_no + # inventory: | + # num_hosts=$((9999 + $SAKE_NUM_HOSTS)) + # for port in $(seq 10000 $num_hosts); do echo "0.0.0.0:$port"; done + # env: + # SAKE_NUM_HOSTS: 1 + list: desc: many hosts using list of hosts hosts: - 172.24.2.2 - 172.24.2.4 - 172.24.2.5 - - 2001:3984:3989::10 + - 172.24.2.6 + - 172.24.2.7 user: test identity_file: ../keys/id_ed25519_pem_no - tags: [remote, pi, many, list] + tags: [remote, pi, many, list, "hej san"] env: hello: world host: 172.24.2.4 @@ -48,13 +58,13 @@ servers: server-1: desc: server-1 desc host: sake-resolve - tags: [remote, pi] + tags: [remote, pi, bastion] work_dir: /tmp server-2: desc: server-2 desc host: sake-resolve - tags: [remote, pi] + tags: [remote, pi, bastion] server-3: desc: server-3 desc @@ -62,6 +72,8 @@ servers: user: test identity_file: ../keys/id_ed25519_pem_no tags: [remote, pi, pihole] + env: + FOO: 123 server-4: desc: server-4 desc @@ -88,6 +100,7 @@ servers: targets: all: + desc: Select all all: true limit: @@ -96,7 +109,10 @@ targets: limit_p: all: true - limit_p: 50 + limit: 50 + + list: + servers: [list] regex: regex: 192 @@ -108,47 +124,89 @@ targets: tags: [remote, reachable] mult: + desc: Info all: true regex: 192 servers: [server-1, range] tags: [remote] limit: 3 - limit_p: 50 specs: table: output: table + # describe: true + silent: true text: output: text info: + desc: Info output: table - parallel: true ignore_errors: true ignore_unreachable: true any_errors_fatal: false - # default: - # output: text - # parallel: true + linear: + desc: Linear + strategy: linear + # forks: 2 + batch: 2 + # batch_p: 50 + output: text + ignore_errors: true + ignore_unreachable: false + any_errors_fatal: true + # max_fail_percentage: 60 + report: [recap] + + host_pinned: + desc: Host Pinned + strategy: host_pinned + order: sorted + # forks: 2 + batch: 2 + # batch_p: 50 + output: text + ignore_errors: true + ignore_unreachable: true + # any_errors_fatal: false + max_fail_percentage: 60 + report: [recap] + + free: + desc: Free + strategy: free + batch: 2 + # forks: 2 + output: text + ignore_errors: false + ignore_unreachable: true + # any_errors_fatal: true + max_fail_percentage: 60 + +# themes: +# default: +# text: +# header: '{{ .Style "TASK" "bold" }} {{ .Name }}' +# header_filler: "-" env: VERSION: v0.1.0 DATE: $(date -u +"%Y-%m-%dT%H:%M:%S%Z") tasks: - exit: - # name: hej - local: true - cmd: exit 3 - ping: target: all - spec: info + spec: host_pinned desc: ping server cmd: echo pong + exit: + # name: Exit + local: true + cmd: exit 3 + sleep: desc: ping server cmd: sleep 2 & echo done @@ -169,7 +227,7 @@ tasks: desc: print host spec: info target: all - cmd: echo $SAKE_SERVER_HOST + cmd: echo $S_HOST print-hostname: name: Hostname @@ -194,3 +252,39 @@ tasks: spec: info target: all cmd: uname -r | awk -v FS='-' '{print $1}' + + register: + tasks: + - cmd: echo "foo" && >&2 echo "error" + register: out + - cmd: | + echo "status: $out_status" + echo "rc: $out_rc" + echo "failed: $out_failed" + echo "stdout: $out_stdout" + echo "stderr: $out_stderr" + echo "out: $out" + + - cmd: echo "xyz" && >&2 echo "error 2" + register: out2 + - cmd: | + echo "status: $out_status" + echo "rc: $out_rc" + echo "failed: $out_failed" + echo "stdout: $out_stdout" + echo "stderr: $out_stderr" + echo "out: $out" + + echo "-------------" + + echo "status: $out2_status" + echo "rc: $out2_rc" + echo "failed: $out2_failed" + echo "stdout: $out2_stdout" + echo "stderr: $out2_stderr" + echo "out: $out2" + + register2: + tasks: + - cmd: echo "foo" && >&2 echo "error" + register: out diff --git a/test/playground/tasks.yaml b/test/playground/tasks/tasks.yaml similarity index 100% rename from test/playground/tasks.yaml rename to test/playground/tasks/tasks.yaml diff --git a/test/profiles/cpu.prof b/test/profiles/cpu.prof deleted file mode 100644 index 9cd0f3cba92e4f39289d9ca437a4f3931e237e3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4071 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 diff --git a/test/profiles/heap.prof b/test/profiles/heap.prof deleted file mode 100644 index ee7215f5d0dd76ad97a5e0187d2fe8f1cceb17cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4780 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 GCIA3&oo%H6 diff --git a/test/profiles/list-servers b/test/profiles/list-servers index f7747e7..2f85d24 100644 --- a/test/profiles/list-servers +++ b/test/profiles/list-servers @@ -1,4 +1,4 @@ Benchmark 1: ../dist/sake list servers - Time (mean ± σ): 5.2 ms ± 0.2 ms [User: 2.6 ms, System: 3.3 ms] - Range (min … max): 5.0 ms … 5.5 ms 5 runs + Time (mean ± σ): 5.2 ms ± 0.2 ms [User: 3.5 ms, System: 2.1 ms] + Range (min … max): 4.9 ms … 5.5 ms 10 runs diff --git a/test/profiles/nested b/test/profiles/nested index f7e6572..e837e2f 100644 --- a/test/profiles/nested +++ b/test/profiles/nested @@ -1,4 +1,4 @@ -Benchmark 1: ../dist/sake run d -t reachable - Time (mean ± σ): 839.9 ms ± 148.9 ms [User: 407.8 ms, System: 71.7 ms] - Range (min … max): 587.5 ms … 960.7 ms 5 runs +Benchmark 1: ../dist/sake run d --forks=1 -t reachable + Time (mean ± σ): 544.8 ms ± 21.1 ms [User: 408.1 ms, System: 62.8 ms] + Range (min … max): 505.6 ms … 568.8 ms 10 runs diff --git a/test/profiles/nested-parallel b/test/profiles/nested-parallel index 07d6a57..008595c 100644 --- a/test/profiles/nested-parallel +++ b/test/profiles/nested-parallel @@ -1,4 +1,4 @@ -Benchmark 1: ../dist/sake run d -p -t reachable - Time (mean ± σ): 516.5 ms ± 15.4 ms [User: 365.5 ms, System: 50.5 ms] - Range (min … max): 504.7 ms … 542.5 ms 5 runs +Benchmark 1: ../dist/sake run d --strategy=free -t reachable + Time (mean ± σ): 452.9 ms ± 36.7 ms [User: 364.3 ms, System: 34.0 ms] + Range (min … max): 416.8 ms … 525.4 ms 10 runs diff --git a/test/profiles/ping b/test/profiles/ping index 90c5af8..f753ba9 100644 --- a/test/profiles/ping +++ b/test/profiles/ping @@ -1,4 +1,4 @@ -Benchmark 1: ../dist/sake run ping -t reachable - Time (mean ± σ): 487.2 ms ± 65.1 ms [User: 338.6 ms, System: 22.6 ms] - Range (min … max): 417.1 ms … 557.9 ms 5 runs +Benchmark 1: ../dist/sake run ping --forks=1 -t reachable + Time (mean ± σ): 578.6 ms ± 22.1 ms [User: 351.1 ms, System: 31.7 ms] + Range (min … max): 552.7 ms … 613.5 ms 10 runs diff --git a/test/profiles/ping-no-key b/test/profiles/ping-no-key index 7a27ac9..d25bfa4 100644 --- a/test/profiles/ping-no-key +++ b/test/profiles/ping-no-key @@ -1,4 +1,4 @@ Benchmark 1: ../dist/sake run ping -s server-9 - Time (mean ± σ): 134.9 ms ± 31.6 ms [User: 9.5 ms, System: 6.2 ms] - Range (min … max): 97.9 ms … 169.3 ms 5 runs + Time (mean ± σ): 139.8 ms ± 31.8 ms [User: 8.7 ms, System: 9.4 ms] + Range (min … max): 106.4 ms … 199.5 ms 10 runs diff --git a/test/profiles/ping-parallel b/test/profiles/ping-parallel index 5fe8607..6f6a102 100644 --- a/test/profiles/ping-parallel +++ b/test/profiles/ping-parallel @@ -1,4 +1,4 @@ -Benchmark 1: ../dist/sake run ping -p -t reachable - Time (mean ± σ): 510.9 ms ± 43.9 ms [User: 351.5 ms, System: 30.6 ms] - Range (min … max): 459.1 ms … 573.6 ms 5 runs +Benchmark 1: ../dist/sake run ping --strategy=free -t reachable + Time (mean ± σ): 548.0 ms ± 40.0 ms [User: 324.8 ms, System: 39.7 ms] + Range (min … max): 471.7 ms … 603.9 ms 10 runs diff --git a/test/sake.yaml b/test/sake.yaml index bc47026..abc66af 100644 --- a/test/sake.yaml +++ b/test/sake.yaml @@ -11,6 +11,11 @@ targets: all: true specs: + default: + output: table + strategy: linear + batch: 1 + table: output: table @@ -19,7 +24,7 @@ specs: info: output: table - parallel: true + strategy: free ignore_errors: true ignore_unreachable: true any_errors_fatal: false diff --git a/test/servers.yaml b/test/servers.yaml index c0afc27..d95d4d7 100644 --- a/test/servers.yaml +++ b/test/servers.yaml @@ -56,7 +56,7 @@ servers: password: testing tags: [remote,prod,reachable] env: - host: 172.24.2.2 + host: 172.24.2.2 server-2: desc: server-2 diff --git a/test/tasks.yaml b/test/tasks.yaml index 679ce8c..fa35aa4 100644 --- a/test/tasks.yaml +++ b/test/tasks.yaml @@ -8,7 +8,7 @@ tasks: target: all local: true desc: ping server - cmd: ping $SAKE_SERVER_HOST -c 2 + cmd: ping $S_HOST -c 2 # Info print-host: @@ -16,7 +16,7 @@ tasks: desc: print host spec: info target: all - cmd: echo $SAKE_SERVER_HOST + cmd: echo $S_HOST print-hostname: name: Hostname @@ -105,24 +105,10 @@ tasks: all: true cmd: | echo "# SERVER" - echo "SAKE_SERVER_NAME $SAKE_SERVER_NAME" - echo "SAKE_SERVER_DESC $SAKE_SERVER_DESC" - echo "SAKE_SERVER_TAGS $SAKE_SERVER_TAGS" - echo "SAKE_SERVER_HOST $SAKE_SERVER_HOST" - echo "SAKE_SERVER_USER $SAKE_SERVER_USER" - echo "SAKE_SERVER_PORT $SAKE_SERVER_PORT" - echo "SAKE_SERVER_LOCAL $SAKE_SERVER_LOCAL" - - echo - echo "# TASK" - echo "SAKE_TASK_ID $SAKE_TASK_ID" - echo "SAKE_TASK_NAME $SAKE_TASK_NAME" - echo "SAKE_TASK_DESC $SAKE_TASK_DESC" - echo "SAKE_TASK_LOCAL $SAKE_TASK_LOCAL" - - echo - echo "# CONFIG" - echo "SAKE_KNOWN_HOSTS_FILE $SAKE_KNOWN_HOSTS_FILE" + echo "S_TAGS $S_TAGS" + echo "S_HOST $S_HOST" + echo "S_USER $S_USER" + echo "S_PORT $S_PORT" ################ # NESTED TASKS # @@ -214,6 +200,43 @@ tasks: - task: work-nested work_dir: /etc + ############ + # REGISTER # + ############ + register-1: + tasks: + - cmd: echo "foo" + register: out + + register-2: + tasks: + - cmd: echo "foo" + register: out + - cmd: | + echo "status: $out_status" + echo "rc: $out_rc" + echo "failed: $out_failed" + echo "stdout: $out_stdout" + echo "stderr: $out_stderr" + + - cmd: | + >&2 echo "error 2" + register: out2 + - cmd: | + echo "status: $out_status" + echo "rc: $out_rc" + echo "failed: $out_failed" + echo "stdout: $out_stdout" + echo "stderr: $out_stderr" + + echo "-------------" + + echo "status: $out2_status" + echo "rc: $out2_rc" + echo "failed: $out2_failed" + echo "stdout: $out2_stdout" + echo "stderr: $out2_stderr" + ######## # Spec # ######## @@ -272,7 +295,7 @@ tasks: empty: spec: - omit_empty: false + omit_empty_rows: false output: table target: tags: [reachable] @@ -284,7 +307,7 @@ tasks: empty-true: spec: - omit_empty: true + omit_empty_rows: true output: table target: tags: [reachable] From d628c6a9a435d8ad5dea0493d3bc51fb1d369aca Mon Sep 17 00:00:00 2001 From: Samir Alajmovic <5246600+alajmo@users.noreply.github.com> Date: Sun, 1 Jan 2023 14:25:03 +0100 Subject: [PATCH 03/18] Add ability to modify prefix in text and table themes --- core/config.man | 9 ++- core/dao/theme.go | 18 +++-- core/run/client.go | 2 +- core/run/localhost.go | 5 +- core/run/ssh.go | 4 +- core/run/table.go | 40 ++++++++++- core/run/text.go | 138 ++++++++++++++++++++++++++++++++------ core/sake.1 | 11 ++- docs/changelog.md | 42 +++++++----- docs/config-reference.md | 9 ++- docs/contributing.md | 2 +- docs/roadmap.md | 8 +-- docs/variables.md | 3 +- docs/work-dir.md | 16 ++--- test/playground/sake.yaml | 13 ++++ 15 files changed, 247 insertions(+), 73 deletions(-) diff --git a/core/config.man b/core/config.man index 0e25328..a0130be 100644 --- a/core/config.man +++ b/core/config.man @@ -112,8 +112,9 @@ Below is a config file detailing all of the available options and their defaults default: # Text options [optional] text: - # Include server name prefix for each line [optional] - prefix: true + # Set host prefix for each line [optional] + # Available variables: `.Name`, `.Index`, `.Host`, `.Port`, `.User` + prefix: '{{ .Host }}' # Colors to alternate between for each server prefix [optional] # Available options: green, blue, red, yellow, magenta, cyan @@ -136,6 +137,10 @@ Below is a config file detailing all of the available options and their defaults # Available options: ascii, connected-light style: ascii + # Set host prefix [optional] + # Available variables: `.Name`, `.Index`, `.Host`, `.Port`, `.User` + prefix: '{{ .Host }}' + # Border options for table output [optional] options: draw_border: false diff --git a/core/dao/theme.go b/core/dao/theme.go index c5f019d..43899d6 100644 --- a/core/dao/theme.go +++ b/core/dao/theme.go @@ -16,6 +16,7 @@ type Table struct { // Stylable via YAML Name string `yaml:"name"` Style string `yaml:"style"` + Prefix string `yaml:"prefix"` Options *TableOptions `yaml:"options"` Border *BorderColors `yaml:"border"` @@ -53,7 +54,7 @@ type CellColors struct { } type Text struct { - Prefix bool `yaml:"prefix"` + Prefix string `yaml:"prefix"` PrefixColors []string `yaml:"prefix_colors"` Header string `yaml:"header"` HeaderFiller string `yaml:"header_filler"` @@ -154,15 +155,16 @@ var StyleBoxASCII = table.BoxStyle{ } var DefaultText = Text{ - Prefix: true, + Prefix: `{{ .Host }}`, PrefixColors: []string{"green", "blue", "red", "yellow", "magenta", "cyan"}, Header: `{{ .Style "TASK" "bold" }}{{ if ne .NumTasks 1 }} ({{ .Index }}/{{ .NumTasks }}){{end}}{{ if and .Name .Desc }} [{{.Style .Name "bold"}}: {{ .Desc }}] {{ else if .Name }} [{{ .Name }}] {{ else if .Desc }} [{{ .Desc }}] {{end}}`, HeaderFiller: "*", } var DefaultTable = Table{ - Style: "default", - Box: StyleBoxASCII, + Style: "default", + Box: StyleBoxASCII, + Prefix: `{{ .Host }}`, Options: &TableOptions{ DrawBorder: core.Ptr(false), @@ -266,6 +268,10 @@ func (c *ConfigYAML) ParseThemesYAML() ([]Theme, []ResourceErrors[Theme]) { themes[i].Text.PrefixColors = DefaultText.PrefixColors } + if themes[i].Text.Prefix == "" { + themes[i].Text.Prefix = DefaultText.Prefix + } + // TABLE if themes[i].Table.Style == "connected-light" { themes[i].Table.Box = StyleBoxLight @@ -274,6 +280,10 @@ func (c *ConfigYAML) ParseThemesYAML() ([]Theme, []ResourceErrors[Theme]) { themes[i].Table.Box = StyleBoxASCII } + if themes[i].Table.Prefix == "" { + themes[i].Table.Prefix = DefaultTable.Prefix + } + if themes[i].Table.Options == nil { themes[i].Table.Options = DefaultTable.Options } else { diff --git a/core/run/client.go b/core/run/client.go index 08d1ad6..5d16310 100644 --- a/core/run/client.go +++ b/core/run/client.go @@ -18,7 +18,7 @@ type Client interface { Stdout(int) io.Reader Signal(int, os.Signal) error GetName() string - Prefix() string + Prefix() (string, string, string, uint16) Connected() bool } diff --git a/core/run/localhost.go b/core/run/localhost.go index 3209181..ce9c0d0 100644 --- a/core/run/localhost.go +++ b/core/run/localhost.go @@ -15,6 +15,7 @@ type LocalhostClient struct { Name string User string Host string + Port uint16 Sessions []LocalSession } @@ -115,8 +116,8 @@ func (c *LocalhostClient) Stdout(i int) io.Reader { return c.Sessions[i].stdout } -func (c *LocalhostClient) Prefix() string { - return c.Host +func (c *LocalhostClient) Prefix() (string, string, string, uint16) { + return c.Name, c.Host, c.User, c.Port } func (c *LocalhostClient) Signal(i int, sig os.Signal) error { diff --git a/core/run/ssh.go b/core/run/ssh.go index f94456b..92c3d62 100644 --- a/core/run/ssh.go +++ b/core/run/ssh.go @@ -233,8 +233,8 @@ func (c *SSHClient) DialThrough(net, addr string, config *ssh.ClientConfig) (*ss return ssh.NewClient(client, chans, reqs), nil } -func (c *SSHClient) Prefix() string { - return c.Host +func (c *SSHClient) Prefix() (string, string, string, uint16) { + return c.Name, c.Host, c.User, c.Port } func (c *SSHClient) Write(i int, p []byte) (n int, err error) { diff --git a/core/run/table.go b/core/run/table.go index 31e31d7..be94520 100644 --- a/core/run/table.go +++ b/core/run/table.go @@ -41,15 +41,30 @@ func (run *Run) Table(dryRun bool) (dao.TableOutput, dao.ReportData, error) { data.Headers = append(data.Headers, subTask.Name) reportData.Headers = append(reportData.Headers, subTask.Name) } + // Populate the rows (server name is first cell, then commands and cmd output is set to empty string) for i, p := range servers { - data.Rows = append(data.Rows, dao.Row{Columns: []string{p.Host}}) - reportData.Tasks = append(reportData.Tasks, dao.ReportRow{Name: p.Host, Rows: []dao.Report{}}) + + var client Client + if p.Local { + client = run.LocalClients[p.Name] + } else { + client = run.RemoteClients[p.Name] + } + + title, err := getServerTitle(client, i, task.Theme.Table) + if err != nil { + return data, reportData, err + } + // p.Host + data.Rows = append(data.Rows, dao.Row{Columns: []string{title}}) + reportData.Tasks = append(reportData.Tasks, dao.ReportRow{Name: title, Rows: []dao.Report{}}) for range task.Tasks { data.Rows[i].Columns = append(data.Rows[i].Columns, "") reportData.Tasks[i].Rows = append(reportData.Tasks[i].Rows, dao.Report{}) } } + k := len(servers) for i, p := range uServers { reportData.Tasks = append(reportData.Tasks, dao.ReportRow{Name: p.Host, Rows: []dao.Report{}}) @@ -604,3 +619,24 @@ func runTableCmd(i int, t TaskContext, wg *sync.WaitGroup) (string, string, stri return buf.String(), bufOut.String(), bufErr.String(), nil } + +func getServerTitle(client Client, i int, ts dao.Table) (string, error) { + if ts.Prefix == "" { + return "", nil + } + + name, host, user, port := client.Prefix() + data := PrefixData{ + Name: name, + Host: host, + User: user, + Port: port, + Index: i, + } + prefix, err := PrefixTemplate(ts.Prefix, data) + if err != nil { + return "", err + } + + return prefix, nil +} diff --git a/core/run/text.go b/core/run/text.go index b4efa41..cdcccbf 100644 --- a/core/run/text.go +++ b/core/run/text.go @@ -27,7 +27,10 @@ func (run *Run) Text(dryRun bool) (dao.ReportData, error) { servers := run.Servers uServers := run.UnreachableServers - prefixMaxLen := calcMaxPrefixLength(run.LocalClients) + prefixMaxLen, perr := calcMaxPrefixLength(run.RemoteClients, *task) + if perr != nil { + return dao.ReportData{}, perr + } // TODO: reportData should be pointer? var reportData dao.ReportData @@ -491,8 +494,6 @@ func (run *Run) textWork( dryRun bool, batch int, ) error { - prefix := getPrefixer(run.LocalClients[r.Server.Name], r.i, prefixMaxLen, r.Task.Theme.Text, batch) - numTasks := len(r.Task.Tasks) var registerEnvs []string @@ -508,6 +509,11 @@ func (run *Run) textWork( client = run.RemoteClients[r.Server.Name] } + prefix, err := getPrefixer(client, r.i, prefixMaxLen, r.Task.Theme.Text, batch) + if err != nil { + return err + } + shell := dao.SelectFirstNonEmpty(r.Task.Shell, r.Server.Shell, run.Config.Shell) shell = core.FormatShell(shell) workDir := getWorkDir((*r.Cmd).Local, (*r.Server).Local, (*r.Cmd).WorkDir, (*r.Server).WorkDir, (*r.Cmd).RootDir, (*r.Server).RootDir) @@ -762,48 +768,136 @@ func printTaskHeader(i int, numTasks int, name string, desc string, ts dao.Text) return nil } -func getPrefixer(client Client, i, prefixMaxLen int, textStyle dao.Text, batch int) string { - if !textStyle.Prefix { - return "" +func PrefixTemplate(prefix string, data PrefixData) (string, error) { + tmpl, err := template.New("prefix.tmpl").Parse(prefix) + if err != nil { + return "", &core.TemplateParseError{Msg: err.Error()} } - prefix := client.Prefix() - prefixLen := len(prefix) - var prefixColor *text.Color - if len(textStyle.PrefixColors) < 1 { - prefixColor = print.GetFg("") - } else { - prefixColor = print.GetFg(textStyle.PrefixColors[i%len(textStyle.PrefixColors)]) + buf := &bytes.Buffer{} + err = tmpl.Execute(buf, data) + if err != nil { + return "", &core.TemplateParseError{Msg: err.Error()} } - // When batch = 1 correctly align the prefix to current host - // When batch > 1 correctly align the prefix to the largest host + s := buf.String() + + return s, nil +} + +type PrefixData struct { + Name string + Host string + User string + Index int + Port uint16 +} + +func (h PrefixData) Style(s any, args ...string) string { + v := core.AnyToString(s) + colors := text.Colors{} + + for _, k := range args { + switch { + case strings.Contains(k, "fg_"): + fg := print.GetFg(strings.TrimPrefix(k, "fg_")) + colors = append(colors, *fg) + case strings.Contains(k, "bg_"): + bg := print.GetBg(strings.TrimPrefix(k, "bg_")) + colors = append(colors, *bg) + case slices.Contains([]string{"normal", "bold", "faint", "italic", "underline", "crossed_out"}, k): + attr := print.GetAttr(k) + colors = append(colors, *attr) + } + } + + return colors.Sprintf(v) +} + +func getPrefixer(client Client, i int, prefixMaxLen int, ts dao.Text, batch int) (string, error) { + if ts.Prefix == "" { + return "", nil + } + + name, host, user, port := client.Prefix() + data := PrefixData{ + Name: name, + Host: host, + User: user, + Port: port, + Index: i, + } + prefix, err := PrefixTemplate(ts.Prefix, data) + if err != nil { + return "", err + } + + prefixLen := len(prefix) + + // When batch = 1 correctly align the prefix to current prefix + // When batch > 1 correctly align the prefix to the largest prefix var prefixString string - if batch > 1 && len(prefix) < prefixMaxLen { // Left padding. + if batch > 1 && prefixLen < prefixMaxLen { // Left padding. prefixString = prefix + strings.Repeat(" ", prefixMaxLen-prefixLen) + " | " } else { prefixString = prefix + " | " } + var prefixColor *text.Color + if len(ts.PrefixColors) < 1 { + prefixColor = print.GetFg("") + } else { + prefixColor = print.GetFg(ts.PrefixColors[i%len(ts.PrefixColors)]) + } + if prefixColor != nil { prefix = prefixColor.Sprintf(prefixString) } else { prefix = prefixString } - return prefix + return prefix, nil } -func calcMaxPrefixLength(clients map[string]Client) int { +func getPrefixLength(client Client, i int, prefixMaxLen int, ts dao.Text) (int, error) { + if ts.Prefix == "" { + return 0, nil + } + + name, host, user, port := client.Prefix() + data := PrefixData{ + Name: name, + Host: host, + User: user, + Port: port, + Index: i, + } + prefix, err := PrefixTemplate(ts.Prefix, data) + if err != nil { + return 0, err + } + + prefixLen := len(prefix) + + return prefixLen, nil +} + +func calcMaxPrefixLength(clients map[string]Client, task dao.Task) (int, error) { var prefixMaxLen int = 0 + i := 0 for _, c := range clients { - prefix := c.Prefix() - if len(prefix) > prefixMaxLen { - prefixMaxLen = len(prefix) + prefixLen, err := getPrefixLength(c, i, prefixMaxLen, task.Theme.Text) + if err != nil { + return 0, err + } + + if prefixLen > prefixMaxLen { + prefixMaxLen = prefixLen } + i += 1 } - return prefixMaxLen + return prefixMaxLen, nil } func printCmd(prefix string, cmd string) { diff --git a/core/sake.1 b/core/sake.1 index d338756..ff2dff9 100644 --- a/core/sake.1 +++ b/core/sake.1 @@ -1,4 +1,4 @@ -.TH "SAKE" "1" "2022-12-04T21:04:49CET" "v0.13.0" "Sake Manual" "sake" +.TH "SAKE" "1" "2023-01-01T14:04:13CET" "v0.13.0" "Sake Manual" "sake" .SH NAME sake - sake is a task runner for local and remote hosts @@ -625,8 +625,9 @@ Below is a config file detailing all of the available options and their defaults default: # Text options [optional] text: - # Include server name prefix for each line [optional] - prefix: true + # Set host prefix for each line [optional] + # Available variables: `.Name`, `.Index`, `.Host`, `.Port`, `.User` + prefix: '{{ .Host }}' # Colors to alternate between for each server prefix [optional] # Available options: green, blue, red, yellow, magenta, cyan @@ -649,6 +650,10 @@ Below is a config file detailing all of the available options and their defaults # Available options: ascii, connected-light style: ascii + # Set host prefix [optional] + # Available variables: `.Name`, `.Index`, `.Host`, `.Port`, `.User` + prefix: '{{ .Host }}' + # Border options for table output [optional] options: draw_border: false diff --git a/docs/changelog.md b/docs/changelog.md index ba352d2..073a99b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,9 +1,19 @@ # Changelog +## Unreleased + +### Features + +- Add ability to modify prefix in text and table themes + ## 0.13.0 ### Features +- Add ability to register variables which are available to the next tasks +- Add option to ignore errors for indiviual tasks +- Add flag/spec `--list-hosts` option to list targetted hosts +- Support output options `csv`/`json`/`none` - Add new task strategies: linear, host_pinned, free - `linear`: execute task for each host before proceeding to the next task (default) - `host_pinned`: executes tasks (serial) for a host before proceeding to the next host @@ -18,15 +28,12 @@ - `batch`: specify number of hosts - `batch_p`: specify number of hosts in percentage - `forks`: max number of concurrent processes -- Add ability to register variables which are available to the next tasks - Add option to display reports at end of tasks by using `--report` flag or specifying it in `spec` definition - `recap`: show basic report - `rc`: show return code for each host and task - `task`: show task status for each host and task - `time`: show time report for each host and task - `all`: show all reports -- Add flag/spec option to list targetted hosts -- Add option to ignore errors for indiviual tasks - Add confirm/step task capability - `confirm`: for the root task - `step`: per task and host @@ -38,27 +45,26 @@ - Fix server range (previously `[2:100]` didn't work as strings were compared) - Fix empty error for non existing working directory and update how work_dir works -### Changes - -- Switch to default shell when evaluating inventory -- If no command name is set on nested tasks, assign `task-$i` instead of `task` -- If `--limit` flag is higher than available hosts, then select all hosts filtered -- Building `sake` with go 1.19 -- Shorthand flag for silent is now `Q` -- Deprecated the parallel flag, use batch/batch_p/forks instead -- Update flag sorting -- Rename `--omit-empty` to `--omit-empty-rowss` -- [BREAKING CHANGE]: Rename default environment variables from `SAKE_SERVER_*` to `S_*`, and remove task default environment variables - ### Minor +- Add option to omit empty columns via flag `--omit-empty-columns` and spec `omit_empty_columns` +- Add option to specify target and spec via flags `--target`/`--spec` - Add description to targets and specs -- Add option to specify target and spec via flags -- Support output options `csv`/`json`/`none` - Add server identity to environment variables - Add silent/describe attribute to spec definition - Add ssh user flag option -- Add option to omit empty columns via flag `--omit-empty-columns` and spec `omit_empty_columns` + +### Changes + +- [**BREAKING CHANGE**]: Deprecated the parallel flag, use batch/batch_p/forks instead +- [**BREAKING CHANGE**]: Rename default environment variables from `SAKE_SERVER_*` to `S_*`, and remove task default environment variables +- [**BREAKING CHANGE**]: Shorthand flag for silent is now `Q` +- Switch to default shell when evaluating inventory +- If no command name is set on nested tasks, assign `task-$i` instead of `task` +- If `--limit` flag is higher than available hosts, then select all hosts filtered +- Update flag sorting +- Rename `--omit-empty` to `--omit-empty-rows` +- Building `sake` with go 1.19 ## 0.12.1 diff --git a/docs/config-reference.md b/docs/config-reference.md index 5f71297..5e309eb 100644 --- a/docs/config-reference.md +++ b/docs/config-reference.md @@ -105,8 +105,9 @@ themes: default: # Text options [optional] text: - # Include server name prefix for each line [optional] - prefix: true + # Set host prefix for each line [optional] + # Available variables: `.Name`, `.Index`, `.Host`, `.Port`, `.User` + prefix: '{{ .Host }}' # Colors to alternate between for each server prefix [optional] # Available options: green, blue, red, yellow, magenta, cyan @@ -129,6 +130,10 @@ themes: # Available options: ascii, connected-light style: ascii + # Set host prefix [optional] + # Available variables: `.Name`, `.Index`, `.Host`, `.Port`, `.User` + prefix: '{{ .Host }}' + # Border options for table output [optional] options: draw_border: false diff --git a/docs/contributing.md b/docs/contributing.md index b2c7f01..7d0fe5b 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,4 +1,4 @@ # Contributing All contributions are welcome, be it [filing bugs](https://github.com/alajmo/sake/issues), feature suggestions or helping developing `sake`. -For significant changes, please read [project background](project-background.md) and open an issue to discuss the changes before starting development. +For significant changes, please read [project background](background.md) and open an issue to discuss the changes before starting development. diff --git a/docs/roadmap.md b/docs/roadmap.md index 2f3ac50..565cc4c 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -25,9 +25,9 @@ - [ ] Inherit default from `default` spec/target - [ ] Add yaml to command mapper - [ ] Implement facts -- [ ] Configure what to show, host/ip or name, configure via theme/cli flags -- [ ] - Template for server prefix, similar to header -- [ ] - Add colors to describe (key bold, value color), true (green), false (red) -- [ ] - Add Tree output +- [ ] Configure what to show, host/ip or name, configure via theme flags + - [x] Template for server prefix, similar to header + - [ ] Add colors to describe (key bold, value color), true (green), false (red) + - [ ] Add Tree output - [ ] Fix hashed ip6 with port 22 does not work, all other combinations work - [ ] Fix `sake ssh inv` not working diff --git a/docs/variables.md b/docs/variables.md index 057127b..8c70fde 100644 --- a/docs/variables.md +++ b/docs/variables.md @@ -49,8 +49,7 @@ To access a previous tasks output, you can register a variable in the previous t - `_rc`: - `_failed`: - `_stdout`: -- `_stdout`: -- `_status`: +- `_stderr`: ```yaml tasks: diff --git a/docs/work-dir.md b/docs/work-dir.md index 53dd21a..54c8699 100644 --- a/docs/work-dir.md +++ b/docs/work-dir.md @@ -69,7 +69,7 @@ Resolve `work_dir` according to `Server Dir` and `Task Dir`: | Host | Task | Server Dir | Task Dir | work_dir | |--------|--------|------------|----------|---------------------------| | remote | remote | "" | "" | `/home/user` | -| remote | remote | "" | "task" | `/home/user/opt` | +| remote | remote | "" | "task" | `/home/user/task` | | remote | remote | "server" | "" | `/home/user/server` | | remote | remote | "server" | "task" | `/home/user/server/task` | @@ -84,13 +84,6 @@ Resolve `work_dir` according to `Task Context`: | remote | local | "" | "" | `[Task Context]` | | remote | local | "server" | "" | `[Task Context]` | -Resolve `work_dir` according to `Server Context`, `Server Dir` and `Task Dir`: - -| Host | Task | Server Dir | Task Dir | work_dir | -|--------|--------|------------|-----------|--------------------------------| -| local | local | "server" | "task" | `[Server Context]/server/cmd` | -| local | local | "server" | "task" | `[Server Context]/server/cmd` | - Resolve `work_dir` according to `Task Context` and `Task Dir`: | Host | Task | Server Dir | Task Dir | work_dir | @@ -106,3 +99,10 @@ Resolve `work_dir` according to `Server Context` and `Server Dir`: |--------|--------|------------|----------|---------------------------| | local | remote | "server" | "" | `[Server Context]/server` | | local | local | "server" | "" | `[Server Context]/server` | + +Resolve `work_dir` according to `Server Context`, `Server Dir` and `Task Dir`: + +| Host | Task | Server Dir | Task Dir | work_dir | +|--------|--------|------------|-----------|--------------------------------| +| local | local | "server" | "task" | `[Server Context]/server/cmd` | +| local | local | "server" | "task" | `[Server Context]/server/cmd` | diff --git a/test/playground/sake.yaml b/test/playground/sake.yaml index a9add69..c4b7cda 100644 --- a/test/playground/sake.yaml +++ b/test/playground/sake.yaml @@ -191,6 +191,19 @@ specs: # header: '{{ .Style "TASK" "bold" }} {{ .Name }}' # header_filler: "-" +themes: + default: + text: + # prefix: '{{ .Index }}' + # prefix: '{{ .Index }} @ {{ .Name }} @ {{ .Host }}:{{ .Port }} : {{ .User }}' + # prefix: "{{ .Name }}" + # prefix: "{{ .Host }}" + # prefix: "{{ .Host }}:{{ .Port }}" + header: '{{ .Style "TASK" "bold" }}{{ if ne .NumTasks 1 }} ({{ .Index }}/{{ .NumTasks }}){{end}}{{ if and .Name .Desc }} [{{.Style .Name "bold"}}: {{ .Desc }}] {{ else if .Name }} [{{ .Name }}] {{ else if .Desc }} [{{ .Desc }}] {{end}}' + + table: + prefix: "{{ .Host }}:{{ .Port }}" + env: VERSION: v0.1.0 DATE: $(date -u +"%Y-%m-%dT%H:%M:%S%Z") From ef12b25cc3b9e9141c79d9402768443c561765d0 Mon Sep 17 00:00:00 2001 From: Samir Alajmovic <5246600+alajmo@users.noreply.github.com> Date: Mon, 2 Jan 2023 00:16:36 +0100 Subject: [PATCH 04/18] Fix duplicate objects (#51) --- core/dao/config.go | 28 +++- core/dao/import_config.go | 117 +++++++++++++- docs/changelog.md | 5 + test/integration/golden/golden-0.stdout | 2 +- test/integration/golden/golden-1.stdout | 2 +- test/integration/golden/golden-10.stdout | 2 +- test/integration/golden/golden-11.stdout | 2 +- test/integration/golden/golden-12.stdout | 2 +- test/integration/golden/golden-13.stdout | 2 +- test/integration/golden/golden-14.stdout | 2 +- test/integration/golden/golden-15.stdout | 2 +- test/integration/golden/golden-16.stdout | 2 +- test/integration/golden/golden-17.stdout | 2 +- test/integration/golden/golden-18.stdout | 2 +- test/integration/golden/golden-19.stdout | 2 +- test/integration/golden/golden-2.stdout | 2 +- test/integration/golden/golden-20.stdout | 2 +- test/integration/golden/golden-21.stdout | 2 +- test/integration/golden/golden-22.stdout | 2 +- test/integration/golden/golden-23.stdout | 2 +- test/integration/golden/golden-24.stdout | 2 +- test/integration/golden/golden-25.stdout | 2 +- test/integration/golden/golden-26.stdout | 2 +- test/integration/golden/golden-27.stdout | 2 +- test/integration/golden/golden-28.stdout | 2 +- test/integration/golden/golden-29.stdout | 2 +- test/integration/golden/golden-3.stdout | 2 +- test/integration/golden/golden-30.stdout | 2 +- test/integration/golden/golden-31.stdout | 2 +- test/integration/golden/golden-32.stdout | 2 +- test/integration/golden/golden-33.stdout | 2 +- test/integration/golden/golden-34.stdout | 2 +- test/integration/golden/golden-35.stdout | 2 +- test/integration/golden/golden-36.stdout | 2 +- test/integration/golden/golden-37.stdout | 2 +- test/integration/golden/golden-38.stdout | 2 +- test/integration/golden/golden-4.stdout | 2 +- test/integration/golden/golden-5.stdout | 2 +- test/integration/golden/golden-6.stdout | 2 +- test/integration/golden/golden-7.stdout | 2 +- test/integration/golden/golden-8.stdout | 2 +- test/integration/golden/golden-9.stdout | 2 +- test/integration/run_test.go | 78 ++++----- test/playground/sake.yaml | 198 +++++++++++------------ test/playground/tasks/tasks.yaml | 19 ++- test/sake.yaml | 3 +- test/user-config.yaml | 0 47 files changed, 336 insertions(+), 190 deletions(-) create mode 100644 test/user-config.yaml diff --git a/core/dao/config.go b/core/dao/config.go index cae43d8..8564da2 100644 --- a/core/dao/config.go +++ b/core/dao/config.go @@ -109,7 +109,11 @@ func ReadConfig(configFilepath string, userConfigPath string, sshConfigFile stri CheckUserNoColor(noColor) var configPath string - userConfigFile := getUserConfigFile(userConfigPath) + userConfigFile, err := getUserConfigFile(userConfigPath) + if err != nil { + return Config{}, err + } + sshConfigPath, err := getSSHConfigPath(sshConfigFile) if err != nil { return Config{}, err @@ -179,28 +183,38 @@ func ReadConfig(configFilepath string, userConfigPath string, sshConfigFile stri return config, nil } -func getUserConfigFile(userConfigPath string) *string { +func getUserConfigFile(userConfigPath string) (*string, error) { // Flag if userConfigPath != "" { - if _, err := os.Stat(userConfigPath); err == nil { - return &userConfigPath + if _, err := os.Stat(userConfigPath); err != nil { + return nil, fmt.Errorf("user config not found: %w", err) } + return &userConfigPath, nil } // Env val, present := os.LookupEnv("SAKE_USER_CONFIG") if present { - return &val + if _, err := os.Stat(val); err != nil { + return nil, fmt.Errorf("user config not found: %w", err) + } + return &val, nil } // Default defaultUserConfigDir, _ := os.UserConfigDir() + defaultUserConfigPath := filepath.Join(defaultUserConfigDir, "sake", "config.yaml") if _, err := os.Stat(defaultUserConfigPath); err == nil { - return &defaultUserConfigPath + return &defaultUserConfigPath, nil } - return nil + defaultUserConfigPath = filepath.Join(defaultUserConfigDir, "sake", "config.yml") + if _, err := os.Stat(defaultUserConfigPath); err == nil { + return &defaultUserConfigPath, nil + } + + return nil, nil } func getSSHConfigPath(sshConfigPath string) (*string, error) { diff --git a/core/dao/import_config.go b/core/dao/import_config.go index 01ec6a5..99581e6 100644 --- a/core/dao/import_config.go +++ b/core/dao/import_config.go @@ -254,8 +254,10 @@ func (c *ConfigYAML) parseConfig() (Config, error) { // Check duplicate imports importErr := checkDuplicateImports(cr.Imports) + duplicateObjects := checkDuplicateObjects(config) + // Concat errors - errString := concatErrors(importErr, cr, &importCycles, &taskCycles) + errString := concatErrors(importErr, duplicateObjects, cr, &importCycles, &taskCycles) if errString != "" { return config, &core.ConfigErr{Msg: errString} @@ -264,8 +266,9 @@ func (c *ConfigYAML) parseConfig() (Config, error) { return config, nil } -func concatErrors(importErr string, cr ConfigResources, importCycles *[]NodeLink, taskCycles *[]TaskLink) string { +func concatErrors(importErr string, duplicateObjects string, cr ConfigResources, importCycles *[]NodeLink, taskCycles *[]TaskLink) string { errString := importErr + errString += duplicateObjects if len(*importCycles) > 0 { err := &FoundCyclicDependency{Cycles: *importCycles} @@ -656,6 +659,116 @@ func checkDuplicateImports(imports []Import) string { return errString } +type FoundDuplicateObjects struct { + Name string + Type string + Values []string +} + +func (c *FoundDuplicateObjects) Error() string { + var msg string + + var errPrefix = text.FgRed.Sprintf("error") + var ptrPrefix = text.FgBlue.Sprintf("-->") + msg = fmt.Sprintf("%s: %s %s %s\n %s", errPrefix, "found duplicate", c.Type, c.Name, ptrPrefix) + msg += fmt.Sprintf(" %s\n", c.Values[0]) + for i, s := range c.Values[1:] { + if i < len(c.Values[1:])-1 { + msg += fmt.Sprintf(" %s\n", s) + } else { + msg += fmt.Sprintf(" %s", s) + } + } + + return msg +} + +func checkDuplicateObjects(config Config) string { + // Task + taskIDS := []string{} + visitedTasks := make(map[string]bool, 0) + tasks := make(map[string][]string, 0) + for _, t := range config.Tasks { + tasks[t.ID] = append(tasks[t.ID], t.context) + _, exists := visitedTasks[t.ID] + if !exists { + taskIDS = append(taskIDS, t.ID) + visitedTasks[t.ID] = true + } + } + + var errString string + for _, id := range taskIDS { + if len(tasks[id]) > 1 { + err := &FoundDuplicateObjects{Name: id, Type: "task", Values: tasks[id]} + errString = fmt.Sprintf("%s%s\n\n", errString, err.Error()) + } + } + + // Spec + specIDS := []string{} + visitedSpecs := make(map[string]bool, 0) + specs := make(map[string][]string, 0) + for _, s := range config.Specs { + specs[s.Name] = append(specs[s.Name], s.context) + _, exists := visitedSpecs[s.Name] + if !exists { + specIDS = append(specIDS, s.Name) + visitedSpecs[s.Name] = true + } + } + + for _, id := range specIDS { + if len(specs[id]) > 1 { + err := &FoundDuplicateObjects{Name: id, Type: "spec", Values: specs[id]} + errString = fmt.Sprintf("%s%s\n\n", errString, err.Error()) + } + } + + // Target + targetIDS := []string{} + visitedTargets := make(map[string]bool, 0) + targets := make(map[string][]string, 0) + for _, t := range config.Targets { + targets[t.Name] = append(targets[t.Name], t.context) + _, exists := visitedTargets[t.Name] + if !exists { + targetIDS = append(targetIDS, t.Name) + visitedTargets[t.Name] = true + } + } + + for _, id := range targetIDS { + if len(targets[id]) > 1 { + err := &FoundDuplicateObjects{Name: id, Type: "target", Values: targets[id]} + errString = fmt.Sprintf("%s%s\n\n", errString, err.Error()) + } + } + + // Theme + themeIDS := []string{} + visitedThemes := make(map[string]bool, 0) + themes := make(map[string][]string, 0) + for _, t := range config.Themes { + themes[t.Name] = append(themes[t.Name], t.context) + _, exists := visitedThemes[t.Name] + if !exists { + themeIDS = append(themeIDS, t.Name) + visitedThemes[t.Name] = true + } + } + + for _, id := range themeIDS { + if len(themes[id]) > 1 { + err := &FoundDuplicateObjects{Name: id, Type: "theme", Values: themes[id]} + errString = fmt.Sprintf("%s%s\n\n", errString, err.Error()) + } + } + + return errString +} + + // Used for config imports type TaskResources struct { Tasks []Task diff --git a/docs/changelog.md b/docs/changelog.md index 073a99b..dec6f1d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,6 +6,11 @@ - Add ability to modify prefix in text and table themes +### Fixes + +- [BREAKING CHANGE]: No more duplicate tasks, specs, targets, and themes +- Small fix when user config is specified but not found + ## 0.13.0 ### Features diff --git a/test/integration/golden/golden-0.stdout b/test/integration/golden/golden-0.stdout index 36faa91..c1b3174 100755 --- a/test/integration/golden/golden-0.stdout +++ b/test/integration/golden/golden-0.stdout @@ -1,6 +1,6 @@ Index: 0 Name: List tags -Cmd: go run ../../main.go list tags +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list tags WantErr: false --- diff --git a/test/integration/golden/golden-1.stdout b/test/integration/golden/golden-1.stdout index ab81272..47990cb 100755 --- a/test/integration/golden/golden-1.stdout +++ b/test/integration/golden/golden-1.stdout @@ -1,6 +1,6 @@ Index: 1 Name: List tasks -Cmd: go run ../../main.go list tasks +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list tasks WantErr: false --- diff --git a/test/integration/golden/golden-10.stdout b/test/integration/golden/golden-10.stdout index b2b17b6..87100a6 100755 --- a/test/integration/golden/golden-10.stdout +++ b/test/integration/golden/golden-10.stdout @@ -1,6 +1,6 @@ Index: 10 Name: Describe tasks -Cmd: go run ../../main.go describe tasks +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe tasks WantErr: false --- diff --git a/test/integration/golden/golden-11.stdout b/test/integration/golden/golden-11.stdout index 2496527..0ea8d90 100755 --- a/test/integration/golden/golden-11.stdout +++ b/test/integration/golden/golden-11.stdout @@ -1,6 +1,6 @@ Index: 11 Name: Ping all servers -Cmd: go run ../../main.go run ping -q -t reachable +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run ping -q -t reachable WantErr: false --- diff --git a/test/integration/golden/golden-12.stdout b/test/integration/golden/golden-12.stdout index 78fe929..ecaec1b 100755 --- a/test/integration/golden/golden-12.stdout +++ b/test/integration/golden/golden-12.stdout @@ -1,6 +1,6 @@ Index: 12 Name: Multiple commands -Cmd: go run ../../main.go run info -q -t prod +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -t prod WantErr: false --- diff --git a/test/integration/golden/golden-13.stdout b/test/integration/golden/golden-13.stdout index be01ea5..c17bc2f 100755 --- a/test/integration/golden/golden-13.stdout +++ b/test/integration/golden/golden-13.stdout @@ -1,6 +1,6 @@ Index: 13 Name: Filter by hosts server using server name -Cmd: go run ../../main.go run info -q -s list-1 +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -s list-1 WantErr: false --- diff --git a/test/integration/golden/golden-14.stdout b/test/integration/golden/golden-14.stdout index df07caf..8193088 100755 --- a/test/integration/golden/golden-14.stdout +++ b/test/integration/golden/golden-14.stdout @@ -1,6 +1,6 @@ Index: 14 Name: Filter by hosts server using range index -Cmd: go run ../../main.go run info -q -s 'list[0]' +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -s 'list[0]' WantErr: false --- diff --git a/test/integration/golden/golden-15.stdout b/test/integration/golden/golden-15.stdout index 2f68711..a370c3e 100755 --- a/test/integration/golden/golden-15.stdout +++ b/test/integration/golden/golden-15.stdout @@ -1,6 +1,6 @@ Index: 15 Name: Filter by hosts server -Cmd: go run ../../main.go run info -q -s 'list[0:2]' +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -s 'list[0:2]' WantErr: false --- diff --git a/test/integration/golden/golden-16.stdout b/test/integration/golden/golden-16.stdout index 1ada0c3..e23c89c 100755 --- a/test/integration/golden/golden-16.stdout +++ b/test/integration/golden/golden-16.stdout @@ -1,6 +1,6 @@ Index: 16 Name: Filter by host regex -Cmd: go run ../../main.go run info -q -r '172.24.2.(2|4)' +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -r '172.24.2.(2|4)' WantErr: false --- diff --git a/test/integration/golden/golden-17.stdout b/test/integration/golden/golden-17.stdout index 7c16ddf..e53b9eb 100755 --- a/test/integration/golden/golden-17.stdout +++ b/test/integration/golden/golden-17.stdout @@ -1,6 +1,6 @@ Index: 17 Name: Limit to 2 servers -Cmd: go run ../../main.go run ping -q -t reachable -l 2 +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run ping -q -t reachable -l 2 WantErr: false --- diff --git a/test/integration/golden/golden-18.stdout b/test/integration/golden/golden-18.stdout index cd0b2c0..7fa65fc 100755 --- a/test/integration/golden/golden-18.stdout +++ b/test/integration/golden/golden-18.stdout @@ -1,6 +1,6 @@ Index: 18 Name: Limit to 50 percent servers -Cmd: go run ../../main.go run ping -q -t reachable -L 50 +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run ping -q -t reachable -L 50 WantErr: false --- diff --git a/test/integration/golden/golden-19.stdout b/test/integration/golden/golden-19.stdout index 264884e..2f54883 100755 --- a/test/integration/golden/golden-19.stdout +++ b/test/integration/golden/golden-19.stdout @@ -1,6 +1,6 @@ Index: 19 Name: Filter by inverting on tag unreachable -Cmd: go run ../../main.go run ping -q -t unreachable -v +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run ping -q -t unreachable -v WantErr: false --- diff --git a/test/integration/golden/golden-2.stdout b/test/integration/golden/golden-2.stdout index fc62ca4..7078891 100755 --- a/test/integration/golden/golden-2.stdout +++ b/test/integration/golden/golden-2.stdout @@ -1,6 +1,6 @@ Index: 2 Name: List servers -Cmd: go run ../../main.go list servers +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list servers WantErr: false --- diff --git a/test/integration/golden/golden-20.stdout b/test/integration/golden/golden-20.stdout index 5b0da66..4273e02 100755 --- a/test/integration/golden/golden-20.stdout +++ b/test/integration/golden/golden-20.stdout @@ -1,6 +1,6 @@ Index: 20 Name: Simple Envs -Cmd: go run ../../main.go run env -q -t reachable +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run env -q -t reachable WantErr: false --- diff --git a/test/integration/golden/golden-21.stdout b/test/integration/golden/golden-21.stdout index 1bd951c..80deeef 100755 --- a/test/integration/golden/golden-21.stdout +++ b/test/integration/golden/golden-21.stdout @@ -1,6 +1,6 @@ Index: 21 Name: Reference Envs -Cmd: go run ../../main.go run env-complex -q -t reachable +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run env-complex -q -t reachable WantErr: false --- diff --git a/test/integration/golden/golden-22.stdout b/test/integration/golden/golden-22.stdout index 131cd4e..1d60b9e 100755 --- a/test/integration/golden/golden-22.stdout +++ b/test/integration/golden/golden-22.stdout @@ -1,6 +1,6 @@ Index: 22 Name: Default Envs -Cmd: go run ../../main.go run env-default -q -t reachable +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run env-default -q -t reachable WantErr: false --- diff --git a/test/integration/golden/golden-23.stdout b/test/integration/golden/golden-23.stdout index 69334fc..9f00ecd 100755 --- a/test/integration/golden/golden-23.stdout +++ b/test/integration/golden/golden-23.stdout @@ -1,6 +1,6 @@ Index: 23 Name: Nested tasks -Cmd: go run ../../main.go run d -q -t reachable +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run d -q -t reachable WantErr: false --- diff --git a/test/integration/golden/golden-24.stdout b/test/integration/golden/golden-24.stdout index 7d5cb06..d8dba75 100755 --- a/test/integration/golden/golden-24.stdout +++ b/test/integration/golden/golden-24.stdout @@ -1,6 +1,6 @@ Index: 24 Name: Work Dir 1 -Cmd: go run ../../main.go run work-dir-1 -q -t reachable +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run work-dir-1 -q -t reachable WantErr: false --- diff --git a/test/integration/golden/golden-25.stdout b/test/integration/golden/golden-25.stdout index 7bf7182..466e3a5 100755 --- a/test/integration/golden/golden-25.stdout +++ b/test/integration/golden/golden-25.stdout @@ -1,6 +1,6 @@ Index: 25 Name: Work Dir 2 -Cmd: go run ../../main.go run work-dir-2 -q -t reachable +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run work-dir-2 -q -t reachable WantErr: false --- diff --git a/test/integration/golden/golden-26.stdout b/test/integration/golden/golden-26.stdout index e6e9d57..c7b1b74 100755 --- a/test/integration/golden/golden-26.stdout +++ b/test/integration/golden/golden-26.stdout @@ -1,6 +1,6 @@ Index: 26 Name: Work Dir 3 -Cmd: go run ../../main.go run work-dir-3 -q -t reachable +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run work-dir-3 -q -t reachable WantErr: false --- diff --git a/test/integration/golden/golden-27.stdout b/test/integration/golden/golden-27.stdout index bb18184..a0f21fa 100755 --- a/test/integration/golden/golden-27.stdout +++ b/test/integration/golden/golden-27.stdout @@ -1,6 +1,6 @@ Index: 27 Name: Register 1 -Cmd: go run ../../main.go run register-1 -q -t reachable +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run register-1 -q -t reachable WantErr: false --- diff --git a/test/integration/golden/golden-28.stdout b/test/integration/golden/golden-28.stdout index 5d7142b..75efa13 100755 --- a/test/integration/golden/golden-28.stdout +++ b/test/integration/golden/golden-28.stdout @@ -1,6 +1,6 @@ Index: 28 Name: Register 2 -Cmd: go run ../../main.go run register-2 -q -t reachable +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run register-2 -q -t reachable WantErr: false --- diff --git a/test/integration/golden/golden-29.stdout b/test/integration/golden/golden-29.stdout index f64b4de..3a123d7 100755 --- a/test/integration/golden/golden-29.stdout +++ b/test/integration/golden/golden-29.stdout @@ -1,6 +1,6 @@ Index: 29 Name: fatal false -Cmd: go run ../../main.go run fatal -q -t reachable +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run fatal -q -t reachable WantErr: true --- diff --git a/test/integration/golden/golden-3.stdout b/test/integration/golden/golden-3.stdout index 77792f1..19884c1 100755 --- a/test/integration/golden/golden-3.stdout +++ b/test/integration/golden/golden-3.stdout @@ -1,6 +1,6 @@ Index: 3 Name: List servers filter on list hosts -Cmd: go run ../../main.go list servers list +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list servers list WantErr: false --- diff --git a/test/integration/golden/golden-30.stdout b/test/integration/golden/golden-30.stdout index 1af6aa3..d2d6ca9 100755 --- a/test/integration/golden/golden-30.stdout +++ b/test/integration/golden/golden-30.stdout @@ -1,6 +1,6 @@ Index: 30 Name: fatal true -Cmd: go run ../../main.go run fatal-true -q -t reachable +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run fatal-true -q -t reachable WantErr: true --- diff --git a/test/integration/golden/golden-31.stdout b/test/integration/golden/golden-31.stdout index 61949c6..cf741d3 100755 --- a/test/integration/golden/golden-31.stdout +++ b/test/integration/golden/golden-31.stdout @@ -1,6 +1,6 @@ Index: 31 Name: ignore_errors false -Cmd: go run ../../main.go run errors -q -t reachable +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run errors -q -t reachable WantErr: true --- diff --git a/test/integration/golden/golden-32.stdout b/test/integration/golden/golden-32.stdout index 60cca28..ba36352 100755 --- a/test/integration/golden/golden-32.stdout +++ b/test/integration/golden/golden-32.stdout @@ -1,6 +1,6 @@ Index: 32 Name: ignore_errors true -Cmd: go run ../../main.go run errors-true -q -t reachable +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run errors-true -q -t reachable WantErr: false --- diff --git a/test/integration/golden/golden-33.stdout b/test/integration/golden/golden-33.stdout index c2e0655..efb8d01 100755 --- a/test/integration/golden/golden-33.stdout +++ b/test/integration/golden/golden-33.stdout @@ -1,6 +1,6 @@ Index: 33 Name: unreachable false -Cmd: go run ../../main.go run unreachable -q -a +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run unreachable -q -a WantErr: true --- diff --git a/test/integration/golden/golden-34.stdout b/test/integration/golden/golden-34.stdout index a453097..e09f447 100755 --- a/test/integration/golden/golden-34.stdout +++ b/test/integration/golden/golden-34.stdout @@ -1,6 +1,6 @@ Index: 34 Name: unreachable true -Cmd: go run ../../main.go run unreachable-true -o table -q -a +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run unreachable-true -o table -q -a WantErr: false --- diff --git a/test/integration/golden/golden-35.stdout b/test/integration/golden/golden-35.stdout index ed180cc..780b945 100755 --- a/test/integration/golden/golden-35.stdout +++ b/test/integration/golden/golden-35.stdout @@ -1,6 +1,6 @@ Index: 35 Name: omit_empty false -Cmd: go run ../../main.go run empty -q -t reachable +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run empty -q -t reachable WantErr: false --- diff --git a/test/integration/golden/golden-36.stdout b/test/integration/golden/golden-36.stdout index d3a1ddb..8ee5ffe 100755 --- a/test/integration/golden/golden-36.stdout +++ b/test/integration/golden/golden-36.stdout @@ -1,6 +1,6 @@ Index: 36 Name: omit_empty true -Cmd: go run ../../main.go run empty-true -q -t reachable +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run empty-true -q -t reachable WantErr: false --- diff --git a/test/integration/golden/golden-37.stdout b/test/integration/golden/golden-37.stdout index 41554e9..7da0a3c 100755 --- a/test/integration/golden/golden-37.stdout +++ b/test/integration/golden/golden-37.stdout @@ -1,6 +1,6 @@ Index: 37 Name: output -Cmd: go run ../../main.go run output -q -t reachable +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run output -q -t reachable WantErr: false --- diff --git a/test/integration/golden/golden-38.stdout b/test/integration/golden/golden-38.stdout index 227cca1..f3f3f74 100755 --- a/test/integration/golden/golden-38.stdout +++ b/test/integration/golden/golden-38.stdout @@ -1,6 +1,6 @@ Index: 38 Name: Run exec command -Cmd: go run ../../main.go exec 'echo 123' -q -t reachable +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go exec 'echo 123' -q -t reachable WantErr: false --- diff --git a/test/integration/golden/golden-4.stdout b/test/integration/golden/golden-4.stdout index 151fa83..ed10245 100755 --- a/test/integration/golden/golden-4.stdout +++ b/test/integration/golden/golden-4.stdout @@ -1,6 +1,6 @@ Index: 4 Name: List servers filter on range hosts -Cmd: go run ../../main.go list servers range +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list servers range WantErr: false --- diff --git a/test/integration/golden/golden-5.stdout b/test/integration/golden/golden-5.stdout index dff04f3..973a8df 100755 --- a/test/integration/golden/golden-5.stdout +++ b/test/integration/golden/golden-5.stdout @@ -1,6 +1,6 @@ Index: 5 Name: List servers filter on inventory hosts -Cmd: go run ../../main.go list servers inv +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list servers inv WantErr: false --- diff --git a/test/integration/golden/golden-6.stdout b/test/integration/golden/golden-6.stdout index 13e67a5..27caec8 100755 --- a/test/integration/golden/golden-6.stdout +++ b/test/integration/golden/golden-6.stdout @@ -1,6 +1,6 @@ Index: 6 Name: Describe servers -Cmd: go run ../../main.go describe servers +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe servers WantErr: false --- diff --git a/test/integration/golden/golden-7.stdout b/test/integration/golden/golden-7.stdout index e81ea57..f869233 100755 --- a/test/integration/golden/golden-7.stdout +++ b/test/integration/golden/golden-7.stdout @@ -1,6 +1,6 @@ Index: 7 Name: Describe servers filter on list hosts -Cmd: go run ../../main.go describe servers list +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe servers list WantErr: false --- diff --git a/test/integration/golden/golden-8.stdout b/test/integration/golden/golden-8.stdout index f51400f..f30af95 100755 --- a/test/integration/golden/golden-8.stdout +++ b/test/integration/golden/golden-8.stdout @@ -1,6 +1,6 @@ Index: 8 Name: Describe servers filter on range hosts -Cmd: go run ../../main.go describe servers range +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe servers range WantErr: false --- diff --git a/test/integration/golden/golden-9.stdout b/test/integration/golden/golden-9.stdout index 6549467..dbf85dc 100755 --- a/test/integration/golden/golden-9.stdout +++ b/test/integration/golden/golden-9.stdout @@ -1,6 +1,6 @@ Index: 9 Name: Describe servers filter on inventory hosts -Cmd: go run ../../main.go describe servers inv +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe servers inv WantErr: false --- diff --git a/test/integration/run_test.go b/test/integration/run_test.go index 64ab02a..22332bf 100644 --- a/test/integration/run_test.go +++ b/test/integration/run_test.go @@ -9,219 +9,219 @@ var cases = []TemplateTest{ // list tags { TestName: "List tags", - TestCmd: "go run ../../main.go list tags", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list tags`, WantErr: false, }, // list tasks { TestName: "List tasks", - TestCmd: "go run ../../main.go list tasks", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list tasks`, WantErr: false, }, // list servers { TestName: "List servers", - TestCmd: "go run ../../main.go list servers", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list servers`, WantErr: false, }, { TestName: "List servers filter on list hosts", - TestCmd: "go run ../../main.go list servers list", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list servers list`, WantErr: false, }, { TestName: "List servers filter on range hosts", - TestCmd: "go run ../../main.go list servers range", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list servers range`, WantErr: false, }, { TestName: "List servers filter on inventory hosts", - TestCmd: "go run ../../main.go list servers inv", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list servers inv`, WantErr: false, }, // describe servers { TestName: "Describe servers", - TestCmd: "go run ../../main.go describe servers", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe servers`, WantErr: false, }, { TestName: "Describe servers filter on list hosts", - TestCmd: "go run ../../main.go describe servers list", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe servers list`, WantErr: false, }, { TestName: "Describe servers filter on range hosts", - TestCmd: "go run ../../main.go describe servers range", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe servers range`, WantErr: false, }, { TestName: "Describe servers filter on inventory hosts", - TestCmd: "go run ../../main.go describe servers inv", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe servers inv`, WantErr: false, }, // describe tasks { TestName: "Describe tasks", - TestCmd: "go run ../../main.go describe tasks", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe tasks`, WantErr: false, }, // run basic tasks { TestName: "Ping all servers", - TestCmd: "go run ../../main.go run ping -q -t reachable", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run ping -q -t reachable`, WantErr: false, }, { TestName: "Multiple commands", - TestCmd: "go run ../../main.go run info -q -t prod", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -t prod`, WantErr: false, }, { TestName: "Filter by hosts server using server name", - TestCmd: "go run ../../main.go run info -q -s list-1", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -s list-1`, WantErr: false, }, { TestName: "Filter by hosts server using range index", - TestCmd: "go run ../../main.go run info -q -s 'list[0]'", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -s 'list[0]'`, WantErr: false, }, { TestName: "Filter by hosts server", - TestCmd: "go run ../../main.go run info -q -s 'list[0:2]'", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -s 'list[0:2]'`, WantErr: false, }, { TestName: "Filter by host regex", - TestCmd: "go run ../../main.go run info -q -r '172.24.2.(2|4)'", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -r '172.24.2.(2|4)'`, WantErr: false, }, { TestName: "Limit to 2 servers", - TestCmd: "go run ../../main.go run ping -q -t reachable -l 2", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run ping -q -t reachable -l 2`, WantErr: false, }, { TestName: "Limit to 50 percent servers", - TestCmd: "go run ../../main.go run ping -q -t reachable -L 50", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run ping -q -t reachable -L 50`, WantErr: false, }, { TestName: "Filter by inverting on tag unreachable", - TestCmd: "go run ../../main.go run ping -q -t unreachable -v", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run ping -q -t unreachable -v`, WantErr: false, }, // run tasks and display env { TestName: "Simple Envs", - TestCmd: "go run ../../main.go run env -q -t reachable", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run env -q -t reachable`, WantErr: false, }, { TestName: "Reference Envs", - TestCmd: "go run ../../main.go run env-complex -q -t reachable", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run env-complex -q -t reachable`, WantErr: false, }, { TestName: "Default Envs", - TestCmd: "go run ../../main.go run env-default -q -t reachable", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run env-default -q -t reachable`, WantErr: false, }, // run nested tasks { TestName: "Nested tasks", - TestCmd: "go run ../../main.go run d -q -t reachable", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run d -q -t reachable`, WantErr: false, }, // run tasks and modify work dir { TestName: "Work Dir 1", - TestCmd: "go run ../../main.go run work-dir-1 -q -t reachable", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run work-dir-1 -q -t reachable`, WantErr: false, }, { TestName: "Work Dir 2", - TestCmd: "go run ../../main.go run work-dir-2 -q -t reachable", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run work-dir-2 -q -t reachable`, WantErr: false, }, { TestName: "Work Dir 3", - TestCmd: "go run ../../main.go run work-dir-3 -q -t reachable", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run work-dir-3 -q -t reachable`, WantErr: false, }, // run tasks and register variables { TestName: "Register 1", - TestCmd: "go run ../../main.go run register-1 -q -t reachable", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run register-1 -q -t reachable`, WantErr: false, }, { TestName: "Register 2", - TestCmd: "go run ../../main.go run register-2 -q -t reachable", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run register-2 -q -t reachable`, WantErr: false, }, // Tests for running tasks with various specs { TestName: "fatal false", - TestCmd: "go run ../../main.go run fatal -q -t reachable", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run fatal -q -t reachable`, WantErr: true, }, { TestName: "fatal true", - TestCmd: "go run ../../main.go run fatal-true -q -t reachable", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run fatal-true -q -t reachable`, WantErr: true, }, { TestName: "ignore_errors false", - TestCmd: "go run ../../main.go run errors -q -t reachable", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run errors -q -t reachable`, WantErr: true, }, { TestName: "ignore_errors true", - TestCmd: "go run ../../main.go run errors-true -q -t reachable", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run errors-true -q -t reachable`, WantErr: false, }, { TestName: "unreachable false", - TestCmd: "go run ../../main.go run unreachable -q -a", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run unreachable -q -a`, WantErr: true, }, { TestName: "unreachable true", - TestCmd: "go run ../../main.go run unreachable-true -o table -q -a", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run unreachable-true -o table -q -a`, WantErr: false, }, { TestName: "omit_empty false", - TestCmd: "go run ../../main.go run empty -q -t reachable", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run empty -q -t reachable`, WantErr: false, }, { TestName: "omit_empty true", - TestCmd: "go run ../../main.go run empty-true -q -t reachable", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run empty-true -q -t reachable`, WantErr: false, }, { TestName: "output", - TestCmd: "go run ../../main.go run output -q -t reachable", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run output -q -t reachable`, WantErr: false, }, // exec { TestName: "Run exec command", - TestCmd: "go run ../../main.go exec 'echo 123' -q -t reachable", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go exec 'echo 123' -q -t reachable`, WantErr: false, }, } diff --git a/test/playground/sake.yaml b/test/playground/sake.yaml index c4b7cda..6d2166b 100644 --- a/test/playground/sake.yaml +++ b/test/playground/sake.yaml @@ -132,6 +132,9 @@ targets: limit: 3 specs: + default: + output: json + table: output: table # describe: true @@ -188,21 +191,14 @@ specs: # themes: # default: # text: -# header: '{{ .Style "TASK" "bold" }} {{ .Name }}' -# header_filler: "-" - -themes: - default: - text: - # prefix: '{{ .Index }}' - # prefix: '{{ .Index }} @ {{ .Name }} @ {{ .Host }}:{{ .Port }} : {{ .User }}' - # prefix: "{{ .Name }}" - # prefix: "{{ .Host }}" - # prefix: "{{ .Host }}:{{ .Port }}" - header: '{{ .Style "TASK" "bold" }}{{ if ne .NumTasks 1 }} ({{ .Index }}/{{ .NumTasks }}){{end}}{{ if and .Name .Desc }} [{{.Style .Name "bold"}}: {{ .Desc }}] {{ else if .Name }} [{{ .Name }}] {{ else if .Desc }} [{{ .Desc }}] {{end}}' - - table: - prefix: "{{ .Host }}:{{ .Port }}" +# # prefix: '{{ .Index }}' +# # prefix: '{{ .Index }} @ {{ .Name }} @ {{ .Host }}:{{ .Port }} : {{ .User }}' +# # prefix: "{{ .Name }}" +# # prefix: "{{ .Host }}" +# # prefix: "{{ .Host }}:{{ .Port }}" +# header: '{{ .Style "TASK" "bold" }}{{ if ne .NumTasks 1 }} ({{ .Index }}/{{ .NumTasks }}){{end}}{{ if and .Name .Desc }} [{{.Style .Name "bold"}}: {{ .Desc }}] {{ else if .Name }} [{{ .Name }}] {{ else if .Desc }} [{{ .Desc }}] {{end}}' +# table: +# prefix: "{{ .Host }}:{{ .Port }}" env: VERSION: v0.1.0 @@ -215,89 +211,89 @@ tasks: desc: ping server cmd: echo pong - exit: - # name: Exit - local: true - cmd: exit 3 - - sleep: - desc: ping server - cmd: sleep 2 & echo done - - info: - name: Info - desc: print info - target: all - tasks: - - task: print-host - - task: print-hostname - - task: print-os - - task: print-kernel - - # Info - print-host: - name: Host - desc: print host - spec: info - target: all - cmd: echo $S_HOST - - print-hostname: - name: Hostname - desc: print hostname - spec: info - target: all - cmd: hostname - - print-os: - name: OS - desc: print OS - spec: info - target: all - cmd: | - os=$(lsb_release -si) - release=$(lsb_release -sr) - echo "$os $release" - - print-kernel: - name: Kernel - desc: Print kernel version - spec: info - target: all - cmd: uname -r | awk -v FS='-' '{print $1}' - - register: - tasks: - - cmd: echo "foo" && >&2 echo "error" - register: out - - cmd: | - echo "status: $out_status" - echo "rc: $out_rc" - echo "failed: $out_failed" - echo "stdout: $out_stdout" - echo "stderr: $out_stderr" - echo "out: $out" - - - cmd: echo "xyz" && >&2 echo "error 2" - register: out2 - - cmd: | - echo "status: $out_status" - echo "rc: $out_rc" - echo "failed: $out_failed" - echo "stdout: $out_stdout" - echo "stderr: $out_stderr" - echo "out: $out" - - echo "-------------" - - echo "status: $out2_status" - echo "rc: $out2_rc" - echo "failed: $out2_failed" - echo "stdout: $out2_stdout" - echo "stderr: $out2_stderr" - echo "out: $out2" - - register2: - tasks: - - cmd: echo "foo" && >&2 echo "error" - register: out + # exit: + # # name: Exit + # local: true + # cmd: exit 3 + + # sleep: + # desc: ping server + # cmd: sleep 2 & echo done + + # info: + # name: Info + # desc: print info + # target: all + # tasks: + # - task: print-host + # - task: print-hostname + # - task: print-os + # - task: print-kernel + + # # Info + # print-host: + # name: Host + # desc: print host + # spec: info + # target: all + # cmd: echo $S_HOST + + # print-hostname: + # name: Hostname + # desc: print hostname + # spec: info + # target: all + # cmd: hostname + + # print-os: + # name: OS + # desc: print OS + # spec: info + # target: all + # cmd: | + # os=$(lsb_release -si) + # release=$(lsb_release -sr) + # echo "$os $release" + + # print-kernel: + # name: Kernel + # desc: Print kernel version + # spec: info + # target: all + # cmd: uname -r | awk -v FS='-' '{print $1}' + + # register: + # tasks: + # - cmd: echo "foo" && >&2 echo "error" + # register: out + # - cmd: | + # echo "status: $out_status" + # echo "rc: $out_rc" + # echo "failed: $out_failed" + # echo "stdout: $out_stdout" + # echo "stderr: $out_stderr" + # echo "out: $out" + + # - cmd: echo "xyz" && >&2 echo "error 2" + # register: out2 + # - cmd: | + # echo "status: $out_status" + # echo "rc: $out_rc" + # echo "failed: $out_failed" + # echo "stdout: $out_stdout" + # echo "stderr: $out_stderr" + # echo "out: $out" + + # echo "-------------" + + # echo "status: $out2_status" + # echo "rc: $out2_rc" + # echo "failed: $out2_failed" + # echo "stdout: $out2_stdout" + # echo "stderr: $out2_stderr" + # echo "out: $out2" + + # register2: + # tasks: + # - cmd: echo "foo" && >&2 echo "error" + # register: out diff --git a/test/playground/tasks/tasks.yaml b/test/playground/tasks/tasks.yaml index 1f7d575..24dc114 100644 --- a/test/playground/tasks/tasks.yaml +++ b/test/playground/tasks/tasks.yaml @@ -1,3 +1,20 @@ tasks: - hello: echo world pwd: pwd + +# specs: +# table: +# output: table +# # describe: true +# silent: true + +# themes: +# kaka: +# text: +# # prefix: '{{ .Index }}' +# # prefix: '{{ .Index }} @ {{ .Name }} @ {{ .Host }}:{{ .Port }} : {{ .User }}' +# # prefix: "{{ .Name }}" +# # prefix: "{{ .Host }}" +# # prefix: "{{ .Host }}:{{ .Port }}" +# header: '{{ .Style "TASK" "bold" }}{{ if ne .NumTasks 1 }} ({{ .Index }}/{{ .NumTasks }}){{end}}{{ if and .Name .Desc }} [{{.Style .Name "bold"}}: {{ .Desc }}] {{ else if .Name }} [{{ .Name }}] {{ else if .Desc }} [{{ .Desc }}] {{end}}' +# table: +# prefix: "{{ .Host }}:{{ .Port }}" diff --git a/test/sake.yaml b/test/sake.yaml index abc66af..d2131af 100644 --- a/test/sake.yaml +++ b/test/sake.yaml @@ -32,9 +32,10 @@ specs: themes: default: text: - prefix: true + prefix: "{{ .Host }}" header: '{{ .Style "TASK" "bold" }}{{ if ne .NumTasks 1 }} ({{ .Index }}/{{ .NumTasks }}){{end}}{{ if and .Name .Desc }} [{{.Style .Name "bold"}}: {{ .Desc }}] {{ else if .Name }} [{{ .Name }}] {{ else if .Desc }} [{{ .Desc }}] {{end}}' table: + prefix: "{{ .Host }}" tasks: ping: diff --git a/test/user-config.yaml b/test/user-config.yaml new file mode 100644 index 0000000..e69de29 From 09c033aeb0c4652eaf753d1956675c50ba90f8ab Mon Sep 17 00:00:00 2001 From: Samir Alajmovic <5246600+alajmo@users.noreply.github.com> Date: Mon, 2 Jan 2023 00:37:57 +0100 Subject: [PATCH 05/18] Hide tasks from auto-completion via spec attribute hidden equals true (#52) --- cmd/list_specs.go | 2 +- core/dao/spec.go | 9 + core/dao/task.go | 4 + core/print/print_block.go | 5 + docs/changelog.md | 1 + test/integration/golden/golden-1.stdout | 43 +- test/integration/golden/golden-10.stdout | 533 ++++++----------------- test/integration/golden/golden-11.stdout | 62 +-- test/integration/golden/golden-12.stdout | 46 +- test/integration/golden/golden-13.stdout | 32 +- test/integration/golden/golden-14.stdout | 467 +++++++++++++++++++- test/integration/golden/golden-15.stdout | 50 ++- test/integration/golden/golden-16.stdout | 8 +- test/integration/golden/golden-17.stdout | 16 +- test/integration/golden/golden-18.stdout | 28 +- test/integration/golden/golden-19.stdout | 50 +-- test/integration/golden/golden-2.stdout | 27 +- test/integration/golden/golden-20.stdout | 126 +----- test/integration/golden/golden-21.stdout | 130 +----- test/integration/golden/golden-22.stdout | 126 +----- test/integration/golden/golden-23.stdout | 74 ++-- test/integration/golden/golden-24.stdout | 138 ++++-- test/integration/golden/golden-25.stdout | 154 +++++-- test/integration/golden/golden-26.stdout | 138 ++++-- test/integration/golden/golden-27.stdout | 74 ++-- test/integration/golden/golden-28.stdout | 200 +-------- test/integration/golden/golden-29.stdout | 93 ++-- test/integration/golden/golden-3.stdout | 41 +- test/integration/golden/golden-30.stdout | 93 ++-- test/integration/golden/golden-31.stdout | 97 ++--- test/integration/golden/golden-32.stdout | 254 ++++++++--- test/integration/golden/golden-33.stdout | 67 ++- test/integration/golden/golden-34.stdout | 102 +++-- test/integration/golden/golden-35.stdout | 97 +++-- test/integration/golden/golden-36.stdout | 93 ++-- test/integration/golden/golden-37.stdout | 52 +-- test/integration/golden/golden-38.stdout | 51 ++- test/integration/golden/golden-39.stdout | 47 ++ test/integration/golden/golden-4.stdout | 27 +- test/integration/golden/golden-40.stdout | 46 ++ test/integration/golden/golden-41.stdout | 47 ++ test/integration/golden/golden-42.stdout | 47 ++ test/integration/golden/golden-5.stdout | 12 +- test/integration/golden/golden-6.stdout | 183 +------- test/integration/golden/golden-7.stdout | 29 +- test/integration/golden/golden-8.stdout | 43 +- test/integration/golden/golden-9.stdout | 28 +- test/integration/run_test.go | 28 ++ test/playground/sake.yaml | 178 ++++---- 49 files changed, 2209 insertions(+), 2089 deletions(-) create mode 100755 test/integration/golden/golden-39.stdout create mode 100755 test/integration/golden/golden-40.stdout create mode 100755 test/integration/golden/golden-41.stdout create mode 100755 test/integration/golden/golden-42.stdout diff --git a/cmd/list_specs.go b/cmd/list_specs.go index c49d593..2f84dce 100644 --- a/cmd/list_specs.go +++ b/cmd/list_specs.go @@ -8,7 +8,7 @@ import ( "github.com/alajmo/sake/core/print" ) -var specHeaders = []string{"spec", "desc", "describe", "list_hosts", "order", "silent", "strategy", "batch", "batch_p", "forks", "output", "any_errors_fatal", "max_fail_percentage", "ignore_errors", "ignore_unreachable", "omit_empty", "report"} +var specHeaders = []string{"spec", "desc", "describe", "list_hosts", "order", "silent", "hidden", "strategy", "batch", "batch_p", "forks", "output", "any_errors_fatal", "max_fail_percentage", "ignore_errors", "ignore_unreachable", "omit_empty", "report", "verbose", "confirm", "step"} func listSpecsCmd(config *dao.Config, configErr *error, listFlags *core.ListFlags) *cobra.Command { var specFlags core.SpecFlags diff --git a/core/dao/spec.go b/core/dao/spec.go index d3f1e14..01133ed 100644 --- a/core/dao/spec.go +++ b/core/dao/spec.go @@ -18,6 +18,7 @@ type Spec struct { ListHosts bool `yaml:"list_hosts"` Order string `yaml:"order"` Silent bool `yaml:"silent"` + Hidden bool `yaml:"hidden"` Strategy string `yaml:"strategy"` Batch uint32 `yaml:"batch"` BatchP uint8 `yaml:"batch_p"` @@ -59,6 +60,14 @@ func (s Spec) GetValue(key string, _ int) string { return strconv.FormatBool(s.ListHosts) case "silent", "Silent": return strconv.FormatBool(s.Silent) + case "hidden", "Hidden": + return strconv.FormatBool(s.Hidden) + case "verbose", "Verbose": + return strconv.FormatBool(s.Verbose) + case "confirm", "Confirm": + return strconv.FormatBool(s.Confirm) + case "step", "Step": + return strconv.FormatBool(s.Step) case "strategy": return s.Strategy case "forks": diff --git a/core/dao/task.go b/core/dao/task.go index 754ccc3..f99960d 100644 --- a/core/dao/task.go +++ b/core/dao/task.go @@ -474,6 +474,10 @@ func (c *Config) GetTaskNames() []string { func (c *Config) GetTaskIDAndDesc() []string { taskNames := []string{} for _, task := range c.Tasks { + if task.Spec.Hidden { + continue + } + if task.Desc != "" { taskNames = append(taskNames, fmt.Sprintf("%s\t%s", task.ID, task.Desc)) } else if task.ID != task.Name { diff --git a/core/print/print_block.go b/core/print/print_block.go index a7cf911..118d681 100644 --- a/core/print/print_block.go +++ b/core/print/print_block.go @@ -174,6 +174,8 @@ func PrintSpecBlocks(specs []dao.Spec, indent bool, name bool) { output += printBoolField("describe", spec.Describe, indent) output += printBoolField("list_hosts", spec.ListHosts, indent) output += printStringField("order", spec.Order, indent) + output += printBoolField("Silent", spec.Silent, indent) + output += printBoolField("Hidden", spec.Hidden, indent) output += printStringField("strategy", spec.Strategy, indent) output += printNumberField("batch", int(spec.Batch), indent) output += printNumberField("batch_p", int(spec.BatchP), indent) @@ -186,6 +188,9 @@ func PrintSpecBlocks(specs []dao.Spec, indent bool, name bool) { output += printBoolField("omit_empty_rows", spec.OmitEmptyRows, indent) output += printBoolField("omit_empty_columns", spec.OmitEmptyColumns, indent) output += printSliceField("report", spec.Report, indent) + output += printBoolField("verbose", spec.Verbose, indent) + output += printBoolField("confirm", spec.Confirm, indent) + output += printBoolField("step", spec.Step, indent) if output == "" { continue diff --git a/docs/changelog.md b/docs/changelog.md index dec6f1d..2ab4d0f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,7 @@ ### Features - Add ability to modify prefix in text and table themes +- Hide tasks from auto-completion via spec attribute `hidden: true` ### Fixes diff --git a/test/integration/golden/golden-1.stdout b/test/integration/golden/golden-1.stdout index 47990cb..8d92d0b 100755 --- a/test/integration/golden/golden-1.stdout +++ b/test/integration/golden/golden-1.stdout @@ -1,41 +1,14 @@ Index: 1 -Name: List tasks -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list tasks +Name: List specs +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list specs WantErr: false --- - task | desc -------------------+---------------------- - ping | ping server - real-ping | ping server - Host | print host - Hostname | print hostname - OS | print OS - Kernel | Print kernel version - info | get remote info - env | - env-ref | - env-complex | - env-default | - a | - b | - c | - d | - ref | - nested | - work-dir-1 | - work-dir-2 | - work-dir-3 | - register-1 | - register-2 | - fatal | - fatal-true | - errors | - errors-true | - unreachable | - unreachable-true | - empty | - empty-true | - output | + spec | describe | list_hosts | silent | hidden | strategy | batch | batch_p | forks | output | any_errors_fatal | max_fail_percentage | ignore_errors | ignore_unreachable | report | verbose | confirm | step +---------+----------+------------+--------+--------+----------+-------+---------+-------+--------+------------------+---------------------+---------------+--------------------+--------+---------+---------+------- + default | false | false | false | false | linear | 1 | 0 | 0 | table | false | 0 | false | false | recap | false | false | false + table | false | false | false | false | | 0 | 0 | 0 | table | false | 0 | false | false | recap | false | false | false + text | false | false | false | false | | 0 | 0 | 0 | text | false | 0 | false | false | recap | false | false | false + info | false | false | false | false | free | 0 | 0 | 0 | table | false | 0 | true | true | recap | false | false | false diff --git a/test/integration/golden/golden-10.stdout b/test/integration/golden/golden-10.stdout index 87100a6..df43561 100755 --- a/test/integration/golden/golden-10.stdout +++ b/test/integration/golden/golden-10.stdout @@ -1,466 +1,183 @@ Index: 10 -Name: Describe tasks -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe tasks +Name: Describe servers +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe servers WantErr: false --- -name: ping -desc: ping server -theme: default -spec: - strategy: linear - batch: 1 - output: table - report: recap -target: - all: true -cmd: - echo pong - --- - -name: real-ping -desc: ping server -theme: default +name: localhost +desc: localhost +user: test +host: localhost +port: 22 local: true -spec: - strategy: linear - batch: 1 - output: table - report: recap -target: - all: true -cmd: - ping $S_HOST -c 2 - --- - -task: print-host -name: Host -desc: print host -theme: default -spec: - strategy: free - output: table - ignore_errors: true - ignore_unreachable: true - report: recap -target: - all: true -cmd: - echo $S_HOST - --- - -task: print-hostname -name: Hostname -desc: print hostname -theme: default -spec: - strategy: free - output: table - ignore_errors: true - ignore_unreachable: true - report: recap -target: - all: true -cmd: - hostname - --- - -task: print-os -name: OS -desc: print OS -theme: default -spec: - strategy: free - output: table - ignore_errors: true - ignore_unreachable: true - report: recap -target: - all: true -cmd: - echo OS +work_dir: /tmp +tags: local, reachable -- -task: print-kernel -name: Kernel -desc: Print kernel version -theme: default -spec: - strategy: free - output: table - ignore_errors: true - ignore_unreachable: true - report: recap -target: - all: true -cmd: - echo kernel - --- - -name: info -desc: get remote info -theme: default -spec: - strategy: free - output: table - ignore_errors: true - ignore_unreachable: true - report: recap -target: - all: true -tasks: - - OS: print OS - - Kernel: Print kernel version +name: unreachable +user: test +host: unreachable.lan +port: 22 +tags: unreachable -- -name: env -theme: default -spec: - output: table - report: recap -target: - all: true +name: list-0 +group: list +desc: many hosts using list +user: test +host: 172.24.2.2 +port: 22 +tags: remote, prod, list, reachable env: - foo: xyz - task: local -cmd: - echo "foo $foo" - echo "hello $hello" - echo "cookie $cookie" - echo "release $release" - echo "task $task" + hello: world -- -name: env-ref -theme: default -spec: - output: table - report: recap -target: - all: true +name: list-1 +group: list +desc: many hosts using list +user: test +host: 172.24.2.4 +port: 22 +tags: remote, prod, list, reachable env: - task: 123 - xyz: xyz -cmd: - echo "foo $foo" - echo "hello $hello" - echo "cookie $cookie" - echo "release $release" - echo "task $task" - echo "xyz $xyz" + hello: world -- -name: env-complex -theme: default -spec: - output: table - report: recap -target: - all: true +name: range-0 +group: range +desc: many hosts using range +user: test +host: 172.24.2.2 +port: 22 +tags: remote, prod, range, reachable env: - foo: xyz - task: local -tasks: - - env-ref - - env-ref - --- - -name: env-default -theme: default -spec: - output: table - report: recap -target: - all: true -cmd: - echo "# SERVER" - echo "S_TAGS $S_TAGS" - echo "S_HOST $S_HOST" - echo "S_USER $S_USER" - echo "S_PORT $S_PORT" - --- - -name: a -theme: default -spec: - strategy: linear - batch: 1 - output: table - report: recap -tasks: - - ping: ping server - --- - -name: b -theme: default -spec: - strategy: linear - batch: 1 - output: table - report: recap -tasks: - - ping: ping server - - ping: ping server - --- - -name: c -theme: default -spec: - strategy: linear - batch: 1 - output: table - report: recap -tasks: - - ping: ping server - - ping: ping server - - ping: ping server - --- - -name: d -theme: default -spec: - output: table - report: recap -target: - all: true -tasks: - - ping: ping server - - ping: ping server - - ping: ping server - - ping: ping server - - ping: ping server - - ping: ping server - --- - -task: work-ref -name: ref -theme: default -work_dir: /usr -spec: - strategy: linear - batch: 1 - output: table - report: recap -cmd: - pwd - --- - -task: work-nested -name: nested -theme: default -spec: - strategy: linear - batch: 1 - output: table - report: recap -tasks: - - ref - --- - -name: work-dir-1 -theme: default -work_dir: /home -spec: - output: table - report: recap -target: - all: true -tasks: - - ref - - Override inline ref - - Inline - - Override inline - --- - -name: work-dir-2 -theme: default -spec: - output: table - report: recap -target: - all: true -tasks: - - ref - - Override inline ref - - Inline - - Override inline + hello: world -- -name: work-dir-3 -theme: default -spec: - output: table - report: recap -target: - all: true -tasks: - - ref - - ref +name: range-1 +group: range +desc: many hosts using range +user: test +host: 172.24.2.4 +port: 22 +tags: remote, prod, range, reachable +env: + hello: world -- -name: register-1 -theme: default -spec: - strategy: linear - batch: 1 - output: table - report: recap -tasks: - - task-0 +name: inv-0 +group: inv +desc: many hosts using inventory +user: test +host: 172.24.2.2 +port: 22 +tags: remote, prod, inv, reachable +env: + hello: world + host: 172.24.2.4 -- -name: register-2 -theme: default -spec: - strategy: linear - batch: 1 - output: table - report: recap -tasks: - - task-0 - - task-1 - - task-2 - - task-3 +name: inv-1 +group: inv +desc: many hosts using inventory +user: test +host: 172.24.2.4 +port: 22 +tags: remote, prod, inv, reachable +env: + hello: world + host: 172.24.2.4 -- -name: fatal -theme: default -spec: - output: table - report: recap -target: - tags: reachable -cmd: - exit 1 +name: server-1 +desc: server-1 +user: test +host: 172.24.2.2 +port: 22 +bastion: 172.24.2.99 +work_dir: /home/test +tags: remote, prod, reachable +env: + host: 172.24.2.2 -- -name: fatal-true -theme: default -spec: - output: table - any_errors_fatal: true - report: recap -target: - tags: reachable -cmd: - exit 1 +name: server-2 +desc: server-2 +user: test +host: 172.24.2.3 +port: 33 +tags: remote, prod, reachable -- -name: errors -theme: default -spec: - output: table - report: recap -target: - tags: reachable -tasks: - - task-0 - - task-1 - - task-2 +name: server-3 +desc: server-3 +user: test +host: 172.24.2.4 +port: 22 +tags: remote, demo, reachable -- -name: errors-true -theme: default -spec: - output: table - ignore_errors: true - report: recap -target: - tags: reachable -tasks: - - task-0 - - task-1 - - task-2 +name: server-4 +desc: server-4 +user: test +host: 172.24.2.5 +port: 22 +tags: remote, demo, reachable -- -name: unreachable -theme: default -spec: - report: recap -target: - all: true -cmd: - echo 123 +name: server-5 +desc: server-5 +user: test +host: 172.24.2.6 +port: 22 +tags: remote, sandbox, reachable -- -name: unreachable-true -theme: default -spec: - ignore_unreachable: true - report: recap -target: - all: true -cmd: - echo 123 +name: server-6 +desc: server-6 +user: test +host: 172.24.2.7 +port: 22 +tags: remote, sandbox, reachable -- -name: empty -theme: default -spec: - output: table - report: recap -target: - tags: reachable -cmd: - if [[ -d ".ssh" ]] - then - echo "Exists" - fi +name: server-7 +desc: server-7 +user: test +host: 172.24.2.8 +port: 22 +tags: remote, demo, reachable -- -name: empty-true -theme: default -spec: - output: table - omit_empty_rows: true - report: recap -target: - tags: reachable -cmd: - if [[ -d ".ssh" ]] - then - echo "Exists" - fi +name: server-8 +desc: server-8 +user: test +host: 172.24.2.9 +port: 22 +tags: remote, demo, reachable -- -name: output -theme: default -spec: - output: table - report: recap -tasks: - - task-0 - - task-1 - - task-2 +name: server-9 +desc: server-9 +user: test +host: 2001:3984:3989::10 +port: 22 +tags: remote, demo, reachable diff --git a/test/integration/golden/golden-11.stdout b/test/integration/golden/golden-11.stdout index 0ea8d90..431ee93 100755 --- a/test/integration/golden/golden-11.stdout +++ b/test/integration/golden/golden-11.stdout @@ -1,47 +1,29 @@ Index: 11 -Name: Ping all servers -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run ping -q -t reachable +Name: Describe servers filter on list hosts +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe servers list WantErr: false --- -TASKS +name: list-0 +group: list +desc: many hosts using list +user: test +host: 172.24.2.2 +port: 22 +tags: remote, prod, list, reachable +env: + hello: world - host | ping ---------------------+------ - localhost | pong - 172.24.2.2 | pong - 172.24.2.4 | pong - 172.24.2.2 | pong - 172.24.2.4 | pong - 172.24.2.2 | pong - 172.24.2.4 | pong - 172.24.2.2 | pong - 172.24.2.3 | pong - 172.24.2.4 | pong - 172.24.2.5 | pong - 172.24.2.6 | pong - 172.24.2.7 | pong - 172.24.2.8 | pong - 172.24.2.9 | pong - 2001:3984:3989::10 | pong - - localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------- - Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 +-- + +name: list-1 +group: list +desc: many hosts using list +user: test +host: 172.24.2.4 +port: 22 +tags: remote, prod, list, reachable +env: + hello: world diff --git a/test/integration/golden/golden-12.stdout b/test/integration/golden/golden-12.stdout index ecaec1b..3268f1e 100755 --- a/test/integration/golden/golden-12.stdout +++ b/test/integration/golden/golden-12.stdout @@ -1,31 +1,29 @@ Index: 12 -Name: Multiple commands -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -t prod +Name: Describe servers filter on range hosts +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe servers range WantErr: false --- -TASKS +name: range-0 +group: range +desc: many hosts using range +user: test +host: 172.24.2.2 +port: 22 +tags: remote, prod, range, reachable +env: + hello: world - host | OS | Kernel -------------+----+-------- - 172.24.2.2 | OS | kernel - 172.24.2.4 | OS | kernel - 172.24.2.2 | OS | kernel - 172.24.2.4 | OS | kernel - 172.24.2.2 | OS | kernel - 172.24.2.4 | OS | kernel - 172.24.2.2 | OS | kernel - 172.24.2.3 | OS | kernel - - 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.3 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 ----------------------------------------------------------------------- - Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 +-- + +name: range-1 +group: range +desc: many hosts using range +user: test +host: 172.24.2.4 +port: 22 +tags: remote, prod, range, reachable +env: + hello: world diff --git a/test/integration/golden/golden-13.stdout b/test/integration/golden/golden-13.stdout index c17bc2f..b8fe597 100755 --- a/test/integration/golden/golden-13.stdout +++ b/test/integration/golden/golden-13.stdout @@ -1,15 +1,31 @@ Index: 13 -Name: Filter by hosts server using server name -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -s list-1 +Name: Describe servers filter on inventory hosts +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe servers inv WantErr: false --- -TASKS +name: inv-0 +group: inv +desc: many hosts using inventory +user: test +host: 172.24.2.2 +port: 22 +tags: remote, prod, inv, reachable +env: + hello: world + host: 172.24.2.4 - host | OS | Kernel -------------+----+-------- - 172.24.2.4 | OS | kernel - - 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 +-- + +name: inv-1 +group: inv +desc: many hosts using inventory +user: test +host: 172.24.2.4 +port: 22 +tags: remote, prod, inv, reachable +env: + hello: world + host: 172.24.2.4 diff --git a/test/integration/golden/golden-14.stdout b/test/integration/golden/golden-14.stdout index 8193088..30f237b 100755 --- a/test/integration/golden/golden-14.stdout +++ b/test/integration/golden/golden-14.stdout @@ -1,15 +1,466 @@ Index: 14 -Name: Filter by hosts server using range index -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -s 'list[0]' +Name: Describe tasks +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe tasks WantErr: false --- -TASKS +name: ping +desc: ping server +theme: default +spec: + strategy: linear + batch: 1 + output: table + report: recap +target: + all: true +cmd: + echo pong - host | OS | Kernel -------------+----+-------- - 172.24.2.2 | OS | kernel - - 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 +-- + +name: real-ping +desc: ping server +theme: default +local: true +spec: + strategy: linear + batch: 1 + output: table + report: recap +target: + all: true +cmd: + ping $S_HOST -c 2 + +-- + +task: print-host +name: Host +desc: print host +theme: default +spec: + strategy: free + output: table + ignore_errors: true + ignore_unreachable: true + report: recap +target: + all: true +cmd: + echo $S_HOST + +-- + +task: print-hostname +name: Hostname +desc: print hostname +theme: default +spec: + strategy: free + output: table + ignore_errors: true + ignore_unreachable: true + report: recap +target: + all: true +cmd: + hostname + +-- + +task: print-os +name: OS +desc: print OS +theme: default +spec: + strategy: free + output: table + ignore_errors: true + ignore_unreachable: true + report: recap +target: + all: true +cmd: + echo OS + +-- + +task: print-kernel +name: Kernel +desc: Print kernel version +theme: default +spec: + strategy: free + output: table + ignore_errors: true + ignore_unreachable: true + report: recap +target: + all: true +cmd: + echo kernel + +-- + +name: info +desc: get remote info +theme: default +spec: + strategy: free + output: table + ignore_errors: true + ignore_unreachable: true + report: recap +target: + all: true +tasks: + - OS: print OS + - Kernel: Print kernel version + +-- + +name: env +theme: default +spec: + output: table + report: recap +target: + all: true +env: + foo: xyz + task: local +cmd: + echo "foo $foo" + echo "hello $hello" + echo "cookie $cookie" + echo "release $release" + echo "task $task" + +-- + +name: env-ref +theme: default +spec: + output: table + report: recap +target: + all: true +env: + task: 123 + xyz: xyz +cmd: + echo "foo $foo" + echo "hello $hello" + echo "cookie $cookie" + echo "release $release" + echo "task $task" + echo "xyz $xyz" + +-- + +name: env-complex +theme: default +spec: + output: table + report: recap +target: + all: true +env: + foo: xyz + task: local +tasks: + - env-ref + - env-ref + +-- + +name: env-default +theme: default +spec: + output: table + report: recap +target: + all: true +cmd: + echo "# SERVER" + echo "S_TAGS $S_TAGS" + echo "S_HOST $S_HOST" + echo "S_USER $S_USER" + echo "S_PORT $S_PORT" + +-- + +name: a +theme: default +spec: + strategy: linear + batch: 1 + output: table + report: recap +tasks: + - ping: ping server + +-- + +name: b +theme: default +spec: + strategy: linear + batch: 1 + output: table + report: recap +tasks: + - ping: ping server + - ping: ping server + +-- + +name: c +theme: default +spec: + strategy: linear + batch: 1 + output: table + report: recap +tasks: + - ping: ping server + - ping: ping server + - ping: ping server + +-- + +name: d +theme: default +spec: + output: table + report: recap +target: + all: true +tasks: + - ping: ping server + - ping: ping server + - ping: ping server + - ping: ping server + - ping: ping server + - ping: ping server + +-- + +task: work-ref +name: ref +theme: default +work_dir: /usr +spec: + strategy: linear + batch: 1 + output: table + report: recap +cmd: + pwd + +-- + +task: work-nested +name: nested +theme: default +spec: + strategy: linear + batch: 1 + output: table + report: recap +tasks: + - ref + +-- + +name: work-dir-1 +theme: default +work_dir: /home +spec: + output: table + report: recap +target: + all: true +tasks: + - ref + - Override inline ref + - Inline + - Override inline + +-- + +name: work-dir-2 +theme: default +spec: + output: table + report: recap +target: + all: true +tasks: + - ref + - Override inline ref + - Inline + - Override inline + +-- + +name: work-dir-3 +theme: default +spec: + output: table + report: recap +target: + all: true +tasks: + - ref + - ref + +-- + +name: register-1 +theme: default +spec: + strategy: linear + batch: 1 + output: table + report: recap +tasks: + - task-0 + +-- + +name: register-2 +theme: default +spec: + strategy: linear + batch: 1 + output: table + report: recap +tasks: + - task-0 + - task-1 + - task-2 + - task-3 + +-- + +name: fatal +theme: default +spec: + output: table + report: recap +target: + tags: reachable +cmd: + exit 1 + +-- + +name: fatal-true +theme: default +spec: + output: table + any_errors_fatal: true + report: recap +target: + tags: reachable +cmd: + exit 1 + +-- + +name: errors +theme: default +spec: + output: table + report: recap +target: + tags: reachable +tasks: + - task-0 + - task-1 + - task-2 + +-- + +name: errors-true +theme: default +spec: + output: table + ignore_errors: true + report: recap +target: + tags: reachable +tasks: + - task-0 + - task-1 + - task-2 + +-- + +name: unreachable +theme: default +spec: + report: recap +target: + all: true +cmd: + echo 123 + +-- + +name: unreachable-true +theme: default +spec: + ignore_unreachable: true + report: recap +target: + all: true +cmd: + echo 123 + +-- + +name: empty +theme: default +spec: + output: table + report: recap +target: + tags: reachable +cmd: + if [[ -d ".ssh" ]] + then + echo "Exists" + fi + +-- + +name: empty-true +theme: default +spec: + output: table + omit_empty_rows: true + report: recap +target: + tags: reachable +cmd: + if [[ -d ".ssh" ]] + then + echo "Exists" + fi + +-- + +name: output +theme: default +spec: + output: table + report: recap +tasks: + - task-0 + - task-1 + - task-2 diff --git a/test/integration/golden/golden-15.stdout b/test/integration/golden/golden-15.stdout index a370c3e..4f09cf4 100755 --- a/test/integration/golden/golden-15.stdout +++ b/test/integration/golden/golden-15.stdout @@ -1,19 +1,47 @@ Index: 15 -Name: Filter by hosts server -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -s 'list[0:2]' +Name: Ping all servers +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run ping -q -t reachable WantErr: false --- TASKS - host | OS | Kernel -------------+----+-------- - 172.24.2.2 | OS | kernel - 172.24.2.4 | OS | kernel - - 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 ---------------------------------------------------------------------- - Total ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + host | ping +--------------------+------ + localhost | pong + 172.24.2.2 | pong + 172.24.2.4 | pong + 172.24.2.2 | pong + 172.24.2.4 | pong + 172.24.2.2 | pong + 172.24.2.4 | pong + 172.24.2.2 | pong + 172.24.2.3 | pong + 172.24.2.4 | pong + 172.24.2.5 | pong + 172.24.2.6 | pong + 172.24.2.7 | pong + 172.24.2.8 | pong + 172.24.2.9 | pong + 2001:3984:3989::10 | pong + + localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-16.stdout b/test/integration/golden/golden-16.stdout index e23c89c..5ff1a63 100755 --- a/test/integration/golden/golden-16.stdout +++ b/test/integration/golden/golden-16.stdout @@ -1,6 +1,6 @@ Index: 16 -Name: Filter by host regex -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -r '172.24.2.(2|4)' +Name: Multiple commands +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -t prod WantErr: false --- @@ -16,7 +16,7 @@ TASKS 172.24.2.2 | OS | kernel 172.24.2.4 | OS | kernel 172.24.2.2 | OS | kernel - 172.24.2.4 | OS | kernel + 172.24.2.3 | OS | kernel 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 @@ -25,7 +25,7 @@ TASKS 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 ---------------------------------------------------------------------- Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-17.stdout b/test/integration/golden/golden-17.stdout index e53b9eb..ed6c8e9 100755 --- a/test/integration/golden/golden-17.stdout +++ b/test/integration/golden/golden-17.stdout @@ -1,19 +1,15 @@ Index: 17 -Name: Limit to 2 servers -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run ping -q -t reachable -l 2 +Name: Filter by hosts server using server name +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -s list-1 WantErr: false --- TASKS - host | ping -------------+------ - localhost | pong - 172.24.2.2 | pong + host | OS | Kernel +------------+----+-------- + 172.24.2.4 | OS | kernel - localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 ---------------------------------------------------------------------- - Total ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-18.stdout b/test/integration/golden/golden-18.stdout index 7fa65fc..b605dcf 100755 --- a/test/integration/golden/golden-18.stdout +++ b/test/integration/golden/golden-18.stdout @@ -1,31 +1,15 @@ Index: 18 -Name: Limit to 50 percent servers -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run ping -q -t reachable -L 50 +Name: Filter by hosts server using range index +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -s 'list[0]' WantErr: false --- TASKS - host | ping -------------+------ - localhost | pong - 172.24.2.2 | pong - 172.24.2.4 | pong - 172.24.2.2 | pong - 172.24.2.4 | pong - 172.24.2.2 | pong - 172.24.2.4 | pong - 172.24.2.2 | pong + host | OS | Kernel +------------+----+-------- + 172.24.2.2 | OS | kernel - localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 ---------------------------------------------------------------------- - Total ok=8 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-19.stdout b/test/integration/golden/golden-19.stdout index 2f54883..1cab54c 100755 --- a/test/integration/golden/golden-19.stdout +++ b/test/integration/golden/golden-19.stdout @@ -1,47 +1,19 @@ Index: 19 -Name: Filter by inverting on tag unreachable -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run ping -q -t unreachable -v +Name: Filter by hosts server +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -s 'list[0:2]' WantErr: false --- TASKS - host | ping ---------------------+------ - localhost | pong - 172.24.2.2 | pong - 172.24.2.4 | pong - 172.24.2.2 | pong - 172.24.2.4 | pong - 172.24.2.2 | pong - 172.24.2.4 | pong - 172.24.2.2 | pong - 172.24.2.3 | pong - 172.24.2.4 | pong - 172.24.2.5 | pong - 172.24.2.6 | pong - 172.24.2.7 | pong - 172.24.2.8 | pong - 172.24.2.9 | pong - 2001:3984:3989::10 | pong - - localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------- - Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 + host | OS | Kernel +------------+----+-------- + 172.24.2.2 | OS | kernel + 172.24.2.4 | OS | kernel + + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 +--------------------------------------------------------------------- + Total ok=4 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-2.stdout b/test/integration/golden/golden-2.stdout index 7078891..9dccb13 100755 --- a/test/integration/golden/golden-2.stdout +++ b/test/integration/golden/golden-2.stdout @@ -1,27 +1,12 @@ Index: 2 -Name: List servers -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list servers +Name: List targets +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list targets WantErr: false --- - server | host | tags | desc --------------+--------------------+-----------------------------+---------------------------- - localhost | localhost | local,reachable | localhost - unreachable | unreachable.lan | unreachable | - list-0 | 172.24.2.2 | remote,prod,list,reachable | many hosts using list - list-1 | 172.24.2.4 | remote,prod,list,reachable | many hosts using list - range-0 | 172.24.2.2 | remote,prod,range,reachable | many hosts using range - range-1 | 172.24.2.4 | remote,prod,range,reachable | many hosts using range - inv-0 | 172.24.2.2 | remote,prod,inv,reachable | many hosts using inventory - inv-1 | 172.24.2.4 | remote,prod,inv,reachable | many hosts using inventory - server-1 | 172.24.2.2 | remote,prod,reachable | server-1 - server-2 | 172.24.2.3 | remote,prod,reachable | server-2 - server-3 | 172.24.2.4 | remote,demo,reachable | server-3 - server-4 | 172.24.2.5 | remote,demo,reachable | server-4 - server-5 | 172.24.2.6 | remote,sandbox,reachable | server-5 - server-6 | 172.24.2.7 | remote,sandbox,reachable | server-6 - server-7 | 172.24.2.8 | remote,demo,reachable | server-7 - server-8 | 172.24.2.9 | remote,demo,reachable | server-8 - server-9 | 2001:3984:3989::10 | remote,demo,reachable | server-9 + target | all | invert | limit | limit_p +---------+-------+--------+-------+--------- + all | true | false | 0 | 0 + default | false | false | 0 | 0 diff --git a/test/integration/golden/golden-20.stdout b/test/integration/golden/golden-20.stdout index 4273e02..0013311 100755 --- a/test/integration/golden/golden-20.stdout +++ b/test/integration/golden/golden-20.stdout @@ -1,111 +1,31 @@ Index: 20 -Name: Simple Envs -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run env -q -t reachable +Name: Filter by host regex +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run info -q -r '172.24.2.(2|4)' WantErr: false --- TASKS - host | env ---------------------+------------- - localhost | foo xyz - | hello - | cookie - | release - | task local - 172.24.2.2 | foo xyz - | hello world - | cookie - | release - | task local - 172.24.2.4 | foo xyz - | hello world - | cookie - | release - | task local - 172.24.2.2 | foo xyz - | hello world - | cookie - | release - | task local - 172.24.2.4 | foo xyz - | hello world - | cookie - | release - | task local - 172.24.2.2 | foo xyz - | hello world - | cookie - | release - | task local - 172.24.2.4 | foo xyz - | hello world - | cookie - | release - | task local - 172.24.2.2 | foo xyz - | hello - | cookie - | release - | task local - 172.24.2.3 | foo xyz - | hello - | cookie - | release - | task local - 172.24.2.4 | foo xyz - | hello - | cookie - | release - | task local - 172.24.2.5 | foo xyz - | hello - | cookie - | release - | task local - 172.24.2.6 | foo xyz - | hello - | cookie - | release - | task local - 172.24.2.7 | foo xyz - | hello - | cookie - | release - | task local - 172.24.2.8 | foo xyz - | hello - | cookie - | release - | task local - 172.24.2.9 | foo xyz - | hello - | cookie - | release - | task local - 2001:3984:3989::10 | foo xyz - | hello - | cookie - | release - | task local - - localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------- - Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 + host | OS | Kernel +------------+----+-------- + 172.24.2.2 | OS | kernel + 172.24.2.4 | OS | kernel + 172.24.2.2 | OS | kernel + 172.24.2.4 | OS | kernel + 172.24.2.2 | OS | kernel + 172.24.2.4 | OS | kernel + 172.24.2.2 | OS | kernel + 172.24.2.4 | OS | kernel + + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 +---------------------------------------------------------------------- + Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-21.stdout b/test/integration/golden/golden-21.stdout index 80deeef..4cc04b9 100755 --- a/test/integration/golden/golden-21.stdout +++ b/test/integration/golden/golden-21.stdout @@ -1,127 +1,19 @@ Index: 21 -Name: Reference Envs -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run env-complex -q -t reachable +Name: Limit to 2 servers +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run ping -q -t reachable -l 2 WantErr: false --- TASKS - host | env-ref | env-ref ---------------------+-------------+------------- - localhost | foo xyz | foo xyz - | hello | hello - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - 172.24.2.2 | foo xyz | foo xyz - | hello world | hello world - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - 172.24.2.4 | foo xyz | foo xyz - | hello world | hello world - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - 172.24.2.2 | foo xyz | foo xyz - | hello world | hello world - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - 172.24.2.4 | foo xyz | foo xyz - | hello world | hello world - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - 172.24.2.2 | foo xyz | foo xyz - | hello world | hello world - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - 172.24.2.4 | foo xyz | foo xyz - | hello world | hello world - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - 172.24.2.2 | foo xyz | foo xyz - | hello | hello - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - 172.24.2.3 | foo xyz | foo xyz - | hello | hello - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - 172.24.2.4 | foo xyz | foo xyz - | hello | hello - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - 172.24.2.5 | foo xyz | foo xyz - | hello | hello - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - 172.24.2.6 | foo xyz | foo xyz - | hello | hello - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - 172.24.2.7 | foo xyz | foo xyz - | hello | hello - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - 172.24.2.8 | foo xyz | foo xyz - | hello | hello - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - 172.24.2.9 | foo xyz | foo xyz - | hello | hello - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - 2001:3984:3989::10 | foo xyz | foo xyz - | hello | hello - | cookie | cookie - | release | release - | task local | task remote - | xyz xyz | xyz xyz - - localhost ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.3 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.5 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.6 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.7 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.8 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.9 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------- - Total ok=32 unreachable=0 ignored=0 failed=0 skipped=0 + host | ping +------------+------ + localhost | pong + 172.24.2.2 | pong + + localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 +--------------------------------------------------------------------- + Total ok=2 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-22.stdout b/test/integration/golden/golden-22.stdout index 1d60b9e..5a658eb 100755 --- a/test/integration/golden/golden-22.stdout +++ b/test/integration/golden/golden-22.stdout @@ -1,111 +1,31 @@ Index: 22 -Name: Default Envs -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run env-default -q -t reachable +Name: Limit to 50 percent servers +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run ping -q -t reachable -L 50 WantErr: false --- TASKS - host | env-default ---------------------+------------------------------------ - localhost | # SERVER - | S_TAGS local,reachable - | S_HOST localhost - | S_USER test - | S_PORT 22 - 172.24.2.2 | # SERVER - | S_TAGS remote,prod,list,reachable - | S_HOST 172.24.2.2 - | S_USER test - | S_PORT 22 - 172.24.2.4 | # SERVER - | S_TAGS remote,prod,list,reachable - | S_HOST 172.24.2.4 - | S_USER test - | S_PORT 22 - 172.24.2.2 | # SERVER - | S_TAGS remote,prod,range,reachable - | S_HOST 172.24.2.2 - | S_USER test - | S_PORT 22 - 172.24.2.4 | # SERVER - | S_TAGS remote,prod,range,reachable - | S_HOST 172.24.2.4 - | S_USER test - | S_PORT 22 - 172.24.2.2 | # SERVER - | S_TAGS remote,prod,inv,reachable - | S_HOST 172.24.2.2 - | S_USER test - | S_PORT 22 - 172.24.2.4 | # SERVER - | S_TAGS remote,prod,inv,reachable - | S_HOST 172.24.2.4 - | S_USER test - | S_PORT 22 - 172.24.2.2 | # SERVER - | S_TAGS remote,prod,reachable - | S_HOST 172.24.2.2 - | S_USER test - | S_PORT 22 - 172.24.2.3 | # SERVER - | S_TAGS remote,prod,reachable - | S_HOST 172.24.2.3 - | S_USER test - | S_PORT 33 - 172.24.2.4 | # SERVER - | S_TAGS remote,demo,reachable - | S_HOST 172.24.2.4 - | S_USER test - | S_PORT 22 - 172.24.2.5 | # SERVER - | S_TAGS remote,demo,reachable - | S_HOST 172.24.2.5 - | S_USER test - | S_PORT 22 - 172.24.2.6 | # SERVER - | S_TAGS remote,sandbox,reachable - | S_HOST 172.24.2.6 - | S_USER test - | S_PORT 22 - 172.24.2.7 | # SERVER - | S_TAGS remote,sandbox,reachable - | S_HOST 172.24.2.7 - | S_USER test - | S_PORT 22 - 172.24.2.8 | # SERVER - | S_TAGS remote,demo,reachable - | S_HOST 172.24.2.8 - | S_USER test - | S_PORT 22 - 172.24.2.9 | # SERVER - | S_TAGS remote,demo,reachable - | S_HOST 172.24.2.9 - | S_USER test - | S_PORT 22 - 2001:3984:3989::10 | # SERVER - | S_TAGS remote,demo,reachable - | S_HOST 2001:3984:3989::10 - | S_USER test - | S_PORT 22 - - localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------- - Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 + host | ping +------------+------ + localhost | pong + 172.24.2.2 | pong + 172.24.2.4 | pong + 172.24.2.2 | pong + 172.24.2.4 | pong + 172.24.2.2 | pong + 172.24.2.4 | pong + 172.24.2.2 | pong + + localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 +--------------------------------------------------------------------- + Total ok=8 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-23.stdout b/test/integration/golden/golden-23.stdout index 9f00ecd..defa7ff 100755 --- a/test/integration/golden/golden-23.stdout +++ b/test/integration/golden/golden-23.stdout @@ -1,47 +1,47 @@ Index: 23 -Name: Nested tasks -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run d -q -t reachable +Name: Filter by inverting on tag unreachable +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run ping -q -t unreachable -v WantErr: false --- TASKS - host | ping | ping | ping | ping | ping | ping ---------------------+------+------+------+------+------+------ - localhost | pong | pong | pong | pong | pong | pong - 172.24.2.2 | pong | pong | pong | pong | pong | pong - 172.24.2.4 | pong | pong | pong | pong | pong | pong - 172.24.2.2 | pong | pong | pong | pong | pong | pong - 172.24.2.4 | pong | pong | pong | pong | pong | pong - 172.24.2.2 | pong | pong | pong | pong | pong | pong - 172.24.2.4 | pong | pong | pong | pong | pong | pong - 172.24.2.2 | pong | pong | pong | pong | pong | pong - 172.24.2.3 | pong | pong | pong | pong | pong | pong - 172.24.2.4 | pong | pong | pong | pong | pong | pong - 172.24.2.5 | pong | pong | pong | pong | pong | pong - 172.24.2.6 | pong | pong | pong | pong | pong | pong - 172.24.2.7 | pong | pong | pong | pong | pong | pong - 172.24.2.8 | pong | pong | pong | pong | pong | pong - 172.24.2.9 | pong | pong | pong | pong | pong | pong - 2001:3984:3989::10 | pong | pong | pong | pong | pong | pong + host | ping +--------------------+------ + localhost | pong + 172.24.2.2 | pong + 172.24.2.4 | pong + 172.24.2.2 | pong + 172.24.2.4 | pong + 172.24.2.2 | pong + 172.24.2.4 | pong + 172.24.2.2 | pong + 172.24.2.3 | pong + 172.24.2.4 | pong + 172.24.2.5 | pong + 172.24.2.6 | pong + 172.24.2.7 | pong + 172.24.2.8 | pong + 172.24.2.9 | pong + 2001:3984:3989::10 | pong - localhost ok=6 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.3 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.5 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.6 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.7 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.8 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.9 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ - Total ok=96 unreachable=0 ignored=0 failed=0 skipped=0 + Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-24.stdout b/test/integration/golden/golden-24.stdout index d8dba75..fd4d9d6 100755 --- a/test/integration/golden/golden-24.stdout +++ b/test/integration/golden/golden-24.stdout @@ -1,47 +1,111 @@ Index: 24 -Name: Work Dir 1 -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run work-dir-1 -q -t reachable +Name: Simple Envs +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run env -q -t reachable WantErr: false --- TASKS - host | ref | Override inline ref | Inline | Override inline ---------------------+-------+---------------------+--------+----------------- - localhost | /home | /opt | /home | / - 172.24.2.2 | /home | /opt | /home | / - 172.24.2.4 | /home | /opt | /home | / - 172.24.2.2 | /home | /opt | /home | / - 172.24.2.4 | /home | /opt | /home | / - 172.24.2.2 | /home | /opt | /home | / - 172.24.2.4 | /home | /opt | /home | / - 172.24.2.2 | /home | /opt | /home | / - 172.24.2.3 | /home | /opt | /home | / - 172.24.2.4 | /home | /opt | /home | / - 172.24.2.5 | /home | /opt | /home | / - 172.24.2.6 | /home | /opt | /home | / - 172.24.2.7 | /home | /opt | /home | / - 172.24.2.8 | /home | /opt | /home | / - 172.24.2.9 | /home | /opt | /home | / - 2001:3984:3989::10 | /home | /opt | /home | / + host | env +--------------------+------------- + localhost | foo xyz + | hello + | cookie + | release + | task local + 172.24.2.2 | foo xyz + | hello world + | cookie + | release + | task local + 172.24.2.4 | foo xyz + | hello world + | cookie + | release + | task local + 172.24.2.2 | foo xyz + | hello world + | cookie + | release + | task local + 172.24.2.4 | foo xyz + | hello world + | cookie + | release + | task local + 172.24.2.2 | foo xyz + | hello world + | cookie + | release + | task local + 172.24.2.4 | foo xyz + | hello world + | cookie + | release + | task local + 172.24.2.2 | foo xyz + | hello + | cookie + | release + | task local + 172.24.2.3 | foo xyz + | hello + | cookie + | release + | task local + 172.24.2.4 | foo xyz + | hello + | cookie + | release + | task local + 172.24.2.5 | foo xyz + | hello + | cookie + | release + | task local + 172.24.2.6 | foo xyz + | hello + | cookie + | release + | task local + 172.24.2.7 | foo xyz + | hello + | cookie + | release + | task local + 172.24.2.8 | foo xyz + | hello + | cookie + | release + | task local + 172.24.2.9 | foo xyz + | hello + | cookie + | release + | task local + 2001:3984:3989::10 | foo xyz + | hello + | cookie + | release + | task local - localhost ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.3 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.5 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.6 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.7 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.8 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.9 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ - Total ok=64 unreachable=0 ignored=0 failed=0 skipped=0 + Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-25.stdout b/test/integration/golden/golden-25.stdout index 466e3a5..f8ac2fa 100755 --- a/test/integration/golden/golden-25.stdout +++ b/test/integration/golden/golden-25.stdout @@ -1,47 +1,127 @@ Index: 25 -Name: Work Dir 2 -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run work-dir-2 -q -t reachable +Name: Reference Envs +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run env-complex -q -t reachable WantErr: false --- TASKS - host | ref | Override inline ref | Inline | Override inline ---------------------+------+---------------------+------------+----------------- - localhost | /usr | /opt | /tmp | / - 172.24.2.2 | /usr | /opt | /home/test | / - 172.24.2.4 | /usr | /opt | /home/test | / - 172.24.2.2 | /usr | /opt | /home/test | / - 172.24.2.4 | /usr | /opt | /home/test | / - 172.24.2.2 | /usr | /opt | /home/test | / - 172.24.2.4 | /usr | /opt | /home/test | / - 172.24.2.2 | /usr | /opt | /home/test | / - 172.24.2.3 | /usr | /opt | /home/test | / - 172.24.2.4 | /usr | /opt | /home/test | / - 172.24.2.5 | /usr | /opt | /home/test | / - 172.24.2.6 | /usr | /opt | /home/test | / - 172.24.2.7 | /usr | /opt | /home/test | / - 172.24.2.8 | /usr | /opt | /home/test | / - 172.24.2.9 | /usr | /opt | /home/test | / - 2001:3984:3989::10 | /usr | /opt | /home/test | / + host | env-ref | env-ref +--------------------+-------------+------------- + localhost | foo xyz | foo xyz + | hello | hello + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.2 | foo xyz | foo xyz + | hello world | hello world + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.4 | foo xyz | foo xyz + | hello world | hello world + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.2 | foo xyz | foo xyz + | hello world | hello world + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.4 | foo xyz | foo xyz + | hello world | hello world + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.2 | foo xyz | foo xyz + | hello world | hello world + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.4 | foo xyz | foo xyz + | hello world | hello world + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.2 | foo xyz | foo xyz + | hello | hello + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.3 | foo xyz | foo xyz + | hello | hello + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.4 | foo xyz | foo xyz + | hello | hello + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.5 | foo xyz | foo xyz + | hello | hello + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.6 | foo xyz | foo xyz + | hello | hello + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.7 | foo xyz | foo xyz + | hello | hello + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.8 | foo xyz | foo xyz + | hello | hello + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 172.24.2.9 | foo xyz | foo xyz + | hello | hello + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz + 2001:3984:3989::10 | foo xyz | foo xyz + | hello | hello + | cookie | cookie + | release | release + | task local | task remote + | xyz xyz | xyz xyz - localhost ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.3 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.5 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.6 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.7 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.8 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.9 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + localhost ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ - Total ok=64 unreachable=0 ignored=0 failed=0 skipped=0 + Total ok=32 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-26.stdout b/test/integration/golden/golden-26.stdout index c7b1b74..2a5b97e 100755 --- a/test/integration/golden/golden-26.stdout +++ b/test/integration/golden/golden-26.stdout @@ -1,47 +1,111 @@ Index: 26 -Name: Work Dir 3 -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run work-dir-3 -q -t reachable +Name: Default Envs +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run env-default -q -t reachable WantErr: false --- TASKS - host | ref | ref ---------------------+------+------ - localhost | /usr | /etc - 172.24.2.2 | /usr | /etc - 172.24.2.4 | /usr | /etc - 172.24.2.2 | /usr | /etc - 172.24.2.4 | /usr | /etc - 172.24.2.2 | /usr | /etc - 172.24.2.4 | /usr | /etc - 172.24.2.2 | /usr | /etc - 172.24.2.3 | /usr | /etc - 172.24.2.4 | /usr | /etc - 172.24.2.5 | /usr | /etc - 172.24.2.6 | /usr | /etc - 172.24.2.7 | /usr | /etc - 172.24.2.8 | /usr | /etc - 172.24.2.9 | /usr | /etc - 2001:3984:3989::10 | /usr | /etc + host | env-default +--------------------+------------------------------------ + localhost | # SERVER + | S_TAGS local,reachable + | S_HOST localhost + | S_USER test + | S_PORT 22 + 172.24.2.2 | # SERVER + | S_TAGS remote,prod,list,reachable + | S_HOST 172.24.2.2 + | S_USER test + | S_PORT 22 + 172.24.2.4 | # SERVER + | S_TAGS remote,prod,list,reachable + | S_HOST 172.24.2.4 + | S_USER test + | S_PORT 22 + 172.24.2.2 | # SERVER + | S_TAGS remote,prod,range,reachable + | S_HOST 172.24.2.2 + | S_USER test + | S_PORT 22 + 172.24.2.4 | # SERVER + | S_TAGS remote,prod,range,reachable + | S_HOST 172.24.2.4 + | S_USER test + | S_PORT 22 + 172.24.2.2 | # SERVER + | S_TAGS remote,prod,inv,reachable + | S_HOST 172.24.2.2 + | S_USER test + | S_PORT 22 + 172.24.2.4 | # SERVER + | S_TAGS remote,prod,inv,reachable + | S_HOST 172.24.2.4 + | S_USER test + | S_PORT 22 + 172.24.2.2 | # SERVER + | S_TAGS remote,prod,reachable + | S_HOST 172.24.2.2 + | S_USER test + | S_PORT 22 + 172.24.2.3 | # SERVER + | S_TAGS remote,prod,reachable + | S_HOST 172.24.2.3 + | S_USER test + | S_PORT 33 + 172.24.2.4 | # SERVER + | S_TAGS remote,demo,reachable + | S_HOST 172.24.2.4 + | S_USER test + | S_PORT 22 + 172.24.2.5 | # SERVER + | S_TAGS remote,demo,reachable + | S_HOST 172.24.2.5 + | S_USER test + | S_PORT 22 + 172.24.2.6 | # SERVER + | S_TAGS remote,sandbox,reachable + | S_HOST 172.24.2.6 + | S_USER test + | S_PORT 22 + 172.24.2.7 | # SERVER + | S_TAGS remote,sandbox,reachable + | S_HOST 172.24.2.7 + | S_USER test + | S_PORT 22 + 172.24.2.8 | # SERVER + | S_TAGS remote,demo,reachable + | S_HOST 172.24.2.8 + | S_USER test + | S_PORT 22 + 172.24.2.9 | # SERVER + | S_TAGS remote,demo,reachable + | S_HOST 172.24.2.9 + | S_USER test + | S_PORT 22 + 2001:3984:3989::10 | # SERVER + | S_TAGS remote,demo,reachable + | S_HOST 2001:3984:3989::10 + | S_USER test + | S_PORT 22 - localhost ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.3 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.5 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.6 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.7 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.8 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.9 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ - Total ok=32 unreachable=0 ignored=0 failed=0 skipped=0 + Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-27.stdout b/test/integration/golden/golden-27.stdout index a0f21fa..b77d2ea 100755 --- a/test/integration/golden/golden-27.stdout +++ b/test/integration/golden/golden-27.stdout @@ -1,47 +1,47 @@ Index: 27 -Name: Register 1 -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run register-1 -q -t reachable +Name: Nested tasks +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run d -q -t reachable WantErr: false --- TASKS - host | task-0 ---------------------+-------- - localhost | foo - 172.24.2.2 | foo - 172.24.2.4 | foo - 172.24.2.2 | foo - 172.24.2.4 | foo - 172.24.2.2 | foo - 172.24.2.4 | foo - 172.24.2.2 | foo - 172.24.2.3 | foo - 172.24.2.4 | foo - 172.24.2.5 | foo - 172.24.2.6 | foo - 172.24.2.7 | foo - 172.24.2.8 | foo - 172.24.2.9 | foo - 2001:3984:3989::10 | foo + host | ping | ping | ping | ping | ping | ping +--------------------+------+------+------+------+------+------ + localhost | pong | pong | pong | pong | pong | pong + 172.24.2.2 | pong | pong | pong | pong | pong | pong + 172.24.2.4 | pong | pong | pong | pong | pong | pong + 172.24.2.2 | pong | pong | pong | pong | pong | pong + 172.24.2.4 | pong | pong | pong | pong | pong | pong + 172.24.2.2 | pong | pong | pong | pong | pong | pong + 172.24.2.4 | pong | pong | pong | pong | pong | pong + 172.24.2.2 | pong | pong | pong | pong | pong | pong + 172.24.2.3 | pong | pong | pong | pong | pong | pong + 172.24.2.4 | pong | pong | pong | pong | pong | pong + 172.24.2.5 | pong | pong | pong | pong | pong | pong + 172.24.2.6 | pong | pong | pong | pong | pong | pong + 172.24.2.7 | pong | pong | pong | pong | pong | pong + 172.24.2.8 | pong | pong | pong | pong | pong | pong + 172.24.2.9 | pong | pong | pong | pong | pong | pong + 2001:3984:3989::10 | pong | pong | pong | pong | pong | pong - localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + localhost ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ - Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 + Total ok=96 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-28.stdout b/test/integration/golden/golden-28.stdout index 75efa13..e6caf46 100755 --- a/test/integration/golden/golden-28.stdout +++ b/test/integration/golden/golden-28.stdout @@ -1,190 +1,30 @@ Index: 28 -Name: Register 2 -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run register-2 -q -t reachable +Name: Work Dir 1 +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run work-dir-1 -q -t reachable WantErr: false --- TASKS - host | task-0 | task-1 | task-2 | task-3 ---------------------+--------+---------------+---------+----------------- - localhost | foo | status: ok | error 2 | status: ok - | | rc: 0 | | rc: 0 - | | failed: false | | failed: false - | | stdout: foo | | stdout: foo - | | stderr: | | stderr: - | | | | ------------- - | | | | status: ok - | | | | rc: 0 - | | | | failed: false - | | | | stdout: - | | | | stderr: error 2 - 172.24.2.2 | foo | status: ok | error 2 | status: ok - | | rc: 0 | | rc: 0 - | | failed: false | | failed: false - | | stdout: foo | | stdout: foo - | | stderr: | | stderr: - | | | | ------------- - | | | | status: ok - | | | | rc: 0 - | | | | failed: false - | | | | stdout: - | | | | stderr: error 2 - 172.24.2.4 | foo | status: ok | error 2 | status: ok - | | rc: 0 | | rc: 0 - | | failed: false | | failed: false - | | stdout: foo | | stdout: foo - | | stderr: | | stderr: - | | | | ------------- - | | | | status: ok - | | | | rc: 0 - | | | | failed: false - | | | | stdout: - | | | | stderr: error 2 - 172.24.2.2 | foo | status: ok | error 2 | status: ok - | | rc: 0 | | rc: 0 - | | failed: false | | failed: false - | | stdout: foo | | stdout: foo - | | stderr: | | stderr: - | | | | ------------- - | | | | status: ok - | | | | rc: 0 - | | | | failed: false - | | | | stdout: - | | | | stderr: error 2 - 172.24.2.4 | foo | status: ok | error 2 | status: ok - | | rc: 0 | | rc: 0 - | | failed: false | | failed: false - | | stdout: foo | | stdout: foo - | | stderr: | | stderr: - | | | | ------------- - | | | | status: ok - | | | | rc: 0 - | | | | failed: false - | | | | stdout: - | | | | stderr: error 2 - 172.24.2.2 | foo | status: ok | error 2 | status: ok - | | rc: 0 | | rc: 0 - | | failed: false | | failed: false - | | stdout: foo | | stdout: foo - | | stderr: | | stderr: - | | | | ------------- - | | | | status: ok - | | | | rc: 0 - | | | | failed: false - | | | | stdout: - | | | | stderr: error 2 - 172.24.2.4 | foo | status: ok | error 2 | status: ok - | | rc: 0 | | rc: 0 - | | failed: false | | failed: false - | | stdout: foo | | stdout: foo - | | stderr: | | stderr: - | | | | ------------- - | | | | status: ok - | | | | rc: 0 - | | | | failed: false - | | | | stdout: - | | | | stderr: error 2 - 172.24.2.2 | foo | status: ok | error 2 | status: ok - | | rc: 0 | | rc: 0 - | | failed: false | | failed: false - | | stdout: foo | | stdout: foo - | | stderr: | | stderr: - | | | | ------------- - | | | | status: ok - | | | | rc: 0 - | | | | failed: false - | | | | stdout: - | | | | stderr: error 2 - 172.24.2.3 | foo | status: ok | error 2 | status: ok - | | rc: 0 | | rc: 0 - | | failed: false | | failed: false - | | stdout: foo | | stdout: foo - | | stderr: | | stderr: - | | | | ------------- - | | | | status: ok - | | | | rc: 0 - | | | | failed: false - | | | | stdout: - | | | | stderr: error 2 - 172.24.2.4 | foo | status: ok | error 2 | status: ok - | | rc: 0 | | rc: 0 - | | failed: false | | failed: false - | | stdout: foo | | stdout: foo - | | stderr: | | stderr: - | | | | ------------- - | | | | status: ok - | | | | rc: 0 - | | | | failed: false - | | | | stdout: - | | | | stderr: error 2 - 172.24.2.5 | foo | status: ok | error 2 | status: ok - | | rc: 0 | | rc: 0 - | | failed: false | | failed: false - | | stdout: foo | | stdout: foo - | | stderr: | | stderr: - | | | | ------------- - | | | | status: ok - | | | | rc: 0 - | | | | failed: false - | | | | stdout: - | | | | stderr: error 2 - 172.24.2.6 | foo | status: ok | error 2 | status: ok - | | rc: 0 | | rc: 0 - | | failed: false | | failed: false - | | stdout: foo | | stdout: foo - | | stderr: | | stderr: - | | | | ------------- - | | | | status: ok - | | | | rc: 0 - | | | | failed: false - | | | | stdout: - | | | | stderr: error 2 - 172.24.2.7 | foo | status: ok | error 2 | status: ok - | | rc: 0 | | rc: 0 - | | failed: false | | failed: false - | | stdout: foo | | stdout: foo - | | stderr: | | stderr: - | | | | ------------- - | | | | status: ok - | | | | rc: 0 - | | | | failed: false - | | | | stdout: - | | | | stderr: error 2 - 172.24.2.8 | foo | status: ok | error 2 | status: ok - | | rc: 0 | | rc: 0 - | | failed: false | | failed: false - | | stdout: foo | | stdout: foo - | | stderr: | | stderr: - | | | | ------------- - | | | | status: ok - | | | | rc: 0 - | | | | failed: false - | | | | stdout: - | | | | stderr: error 2 - 172.24.2.9 | foo | status: ok | error 2 | status: ok - | | rc: 0 | | rc: 0 - | | failed: false | | failed: false - | | stdout: foo | | stdout: foo - | | stderr: | | stderr: - | | | | ------------- - | | | | status: ok - | | | | rc: 0 - | | | | failed: false - | | | | stdout: - | | | | stderr: error 2 - 2001:3984:3989::10 | foo | status: ok | error 2 | status: ok - | | rc: 0 | | rc: 0 - | | failed: false | | failed: false - | | stdout: foo | | stdout: foo - | | stderr: | | stderr: - | | | | ------------- - | | | | status: ok - | | | | rc: 0 - | | | | failed: false - | | | | stdout: - | | | | stderr: error 2 + host | ref | Override inline ref | Inline | Override inline +--------------------+-------+---------------------+--------+----------------- + localhost | /home | /opt | /home | / + 172.24.2.2 | /home | /opt | /home | / + 172.24.2.4 | /home | /opt | /home | / + 172.24.2.2 | /home | /opt | /home | / + 172.24.2.4 | /home | /opt | /home | / + 172.24.2.2 | /home | /opt | /home | / + 172.24.2.4 | /home | /opt | /home | / + 172.24.2.2 | /home | /opt | /home | / + 172.24.2.3 | /home | /opt | /home | / + 172.24.2.4 | /home | /opt | /home | / + 172.24.2.5 | /home | /opt | /home | / + 172.24.2.6 | /home | /opt | /home | / + 172.24.2.7 | /home | /opt | /home | / + 172.24.2.8 | /home | /opt | /home | / + 172.24.2.9 | /home | /opt | /home | / + 2001:3984:3989::10 | /home | /opt | /home | / localhost ok=4 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-29.stdout b/test/integration/golden/golden-29.stdout index 3a123d7..6b0a1ba 100755 --- a/test/integration/golden/golden-29.stdout +++ b/test/integration/golden/golden-29.stdout @@ -1,64 +1,47 @@ Index: 29 -Name: fatal false -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run fatal -q -t reachable -WantErr: true +Name: Work Dir 2 +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run work-dir-2 -q -t reachable +WantErr: false --- TASKS - host | fatal ---------------------+------------------------------ - localhost | - | exit status 1 - 172.24.2.2 | - | Process exited with status 1 - 172.24.2.4 | - | Process exited with status 1 - 172.24.2.2 | - | Process exited with status 1 - 172.24.2.4 | - | Process exited with status 1 - 172.24.2.2 | - | Process exited with status 1 - 172.24.2.4 | - | Process exited with status 1 - 172.24.2.2 | - | Process exited with status 1 - 172.24.2.3 | - | Process exited with status 1 - 172.24.2.4 | - | Process exited with status 1 - 172.24.2.5 | - | Process exited with status 1 - 172.24.2.6 | - | Process exited with status 1 - 172.24.2.7 | - | Process exited with status 1 - 172.24.2.8 | - | Process exited with status 1 - 172.24.2.9 | - | Process exited with status 1 - 2001:3984:3989::10 | - | Process exited with status 1 + host | ref | Override inline ref | Inline | Override inline +--------------------+------+---------------------+------------+----------------- + localhost | /usr | /opt | /tmp | / + 172.24.2.2 | /usr | /opt | /home/test | / + 172.24.2.4 | /usr | /opt | /home/test | / + 172.24.2.2 | /usr | /opt | /home/test | / + 172.24.2.4 | /usr | /opt | /home/test | / + 172.24.2.2 | /usr | /opt | /home/test | / + 172.24.2.4 | /usr | /opt | /home/test | / + 172.24.2.2 | /usr | /opt | /home/test | / + 172.24.2.3 | /usr | /opt | /home/test | / + 172.24.2.4 | /usr | /opt | /home/test | / + 172.24.2.5 | /usr | /opt | /home/test | / + 172.24.2.6 | /usr | /opt | /home/test | / + 172.24.2.7 | /usr | /opt | /home/test | / + 172.24.2.8 | /usr | /opt | /home/test | / + 172.24.2.9 | /usr | /opt | /home/test | / + 2001:3984:3989::10 | /usr | /opt | /home/test | / - localhost ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.3 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.5 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.6 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.7 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.8 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.9 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 2001:3984:3989::10 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + localhost ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ - Total ok=0 unreachable=0 ignored=0 failed=16 skipped=0 + Total ok=64 unreachable=0 ignored=0 failed=0 skipped=0 -exit status 1 diff --git a/test/integration/golden/golden-3.stdout b/test/integration/golden/golden-3.stdout index 19884c1..ee8f050 100755 --- a/test/integration/golden/golden-3.stdout +++ b/test/integration/golden/golden-3.stdout @@ -1,12 +1,41 @@ Index: 3 -Name: List servers filter on list hosts -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list servers list +Name: List tasks +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list tasks WantErr: false --- - server | host | tags | desc ---------+------------+----------------------------+----------------------- - list-0 | 172.24.2.2 | remote,prod,list,reachable | many hosts using list - list-1 | 172.24.2.4 | remote,prod,list,reachable | many hosts using list + task | desc +------------------+---------------------- + ping | ping server + real-ping | ping server + Host | print host + Hostname | print hostname + OS | print OS + Kernel | Print kernel version + info | get remote info + env | + env-ref | + env-complex | + env-default | + a | + b | + c | + d | + ref | + nested | + work-dir-1 | + work-dir-2 | + work-dir-3 | + register-1 | + register-2 | + fatal | + fatal-true | + errors | + errors-true | + unreachable | + unreachable-true | + empty | + empty-true | + output | diff --git a/test/integration/golden/golden-30.stdout b/test/integration/golden/golden-30.stdout index d2d6ca9..1fb1a22 100755 --- a/test/integration/golden/golden-30.stdout +++ b/test/integration/golden/golden-30.stdout @@ -1,64 +1,47 @@ Index: 30 -Name: fatal true -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run fatal-true -q -t reachable -WantErr: true +Name: Work Dir 3 +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run work-dir-3 -q -t reachable +WantErr: false --- TASKS - host | fatal-true ---------------------+------------------------------ - localhost | - | exit status 1 - 172.24.2.2 | - | Process exited with status 1 - 172.24.2.4 | - | Process exited with status 1 - 172.24.2.2 | - | Process exited with status 1 - 172.24.2.4 | - | Process exited with status 1 - 172.24.2.2 | - | Process exited with status 1 - 172.24.2.4 | - | Process exited with status 1 - 172.24.2.2 | - | Process exited with status 1 - 172.24.2.3 | - | Process exited with status 1 - 172.24.2.4 | - | Process exited with status 1 - 172.24.2.5 | - | Process exited with status 1 - 172.24.2.6 | - | Process exited with status 1 - 172.24.2.7 | - | Process exited with status 1 - 172.24.2.8 | - | Process exited with status 1 - 172.24.2.9 | - | Process exited with status 1 - 2001:3984:3989::10 | - | Process exited with status 1 + host | ref | ref +--------------------+------+------ + localhost | /usr | /etc + 172.24.2.2 | /usr | /etc + 172.24.2.4 | /usr | /etc + 172.24.2.2 | /usr | /etc + 172.24.2.4 | /usr | /etc + 172.24.2.2 | /usr | /etc + 172.24.2.4 | /usr | /etc + 172.24.2.2 | /usr | /etc + 172.24.2.3 | /usr | /etc + 172.24.2.4 | /usr | /etc + 172.24.2.5 | /usr | /etc + 172.24.2.6 | /usr | /etc + 172.24.2.7 | /usr | /etc + 172.24.2.8 | /usr | /etc + 172.24.2.9 | /usr | /etc + 2001:3984:3989::10 | /usr | /etc - localhost ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.3 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.5 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.6 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.7 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.8 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 172.24.2.9 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 2001:3984:3989::10 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + localhost ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ - Total ok=0 unreachable=0 ignored=0 failed=16 skipped=0 + Total ok=32 unreachable=0 ignored=0 failed=0 skipped=0 -exit status 1 diff --git a/test/integration/golden/golden-31.stdout b/test/integration/golden/golden-31.stdout index cf741d3..e2634a6 100755 --- a/test/integration/golden/golden-31.stdout +++ b/test/integration/golden/golden-31.stdout @@ -1,64 +1,47 @@ Index: 31 -Name: ignore_errors false -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run errors -q -t reachable -WantErr: true +Name: Register 1 +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run register-1 -q -t reachable +WantErr: false --- TASKS - host | task-0 | task-1 | task-2 ---------------------+--------+-------------------------------+-------- - localhost | 123 | | - | | exit status 65 | - 172.24.2.2 | 123 | | - | | Process exited with status 65 | - 172.24.2.4 | 123 | | - | | Process exited with status 65 | - 172.24.2.2 | 123 | | - | | Process exited with status 65 | - 172.24.2.4 | 123 | | - | | Process exited with status 65 | - 172.24.2.2 | 123 | | - | | Process exited with status 65 | - 172.24.2.4 | 123 | | - | | Process exited with status 65 | - 172.24.2.2 | 123 | | - | | Process exited with status 65 | - 172.24.2.3 | 123 | | - | | Process exited with status 65 | - 172.24.2.4 | 123 | | - | | Process exited with status 65 | - 172.24.2.5 | 123 | | - | | Process exited with status 65 | - 172.24.2.6 | 123 | | - | | Process exited with status 65 | - 172.24.2.7 | 123 | | - | | Process exited with status 65 | - 172.24.2.8 | 123 | | - | | Process exited with status 65 | - 172.24.2.9 | 123 | | - | | Process exited with status 65 | - 2001:3984:3989::10 | 123 | | - | | Process exited with status 65 | - - localhost ok=1 unreachable=0 ignored=0 failed=1 skipped=1 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 - 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 - 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 - 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 - 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 - 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 - 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 - 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 --------------------------------------------------------------------------------- - Total ok=16 unreachable=0 ignored=0 failed=16 skipped=16 + host | task-0 +--------------------+-------- + localhost | foo + 172.24.2.2 | foo + 172.24.2.4 | foo + 172.24.2.2 | foo + 172.24.2.4 | foo + 172.24.2.2 | foo + 172.24.2.4 | foo + 172.24.2.2 | foo + 172.24.2.3 | foo + 172.24.2.4 | foo + 172.24.2.5 | foo + 172.24.2.6 | foo + 172.24.2.7 | foo + 172.24.2.8 | foo + 172.24.2.9 | foo + 2001:3984:3989::10 | foo + + localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 -exit status 65 diff --git a/test/integration/golden/golden-32.stdout b/test/integration/golden/golden-32.stdout index ba36352..9b2199b 100755 --- a/test/integration/golden/golden-32.stdout +++ b/test/integration/golden/golden-32.stdout @@ -1,63 +1,207 @@ Index: 32 -Name: ignore_errors true -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run errors-true -q -t reachable +Name: Register 2 +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run register-2 -q -t reachable WantErr: false --- TASKS - host | task-0 | task-1 | task-2 ---------------------+--------+-------------------------------+-------- - localhost | 123 | | 321 - | | exit status 65 | - 172.24.2.2 | 123 | | 321 - | | Process exited with status 65 | - 172.24.2.4 | 123 | | 321 - | | Process exited with status 65 | - 172.24.2.2 | 123 | | 321 - | | Process exited with status 65 | - 172.24.2.4 | 123 | | 321 - | | Process exited with status 65 | - 172.24.2.2 | 123 | | 321 - | | Process exited with status 65 | - 172.24.2.4 | 123 | | 321 - | | Process exited with status 65 | - 172.24.2.2 | 123 | | 321 - | | Process exited with status 65 | - 172.24.2.3 | 123 | | 321 - | | Process exited with status 65 | - 172.24.2.4 | 123 | | 321 - | | Process exited with status 65 | - 172.24.2.5 | 123 | | 321 - | | Process exited with status 65 | - 172.24.2.6 | 123 | | 321 - | | Process exited with status 65 | - 172.24.2.7 | 123 | | 321 - | | Process exited with status 65 | - 172.24.2.8 | 123 | | 321 - | | Process exited with status 65 | - 172.24.2.9 | 123 | | 321 - | | Process exited with status 65 | - 2001:3984:3989::10 | 123 | | 321 - | | Process exited with status 65 | - - localhost ok=2 unreachable=0 ignored=1 failed=0 skipped=0 - 172.24.2.2 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 - 172.24.2.4 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 - 172.24.2.2 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 - 172.24.2.4 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 - 172.24.2.2 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 - 172.24.2.4 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 - 172.24.2.2 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 - 172.24.2.3 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 - 172.24.2.4 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 - 172.24.2.5 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 - 172.24.2.6 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 - 172.24.2.7 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 - 172.24.2.8 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 - 172.24.2.9 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 - 2001:3984:3989::10 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 -------------------------------------------------------------------------------- - Total ok=32 unreachable=0 ignored=16 failed=0 skipped=0 + host | task-0 | task-1 | task-2 | task-3 +--------------------+--------+---------------+---------+----------------- + localhost | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.2 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.4 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.2 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.4 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.2 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.4 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.2 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.3 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.4 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.5 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.6 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.7 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.8 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 172.24.2.9 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + 2001:3984:3989::10 | foo | status: ok | error 2 | status: ok + | | rc: 0 | | rc: 0 + | | failed: false | | failed: false + | | stdout: foo | | stdout: foo + | | stderr: | | stderr: + | | | | ------------- + | | | | status: ok + | | | | rc: 0 + | | | | failed: false + | | | | stdout: + | | | | stderr: error 2 + + localhost ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=64 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-33.stdout b/test/integration/golden/golden-33.stdout index efb8d01..949d651 100755 --- a/test/integration/golden/golden-33.stdout +++ b/test/integration/golden/golden-33.stdout @@ -1,15 +1,64 @@ Index: 33 -Name: unreachable false -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run unreachable -q -a +Name: fatal false +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run fatal -q -t reachable WantErr: true --- - - Unreachable Hosts - - server | host | user | port | error --------------+-----------------+------+------+------------------------------------------------ - unreachable | unreachable.lan | test | 22 | dial tcp: lookup unreachable.lan: no such host +TASKS -exit status 4 + host | fatal +--------------------+------------------------------ + localhost | + | exit status 1 + 172.24.2.2 | + | Process exited with status 1 + 172.24.2.4 | + | Process exited with status 1 + 172.24.2.2 | + | Process exited with status 1 + 172.24.2.4 | + | Process exited with status 1 + 172.24.2.2 | + | Process exited with status 1 + 172.24.2.4 | + | Process exited with status 1 + 172.24.2.2 | + | Process exited with status 1 + 172.24.2.3 | + | Process exited with status 1 + 172.24.2.4 | + | Process exited with status 1 + 172.24.2.5 | + | Process exited with status 1 + 172.24.2.6 | + | Process exited with status 1 + 172.24.2.7 | + | Process exited with status 1 + 172.24.2.8 | + | Process exited with status 1 + 172.24.2.9 | + | Process exited with status 1 + 2001:3984:3989::10 | + | Process exited with status 1 + + localhost ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.3 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.5 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.6 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.7 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.8 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.9 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 2001:3984:3989::10 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 +------------------------------------------------------------------------------ + Total ok=0 unreachable=0 ignored=0 failed=16 skipped=0 + +exit status 1 diff --git a/test/integration/golden/golden-34.stdout b/test/integration/golden/golden-34.stdout index e09f447..cc9919b 100755 --- a/test/integration/golden/golden-34.stdout +++ b/test/integration/golden/golden-34.stdout @@ -1,56 +1,64 @@ Index: 34 -Name: unreachable true -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run unreachable-true -o table -q -a -WantErr: false +Name: fatal true +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run fatal-true -q -t reachable +WantErr: true --- - - Unreachable Hosts - - server | host | user | port | error --------------+-----------------+------+------+------------------------------------------------ - unreachable | unreachable.lan | test | 22 | dial tcp: lookup unreachable.lan: no such host - - TASKS - host | unreachable-true ---------------------+------------------ - localhost | 123 - 172.24.2.2 | 123 - 172.24.2.4 | 123 - 172.24.2.2 | 123 - 172.24.2.4 | 123 - 172.24.2.2 | 123 - 172.24.2.4 | 123 - 172.24.2.2 | 123 - 172.24.2.3 | 123 - 172.24.2.4 | 123 - 172.24.2.5 | 123 - 172.24.2.6 | 123 - 172.24.2.7 | 123 - 172.24.2.8 | 123 - 172.24.2.9 | 123 - 2001:3984:3989::10 | 123 + host | fatal-true +--------------------+------------------------------ + localhost | + | exit status 1 + 172.24.2.2 | + | Process exited with status 1 + 172.24.2.4 | + | Process exited with status 1 + 172.24.2.2 | + | Process exited with status 1 + 172.24.2.4 | + | Process exited with status 1 + 172.24.2.2 | + | Process exited with status 1 + 172.24.2.4 | + | Process exited with status 1 + 172.24.2.2 | + | Process exited with status 1 + 172.24.2.3 | + | Process exited with status 1 + 172.24.2.4 | + | Process exited with status 1 + 172.24.2.5 | + | Process exited with status 1 + 172.24.2.6 | + | Process exited with status 1 + 172.24.2.7 | + | Process exited with status 1 + 172.24.2.8 | + | Process exited with status 1 + 172.24.2.9 | + | Process exited with status 1 + 2001:3984:3989::10 | + | Process exited with status 1 - localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - unreachable.lan ok=0 unreachable=1 ignored=0 failed=0 skipped=0 + localhost ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.2 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.3 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.4 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.5 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.6 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.7 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.8 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 172.24.2.9 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + 2001:3984:3989::10 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 ------------------------------------------------------------------------------ - Total ok=16 unreachable=1 ignored=0 failed=0 skipped=0 + Total ok=0 unreachable=0 ignored=0 failed=16 skipped=0 +exit status 1 diff --git a/test/integration/golden/golden-35.stdout b/test/integration/golden/golden-35.stdout index 780b945..b29f4bf 100755 --- a/test/integration/golden/golden-35.stdout +++ b/test/integration/golden/golden-35.stdout @@ -1,47 +1,64 @@ Index: 35 -Name: omit_empty false -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run empty -q -t reachable -WantErr: false +Name: ignore_errors false +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run errors -q -t reachable +WantErr: true --- TASKS - host | empty ---------------------+-------- - localhost | - 172.24.2.2 | Exists - 172.24.2.4 | Exists - 172.24.2.2 | Exists - 172.24.2.4 | Exists - 172.24.2.2 | Exists - 172.24.2.4 | Exists - 172.24.2.2 | Exists - 172.24.2.3 | Exists - 172.24.2.4 | Exists - 172.24.2.5 | Exists - 172.24.2.6 | Exists - 172.24.2.7 | Exists - 172.24.2.8 | Exists - 172.24.2.9 | Exists - 2001:3984:3989::10 | Exists - - localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------- - Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 + host | task-0 | task-1 | task-2 +--------------------+--------+-------------------------------+-------- + localhost | 123 | | + | | exit status 65 | + 172.24.2.2 | 123 | | + | | Process exited with status 65 | + 172.24.2.4 | 123 | | + | | Process exited with status 65 | + 172.24.2.2 | 123 | | + | | Process exited with status 65 | + 172.24.2.4 | 123 | | + | | Process exited with status 65 | + 172.24.2.2 | 123 | | + | | Process exited with status 65 | + 172.24.2.4 | 123 | | + | | Process exited with status 65 | + 172.24.2.2 | 123 | | + | | Process exited with status 65 | + 172.24.2.3 | 123 | | + | | Process exited with status 65 | + 172.24.2.4 | 123 | | + | | Process exited with status 65 | + 172.24.2.5 | 123 | | + | | Process exited with status 65 | + 172.24.2.6 | 123 | | + | | Process exited with status 65 | + 172.24.2.7 | 123 | | + | | Process exited with status 65 | + 172.24.2.8 | 123 | | + | | Process exited with status 65 | + 172.24.2.9 | 123 | | + | | Process exited with status 65 | + 2001:3984:3989::10 | 123 | | + | | Process exited with status 65 | + + localhost ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 +-------------------------------------------------------------------------------- + Total ok=16 unreachable=0 ignored=0 failed=16 skipped=16 +exit status 65 diff --git a/test/integration/golden/golden-36.stdout b/test/integration/golden/golden-36.stdout index 8ee5ffe..077b2ec 100755 --- a/test/integration/golden/golden-36.stdout +++ b/test/integration/golden/golden-36.stdout @@ -1,46 +1,63 @@ Index: 36 -Name: omit_empty true -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run empty-true -q -t reachable +Name: ignore_errors true +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run errors-true -q -t reachable WantErr: false --- TASKS - host | empty-true ---------------------+------------ - 172.24.2.2 | Exists - 172.24.2.4 | Exists - 172.24.2.2 | Exists - 172.24.2.4 | Exists - 172.24.2.2 | Exists - 172.24.2.4 | Exists - 172.24.2.2 | Exists - 172.24.2.3 | Exists - 172.24.2.4 | Exists - 172.24.2.5 | Exists - 172.24.2.6 | Exists - 172.24.2.7 | Exists - 172.24.2.8 | Exists - 172.24.2.9 | Exists - 2001:3984:3989::10 | Exists - - localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------- - Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 + host | task-0 | task-1 | task-2 +--------------------+--------+-------------------------------+-------- + localhost | 123 | | 321 + | | exit status 65 | + 172.24.2.2 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.4 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.2 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.4 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.2 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.4 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.2 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.3 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.4 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.5 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.6 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.7 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.8 | 123 | | 321 + | | Process exited with status 65 | + 172.24.2.9 | 123 | | 321 + | | Process exited with status 65 | + 2001:3984:3989::10 | 123 | | 321 + | | Process exited with status 65 | + + localhost ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.2 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.3 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.4 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.5 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.6 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.7 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.8 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 172.24.2.9 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + 2001:3984:3989::10 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 +------------------------------------------------------------------------------- + Total ok=32 unreachable=0 ignored=16 failed=0 skipped=0 diff --git a/test/integration/golden/golden-37.stdout b/test/integration/golden/golden-37.stdout index 7da0a3c..fa90b42 100755 --- a/test/integration/golden/golden-37.stdout +++ b/test/integration/golden/golden-37.stdout @@ -1,47 +1,15 @@ Index: 37 -Name: output -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run output -q -t reachable -WantErr: false +Name: unreachable false +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run unreachable -q -a +WantErr: true --- -TASKS - - host | task-0 | task-1 | task-2 ---------------------+-------------+-----------+------------------- - localhost | Hello world | Bye world | Hello again world - 172.24.2.2 | Hello world | Bye world | Hello again world - 172.24.2.4 | Hello world | Bye world | Hello again world - 172.24.2.2 | Hello world | Bye world | Hello again world - 172.24.2.4 | Hello world | Bye world | Hello again world - 172.24.2.2 | Hello world | Bye world | Hello again world - 172.24.2.4 | Hello world | Bye world | Hello again world - 172.24.2.2 | Hello world | Bye world | Hello again world - 172.24.2.3 | Hello world | Bye world | Hello again world - 172.24.2.4 | Hello world | Bye world | Hello again world - 172.24.2.5 | Hello world | Bye world | Hello again world - 172.24.2.6 | Hello world | Bye world | Hello again world - 172.24.2.7 | Hello world | Bye world | Hello again world - 172.24.2.8 | Hello world | Bye world | Hello again world - 172.24.2.9 | Hello world | Bye world | Hello again world - 2001:3984:3989::10 | Hello world | Bye world | Hello again world - - localhost ok=3 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.2 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.3 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.4 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.5 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.6 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.7 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.8 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 - 172.24.2.9 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------- - Total ok=48 unreachable=0 ignored=0 failed=0 skipped=0 + + Unreachable Hosts + + server | host | user | port | error +-------------+-----------------+------+------+------------------------------------------------ + unreachable | unreachable.lan | test | 22 | dial tcp: lookup unreachable.lan: no such host +exit status 4 diff --git a/test/integration/golden/golden-38.stdout b/test/integration/golden/golden-38.stdout index f3f3f74..67e165d 100755 --- a/test/integration/golden/golden-38.stdout +++ b/test/integration/golden/golden-38.stdout @@ -1,30 +1,38 @@ Index: 38 -Name: Run exec command -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go exec 'echo 123' -q -t reachable +Name: unreachable true +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run unreachable-true -o table -q -a WantErr: false --- + + Unreachable Hosts + + server | host | user | port | error +-------------+-----------------+------+------+------------------------------------------------ + unreachable | unreachable.lan | test | 22 | dial tcp: lookup unreachable.lan: no such host + + TASKS - host | task-0 ---------------------+-------- - localhost | 123 - 172.24.2.2 | 123 - 172.24.2.4 | 123 - 172.24.2.2 | 123 - 172.24.2.4 | 123 - 172.24.2.2 | 123 - 172.24.2.4 | 123 - 172.24.2.2 | 123 - 172.24.2.3 | 123 - 172.24.2.4 | 123 - 172.24.2.5 | 123 - 172.24.2.6 | 123 - 172.24.2.7 | 123 - 172.24.2.8 | 123 - 172.24.2.9 | 123 - 2001:3984:3989::10 | 123 + host | unreachable-true +--------------------+------------------ + localhost | 123 + 172.24.2.2 | 123 + 172.24.2.4 | 123 + 172.24.2.2 | 123 + 172.24.2.4 | 123 + 172.24.2.2 | 123 + 172.24.2.4 | 123 + 172.24.2.2 | 123 + 172.24.2.3 | 123 + 172.24.2.4 | 123 + 172.24.2.5 | 123 + 172.24.2.6 | 123 + 172.24.2.7 | 123 + 172.24.2.8 | 123 + 172.24.2.9 | 123 + 2001:3984:3989::10 | 123 localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 @@ -42,6 +50,7 @@ TASKS 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + unreachable.lan ok=0 unreachable=1 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ - Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 + Total ok=16 unreachable=1 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-39.stdout b/test/integration/golden/golden-39.stdout new file mode 100755 index 0000000..29914a5 --- /dev/null +++ b/test/integration/golden/golden-39.stdout @@ -0,0 +1,47 @@ +Index: 39 +Name: omit_empty false +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run empty -q -t reachable +WantErr: false + +--- + +TASKS + + host | empty +--------------------+-------- + localhost | + 172.24.2.2 | Exists + 172.24.2.4 | Exists + 172.24.2.2 | Exists + 172.24.2.4 | Exists + 172.24.2.2 | Exists + 172.24.2.4 | Exists + 172.24.2.2 | Exists + 172.24.2.3 | Exists + 172.24.2.4 | Exists + 172.24.2.5 | Exists + 172.24.2.6 | Exists + 172.24.2.7 | Exists + 172.24.2.8 | Exists + 172.24.2.9 | Exists + 2001:3984:3989::10 | Exists + + localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 + diff --git a/test/integration/golden/golden-4.stdout b/test/integration/golden/golden-4.stdout index ed10245..8e59c2d 100755 --- a/test/integration/golden/golden-4.stdout +++ b/test/integration/golden/golden-4.stdout @@ -1,12 +1,27 @@ Index: 4 -Name: List servers filter on range hosts -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list servers range +Name: List servers +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list servers WantErr: false --- - server | host | tags | desc ----------+------------+-----------------------------+------------------------ - range-0 | 172.24.2.2 | remote,prod,range,reachable | many hosts using range - range-1 | 172.24.2.4 | remote,prod,range,reachable | many hosts using range + server | host | tags | desc +-------------+--------------------+-----------------------------+---------------------------- + localhost | localhost | local,reachable | localhost + unreachable | unreachable.lan | unreachable | + list-0 | 172.24.2.2 | remote,prod,list,reachable | many hosts using list + list-1 | 172.24.2.4 | remote,prod,list,reachable | many hosts using list + range-0 | 172.24.2.2 | remote,prod,range,reachable | many hosts using range + range-1 | 172.24.2.4 | remote,prod,range,reachable | many hosts using range + inv-0 | 172.24.2.2 | remote,prod,inv,reachable | many hosts using inventory + inv-1 | 172.24.2.4 | remote,prod,inv,reachable | many hosts using inventory + server-1 | 172.24.2.2 | remote,prod,reachable | server-1 + server-2 | 172.24.2.3 | remote,prod,reachable | server-2 + server-3 | 172.24.2.4 | remote,demo,reachable | server-3 + server-4 | 172.24.2.5 | remote,demo,reachable | server-4 + server-5 | 172.24.2.6 | remote,sandbox,reachable | server-5 + server-6 | 172.24.2.7 | remote,sandbox,reachable | server-6 + server-7 | 172.24.2.8 | remote,demo,reachable | server-7 + server-8 | 172.24.2.9 | remote,demo,reachable | server-8 + server-9 | 2001:3984:3989::10 | remote,demo,reachable | server-9 diff --git a/test/integration/golden/golden-40.stdout b/test/integration/golden/golden-40.stdout new file mode 100755 index 0000000..4b7c5b0 --- /dev/null +++ b/test/integration/golden/golden-40.stdout @@ -0,0 +1,46 @@ +Index: 40 +Name: omit_empty true +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run empty-true -q -t reachable +WantErr: false + +--- + +TASKS + + host | empty-true +--------------------+------------ + 172.24.2.2 | Exists + 172.24.2.4 | Exists + 172.24.2.2 | Exists + 172.24.2.4 | Exists + 172.24.2.2 | Exists + 172.24.2.4 | Exists + 172.24.2.2 | Exists + 172.24.2.3 | Exists + 172.24.2.4 | Exists + 172.24.2.5 | Exists + 172.24.2.6 | Exists + 172.24.2.7 | Exists + 172.24.2.8 | Exists + 172.24.2.9 | Exists + 2001:3984:3989::10 | Exists + + localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 + diff --git a/test/integration/golden/golden-41.stdout b/test/integration/golden/golden-41.stdout new file mode 100755 index 0000000..e0ab3f2 --- /dev/null +++ b/test/integration/golden/golden-41.stdout @@ -0,0 +1,47 @@ +Index: 41 +Name: output +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go run output -q -t reachable +WantErr: false + +--- + +TASKS + + host | task-0 | task-1 | task-2 +--------------------+-------------+-----------+------------------- + localhost | Hello world | Bye world | Hello again world + 172.24.2.2 | Hello world | Bye world | Hello again world + 172.24.2.4 | Hello world | Bye world | Hello again world + 172.24.2.2 | Hello world | Bye world | Hello again world + 172.24.2.4 | Hello world | Bye world | Hello again world + 172.24.2.2 | Hello world | Bye world | Hello again world + 172.24.2.4 | Hello world | Bye world | Hello again world + 172.24.2.2 | Hello world | Bye world | Hello again world + 172.24.2.3 | Hello world | Bye world | Hello again world + 172.24.2.4 | Hello world | Bye world | Hello again world + 172.24.2.5 | Hello world | Bye world | Hello again world + 172.24.2.6 | Hello world | Bye world | Hello again world + 172.24.2.7 | Hello world | Bye world | Hello again world + 172.24.2.8 | Hello world | Bye world | Hello again world + 172.24.2.9 | Hello world | Bye world | Hello again world + 2001:3984:3989::10 | Hello world | Bye world | Hello again world + + localhost ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=48 unreachable=0 ignored=0 failed=0 skipped=0 + diff --git a/test/integration/golden/golden-42.stdout b/test/integration/golden/golden-42.stdout new file mode 100755 index 0000000..3bfb112 --- /dev/null +++ b/test/integration/golden/golden-42.stdout @@ -0,0 +1,47 @@ +Index: 42 +Name: Run exec command +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go exec 'echo 123' -q -t reachable +WantErr: false + +--- + +TASKS + + host | task-0 +--------------------+-------- + localhost | 123 + 172.24.2.2 | 123 + 172.24.2.4 | 123 + 172.24.2.2 | 123 + 172.24.2.4 | 123 + 172.24.2.2 | 123 + 172.24.2.4 | 123 + 172.24.2.2 | 123 + 172.24.2.3 | 123 + 172.24.2.4 | 123 + 172.24.2.5 | 123 + 172.24.2.6 | 123 + 172.24.2.7 | 123 + 172.24.2.8 | 123 + 172.24.2.9 | 123 + 2001:3984:3989::10 | 123 + + localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.3 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.4 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.5 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.6 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 +------------------------------------------------------------------------------ + Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 + diff --git a/test/integration/golden/golden-5.stdout b/test/integration/golden/golden-5.stdout index 973a8df..1f2863f 100755 --- a/test/integration/golden/golden-5.stdout +++ b/test/integration/golden/golden-5.stdout @@ -1,12 +1,12 @@ Index: 5 -Name: List servers filter on inventory hosts -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list servers inv +Name: List servers filter on list hosts +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list servers list WantErr: false --- - server | host | tags | desc ---------+------------+---------------------------+---------------------------- - inv-0 | 172.24.2.2 | remote,prod,inv,reachable | many hosts using inventory - inv-1 | 172.24.2.4 | remote,prod,inv,reachable | many hosts using inventory + server | host | tags | desc +--------+------------+----------------------------+----------------------- + list-0 | 172.24.2.2 | remote,prod,list,reachable | many hosts using list + list-1 | 172.24.2.4 | remote,prod,list,reachable | many hosts using list diff --git a/test/integration/golden/golden-6.stdout b/test/integration/golden/golden-6.stdout index 27caec8..4085852 100755 --- a/test/integration/golden/golden-6.stdout +++ b/test/integration/golden/golden-6.stdout @@ -1,183 +1,12 @@ Index: 6 -Name: Describe servers -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe servers +Name: List servers filter on range hosts +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list servers range WantErr: false --- -name: localhost -desc: localhost -user: test -host: localhost -port: 22 -local: true -work_dir: /tmp -tags: local, reachable - --- - -name: unreachable -user: test -host: unreachable.lan -port: 22 -tags: unreachable - --- - -name: list-0 -group: list -desc: many hosts using list -user: test -host: 172.24.2.2 -port: 22 -tags: remote, prod, list, reachable -env: - hello: world - --- - -name: list-1 -group: list -desc: many hosts using list -user: test -host: 172.24.2.4 -port: 22 -tags: remote, prod, list, reachable -env: - hello: world - --- - -name: range-0 -group: range -desc: many hosts using range -user: test -host: 172.24.2.2 -port: 22 -tags: remote, prod, range, reachable -env: - hello: world - --- - -name: range-1 -group: range -desc: many hosts using range -user: test -host: 172.24.2.4 -port: 22 -tags: remote, prod, range, reachable -env: - hello: world - --- - -name: inv-0 -group: inv -desc: many hosts using inventory -user: test -host: 172.24.2.2 -port: 22 -tags: remote, prod, inv, reachable -env: - hello: world - host: 172.24.2.4 - --- - -name: inv-1 -group: inv -desc: many hosts using inventory -user: test -host: 172.24.2.4 -port: 22 -tags: remote, prod, inv, reachable -env: - hello: world - host: 172.24.2.4 - --- - -name: server-1 -desc: server-1 -user: test -host: 172.24.2.2 -port: 22 -bastion: 172.24.2.99 -work_dir: /home/test -tags: remote, prod, reachable -env: - host: 172.24.2.2 - --- - -name: server-2 -desc: server-2 -user: test -host: 172.24.2.3 -port: 33 -tags: remote, prod, reachable - --- - -name: server-3 -desc: server-3 -user: test -host: 172.24.2.4 -port: 22 -tags: remote, demo, reachable - --- - -name: server-4 -desc: server-4 -user: test -host: 172.24.2.5 -port: 22 -tags: remote, demo, reachable - --- - -name: server-5 -desc: server-5 -user: test -host: 172.24.2.6 -port: 22 -tags: remote, sandbox, reachable - --- - -name: server-6 -desc: server-6 -user: test -host: 172.24.2.7 -port: 22 -tags: remote, sandbox, reachable - --- - -name: server-7 -desc: server-7 -user: test -host: 172.24.2.8 -port: 22 -tags: remote, demo, reachable - --- - -name: server-8 -desc: server-8 -user: test -host: 172.24.2.9 -port: 22 -tags: remote, demo, reachable - --- - -name: server-9 -desc: server-9 -user: test -host: 2001:3984:3989::10 -port: 22 -tags: remote, demo, reachable + server | host | tags | desc +---------+------------+-----------------------------+------------------------ + range-0 | 172.24.2.2 | remote,prod,range,reachable | many hosts using range + range-1 | 172.24.2.4 | remote,prod,range,reachable | many hosts using range diff --git a/test/integration/golden/golden-7.stdout b/test/integration/golden/golden-7.stdout index f869233..95807fa 100755 --- a/test/integration/golden/golden-7.stdout +++ b/test/integration/golden/golden-7.stdout @@ -1,29 +1,12 @@ Index: 7 -Name: Describe servers filter on list hosts -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe servers list +Name: List servers filter on inventory hosts +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list servers inv WantErr: false --- -name: list-0 -group: list -desc: many hosts using list -user: test -host: 172.24.2.2 -port: 22 -tags: remote, prod, list, reachable -env: - hello: world - --- - -name: list-1 -group: list -desc: many hosts using list -user: test -host: 172.24.2.4 -port: 22 -tags: remote, prod, list, reachable -env: - hello: world + server | host | tags | desc +--------+------------+---------------------------+---------------------------- + inv-0 | 172.24.2.2 | remote,prod,inv,reachable | many hosts using inventory + inv-1 | 172.24.2.4 | remote,prod,inv,reachable | many hosts using inventory diff --git a/test/integration/golden/golden-8.stdout b/test/integration/golden/golden-8.stdout index f30af95..4a21008 100755 --- a/test/integration/golden/golden-8.stdout +++ b/test/integration/golden/golden-8.stdout @@ -1,29 +1,32 @@ Index: 8 -Name: Describe servers filter on range hosts -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe servers range +Name: Describe specs +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe specs WantErr: false --- +name: default +strategy: linear +batch: 1 +output: table +report: recap -name: range-0 -group: range -desc: many hosts using range -user: test -host: 172.24.2.2 -port: 22 -tags: remote, prod, range, reachable -env: - hello: world +-- + +name: table +output: table +report: recap -- -name: range-1 -group: range -desc: many hosts using range -user: test -host: 172.24.2.4 -port: 22 -tags: remote, prod, range, reachable -env: - hello: world +name: text +output: text +report: recap + +-- +name: info +strategy: free +output: table +ignore_errors: true +ignore_unreachable: true +report: recap diff --git a/test/integration/golden/golden-9.stdout b/test/integration/golden/golden-9.stdout index dbf85dc..3c78aa4 100755 --- a/test/integration/golden/golden-9.stdout +++ b/test/integration/golden/golden-9.stdout @@ -1,31 +1,11 @@ Index: 9 -Name: Describe servers filter on inventory hosts -Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe servers inv +Name: Describe targets +Cmd: export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe targets WantErr: false --- - -name: inv-0 -group: inv -desc: many hosts using inventory -user: test -host: 172.24.2.2 -port: 22 -tags: remote, prod, inv, reachable -env: - hello: world - host: 172.24.2.4 +name: all +all: true -- -name: inv-1 -group: inv -desc: many hosts using inventory -user: test -host: 172.24.2.4 -port: 22 -tags: remote, prod, inv, reachable -env: - hello: world - host: 172.24.2.4 - diff --git a/test/integration/run_test.go b/test/integration/run_test.go index 22332bf..a05c778 100644 --- a/test/integration/run_test.go +++ b/test/integration/run_test.go @@ -13,6 +13,20 @@ var cases = []TemplateTest{ WantErr: false, }, + // list specs + { + TestName: "List specs", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list specs`, + WantErr: false, + }, + + // list targets + { + TestName: "List targets", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go list targets`, + WantErr: false, + }, + // list tasks { TestName: "List tasks", @@ -42,6 +56,20 @@ var cases = []TemplateTest{ WantErr: false, }, + // describe specs + { + TestName: "Describe specs", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe specs`, + WantErr: false, + }, + + // describe targets + { + TestName: "Describe targets", + TestCmd: `export SAKE_USER_CONFIG="$PWD/../user-config.yaml" && go run ../../main.go describe targets`, + WantErr: false, + }, + // describe servers { TestName: "Describe servers", diff --git a/test/playground/sake.yaml b/test/playground/sake.yaml index 6d2166b..55fad5e 100644 --- a/test/playground/sake.yaml +++ b/test/playground/sake.yaml @@ -207,93 +207,97 @@ env: tasks: ping: target: all - spec: host_pinned + # spec: host_pinned + spec: + hidden: true desc: ping server cmd: echo pong - # exit: - # # name: Exit - # local: true - # cmd: exit 3 - - # sleep: - # desc: ping server - # cmd: sleep 2 & echo done - - # info: - # name: Info - # desc: print info - # target: all - # tasks: - # - task: print-host - # - task: print-hostname - # - task: print-os - # - task: print-kernel - - # # Info - # print-host: - # name: Host - # desc: print host - # spec: info - # target: all - # cmd: echo $S_HOST - - # print-hostname: - # name: Hostname - # desc: print hostname - # spec: info - # target: all - # cmd: hostname - - # print-os: - # name: OS - # desc: print OS - # spec: info - # target: all - # cmd: | - # os=$(lsb_release -si) - # release=$(lsb_release -sr) - # echo "$os $release" - - # print-kernel: - # name: Kernel - # desc: Print kernel version - # spec: info - # target: all - # cmd: uname -r | awk -v FS='-' '{print $1}' - - # register: - # tasks: - # - cmd: echo "foo" && >&2 echo "error" - # register: out - # - cmd: | - # echo "status: $out_status" - # echo "rc: $out_rc" - # echo "failed: $out_failed" - # echo "stdout: $out_stdout" - # echo "stderr: $out_stderr" - # echo "out: $out" - - # - cmd: echo "xyz" && >&2 echo "error 2" - # register: out2 - # - cmd: | - # echo "status: $out_status" - # echo "rc: $out_rc" - # echo "failed: $out_failed" - # echo "stdout: $out_stdout" - # echo "stderr: $out_stderr" - # echo "out: $out" - - # echo "-------------" - - # echo "status: $out2_status" - # echo "rc: $out2_rc" - # echo "failed: $out2_failed" - # echo "stdout: $out2_stdout" - # echo "stderr: $out2_stderr" - # echo "out: $out2" - - # register2: - # tasks: - # - cmd: echo "foo" && >&2 echo "error" - # register: out + exit: + # name: Exit + local: true + cmd: exit 3 + + sleep: + desc: sleep for x seconds + env: + seconds: 2 + cmd: sleep $seconds & echo done + + info: + name: Info + desc: print info + target: all + tasks: + - task: print-host + - task: print-hostname + - task: print-os + - task: print-kernel + + # Info + print-host: + name: Host + desc: print host + spec: info + target: all + cmd: echo $S_HOST + + print-hostname: + name: Hostname + desc: print hostname + spec: info + target: all + cmd: hostname + + print-os: + name: OS + desc: print OS + spec: info + target: all + cmd: | + os=$(lsb_release -si) + release=$(lsb_release -sr) + echo "$os $release" + + print-kernel: + name: Kernel + desc: Print kernel version + spec: info + target: all + cmd: uname -r | awk -v FS='-' '{print $1}' + + register: + tasks: + - cmd: echo "foo" && >&2 echo "error" + register: out + - cmd: | + echo "status: $out_status" + echo "rc: $out_rc" + echo "failed: $out_failed" + echo "stdout: $out_stdout" + echo "stderr: $out_stderr" + echo "out: $out" + + - cmd: echo "xyz" && >&2 echo "error 2" + register: out2 + - cmd: | + echo "status: $out_status" + echo "rc: $out_rc" + echo "failed: $out_failed" + echo "stdout: $out_stdout" + echo "stderr: $out_stderr" + echo "out: $out" + + echo "-------------" + + echo "status: $out2_status" + echo "rc: $out2_rc" + echo "failed: $out2_failed" + echo "stdout: $out2_stdout" + echo "stderr: $out2_stderr" + echo "out: $out2" + + register2: + tasks: + - cmd: echo "foo" && >&2 echo "error" + register: out From 8c492907caeec829e0b606e0a669bb0cfb6f9ae6 Mon Sep 17 00:00:00 2001 From: Samir Alajmovic <5246600+alajmo@users.noreply.github.com> Date: Wed, 4 Jan 2023 23:10:07 +0100 Subject: [PATCH 06/18] Release v0.14.0 - Default to one of following identity files if no identity specified ~/.ssh/id_rsa, ~/.ssh/id_ecdsa, ~/.ssh/id_dsa - Add print option to limit output to stdout|stderr - Add ability to modify default timeout for ssh connections - Fix some small validation issues with batch and batch-p - A bunch of smaller fixes - Run tests in parallel --- Makefile | 2 +- cmd/exec.go | 80 ++++++++++++++++++------ cmd/list_specs.go | 2 +- cmd/run.go | 80 ++++++++++++++++++------ core/config.man | 9 +++ core/dao/config.go | 26 +++----- core/dao/import_config.go | 16 ++++- core/dao/import_task.go | 7 ++- core/dao/server.go | 22 ++++--- core/dao/spec.go | 11 ++++ core/dao/target.go | 1 + core/dao/theme.go | 6 +- core/dao/unix.go | 23 +++++++ core/dao/windows.go | 8 +++ core/errors.go | 16 +++++ core/flags.go | 3 + core/print/print_block.go | 1 + core/run/client.go | 2 +- core/run/exec.go | 49 ++++++++------- core/run/localhost.go | 2 +- core/run/ssh.go | 13 ++-- core/run/table.go | 22 +++++-- core/run/text.go | 74 ++++++++++++++-------- core/run/unix.go | 11 +++- core/sake.1 | 19 +++++- core/utils.go | 29 ++++++++- docs/changelog.md | 7 ++- docs/command-reference.md | 4 +- docs/config-reference.md | 9 +++ docs/development.md | 9 +-- test/Dockerfile | 1 + test/benchmark.sh | 1 + test/integration/golden/golden-1.stdout | 8 +-- test/integration/golden/golden-14.stdout | 31 +++++++++ test/integration/golden/golden-8.stdout | 4 ++ test/integration/main_test.go | 13 ++-- test/integration/run_test.go | 9 ++- test/playground/sake.yaml | 39 +++++++----- 38 files changed, 500 insertions(+), 169 deletions(-) create mode 100644 core/dao/unix.go create mode 100644 core/dao/windows.go diff --git a/Makefile b/Makefile index 79b94e8..d4f6593 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ NAME := sake PACKAGE := github.com/alajmo/$(NAME) DATE := $(shell date +%FT%T%Z) GIT := $(shell [ -d .git ] && git rev-parse --short HEAD) -VERSION := v0.13.0 +VERSION := v0.14.0 default: build diff --git a/cmd/exec.go b/cmd/exec.go index 6a9f9e3..7cf948d 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -38,6 +38,7 @@ before the command gets executed in each directory.`, setRunFlags.All = cmd.Flags().Changed("all") setRunFlags.AnyErrorsFatal = cmd.Flags().Changed("any-errors-fatal") setRunFlags.Attach = cmd.Flags().Changed("attach") + setRunFlags.Forks = cmd.Flags().Changed("forks") setRunFlags.Batch = cmd.Flags().Changed("batch") setRunFlags.BatchP = cmd.Flags().Changed("batch-p") setRunFlags.IgnoreErrors = cmd.Flags().Changed("ignore-error") @@ -59,29 +60,62 @@ before the command gets executed in each directory.`, setRunFlags.TTY = cmd.Flags().Changed("tty") setRunFlags.Tags = cmd.Flags().Changed("tags") setRunFlags.Verbose = cmd.Flags().Changed("verbose") + setRunFlags.MaxFailPercentage = cmd.Flags().Changed("max-fail-percentage") + + if setRunFlags.MaxFailPercentage { + maxFailPercentage, err := cmd.Flags().GetUint8("max-fail-percentage") + core.CheckIfError(err) + if maxFailPercentage > 100 { + core.Exit(&core.InvalidPercentInput{Name: "max-fail-percentage"}) + } + runFlags.MaxFailPercentage = maxFailPercentage + } - maxFailPercentage, err := cmd.Flags().GetUint8("max-fail-percentage") - core.CheckIfError(err) - runFlags.MaxFailPercentage = maxFailPercentage + if setRunFlags.Forks { + forks, err := cmd.Flags().GetUint32("forks") + core.CheckIfError(err) + if forks == 0 { + core.Exit(&core.ZeroNotAllowed{Name: "forks"}) + } + runFlags.Forks = forks + } - forks, err := cmd.Flags().GetUint32("forks") - core.CheckIfError(err) - runFlags.Forks = forks + if setRunFlags.Batch { + batch, err := cmd.Flags().GetUint32("batch") + core.CheckIfError(err) + if batch == 0 { + core.Exit(&core.ZeroNotAllowed{Name: "batch"}) + } + runFlags.Batch = batch + } - batch, err := cmd.Flags().GetUint32("batch") - core.CheckIfError(err) - batchp, err := cmd.Flags().GetUint8("batch-p") - core.CheckIfError(err) - runFlags.Batch = batch - runFlags.BatchP = batchp + if setRunFlags.BatchP { + batchp, err := cmd.Flags().GetUint8("batch-p") + core.CheckIfError(err) + if batchp == 0 || batchp > 100 { + core.Exit(&core.InvalidPercentInput2{Name: "batch-p"}) + } + runFlags.BatchP = batchp + } - limit, err := cmd.Flags().GetUint32("limit") - core.CheckIfError(err) - limitp, err := cmd.Flags().GetUint8("limit-p") - core.CheckIfError(err) + if setRunFlags.Limit { + limit, err := cmd.Flags().GetUint32("limit") + core.CheckIfError(err) + if limit == 0 { + core.Exit(&core.ZeroNotAllowed{Name: "limit"}) + } + runFlags.Limit = limit + } - runFlags.Limit = limit - runFlags.LimitP = limitp + // Min-limit-p 1 + if setRunFlags.LimitP { + limitp, err := cmd.Flags().GetUint8("limit-p") + core.CheckIfError(err) + if limitp == 0 || limitp > 100 { + core.Exit(&core.InvalidPercentInput2{Name: "limit-p"}) + } + runFlags.LimitP = limitp + } execTask(args, config, &runFlags, &setRunFlags) }, @@ -183,6 +217,16 @@ before the command gets executed in each directory.`, }) core.CheckIfError(err) + cmd.Flags().StringVarP(&runFlags.Print, "print", "p", "", "set print [all|stdout|stderr]") + err = cmd.RegisterFlagCompletionFunc("print", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if *configErr != nil { + return []string{}, cobra.ShellCompDirectiveDefault + } + valid := []string{"all", "stdout", "stderr"} + return valid, cobra.ShellCompDirectiveDefault + }) + core.CheckIfError(err) + cmd.Flags().BoolVar(&runFlags.OmitEmptyRows, "omit-empty-rows", false, "omit empty row for table output") cmd.Flags().BoolVar(&runFlags.OmitEmptyColumns, "omit-empty-columns", false, "omit empty column for table output") cmd.Flags().BoolVarP(&runFlags.Silent, "silent", "q", false, "omit showing loader when running tasks") diff --git a/cmd/list_specs.go b/cmd/list_specs.go index 2f84dce..746813a 100644 --- a/cmd/list_specs.go +++ b/cmd/list_specs.go @@ -8,7 +8,7 @@ import ( "github.com/alajmo/sake/core/print" ) -var specHeaders = []string{"spec", "desc", "describe", "list_hosts", "order", "silent", "hidden", "strategy", "batch", "batch_p", "forks", "output", "any_errors_fatal", "max_fail_percentage", "ignore_errors", "ignore_unreachable", "omit_empty", "report", "verbose", "confirm", "step"} +var specHeaders = []string{"spec", "desc", "describe", "list_hosts", "order", "silent", "hidden", "strategy", "batch", "batch_p", "forks", "output", "print", "any_errors_fatal", "max_fail_percentage", "ignore_errors", "ignore_unreachable", "omit_empty", "report", "verbose", "confirm", "step"} func listSpecsCmd(config *dao.Config, configErr *error, listFlags *core.ListFlags) *cobra.Command { var specFlags core.SpecFlags diff --git a/cmd/run.go b/cmd/run.go index 85f335f..756c1f0 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -58,6 +58,7 @@ func runCmd(config *dao.Config, configErr *error) *cobra.Command { setRunFlags.All = cmd.Flags().Changed("all") setRunFlags.AnyErrorsFatal = cmd.Flags().Changed("any-errors-fatal") setRunFlags.Attach = cmd.Flags().Changed("attach") + setRunFlags.Forks = cmd.Flags().Changed("forks") setRunFlags.Batch = cmd.Flags().Changed("batch") setRunFlags.BatchP = cmd.Flags().Changed("batch-p") setRunFlags.Describe = cmd.Flags().Changed("describe") @@ -80,29 +81,62 @@ func runCmd(config *dao.Config, configErr *error) *cobra.Command { setRunFlags.TTY = cmd.Flags().Changed("tty") setRunFlags.Tags = cmd.Flags().Changed("tags") setRunFlags.Verbose = cmd.Flags().Changed("verbose") + setRunFlags.MaxFailPercentage = cmd.Flags().Changed("max-fail-percentage") - maxFailPercentage, err := cmd.Flags().GetUint8("max-fail-percentage") - core.CheckIfError(err) - runFlags.MaxFailPercentage = maxFailPercentage + if setRunFlags.MaxFailPercentage { + maxFailPercentage, err := cmd.Flags().GetUint8("max-fail-percentage") + core.CheckIfError(err) + if maxFailPercentage > 100 { + core.Exit(&core.InvalidPercentInput{Name: "max-fail-percentage"}) + } + runFlags.MaxFailPercentage = maxFailPercentage + } - forks, err := cmd.Flags().GetUint32("forks") - core.CheckIfError(err) - runFlags.Forks = forks + if setRunFlags.Forks { + forks, err := cmd.Flags().GetUint32("forks") + core.CheckIfError(err) + if forks == 0 { + core.Exit(&core.ZeroNotAllowed{Name: "forks"}) + } + runFlags.Forks = forks + } - batch, err := cmd.Flags().GetUint32("batch") - core.CheckIfError(err) - batchp, err := cmd.Flags().GetUint8("batch-p") - core.CheckIfError(err) - runFlags.Batch = batch - runFlags.BatchP = batchp + if setRunFlags.Batch { + batch, err := cmd.Flags().GetUint32("batch") + core.CheckIfError(err) + if batch == 0 { + core.Exit(&core.ZeroNotAllowed{Name: "batch"}) + } + runFlags.Batch = batch + } - limit, err := cmd.Flags().GetUint32("limit") - core.CheckIfError(err) - limitp, err := cmd.Flags().GetUint8("limit-p") - core.CheckIfError(err) + if setRunFlags.BatchP { + batchp, err := cmd.Flags().GetUint8("batch-p") + core.CheckIfError(err) + if batchp == 0 || batchp > 100 { + core.Exit(&core.InvalidPercentInput2{Name: "batch-p"}) + } + runFlags.BatchP = batchp + } - runFlags.Limit = limit - runFlags.LimitP = limitp + if setRunFlags.Limit { + limit, err := cmd.Flags().GetUint32("limit") + core.CheckIfError(err) + if limit == 0 { + core.Exit(&core.ZeroNotAllowed{Name: "limit"}) + } + runFlags.Limit = limit + } + + // Min-limit-p 1 + if setRunFlags.LimitP { + limitp, err := cmd.Flags().GetUint8("limit-p") + core.CheckIfError(err) + if limitp == 0 || limitp > 100 { + core.Exit(&core.InvalidPercentInput2{Name: "limit-p"}) + } + runFlags.LimitP = limitp + } runTask(args, config, &runFlags, &setRunFlags) }, @@ -212,6 +246,16 @@ func runCmd(config *dao.Config, configErr *error) *cobra.Command { }) core.CheckIfError(err) + cmd.Flags().StringVarP(&runFlags.Print, "print", "p", "", "set print [all|stdout|stderr]") + err = cmd.RegisterFlagCompletionFunc("print", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if *configErr != nil { + return []string{}, cobra.ShellCompDirectiveDefault + } + valid := []string{"all", "stdout", "stderr"} + return valid, cobra.ShellCompDirectiveDefault + }) + core.CheckIfError(err) + cmd.Flags().BoolVar(&runFlags.OmitEmptyRows, "omit-empty-rows", false, "omit empty row for table output") cmd.Flags().BoolVar(&runFlags.OmitEmptyColumns, "omit-empty-columns", false, "omit empty column for table output") cmd.Flags().BoolVarP(&runFlags.Silent, "silent", "q", false, "omit showing loader when running tasks") diff --git a/core/config.man b/core/config.man index a0130be..366c320 100644 --- a/core/config.man +++ b/core/config.man @@ -33,6 +33,9 @@ Below is a config file detailing all of the available options and their defaults # Set known_hosts_file path. Default is users ssh home directory [optional] # known_hosts_file: $HOME/.ssh/known_hosts + # Set timeout for ssh connections in seconds + # default_timeout: 20 + # Shell used for commands [optional] # If you use any other program than bash, zsh, sh, node, or python # then you have to provide the command flag if you want the command-line string evaluted @@ -236,6 +239,12 @@ Below is a config file detailing all of the available options and their defaults # Set task output [text|table|table-2|table-3|table-4|html|markdown|json|csv|none] output: text + # Limit output [stdout|stderr|all] + print: all + + # Hide task from auto-completion + hidden: false + # Continue task execution on errors ignore_errors: true diff --git a/core/dao/config.go b/core/dao/config.go index 8564da2..283029f 100644 --- a/core/dao/config.go +++ b/core/dao/config.go @@ -3,7 +3,6 @@ package dao import ( "fmt" "os" - "os/exec" "path/filepath" "strings" "text/template" @@ -19,6 +18,8 @@ var ( DEFAULT_SHELL = "bash -c" + DEFAULT_TIMEOUT = uint(20) + DEFAULT_THEME = Theme{ Name: "default", Table: DefaultTable, @@ -43,26 +44,29 @@ var ( ListHosts: false, Order: "inventory", Silent: false, + Hidden: false, Strategy: "linear", - Output: "text", + Batch: 0, + BatchP: 0, Forks: 10000, + Output: "text", MaxFailPercentage: 0, AnyErrorsFatal: true, IgnoreErrors: false, IgnoreUnreachable: false, OmitEmptyRows: false, OmitEmptyColumns: false, - Batch: 0, - BatchP: 0, Report: []string{"recap"}, Verbose: false, Confirm: false, Step: false, + Print: "all", } ) type Config struct { SSHConfigFile *string + DefaultTimeout uint DisableVerifyHost bool KnownHostsFile string Shell string @@ -83,6 +87,7 @@ type ConfigYAML struct { // Intermediate DisableVerifyHost *bool `yaml:"disable_verify_host"` + DefaultTimeout *uint `yaml:"default_timeout"` KnownHostsFile *string `yaml:"known_hosts_file"` Shell string `yaml:"shell"` Import yaml.Node `yaml:"import"` @@ -290,18 +295,7 @@ func openEditor(path string, lineNr int) error { args = []string{path} } - editorBin, err := exec.LookPath(editor) - if err != nil { - return err - } - - cmd := exec.Command(editorBin, args...) - cmd.Env = os.Environ() - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - err = cmd.Run() + err := ExecEditor(editor, args, os.Environ()) if err != nil { return err } diff --git a/core/dao/import_config.go b/core/dao/import_config.go index 99581e6..f847c67 100644 --- a/core/dao/import_config.go +++ b/core/dao/import_config.go @@ -29,6 +29,7 @@ func (i *Import) GetContextLine() int { // Used for config imports type ConfigResources struct { DisableVerifyHost *bool + DefaultTimeout *uint KnownHostsFile *string Shell string Imports []Import @@ -235,6 +236,12 @@ func (c *ConfigYAML) parseConfig() (Config, error) { config.DisableVerifyHost = *cr.DisableVerifyHost } + if cr.DefaultTimeout == nil { + config.DefaultTimeout = DEFAULT_TIMEOUT + } else { + config.DefaultTimeout = *cr.DefaultTimeout + } + if cr.Shell != "" { config.Shell = cr.Shell } @@ -357,6 +364,10 @@ func (c *ConfigYAML) loadResources(cr *ConfigResources) { cr.Shell = c.Shell } + if c.DefaultTimeout != nil { + cr.DefaultTimeout = c.DefaultTimeout + } + if c.DisableVerifyHost != nil { cr.DisableVerifyHost = c.DisableVerifyHost } @@ -660,8 +671,8 @@ func checkDuplicateImports(imports []Import) string { } type FoundDuplicateObjects struct { - Name string - Type string + Name string + Type string Values []string } @@ -768,7 +779,6 @@ func checkDuplicateObjects(config Config) string { return errString } - // Used for config imports type TaskResources struct { Tasks []Task diff --git a/core/dao/import_task.go b/core/dao/import_task.go index 688b457..52e84ce 100644 --- a/core/dao/import_task.go +++ b/core/dao/import_task.go @@ -126,6 +126,11 @@ func dfsTask(task *Task, tn *TaskNode, tm map[string]*TaskNode, cycles *[]TaskLi name = tn.TaskRefs[i].Name } + desc := childTask.Desc + if tn.TaskRefs[i].Desc != "" { + desc = tn.TaskRefs[i].Desc + } + local := childTask.Local if tn.TaskRefs[i].Local != nil { local = *tn.TaskRefs[i].Local @@ -150,7 +155,7 @@ func dfsTask(task *Task, tn *TaskNode, tm map[string]*TaskNode, cycles *[]TaskLi t := TaskCmd{ ID: childTask.ID, Name: name, - Desc: childTask.Desc, + Desc: desc, RootDir: filepath.Dir(task.context), WorkDir: workDir, Shell: shell, diff --git a/core/dao/server.go b/core/dao/server.go index 4d923a2..06a8c57 100644 --- a/core/dao/server.go +++ b/core/dao/server.go @@ -238,27 +238,31 @@ func (c *ConfigYAML) ParseServersYAML() ([]Server, []ResourceErrors[Server]) { } } + // Same for all servers + var password *string + if serverYAML.Password != nil { + password = serverYAML.Password + } + + if identityFile == nil && password == nil { + idFile := core.GetFirstExistingFile("~/.ssh/id_rsa", "~/.ssh/id_ecdsa", "~/.ssh/id_dsa") + if idFile != "" { + identityFile = &idFile + } + } + var pubKeyFile *string if identityFile != nil { if _, err := os.Stat(*identityFile); errors.Is(err, os.ErrNotExist) { serverErrors[j].Errors = append(serverErrors[j].Errors, err) continue } - if _, err := os.Stat(*identityFile + ".pub"); !errors.Is(err, os.ErrNotExist) { str := *identityFile + ".pub" pubKeyFile = &str } } - // Return error if file not found - - // Same for all servers - var password *string - if serverYAML.Password != nil { - password = serverYAML.Password - } - switch hostDef { case "host": // string to be evaluated and will result in list of hosts diff --git a/core/dao/spec.go b/core/dao/spec.go index 01133ed..de76bb2 100644 --- a/core/dao/spec.go +++ b/core/dao/spec.go @@ -34,6 +34,7 @@ type Spec struct { Verbose bool `yaml:"verbose"` Confirm bool `yaml:"confirm"` Step bool `yaml:"step"` + Print string `yaml:"print"` context string // config path contextLine int // defined at @@ -54,6 +55,8 @@ func (s Spec) GetValue(key string, _ int) string { return s.Name case "desc", "Desc": return s.Desc + case "print", "Print": + return s.Print case "describe", "Describe": return strconv.FormatBool(s.Describe) case "list_hosts": @@ -136,6 +139,14 @@ func (c *ConfigYAML) DecodeSpec(name string, specYAML yaml.Node) (*Spec, []error specErrors = append(specErrors, &core.MultipleFailSet{Name: name}) } + if spec.MaxFailPercentage > 100 { + specErrors = append(specErrors, &core.InvalidPercentInput{Name: "max_fail_percentage"}) + } + + if spec.Forks == 0 { + spec.Forks = 10000 + } + if spec.BatchP > 0 && spec.Batch > 0 { specErrors = append(specErrors, &core.BatchMultipleDef{Name: name}) } diff --git a/core/dao/target.go b/core/dao/target.go index b72ca96..3b2796c 100644 --- a/core/dao/target.go +++ b/core/dao/target.go @@ -97,6 +97,7 @@ func (c *ConfigYAML) DecodeTarget(name string, targetYAML yaml.Node) (*Target, [ targetErrors = append(targetErrors, &core.LimitMultipleDef{Name: name}) } + // Min limit-p 1 if target.LimitP > 100 { targetErrors = append(targetErrors, &core.InvalidPercentInput{Name: "limit_p"}) } diff --git a/core/dao/theme.go b/core/dao/theme.go index 43899d6..07ef7d9 100644 --- a/core/dao/theme.go +++ b/core/dao/theme.go @@ -268,9 +268,9 @@ func (c *ConfigYAML) ParseThemesYAML() ([]Theme, []ResourceErrors[Theme]) { themes[i].Text.PrefixColors = DefaultText.PrefixColors } - if themes[i].Text.Prefix == "" { - themes[i].Text.Prefix = DefaultText.Prefix - } + // if themes[i].Text.Prefix == "" { + // themes[i].Text.Prefix = DefaultText.Prefix + // } // TABLE if themes[i].Table.Style == "connected-light" { diff --git a/core/dao/unix.go b/core/dao/unix.go new file mode 100644 index 0000000..c4e42c1 --- /dev/null +++ b/core/dao/unix.go @@ -0,0 +1,23 @@ +//go:build !windows +// +build !windows + +package dao + +import ( + "golang.org/x/sys/unix" + "os/exec" +) + +func ExecEditor(editor string, args []string, env []string) error { + editorBin, err := exec.LookPath(editor) + if err != nil { + return err + } + + err = unix.Exec(editorBin, args, env) + if err != nil { + return err + } + + return nil +} diff --git a/core/dao/windows.go b/core/dao/windows.go new file mode 100644 index 0000000..5519ad9 --- /dev/null +++ b/core/dao/windows.go @@ -0,0 +1,8 @@ +//go:build windows +// +build windows + +package dao + +func ExecEditor(_ string, _ []string, _ []string) error { + return nil +} diff --git a/core/errors.go b/core/errors.go index e081257..16b235a 100644 --- a/core/errors.go +++ b/core/errors.go @@ -138,6 +138,14 @@ func (c *BatchMultipleDef) Error() string { return "can only define one of the following for spec: batch, batch_p" } +type ZeroNotAllowed struct { + Name string +} + +func (c *ZeroNotAllowed) Error() string { + return fmt.Sprintf("invalid value for %s, cannot be 0", c.Name) +} + type InvalidPercentInput struct { Name string } @@ -146,6 +154,14 @@ func (c *InvalidPercentInput) Error() string { return fmt.Sprintf("percentage can only be between 0 and 100 for property `%s`", c.Name) } +type InvalidPercentInput2 struct { + Name string +} + +func (c *InvalidPercentInput2) Error() string { + return fmt.Sprintf("percentage can only be between 1 and 100 for property `%s`", c.Name) +} + type RegisterInvalidName struct { Value string } diff --git a/core/flags.go b/core/flags.go index 5a0b8a9..8d2a3e5 100644 --- a/core/flags.go +++ b/core/flags.go @@ -88,6 +88,7 @@ type RunFlags struct { Batch uint32 BatchP uint8 Output string + Print string Strategy string } @@ -107,6 +108,7 @@ type SetRunFlags struct { IgnoreUnreachable bool Order bool Report bool + Forks bool Batch bool BatchP bool Servers bool @@ -117,4 +119,5 @@ type SetRunFlags struct { Verbose bool Confirm bool Step bool + MaxFailPercentage bool } diff --git a/core/print/print_block.go b/core/print/print_block.go index 118d681..9cd309c 100644 --- a/core/print/print_block.go +++ b/core/print/print_block.go @@ -181,6 +181,7 @@ func PrintSpecBlocks(specs []dao.Spec, indent bool, name bool) { output += printNumberField("batch_p", int(spec.BatchP), indent) output += printNumberField("forks", int(spec.Forks), indent) output += printStringField("output", spec.Output, indent) + output += printStringField("print", spec.Print, indent) output += printNumberField("max_fail_percentage", int(spec.MaxFailPercentage), indent) output += printBoolField("any_errors_fatal", spec.AnyErrorsFatal, indent) output += printBoolField("ignore_errors", spec.IgnoreErrors, indent) diff --git a/core/run/client.go b/core/run/client.go index 5d16310..5c0601b 100644 --- a/core/run/client.go +++ b/core/run/client.go @@ -7,7 +7,7 @@ import ( ) type Client interface { - Connect(SSHDialFunc, bool, string, *sync.Mutex) *ErrConnect + Connect(SSHDialFunc, bool, string, uint, *sync.Mutex) *ErrConnect Run(int, []string, string, string, string) error Wait(int) error Close(int) error diff --git a/core/run/exec.go b/core/run/exec.go index 002c8fe..00dbb68 100644 --- a/core/run/exec.go +++ b/core/run/exec.go @@ -35,6 +35,7 @@ type TaskContext struct { client Client dryRun bool tty bool + print string desc string name string @@ -322,18 +323,18 @@ func (run *Run) SetClients( AuthMethod: authMethod, } // Connect to bastion - if err := bastion.Connect(ssh.Dial, run.Config.DisableVerifyHost, run.Config.KnownHostsFile, mu); err != nil { + if err := bastion.Connect(ssh.Dial, run.Config.DisableVerifyHost, run.Config.KnownHostsFile, run.Config.DefaultTimeout, mu); err != nil { errCh <- *err return } // Connect to server through bastion - if err := remote.Connect(bastion.DialThrough, run.Config.DisableVerifyHost, run.Config.KnownHostsFile, mu); err != nil { + if err := remote.Connect(bastion.DialThrough, run.Config.DisableVerifyHost, run.Config.KnownHostsFile, run.Config.DefaultTimeout, mu); err != nil { errCh <- *err return } } else { - if err := remote.Connect(ssh.Dial, run.Config.DisableVerifyHost, run.Config.KnownHostsFile, mu); err != nil { + if err := remote.Connect(ssh.Dial, run.Config.DisableVerifyHost, run.Config.KnownHostsFile, run.Config.DefaultTimeout, mu); err != nil { errCh <- *err return } @@ -641,15 +642,15 @@ func (run *Run) ParseTask( run.Task.Spec = *spec } - if run.Task.Spec.Forks == 0 { - run.Task.Spec.Forks = 10000 + if setRunFlags.Forks { + run.Task.Spec.Forks = runFlags.Forks } - if setRunFlags.Batch { + if setRunFlags.Batch { // Flag run.Task.Spec.Batch = runFlags.Batch - } else if setRunFlags.BatchP { + } else if setRunFlags.BatchP { // Flag tot := float64(len(run.Servers)) - percentage := float64(run.Task.Spec.BatchP) / float64(100) + percentage := float64(runFlags.BatchP) / float64(100) batch := uint32(math.Floor(percentage * tot)) if batch > 0 { @@ -657,8 +658,7 @@ func (run *Run) ParseTask( } else { run.Task.Spec.Batch = 1 } - } else { - // Batch or BatchP must be > 0 + } else { // Spec if run.Task.Spec.Batch == 0 && run.Task.Spec.BatchP == 0 { run.Task.Spec.Batch = uint32(len(run.Servers)) } else if run.Task.Spec.BatchP > 0 { @@ -713,6 +713,10 @@ func (run *Run) ParseTask( run.Task.Spec.Output = runFlags.Output } + if runFlags.Print != "" { + run.Task.Spec.Print = runFlags.Print + } + // Omit empty row if setRunFlags.OmitEmptyRows { run.Task.Spec.OmitEmptyRows = runFlags.OmitEmptyRows @@ -985,7 +989,7 @@ func populateSigners(server dao.Server, signers *Signers) error { return nil } else { // If identity key -> try first without passphrase, if passphrase required prompt password, return - signer, err := GetSigner(server) + signer, err := GetSigner(*server.IdentityFile) if err != nil { return err } @@ -996,27 +1000,30 @@ func populateSigners(server dao.Server, signers *Signers) error { func getAuthMethod(server dao.Server, signers *Signers) []ssh.AuthMethod { var authMethods []ssh.AuthMethod + var publicKeys []ssh.Signer + + if len(signers.agentSigners) > 0 { + publicKeys = append(publicKeys, signers.agentSigners...) + } if server.IdentityFile != nil { - v, found := signers.identities[*server.IdentityFile] + pubKey, found := signers.identities[*server.IdentityFile] if found { - authMethods = append(authMethods, ssh.PublicKeys(v)) - return authMethods + publicKeys = append(publicKeys, pubKey) } } + if len(publicKeys) > 0 { + authMethods = append(authMethods, ssh.PublicKeys(publicKeys...)) + } + if server.Password != nil { - v, found := signers.passwords[*server.Password] + pwSigner, found := signers.passwords[*server.Password] if found { - authMethods = append(authMethods, v) + authMethods = append(authMethods, pwSigner) } } - // No signers found, use agent signers - if len(signers.agentSigners) > 0 { - authMethods = append(authMethods, ssh.PublicKeys(signers.agentSigners...)) - } - return authMethods } diff --git a/core/run/localhost.go b/core/run/localhost.go index ce9c0d0..2c5b0c0 100644 --- a/core/run/localhost.go +++ b/core/run/localhost.go @@ -28,7 +28,7 @@ type LocalSession struct { running bool } -func (c *LocalhostClient) Connect(dialer SSHDialFunc, _ bool, _ string, mu *sync.Mutex) *ErrConnect { +func (c *LocalhostClient) Connect(dialer SSHDialFunc, _ bool, _ string, _ uint, mu *sync.Mutex) *ErrConnect { return nil } diff --git a/core/run/ssh.go b/core/run/ssh.go index 92c3d62..de63075 100644 --- a/core/run/ssh.go +++ b/core/run/ssh.go @@ -24,7 +24,6 @@ import ( ) var ResetColor = "\033[0m" -var DefaultTimeout = 20 * time.Second // Client is a wrapper over the SSH connection/sessions. type SSHClient struct { @@ -66,9 +65,10 @@ func (c *SSHClient) Connect( dialer SSHDialFunc, disableVerifyHost bool, knownHostsFile string, + defaultTimeout uint, mu *sync.Mutex, ) *ErrConnect { - return c.ConnectWith(dialer, disableVerifyHost, knownHostsFile, mu) + return c.ConnectWith(dialer, disableVerifyHost, knownHostsFile, defaultTimeout, mu) } // ConnectWith creates a SSH connection to a specified host. It will use dialer to establish the @@ -77,6 +77,7 @@ func (c *SSHClient) ConnectWith( dialer SSHDialFunc, disableVerifyHost bool, knownHostsFile string, + defaultTimeout uint, mu *sync.Mutex, ) *ErrConnect { if c.connOpened { @@ -100,7 +101,7 @@ func (c *SSHClient) ConnectWith( } return nil }, - Timeout: DefaultTimeout, + Timeout: time.Duration(defaultTimeout) * time.Second, } var err error @@ -475,9 +476,9 @@ func GetFingerprintPubKey(server dao.Server) (string, error) { return ssh.FingerprintSHA256(pk), nil } -func GetSigner(server dao.Server) (ssh.Signer, error) { +func GetSigner(identityFile string) (ssh.Signer, error) { var signer ssh.Signer - data, err := os.ReadFile(*server.IdentityFile) + data, err := os.ReadFile(identityFile) if err != nil { return nil, err } @@ -490,7 +491,7 @@ func GetSigner(server dao.Server) (ssh.Signer, error) { switch e := err.(type) { case *ssh.PassphraseMissingError: // TODO: Let user enter password 3 times, then fail - fmt.Printf("Enter passphrase for %s: ", *server.IdentityFile) + fmt.Printf("Enter passphrase for %s: ", identityFile) pass, err := term.ReadPassword(int(syscall.Stdin)) fmt.Println() if err != nil { diff --git a/core/run/table.go b/core/run/table.go index be94520..84c35de 100644 --- a/core/run/table.go +++ b/core/run/table.go @@ -495,7 +495,7 @@ func (run *Run) tableWork( client = run.RemoteClients[r.Server.Name] } - shell := dao.SelectFirstNonEmpty(r.Task.Shell, r.Server.Shell, run.Config.Shell) + shell := dao.SelectFirstNonEmpty((*r.Cmd).Shell, r.Task.Shell, r.Server.Shell, run.Config.Shell) shell = core.FormatShell(shell) workDir := getWorkDir((*r.Cmd).Local, (*r.Server).Local, (*r.Cmd).WorkDir, (*r.Server).WorkDir, (*r.Cmd).RootDir, (*r.Server).RootDir) t := TaskContext{ @@ -507,7 +507,7 @@ func (run *Run) tableWork( workDir: workDir, shell: shell, cmd: r.Cmd.Cmd, - tty: r.Task.TTY, + tty: r.Cmd.TTY, } start := time.Now() @@ -527,9 +527,23 @@ func (run *Run) tableWork( // out, err := io.ReadAll(client.Stderr()) // dataMutex.Unlock() if err != nil { - data.Rows[t.rIndex].Columns[t.cIndex] = fmt.Sprintf("%s\n%s", out, err.Error()) + switch r.Task.Spec.Print { + case "stdout": + data.Rows[t.rIndex].Columns[t.cIndex] = stdout + case "stderr": + data.Rows[t.rIndex].Columns[t.cIndex] = fmt.Sprintf("%s\n%s", stderr, err.Error()) + default: + data.Rows[t.rIndex].Columns[t.cIndex] = fmt.Sprintf("%s\n%s", out, err.Error()) + } } else { - data.Rows[t.rIndex].Columns[t.cIndex] = strings.TrimSuffix(out, "\n") + switch r.Task.Spec.Print { + case "stdout": + data.Rows[t.rIndex].Columns[t.cIndex] = stdout + case "stderr": + data.Rows[t.rIndex].Columns[t.cIndex] = stderr + default: + data.Rows[t.rIndex].Columns[t.cIndex] = strings.TrimSuffix(out, "\n") + } } reportData.Tasks[r.i].Rows[r.j].ReturnCode = errCode diff --git a/core/run/text.go b/core/run/text.go index cdcccbf..28f8f27 100644 --- a/core/run/text.go +++ b/core/run/text.go @@ -514,7 +514,7 @@ func (run *Run) textWork( return err } - shell := dao.SelectFirstNonEmpty(r.Task.Shell, r.Server.Shell, run.Config.Shell) + shell := dao.SelectFirstNonEmpty((*r.Cmd).Shell, r.Task.Shell, r.Server.Shell, run.Config.Shell) shell = core.FormatShell(shell) workDir := getWorkDir((*r.Cmd).Local, (*r.Server).Local, (*r.Cmd).WorkDir, (*r.Server).WorkDir, (*r.Cmd).RootDir, (*r.Server).RootDir) t := TaskContext{ @@ -530,6 +530,7 @@ func (run *Run) textWork( name: r.Cmd.Name, numTasks: numTasks, tty: r.Cmd.TTY, + print: r.Task.Spec.Print, } start := time.Now() @@ -618,19 +619,28 @@ func runTextCmd( var err error if register == "" { - if prefix != "" { - _, err = io.Copy(os.Stdout, core.NewPrefixer(client.Stdout(i), prefix)) - } else { - _, err = io.Copy(os.Stdout, client.Stdout(i)) + if t.print != "stderr" { + if prefix != "" { + _, err = io.Copy(os.Stdout, core.NewPrefixer(client.Stdout(i), prefix)) + } else { + _, err = io.Copy(os.Stdout, client.Stdout(i)) + } } } else { - mw := io.MultiWriter(buf, bufOut) - r := io.TeeReader(client.Stdout(i), mw) - // TODO: Refactor to NewReader: https://pkg.go.dev/golang.org/x/text/transform?utm_source=godoc#NewReader - if prefix != "" { - _, err = io.Copy(os.Stdout, core.NewPrefixer(r, prefix)) - } else { - _, err = io.Copy(os.Stdout, r) + if t.print != "stderr" { + mw := io.MultiWriter(buf, bufOut) + r := io.TeeReader(client.Stdout(i), mw) + // TODO: Refactor to NewReader: https://pkg.go.dev/golang.org/x/text/transform?utm_source=godoc#NewReader + if prefix != "" { + _, err = io.Copy(os.Stdout, core.NewPrefixer(r, prefix)) + } else { + _, err = io.Copy(os.Stdout, r) + } + } else { // don't write to stdout + mw := io.MultiWriter(buf, bufOut) + r := io.TeeReader(client.Stdout(i), mw) + // TODO: Refactor to NewReader: https://pkg.go.dev/golang.org/x/text/transform?utm_source=godoc#NewReader + _, err = io.Copy(mw, r) } } @@ -646,18 +656,28 @@ func runTextCmd( var err error if register == "" { - if prefix != "" { - _, err = io.Copy(os.Stderr, core.NewPrefixer(client.Stderr(i), prefix)) - } else { - _, err = io.Copy(os.Stderr, client.Stderr(i)) + if t.print != "stdout" { + if prefix != "" { + _, err = io.Copy(os.Stderr, core.NewPrefixer(client.Stderr(i), prefix)) + } else { + _, err = io.Copy(os.Stderr, client.Stderr(i)) + } } } else { - mw := io.MultiWriter(buf, bufErr) - r := io.TeeReader(client.Stderr(i), mw) - if prefix != "" { - _, err = io.Copy(os.Stderr, core.NewPrefixer(r, prefix)) - } else { - _, err = io.Copy(os.Stderr, r) + if t.print != "stdout" { + mw := io.MultiWriter(buf, bufErr) + r := io.TeeReader(client.Stderr(i), mw) + // TODO: Refactor to NewReader: https://pkg.go.dev/golang.org/x/text/transform?utm_source=godoc#NewReader + if prefix != "" { + _, err = io.Copy(os.Stderr, core.NewPrefixer(r, prefix)) + } else { + _, err = io.Copy(os.Stderr, r) + } + } else { // don't write to stdout + mw := io.MultiWriter(buf, bufErr) + r := io.TeeReader(client.Stderr(i), mw) + // TODO: Refactor to NewReader: https://pkg.go.dev/golang.org/x/text/transform?utm_source=godoc#NewReader + _, err = io.Copy(mw, r) } } @@ -670,10 +690,12 @@ func runTextCmd( wg.Wait() if err := t.client.Wait(i); err != nil { - if prefix != "" { - fmt.Printf("%s%s\n", prefix, err.Error()) - } else { - fmt.Printf("%s\n", err.Error()) + if t.print != "stdout" { + if prefix != "" { + fmt.Printf("%s%s\n", prefix, err.Error()) + } else { + fmt.Printf("%s\n", err.Error()) + } } return buf.String(), bufOut.String(), bufErr.String(), err diff --git a/core/run/unix.go b/core/run/unix.go index 72832dd..3122ad3 100644 --- a/core/run/unix.go +++ b/core/run/unix.go @@ -43,14 +43,19 @@ func SSHToServer(server dao.Server, disableVerifyHost bool, knownHostFile string } func ExecTTY(cmd string, envs []string) error { - execBin, err := exec.LookPath("bash") + shell := "bash" + foundShell, found := os.LookupEnv("SHELL") + if found { + shell = foundShell + } + + execBin, err := exec.LookPath(shell) if err != nil { return err } userEnv := append(os.Environ(), envs...) - // TODO: default shell - err = unix.Exec(execBin, []string{"bash", "-c", cmd}, userEnv) + err = unix.Exec(execBin, []string{shell, "-c", cmd}, userEnv) if err != nil { return err } diff --git a/core/sake.1 b/core/sake.1 index ff2dff9..effd398 100644 --- a/core/sake.1 +++ b/core/sake.1 @@ -1,4 +1,4 @@ -.TH "SAKE" "1" "2023-01-01T14:04:13CET" "v0.13.0" "Sake Manual" "sake" +.TH "SAKE" "1" "2023-01-04T23:00:29CET" "v0.14.0" "Sake Manual" "sake" .SH NAME sake - sake is a task runner for local and remote hosts @@ -111,6 +111,9 @@ set spec \fB-o, --output=""\fR set task output [text|table|table-2|table-3|table-4|html|markdown|json|csv|none] .TP +\fB-p, --print=""\fR +set print [all|stdout|stderr] +.TP \fB--omit-empty-rows[=false]\fR omit empty row for table output .TP @@ -239,6 +242,9 @@ set spec \fB-o, --output=""\fR set task output [text|table|table-2|table-3|table-4|html|markdown|json|csv|none] .TP +\fB-p, --print=""\fR +set print [all|stdout|stderr] +.TP \fB--omit-empty-rows[=false]\fR omit empty row for table output .TP @@ -410,7 +416,7 @@ List specs. .RS .RS .TP -\fB--headers=[spec,desc,describe,list_hosts,order,silent,strategy,batch,batch_p,forks,output,any_errors_fatal,max_fail_percentage,ignore_errors,ignore_unreachable,omit_empty,report]\fR +\fB--headers=[spec,desc,describe,list_hosts,order,silent,hidden,strategy,batch,batch_p,forks,output,print,any_errors_fatal,max_fail_percentage,ignore_errors,ignore_unreachable,omit_empty,report,verbose,confirm,step]\fR set headers .TP \fB-o, --output="table"\fR @@ -546,6 +552,9 @@ Below is a config file detailing all of the available options and their defaults # Set known_hosts_file path. Default is users ssh home directory [optional] # known_hosts_file: $HOME/.ssh/known_hosts + # Set timeout for ssh connections in seconds + # default_timeout: 20 + # Shell used for commands [optional] # If you use any other program than bash, zsh, sh, node, or python # then you have to provide the command flag if you want the command-line string evaluted @@ -749,6 +758,12 @@ Below is a config file detailing all of the available options and their defaults # Set task output [text|table|table-2|table-3|table-4|html|markdown|json|csv|none] output: text + # Limit output [stdout|stderr|all] + print: all + + # Hide task from auto-completion + hidden: false + # Continue task execution on errors ignore_errors: true diff --git a/core/utils.go b/core/utils.go index 66709cd..efeb01f 100644 --- a/core/utils.go +++ b/core/utils.go @@ -106,10 +106,10 @@ func FormatShell(shell string) string { return shell + " -c" } else if strings.Contains(shell, "sh") { // sh, /bin/sh return shell + " -c" - } else if strings.Contains(shell, "node") { // node, /bin/node - return shell + " -e" } else if strings.Contains(shell, "python") { // python, /bin/python return shell + " -c" + } else if strings.Contains(shell, "node") { // node, /bin/node + return shell + " -e" } return shell @@ -277,3 +277,28 @@ func SplitString(s, sep string) []string { } return strings.Split(s, sep) } + +func GetFirstExistingFile(files ...string) string { + for _, file := range files { + expandedFile := os.ExpandEnv(file) + expandedFile, err := expandTilde(expandedFile) + if err != nil { + continue + } + if _, err := os.Stat(expandedFile); err == nil { + return expandedFile + } + } + return "" +} + +func expandTilde(path string) (string, error) { + if path[0] != '~' { + return path, nil + } + usr, err := user.Current() + if err != nil { + return "", err + } + return filepath.Join(usr.HomeDir, path[1:]), nil +} diff --git a/docs/changelog.md b/docs/changelog.md index 2ab4d0f..550dbf9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,16 +1,21 @@ # Changelog -## Unreleased +## 0.14.0 ### Features - Add ability to modify prefix in text and table themes - Hide tasks from auto-completion via spec attribute `hidden: true` +- Add print option to limit output to stdout|stderr +- Default to one of following identity files if no identity specified `~/.ssh/id_rsa`, `~/.ssh/id_ecdsa`, `~/.ssh/id_dsa` +- Add ability to modify default timeout for ssh connections ### Fixes - [BREAKING CHANGE]: No more duplicate tasks, specs, targets, and themes - Small fix when user config is specified but not found +- Fix some small validation issues with batch and batch-p +- A bunch of smaller fixes ## 0.13.0 diff --git a/docs/command-reference.md b/docs/command-reference.md index 03712cc..098f342 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -97,6 +97,7 @@ run [flags] --ignore-errors continue task execution on errors -J, --spec string set spec -o, --output string set task output [text|table|table-2|table-3|table-4|html|markdown|json|csv|none] + -p, --print string set print [all|stdout|stderr] --omit-empty-rows omit empty row for table output --omit-empty-columns omit empty column for table output -q, --silent omit showing loader when running tasks @@ -167,6 +168,7 @@ exec [flags] --ignore-errors continue task execution on errors -J, --spec string set spec -o, --output string set task output [text|table|table-2|table-3|table-4|html|markdown|json|csv|none] + -p, --print string set print [all|stdout|stderr] --omit-empty-rows omit empty row for table output --omit-empty-columns omit empty column for table output -q, --silent omit showing loader when running tasks @@ -514,7 +516,7 @@ list specs [specs] [flags] ### Options ``` - --headers strings set headers (default [spec,desc,describe,list_hosts,order,silent,strategy,batch,batch_p,forks,output,any_errors_fatal,max_fail_percentage,ignore_errors,ignore_unreachable,omit_empty,report]) + --headers strings set headers (default [spec,desc,describe,list_hosts,order,silent,hidden,strategy,batch,batch_p,forks,output,print,any_errors_fatal,max_fail_percentage,ignore_errors,ignore_unreachable,omit_empty,report,verbose,confirm,step]) -h, --help help for specs ``` diff --git a/docs/config-reference.md b/docs/config-reference.md index 5e309eb..db60437 100644 --- a/docs/config-reference.md +++ b/docs/config-reference.md @@ -26,6 +26,9 @@ disable_verify_host: false # Set known_hosts_file path. Default is users ssh home directory [optional] # known_hosts_file: $HOME/.ssh/known_hosts +# Set timeout for ssh connections in seconds +# default_timeout: 20 + # Shell used for commands [optional] # If you use any other program than bash, zsh, sh, node, or python # then you have to provide the command flag if you want the command-line string evaluted @@ -229,6 +232,12 @@ specs: # Set task output [text|table|table-2|table-3|table-4|html|markdown|json|csv|none] output: text + # Limit output [stdout|stderr|all] + print: all + + # Hide task from auto-completion + hidden: false + # Continue task execution on errors ignore_errors: true diff --git a/docs/development.md b/docs/development.md index 54e1610..57840d4 100644 --- a/docs/development.md +++ b/docs/development.md @@ -42,13 +42,14 @@ go run ../main.go run ping -a The following workflow is used for releasing a new `sake` version: 1. Create pull request with changes -2. Pass all integration and unit tests locally +2. Verify build works (especially windows build) + - `make build` + - `make build-all` +3. Pass all integration and unit tests locally - `make integration-test` - `make unit-test` -3. Run benchmarks and profiler to check performance +4. Run benchmarks and profiler to check performance - `make benchmark` -4. Verify build works (especially windows build) - - `make build-all` 5. Update `config-reference.md` and `config.man` if any config changes and generate manpage - `make gen-man` 6. Update `Makefile` and `CHANGELOG.md` with correct version, and add all changes to `CHANGELOG.md` diff --git a/test/Dockerfile b/test/Dockerfile index 0b761e4..f44fc0d 100644 --- a/test/Dockerfile +++ b/test/Dockerfile @@ -16,6 +16,7 @@ USER root RUN usermod -aG sudo test RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers +RUN echo 'MaxStartups 100' >> /etc/ssh/sshd_config RUN service ssh start diff --git a/test/benchmark.sh b/test/benchmark.sh index 05528be..ae9ea7a 100755 --- a/test/benchmark.sh +++ b/test/benchmark.sh @@ -20,6 +20,7 @@ function parse_options() { } function __main__() { + export SAKE_USER_CONFIG="$PWD/user-config.yaml" parse_options $@ if [[ "$SAVE" ]]; then hyperfine -N --runs 10 '../dist/sake run ping -s server-9' > ./profiles/ping-no-key diff --git a/test/integration/golden/golden-1.stdout b/test/integration/golden/golden-1.stdout index 8d92d0b..b40c588 100755 --- a/test/integration/golden/golden-1.stdout +++ b/test/integration/golden/golden-1.stdout @@ -7,8 +7,8 @@ WantErr: false spec | describe | list_hosts | silent | hidden | strategy | batch | batch_p | forks | output | any_errors_fatal | max_fail_percentage | ignore_errors | ignore_unreachable | report | verbose | confirm | step ---------+----------+------------+--------+--------+----------+-------+---------+-------+--------+------------------+---------------------+---------------+--------------------+--------+---------+---------+------- - default | false | false | false | false | linear | 1 | 0 | 0 | table | false | 0 | false | false | recap | false | false | false - table | false | false | false | false | | 0 | 0 | 0 | table | false | 0 | false | false | recap | false | false | false - text | false | false | false | false | | 0 | 0 | 0 | text | false | 0 | false | false | recap | false | false | false - info | false | false | false | false | free | 0 | 0 | 0 | table | false | 0 | true | true | recap | false | false | false + default | false | false | false | false | linear | 1 | 0 | 10000 | table | false | 0 | false | false | recap | false | false | false + table | false | false | false | false | | 0 | 0 | 10000 | table | false | 0 | false | false | recap | false | false | false + text | false | false | false | false | | 0 | 0 | 10000 | text | false | 0 | false | false | recap | false | false | false + info | false | false | false | false | free | 0 | 0 | 10000 | table | false | 0 | true | true | recap | false | false | false diff --git a/test/integration/golden/golden-14.stdout b/test/integration/golden/golden-14.stdout index 30f237b..b2b3ef8 100755 --- a/test/integration/golden/golden-14.stdout +++ b/test/integration/golden/golden-14.stdout @@ -11,6 +11,7 @@ theme: default spec: strategy: linear batch: 1 + forks: 10000 output: table report: recap target: @@ -27,6 +28,7 @@ local: true spec: strategy: linear batch: 1 + forks: 10000 output: table report: recap target: @@ -42,6 +44,7 @@ desc: print host theme: default spec: strategy: free + forks: 10000 output: table ignore_errors: true ignore_unreachable: true @@ -59,6 +62,7 @@ desc: print hostname theme: default spec: strategy: free + forks: 10000 output: table ignore_errors: true ignore_unreachable: true @@ -76,6 +80,7 @@ desc: print OS theme: default spec: strategy: free + forks: 10000 output: table ignore_errors: true ignore_unreachable: true @@ -93,6 +98,7 @@ desc: Print kernel version theme: default spec: strategy: free + forks: 10000 output: table ignore_errors: true ignore_unreachable: true @@ -109,6 +115,7 @@ desc: get remote info theme: default spec: strategy: free + forks: 10000 output: table ignore_errors: true ignore_unreachable: true @@ -124,6 +131,7 @@ tasks: name: env theme: default spec: + forks: 10000 output: table report: recap target: @@ -143,6 +151,7 @@ cmd: name: env-ref theme: default spec: + forks: 10000 output: table report: recap target: @@ -163,6 +172,7 @@ cmd: name: env-complex theme: default spec: + forks: 10000 output: table report: recap target: @@ -179,6 +189,7 @@ tasks: name: env-default theme: default spec: + forks: 10000 output: table report: recap target: @@ -197,6 +208,7 @@ theme: default spec: strategy: linear batch: 1 + forks: 10000 output: table report: recap tasks: @@ -209,6 +221,7 @@ theme: default spec: strategy: linear batch: 1 + forks: 10000 output: table report: recap tasks: @@ -222,6 +235,7 @@ theme: default spec: strategy: linear batch: 1 + forks: 10000 output: table report: recap tasks: @@ -234,6 +248,7 @@ tasks: name: d theme: default spec: + forks: 10000 output: table report: recap target: @@ -255,6 +270,7 @@ work_dir: /usr spec: strategy: linear batch: 1 + forks: 10000 output: table report: recap cmd: @@ -268,6 +284,7 @@ theme: default spec: strategy: linear batch: 1 + forks: 10000 output: table report: recap tasks: @@ -279,6 +296,7 @@ name: work-dir-1 theme: default work_dir: /home spec: + forks: 10000 output: table report: recap target: @@ -294,6 +312,7 @@ tasks: name: work-dir-2 theme: default spec: + forks: 10000 output: table report: recap target: @@ -309,6 +328,7 @@ tasks: name: work-dir-3 theme: default spec: + forks: 10000 output: table report: recap target: @@ -324,6 +344,7 @@ theme: default spec: strategy: linear batch: 1 + forks: 10000 output: table report: recap tasks: @@ -336,6 +357,7 @@ theme: default spec: strategy: linear batch: 1 + forks: 10000 output: table report: recap tasks: @@ -349,6 +371,7 @@ tasks: name: fatal theme: default spec: + forks: 10000 output: table report: recap target: @@ -361,6 +384,7 @@ cmd: name: fatal-true theme: default spec: + forks: 10000 output: table any_errors_fatal: true report: recap @@ -374,6 +398,7 @@ cmd: name: errors theme: default spec: + forks: 10000 output: table report: recap target: @@ -388,6 +413,7 @@ tasks: name: errors-true theme: default spec: + forks: 10000 output: table ignore_errors: true report: recap @@ -403,6 +429,7 @@ tasks: name: unreachable theme: default spec: + forks: 10000 report: recap target: all: true @@ -414,6 +441,7 @@ cmd: name: unreachable-true theme: default spec: + forks: 10000 ignore_unreachable: true report: recap target: @@ -426,6 +454,7 @@ cmd: name: empty theme: default spec: + forks: 10000 output: table report: recap target: @@ -441,6 +470,7 @@ cmd: name: empty-true theme: default spec: + forks: 10000 output: table omit_empty_rows: true report: recap @@ -457,6 +487,7 @@ cmd: name: output theme: default spec: + forks: 10000 output: table report: recap tasks: diff --git a/test/integration/golden/golden-8.stdout b/test/integration/golden/golden-8.stdout index 4a21008..9ed5db1 100755 --- a/test/integration/golden/golden-8.stdout +++ b/test/integration/golden/golden-8.stdout @@ -7,18 +7,21 @@ WantErr: false name: default strategy: linear batch: 1 +forks: 10000 output: table report: recap -- name: table +forks: 10000 output: table report: recap -- name: text +forks: 10000 output: text report: recap @@ -26,6 +29,7 @@ report: recap name: info strategy: free +forks: 10000 output: table ignore_errors: true ignore_unreachable: true diff --git a/test/integration/main_test.go b/test/integration/main_test.go index ee15931..d2e4bc1 100644 --- a/test/integration/main_test.go +++ b/test/integration/main_test.go @@ -81,6 +81,7 @@ func TestMain(m *testing.M) { } func Run(t *testing.T, tt TemplateTest) { + var err error log.SetFlags(0) // var goldenFile = filepath.Join(tmpDir, tt.Golden) if _, err := os.Stat(tt.Golden); os.IsNotExist(err) { @@ -90,12 +91,6 @@ func Run(t *testing.T, tt TemplateTest) { } } - t.Cleanup(func() { - if *clean { - clearTmp() - } - }) - // Run test command cmd := exec.Command("bash", "-c", tt.TestCmd) // export GPG_TTY=$(tty) @@ -159,4 +154,10 @@ func Run(t *testing.T, tt TemplateTest) { t.Fatalf("Error: %v", err) } } + + t.Cleanup(func() { + if *clean && err == nil { + clearGolden(tt.Golden) + } + }) } diff --git a/test/integration/run_test.go b/test/integration/run_test.go index a05c778..3f6ceea 100644 --- a/test/integration/run_test.go +++ b/test/integration/run_test.go @@ -1,10 +1,13 @@ package integration import ( + "flag" "fmt" "testing" ) +var par = flag.Bool("par", true, "run tests in parallel") + var cases = []TemplateTest{ // list tags { @@ -256,10 +259,14 @@ var cases = []TemplateTest{ func TestRunCmd(t *testing.T) { for i := range cases { + i := i cases[i].Golden = fmt.Sprintf("golden-%d.stdout", i) cases[i].Index = i - t.Run(cases[i].TestName, func(t *testing.T) { + if *par { + t.Parallel() + } + Run(t, cases[i]) }) } diff --git a/test/playground/sake.yaml b/test/playground/sake.yaml index 55fad5e..8c99673 100644 --- a/test/playground/sake.yaml +++ b/test/playground/sake.yaml @@ -29,7 +29,8 @@ servers: - 172.24.2.6 - 172.24.2.7 user: test - identity_file: ../keys/id_ed25519_pem_no + identity_file: ../keys/id_ed25519_pem + password: testing tags: [remote, pi, many, list, "hej san"] env: hello: world @@ -188,30 +189,36 @@ specs: # any_errors_fatal: true max_fail_percentage: 60 -# themes: -# default: -# text: -# # prefix: '{{ .Index }}' -# # prefix: '{{ .Index }} @ {{ .Name }} @ {{ .Host }}:{{ .Port }} : {{ .User }}' -# # prefix: "{{ .Name }}" -# # prefix: "{{ .Host }}" -# # prefix: "{{ .Host }}:{{ .Port }}" -# header: '{{ .Style "TASK" "bold" }}{{ if ne .NumTasks 1 }} ({{ .Index }}/{{ .NumTasks }}){{end}}{{ if and .Name .Desc }} [{{.Style .Name "bold"}}: {{ .Desc }}] {{ else if .Name }} [{{ .Name }}] {{ else if .Desc }} [{{ .Desc }}] {{end}}' -# table: -# prefix: "{{ .Host }}:{{ .Port }}" - env: VERSION: v0.1.0 DATE: $(date -u +"%Y-%m-%dT%H:%M:%S%Z") tasks: + hello: + name: Hi + cmd: echo PONG + + kaka: + name: KAKA + desc: KAKA DESC + cmd: echo 23 + ping: target: all # spec: host_pinned + name: hej + desc: foo spec: - hidden: true - desc: ping server - cmd: echo pong + print: all + ignore_errors: true + # strategy: free + # desc: ping server + # cmd: echo pong + tty: true + tasks: + - cmd: | + ps -p $$ + - cmd: pwd exit: # name: Exit From 2c96becd0f9e68dda3e69618a0f64995203f851f Mon Sep 17 00:00:00 2001 From: samiralajmovic Date: Wed, 4 Jan 2023 23:24:25 +0100 Subject: [PATCH 07/18] Proper release v0.14.0 --- core/dao/config.go | 14 +++++++++++++- core/dao/unix.go | 23 ----------------------- core/dao/windows.go | 8 -------- core/sake.1 | 2 +- 4 files changed, 14 insertions(+), 33 deletions(-) delete mode 100644 core/dao/unix.go delete mode 100644 core/dao/windows.go diff --git a/core/dao/config.go b/core/dao/config.go index 283029f..988617d 100644 --- a/core/dao/config.go +++ b/core/dao/config.go @@ -3,6 +3,7 @@ package dao import ( "fmt" "os" + "os/exec" "path/filepath" "strings" "text/template" @@ -295,7 +296,18 @@ func openEditor(path string, lineNr int) error { args = []string{path} } - err := ExecEditor(editor, args, os.Environ()) + editorBin, err := exec.LookPath(editor) + if err != nil { + return err + } + + cmd := exec.Command(editorBin, args...) + cmd.Env = os.Environ() + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err = cmd.Run() if err != nil { return err } diff --git a/core/dao/unix.go b/core/dao/unix.go deleted file mode 100644 index c4e42c1..0000000 --- a/core/dao/unix.go +++ /dev/null @@ -1,23 +0,0 @@ -//go:build !windows -// +build !windows - -package dao - -import ( - "golang.org/x/sys/unix" - "os/exec" -) - -func ExecEditor(editor string, args []string, env []string) error { - editorBin, err := exec.LookPath(editor) - if err != nil { - return err - } - - err = unix.Exec(editorBin, args, env) - if err != nil { - return err - } - - return nil -} diff --git a/core/dao/windows.go b/core/dao/windows.go deleted file mode 100644 index 5519ad9..0000000 --- a/core/dao/windows.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build windows -// +build windows - -package dao - -func ExecEditor(_ string, _ []string, _ []string) error { - return nil -} diff --git a/core/sake.1 b/core/sake.1 index effd398..1e1f193 100644 --- a/core/sake.1 +++ b/core/sake.1 @@ -1,4 +1,4 @@ -.TH "SAKE" "1" "2023-01-04T23:00:29CET" "v0.14.0" "Sake Manual" "sake" +.TH "SAKE" "1" "2023-01-04T23:23:54CET" "v0.14.0" "Sake Manual" "sake" .SH NAME sake - sake is a task runner for local and remote hosts From 5ce9fb3f89612c16b678aebc08bcc85017d1fa25 Mon Sep 17 00:00:00 2001 From: samiralajmovic Date: Fri, 10 Feb 2023 10:38:40 +0100 Subject: [PATCH 08/18] Add install instructions for macport and arch --- README.md | 12 +++++++++++- docs/installation.md | 10 ++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d82873..3f0918e 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,17 @@ Interested in managing your git repositories in a similar way? Check out [mani]( brew install sake ``` -* Via GO install +* via MacPorts + ```sh + sudo port install sake + ``` + +* via Arch + ```sh + pacman -S sake + ``` + +* Via Go ```sh go install github.com/alajmo/sake@latest ``` diff --git a/docs/installation.md b/docs/installation.md index 57f1318..5779604 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -15,6 +15,16 @@ brew install sake ``` +* via MacPorts + ```sh + sudo port install sake + ``` + +* via Arch + ```sh + pacman -S sake + ``` + * via Go ```bash go install github.com/alajmo/sake@latest From 1f40858812bbd4e1ccb06d5f4fe8679f09c5cc65 Mon Sep 17 00:00:00 2001 From: samiralajmovic Date: Thu, 25 May 2023 19:55:48 +0200 Subject: [PATCH 09/18] refactor and update dependencies --- cmd/check.go | 3 +-- cmd/describe_specs.go | 2 +- cmd/edit.go | 4 ++-- cmd/gen_docs.go | 2 +- cmd/root.go | 2 +- core/dao/import_config.go | 4 ++-- core/print/print_block.go | 4 ++-- core/run/exec.go | 10 ++++----- core/run/localhost.go | 4 ++-- core/run/ssh.go | 10 ++++----- core/run/table.go | 6 +----- core/run/text.go | 15 +++++-------- go.mod | 18 ++++++++-------- go.sum | 45 +++++++++++++++++---------------------- 14 files changed, 57 insertions(+), 72 deletions(-) diff --git a/cmd/check.go b/cmd/check.go index c14bf6f..d1471e9 100644 --- a/cmd/check.go +++ b/cmd/check.go @@ -6,10 +6,9 @@ import ( "github.com/spf13/cobra" "github.com/alajmo/sake/core" - "github.com/alajmo/sake/core/dao" ) -func checkCmd(config *dao.Config, configErr *error) *cobra.Command { +func checkCmd(configErr *error) *cobra.Command { cmd := cobra.Command{ Use: "check", Short: "Validate config", diff --git a/cmd/describe_specs.go b/cmd/describe_specs.go index 8aee6bd..28ffa40 100644 --- a/cmd/describe_specs.go +++ b/cmd/describe_specs.go @@ -63,5 +63,5 @@ func describeSpecs( specs = config.Specs } - print.PrintSpecBlocks(specs, false, true) + print.PrintSpecBlocks(specs, false) } diff --git a/cmd/edit.go b/cmd/edit.go index 807cb5b..73d5642 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -21,7 +21,7 @@ func editCmd(config *dao.Config, configErr *error) *cobra.Command { case *core.ConfigNotFound: core.CheckIfError(e) default: - runEdit(args, *config) + runEdit(*config) } }, DisableAutoGenTag: true, @@ -37,7 +37,7 @@ func editCmd(config *dao.Config, configErr *error) *cobra.Command { return &cmd } -func runEdit(args []string, config dao.Config) { +func runEdit(config dao.Config) { err := config.EditConfig() core.CheckIfError(err) } diff --git a/cmd/gen_docs.go b/cmd/gen_docs.go index 1f62d70..c0a8cd5 100644 --- a/cmd/gen_docs.go +++ b/cmd/gen_docs.go @@ -22,7 +22,7 @@ func genDocsCmd(longAppDesc string) *cobra.Command { version, date, rootCmd, - checkCmd(&config, &configErr), + checkCmd(&configErr), runCmd(&config, &configErr), execCmd(&config, &configErr), initCmd(), diff --git a/cmd/root.go b/cmd/root.go index 4759bb8..ae698be 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -74,7 +74,7 @@ func init() { execCmd(&config, &configErr), sshCmd(&config, &configErr), editCmd(&config, &configErr), - checkCmd(&config, &configErr), + checkCmd(&configErr), completionCmd(), genCmd(), ) diff --git a/core/dao/import_config.go b/core/dao/import_config.go index f847c67..472c611 100644 --- a/core/dao/import_config.go +++ b/core/dao/import_config.go @@ -332,7 +332,7 @@ func concatErrors(importErr string, duplicateObjects string, cr ConfigResources, return errString } -func parseConfigFile(path string, cr *ConfigResources) (ConfigYAML, error) { +func parseConfigFile(path string) (ConfigYAML, error) { var configYAML ConfigYAML absPath, err := filepath.Abs(path) @@ -580,7 +580,7 @@ func dfsImport(n *Node, m map[string]*Node, cycles *[]NodeLink, cr *ConfigResour } // Import raw configYAML - configYAML, err := parseConfigFile(nc.Path, cr) + configYAML, err := parseConfigFile(nc.Path) // Error belongs to config file trying to import the new config if err != nil { diff --git a/core/print/print_block.go b/core/print/print_block.go index 9cd309c..fd94a02 100644 --- a/core/print/print_block.go +++ b/core/print/print_block.go @@ -95,7 +95,7 @@ func PrintTaskBlock(tasks []dao.Task) { fmt.Print(output) - PrintSpecBlocks([]dao.Spec{task.Spec}, true, false) + PrintSpecBlocks([]dao.Spec{task.Spec}, true) PrintTargetBlocks([]dao.Target{task.Target}, true) if task.Envs != nil { @@ -163,7 +163,7 @@ func PrintTargetBlocks(targets []dao.Target, indent bool) { } } -func PrintSpecBlocks(specs []dao.Spec, indent bool, name bool) { +func PrintSpecBlocks(specs []dao.Spec, indent bool) { if len(specs) == 0 { return } diff --git a/core/run/exec.go b/core/run/exec.go index 00dbb68..ebe7929 100644 --- a/core/run/exec.go +++ b/core/run/exec.go @@ -97,7 +97,7 @@ func (run *Run) RunTask( return err } - return &core.ExecError{Err: errors.New("Parse Error"), ExitCode: 4} + return &core.ExecError{Err: errors.New("parse Error"), ExitCode: 4} } // Remote + Local clients @@ -266,7 +266,7 @@ func (run *Run) SetClients( clientCh chan Client, errCh chan ErrConnect, ) ([]ErrConnect, error) { - createLocalClient := func(strategy string, numTasks int, server dao.Server, wg *sync.WaitGroup, mu *sync.Mutex) { + createLocalClient := func(strategy string, numTasks int, server dao.Server, wg *sync.WaitGroup) { defer wg.Done() local := &LocalhostClient{ @@ -374,7 +374,7 @@ func (run *Run) SetClients( // TODO: Dont create remote clients if task is set to local for _, server := range run.Servers { wg.Add(1) - go createLocalClient(task.Spec.Strategy, len(task.Tasks), server, &wg, &mu) + go createLocalClient(task.Spec.Strategy, len(task.Tasks), server, &wg) if !server.Local { wg.Add(1) authMethods := getAuthMethod(server, &signers) @@ -856,12 +856,12 @@ func getWorkDir( cmdDir string, serverDir string, ) string { - cmdWDTrue := false + var cmdWDTrue bool if cmdWD != "" { cmdWDTrue = true } - serverWDTrue := false + var serverWDTrue bool if serverWD != "" { serverWDTrue = true } diff --git a/core/run/localhost.go b/core/run/localhost.go index 2c5b0c0..b54c57e 100644 --- a/core/run/localhost.go +++ b/core/run/localhost.go @@ -36,7 +36,7 @@ func (c *LocalhostClient) Run(i int, env []string, workDir string, shell string, var err error if c.Sessions[i].running { - return fmt.Errorf("Command already running") + return fmt.Errorf("command already running") } userEnv := os.Environ() @@ -85,7 +85,7 @@ func (c *LocalhostClient) Run(i int, env []string, workDir string, shell string, func (c *LocalhostClient) Wait(i int) error { if !c.Sessions[i].running { - return fmt.Errorf("Trying to wait on stopped command") + return fmt.Errorf("trying to wait on stopped command") } err := c.Sessions[i].cmd.Wait() c.Sessions[i].running = false diff --git a/core/run/ssh.go b/core/run/ssh.go index de63075..2cfab2b 100644 --- a/core/run/ssh.go +++ b/core/run/ssh.go @@ -181,7 +181,7 @@ func (c *SSHClient) Run(i int, env []string, workDir string, shell string, cmdSt // It closes the SSH session. func (c *SSHClient) Wait(i int) error { if !c.Sessions[i].running { - return fmt.Errorf("Trying to wait on stopped session") + return fmt.Errorf("trying to wait on stopped session") } err := c.Sessions[i].sess.Wait() @@ -199,7 +199,7 @@ func (c *SSHClient) Close(i int) error { c.Sessions[i].sessOpened = false } if !c.connOpened { - return fmt.Errorf("Trying to close the already closed connection") + return fmt.Errorf("trying to close the already closed connection") } err := c.conn.Close() @@ -280,11 +280,11 @@ func VerifyHost(knownHostsFile string, mu *sync.Mutex, host string, remote net.A // Host not found, ask user to check if he trust the host public key if !askIsHostTrusted(host, key, mu) { - return errors.New("you typed no, aborted!") + return errors.New("you typed no, aborted") } // Add the new host to known hosts file - return AddKnownHost(host, remote, key, knownHostsFile) + return AddKnownHost(host, key, knownHostsFile) } func CheckKnownHost(host string, remote net.Addr, key ssh.PublicKey, knownFile string) (found bool, err error) { @@ -339,7 +339,7 @@ func askIsHostTrusted(host string, key ssh.PublicKey, mu *sync.Mutex) bool { return strings.ToLower(strings.TrimSpace(a)) == "yes" || strings.ToLower(strings.TrimSpace(a)) == "y" } -func AddKnownHost(host string, remote net.Addr, key ssh.PublicKey, knownFile string) (err error) { +func AddKnownHost(host string, key ssh.PublicKey, knownFile string) (err error) { f, err := os.OpenFile(knownFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) if err != nil { return err diff --git a/core/run/table.go b/core/run/table.go index 84c35de..879c2f7 100644 --- a/core/run/table.go +++ b/core/run/table.go @@ -313,10 +313,6 @@ func (run *Run) linear( r ServerTask, register map[string]string, errCh chan<- error, - failedHosts chan<- struct { - string - bool - }, wg *sync.WaitGroup, ) { defer wg.Done() @@ -335,7 +331,7 @@ func (run *Run) linear( bool }{r.Server.Name, false} } - }(r, register[r.Server.Name], errCh, failedHostsCh, &wg) + }(r, register[r.Server.Name], errCh, &wg) } wg.Wait() diff --git a/core/run/text.go b/core/run/text.go index 28f8f27..9953554 100644 --- a/core/run/text.go +++ b/core/run/text.go @@ -311,10 +311,6 @@ func (run *Run) linearText( r ServerTask, register map[string]string, errCh chan<- error, - failedHosts chan<- struct { - string - bool - }, wg *sync.WaitGroup, ) { defer wg.Done() @@ -333,7 +329,7 @@ func (run *Run) linearText( bool }{r.Server.Name, false} } - }(r, register[r.Server.Name], errCh, failedHostsCh, &wg) + }(r, register[r.Server.Name], errCh, &wg) } wg.Wait() @@ -535,7 +531,7 @@ func (run *Run) textWork( start := time.Now() var wg sync.WaitGroup - out, stdout, stderr, err := runTextCmd(si, t, r.Task.Theme.Text, prefix, r.Cmd.Register, &wg) + out, stdout, stderr, err := runTextCmd(si, t, prefix, r.Cmd.Register, &wg) reportData.Tasks[r.i].Rows[r.j].Duration = time.Since(start) // Add exit code to reportData @@ -590,7 +586,6 @@ func (run *Run) textWork( func runTextCmd( i int, t TaskContext, - textStyle dao.Text, prefix string, register string, wg *sync.WaitGroup, @@ -881,7 +876,7 @@ func getPrefixer(client Client, i int, prefixMaxLen int, ts dao.Text, batch int) return prefix, nil } -func getPrefixLength(client Client, i int, prefixMaxLen int, ts dao.Text) (int, error) { +func getPrefixLength(client Client, i int, ts dao.Text) (int, error) { if ts.Prefix == "" { return 0, nil } @@ -905,10 +900,10 @@ func getPrefixLength(client Client, i int, prefixMaxLen int, ts dao.Text) (int, } func calcMaxPrefixLength(clients map[string]Client, task dao.Task) (int, error) { - var prefixMaxLen int = 0 + var prefixMaxLen = 0 i := 0 for _, c := range clients { - prefixLen, err := getPrefixLength(c, i, prefixMaxLen, task.Theme.Text) + prefixLen, err := getPrefixLength(c, i, task.Theme.Text) if err != nil { return 0, err } diff --git a/go.mod b/go.mod index 5a134c8..f2adbdf 100644 --- a/go.mod +++ b/go.mod @@ -4,28 +4,28 @@ go 1.19 require ( github.com/gobwas/glob v0.2.3 - github.com/jedib0t/go-pretty/v6 v6.4.3 + github.com/jedib0t/go-pretty/v6 v6.4.6 github.com/kevinburke/ssh_config v1.2.0 github.com/kr/pretty v0.2.1 - github.com/spf13/cobra v1.6.1 + github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/theckman/yacspin v0.13.12 - golang.org/x/crypto v0.3.0 - golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 - golang.org/x/sys v0.2.0 - golang.org/x/term v0.2.0 + golang.org/x/crypto v0.9.0 + golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 + golang.org/x/sys v0.8.0 + golang.org/x/term v0.8.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect - github.com/fatih/color v1.13.0 // indirect + github.com/fatih/color v1.15.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/text v0.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect - github.com/rivo/uniseg v0.4.3 // indirect + github.com/rivo/uniseg v0.4.4 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/go.sum b/go.sum index 0689113..fa898f7 100644 --- a/go.sum +++ b/go.sum @@ -3,16 +3,15 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jedib0t/go-pretty/v6 v6.4.3 h1:2n9BZ0YQiXGESUSR+6FLg0WWWE80u+mIz35f0uHWcIE= -github.com/jedib0t/go-pretty/v6 v6.4.3/go.mod h1:MgmISkTWDSFu0xOqiZ0mKNntMQ2mDgOcwOkwBEkMDJI= +github.com/jedib0t/go-pretty/v6 v6.4.6 h1:v6aG9h6Uby3IusSSEjHaZNXpHFhzqMmjXcPq1Rjl9Jw= +github.com/jedib0t/go-pretty/v6 v6.4.6/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= @@ -20,13 +19,11 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= @@ -34,12 +31,12 @@ github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdL github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= -github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -49,19 +46,17 @@ github.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvH github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4= github.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg= -golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= -golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 h1:yZNXmy+j/JpX19vZkVktWqAo7Gny4PBWYYK3zskGpx4= -golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From ac9609aacac6c93f74c387587aa4ca9b2198b389 Mon Sep 17 00:00:00 2001 From: samiralajmovic Date: Thu, 8 Jun 2023 00:13:22 +0200 Subject: [PATCH 10/18] Update readme --- README.md | 7 +++++++ docs/installation.md | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/README.md b/README.md index 3f0918e..620b8e7 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,8 @@ Interested in managing your git repositories in a similar way? Check out [mani]( ## Installation +[![Packaging status](https://repology.org/badge/vertical-allrepos/sake.svg)](https://repology.org/project/sake/versions) + `sake` is available on Linux and Mac. * Binaries are available on the [release](https://github.com/alajmo/sake/releases) page @@ -79,6 +81,11 @@ Interested in managing your git repositories in a similar way? Check out [mani]( pacman -S sake ``` +* via pkg + ```sh + pkg install sake + ``` + * Via Go ```sh go install github.com/alajmo/sake@latest diff --git a/docs/installation.md b/docs/installation.md index 5779604..680ba22 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -25,6 +25,11 @@ pacman -S sake ``` +* via pkg + ```sh + pkg install sake + ``` + * via Go ```bash go install github.com/alajmo/sake@latest From 15276ae83aff2fb1a86716c8eb41a74f210df1e1 Mon Sep 17 00:00:00 2001 From: Samir Alajmovic <5246600+alajmo@users.noreply.github.com> Date: Sat, 10 Jun 2023 22:08:05 +0200 Subject: [PATCH 11/18] Add support for multiple bastions (#55) --- .github/workflows/build.yml | 13 +- .github/workflows/release.yml | 7 +- Makefile | 2 +- core/config.man | 5 +- core/dao/import_config.go | 28 +--- core/dao/server.go | 152 ++++++++++++++------- core/errors.go | 8 ++ core/print/print_block.go | 17 ++- core/run/exec.go | 165 ++++++++++++++--------- core/run/unix.go | 17 ++- core/sake.1 | 7 +- core/utils.go | 2 +- docs/changelog.md | 14 ++ docs/config-reference.md | 3 + go.mod | 2 +- test/Dockerfile | 4 +- test/docker-compose.yaml | 140 ++++++++++++------- test/integration/golden/golden-0.stdout | 4 + test/integration/golden/golden-10.stdout | 27 +++- test/integration/golden/golden-23.stdout | 6 +- test/integration/golden/golden-37.stdout | 6 +- test/integration/golden/golden-38.stdout | 14 +- test/integration/golden/golden-4.stdout | 4 +- test/playground/sake.yaml | 71 +++++++++- test/servers.yaml | 41 ++++-- 25 files changed, 520 insertions(+), 239 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 218aaa2..b61b640 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,16 +12,17 @@ jobs: strategy: matrix: os: [ubuntu-latest] - go: [1.19] + go: ['1.20'] steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: - go-version: 1.19 - - - name: Check out code - uses: actions/checkout@v3 + go-version: '1.20' + cache: false - name: golangci-lint uses: golangci/golangci-lint-action@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8128662..f5e38a2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,13 +12,12 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - with: - fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: - go-version: 1.19 + go-version: '1.20' + cache: false - name: Create release notes run: ./scripts/release.sh diff --git a/Makefile b/Makefile index d4f6593..8e12f4b 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ NAME := sake PACKAGE := github.com/alajmo/$(NAME) DATE := $(shell date +%FT%T%Z) GIT := $(shell [ -d .git ] && git rev-parse --short HEAD) -VERSION := v0.14.0 +VERSION := v0.15.0 default: build diff --git a/core/config.man b/core/config.man index 366c320..a8b7eee 100644 --- a/core/config.man +++ b/core/config.man @@ -66,7 +66,10 @@ Below is a config file detailing all of the available options and their defaults # inventory: echo samir@192.168.0.1:22 samir@192.168.1.1:22 # Bastion [optional] - bastion: samir@192.168.1.1:2222 + bastion: samir@192.168.1.1:2222 + + # Bastions [optional] + # bastions: [samir@192.168.1.1:2222, samir@192.168.1.2:3333] # User to connect as. It defaults to the current user [optional] user: samir diff --git a/core/dao/import_config.go b/core/dao/import_config.go index 472c611..689896a 100644 --- a/core/dao/import_config.go +++ b/core/dao/import_config.go @@ -117,9 +117,7 @@ func (c *ConfigYAML) parseConfig() (Config, error) { cr.ConfigErrors = append(cr.ConfigErrors, configError) } else { imports, importErrors := c.ParseImportsYAML() - for i := range importErrors { - cr.ImportErrors = append(cr.ImportErrors, importErrors[i]) - } + cr.ImportErrors = append(cr.ImportErrors, importErrors...) cr.Imports = append(cr.Imports, imports...) } @@ -412,9 +410,7 @@ func (c *ConfigYAML) loadResources(cr *ConfigResources) { cr.ConfigErrors = append(cr.ConfigErrors, configError) } else { tasks, taskErrors := c.ParseTasksYAML() - for i := range taskErrors { - cr.TaskErrors = append(cr.TaskErrors, taskErrors[i]) - } + cr.TaskErrors = append(cr.TaskErrors, taskErrors...) cr.Tasks = append(cr.Tasks, tasks...) } } @@ -433,9 +429,7 @@ func (c *ConfigYAML) loadResources(cr *ConfigResources) { } else { servers, serverErrors := c.ParseServersYAML() cr.Servers = append(cr.Servers, servers...) - for i := range serverErrors { - cr.ServerErrors = append(cr.ServerErrors, serverErrors[i]) - } + cr.ServerErrors = append(cr.ServerErrors, serverErrors...) } } @@ -453,9 +447,7 @@ func (c *ConfigYAML) loadResources(cr *ConfigResources) { } else { themes, themeErrors := c.ParseThemesYAML() cr.Themes = append(cr.Themes, themes...) - for i := range themeErrors { - cr.ThemeErrors = append(cr.ThemeErrors, themeErrors[i]) - } + cr.ThemeErrors = append(cr.ThemeErrors, themeErrors...) } } @@ -473,9 +465,7 @@ func (c *ConfigYAML) loadResources(cr *ConfigResources) { } else { specs, specErrors := c.ParseSpecsYAML() cr.Specs = append(cr.Specs, specs...) - for i := range specErrors { - cr.SpecErrors = append(cr.SpecErrors, specErrors[i]) - } + cr.SpecErrors = append(cr.SpecErrors, specErrors...) } } @@ -493,9 +483,7 @@ func (c *ConfigYAML) loadResources(cr *ConfigResources) { } else { targets, targetErrors := c.ParseTargetsYAML() cr.Targets = append(cr.Targets, targets...) - for i := range targetErrors { - cr.TargetErrors = append(cr.TargetErrors, targetErrors[i]) - } + cr.TargetErrors = append(cr.TargetErrors, targetErrors...) } } @@ -605,9 +593,7 @@ func dfsImport(n *Node, m map[string]*Node, cycles *[]NodeLink, cr *ConfigResour continue } else { imports, importErrors := configYAML.ParseImportsYAML() - for i := range importErrors { - cr.ImportErrors = append(cr.ImportErrors, importErrors[i]) - } + cr.ImportErrors = append(cr.ImportErrors, importErrors...) nc.Imports = imports } } diff --git a/core/dao/server.go b/core/dao/server.go index 06a8c57..82ef2ca 100644 --- a/core/dao/server.go +++ b/core/dao/server.go @@ -24,9 +24,7 @@ type Server struct { Desc string Host string Inventory string - BastionHost string - BastionUser string - BastionPort uint16 + Bastions []Bastion User string Port uint16 Local bool @@ -46,6 +44,16 @@ type Server struct { contextLine int // defined at } +type Bastion struct { + Host string + User string + Port uint16 +} + +func (b Bastion) GetPrint() string { + return fmt.Sprintf("%s@%s:%d", b.User, b.Host, b.Port) +} + type ServerYAML struct { Name string `yaml:"-"` Desc string `yaml:"desc"` @@ -53,6 +61,7 @@ type ServerYAML struct { Hosts yaml.Node `yaml:"hosts"` Inventory string `yaml:"inventory"` Bastion string `yaml:"bastion"` + Bastions []string `yaml:"bastions"` User string `yaml:"user"` Port uint16 `yaml:"port"` Local bool `yaml:"local"` @@ -74,7 +83,7 @@ func (s Server) GetValue(key string, _ int) string { case "host": return s.Host case "bastion": - return s.BastionHost + return getBastionHosts(s.Bastions, "\n") case "user": return s.User case "port": @@ -162,12 +171,6 @@ func (c *ConfigYAML) ParseServersYAML() ([]Server, []ResourceErrors[Server]) { serverYAML.Port = 22 } - hostDef, err := getServerHostDefinition(serverYAML) - if err != nil { - serverErrors[j].Errors = append(serverErrors[j].Errors, err) - continue - } - var envs []string if !IsNullNode(serverYAML.Env) { err := CheckIsMappingNode(serverYAML.Env) @@ -184,31 +187,51 @@ func (c *ConfigYAML) ParseServersYAML() ([]Server, []ResourceErrors[Server]) { } } - defaultEnvs := []string{} - if serverYAML.IdentityFile != nil { - defaultEnvs = append(defaultEnvs, fmt.Sprintf("S_IDENTITY=%s", *serverYAML.IdentityFile)) - } - if len(serverYAML.Tags) > 0 { - defaultEnvs = append(defaultEnvs, fmt.Sprintf("S_TAGS=%s", strings.Join(serverYAML.Tags, ","))) - } - if serverYAML.Bastion != "" { - defaultEnvs = append(defaultEnvs, fmt.Sprintf("S_BASTION=%s", serverYAML.Bastion)) + bastionDef, err := getServerBastionDefinition(serverYAML) + bastions := []Bastion{} + if err != nil { + serverErrors[j].Errors = append(serverErrors[j].Errors, err) + continue } - - // Same for all servers - var bastionUser string - var bastionHost string - var bastionPort uint16 - if serverYAML.Bastion != "" { + switch bastionDef { + case "bastion": bUser, bHost, bPort, err := core.ParseHostName(serverYAML.Bastion, serverYAML.User, serverYAML.Port) if err != nil { serverErrors[j].Errors = append(serverErrors[j].Errors, err) continue } - bastionHost = bHost - bastionUser = bUser - bastionPort = bPort + bastions = append(bastions, Bastion{ + User: bUser, + Host: bHost, + Port: bPort, + }) + case "bastions": + for _, bastionStr := range serverYAML.Bastions { + bUser, bHost, bPort, err := core.ParseHostName(bastionStr, serverYAML.User, serverYAML.Port) + if err != nil { + serverErrors[j].Errors = append(serverErrors[j].Errors, err) + continue + } + + bastions = append(bastions, Bastion{ + User: bUser, + Host: bHost, + Port: bPort, + }) + + } + } + + defaultEnvs := []string{} + if serverYAML.IdentityFile != nil { + defaultEnvs = append(defaultEnvs, fmt.Sprintf("S_IDENTITY=%s", *serverYAML.IdentityFile)) + } + if len(serverYAML.Tags) > 0 { + defaultEnvs = append(defaultEnvs, fmt.Sprintf("S_TAGS=%s", strings.Join(serverYAML.Tags, ","))) + } + if len(bastions) > 0 { + defaultEnvs = append(defaultEnvs, fmt.Sprintf("S_BASTION=%s", getBastionHosts(bastions, ","))) } var identityFile *string @@ -263,9 +286,14 @@ func (c *ConfigYAML) ParseServersYAML() ([]Server, []ResourceErrors[Server]) { } } + hostDef, err := getServerHostDefinition(serverYAML) + if err != nil { + serverErrors[j].Errors = append(serverErrors[j].Errors, err) + continue + } switch hostDef { case "host": - // string to be evaluated and will result in list of hosts + // host: test@192.168.0.1:22 user, host, port, err := core.ParseHostName(serverYAML.Host, serverYAML.User, serverYAML.Port) if err != nil { serverErrors[j].Errors = append(serverErrors[j].Errors, err) @@ -292,9 +320,7 @@ func (c *ConfigYAML) ParseServersYAML() ([]Server, []ResourceErrors[Server]) { Shell: serverYAML.Shell, WorkDir: serverYAML.WorkDir, Envs: serverEnvs, - BastionHost: bastionHost, - BastionUser: bastionUser, - BastionPort: bastionPort, + Bastions: bastions, IdentityFile: identityFile, PubFile: pubKeyFile, Password: password, @@ -306,7 +332,7 @@ func (c *ConfigYAML) ParseServersYAML() ([]Server, []ResourceErrors[Server]) { servers = append(servers, *hServer) case "hosts": - // list of servers + // list of hosts for k, s := range serverYAML.Hosts.Content { user, host, port, err := core.ParseHostName(s.Value, serverYAML.User, serverYAML.Port) @@ -335,9 +361,7 @@ func (c *ConfigYAML) ParseServersYAML() ([]Server, []ResourceErrors[Server]) { Shell: serverYAML.Shell, WorkDir: serverYAML.WorkDir, Envs: serverEnvs, - BastionHost: bastionHost, - BastionUser: bastionUser, - BastionPort: bastionPort, + Bastions: bastions, IdentityFile: identityFile, PubFile: pubKeyFile, Password: password, @@ -350,6 +374,7 @@ func (c *ConfigYAML) ParseServersYAML() ([]Server, []ResourceErrors[Server]) { servers = append(servers, *hServer) } case "hosts-string": + // hosts: 192.168.[0:3].1 hosts, err := core.EvaluateRange(serverYAML.Hosts.Value) if err != nil { serverErrors[j].Errors = append(serverErrors[j].Errors, err) @@ -383,9 +408,7 @@ func (c *ConfigYAML) ParseServersYAML() ([]Server, []ResourceErrors[Server]) { Shell: serverYAML.Shell, WorkDir: serverYAML.WorkDir, Envs: serverEnvs, - BastionHost: bastionHost, - BastionUser: bastionUser, - BastionPort: bastionPort, + Bastions: bastions, IdentityFile: identityFile, PubFile: pubKeyFile, Password: password, @@ -398,6 +421,7 @@ func (c *ConfigYAML) ParseServersYAML() ([]Server, []ResourceErrors[Server]) { servers = append(servers, *hServer) } case "inventory": + // User provides command to evaluate to a list of hosts serverEnvs := append(defaultEnvs, envs...) hServer := &Server{ Name: c.Servers.Content[i].Value, @@ -411,9 +435,7 @@ func (c *ConfigYAML) ParseServersYAML() ([]Server, []ResourceErrors[Server]) { Shell: serverYAML.Shell, WorkDir: serverYAML.WorkDir, Envs: serverEnvs, - BastionHost: bastionHost, - BastionUser: bastionUser, - BastionPort: bastionPort, + Bastions: bastions, IdentityFile: identityFile, PubFile: pubKeyFile, Password: password, @@ -430,6 +452,25 @@ func (c *ConfigYAML) ParseServersYAML() ([]Server, []ResourceErrors[Server]) { return servers, serverErrors } +func getServerBastionDefinition(serverYAML *ServerYAML) (string, error) { + bastionDef := "" + numDefined := 0 + if serverYAML.Bastion != "" { + bastionDef = "bastion" + numDefined += 1 + } + if len(serverYAML.Bastions) > 0 { + numDefined += 1 + bastionDef = "bastions" + } + + if numDefined > 1 { + return "", &core.ServerBastionMultipleDef{Name: serverYAML.Name} + } + + return bastionDef, nil +} + func getServerHostDefinition(serverYAML *ServerYAML) (string, error) { hostDef := "" numDefined := 0 @@ -845,7 +886,7 @@ func (c *Config) GetServersByTags(tags []string) ([]Server, error) { var servers []Server for _, server := range c.Servers { // Variable use to check that all tags are matched - var numMatched int = 0 + numMatched := 0 for _, tag := range tags { for _, serverTag := range server.Tags { if serverTag == tag { @@ -957,9 +998,6 @@ func CreateInventoryServers(inputHost string, i int, server Server, userArgs []s Shell: server.Shell, WorkDir: server.WorkDir, Envs: serverEnvs, - BastionHost: server.BastionHost, - BastionUser: server.BastionUser, - BastionPort: server.BastionPort, IdentityFile: server.IdentityFile, PubFile: server.PubFile, Password: server.Password, @@ -987,7 +1025,25 @@ func SortServers(order string, servers *[]Server) { return (*servers)[i].Host > (*servers)[j].Host }) case "random": - rand.Seed(time.Now().UnixNano()) - rand.Shuffle(len((*servers)), func(i, j int) { (*servers)[i], (*servers)[j] = (*servers)[j], (*servers)[i] }) + r := rand.New(rand.NewSource(time.Now().UnixNano())) + r.Shuffle(len((*servers)), func(i, j int) { (*servers)[i], (*servers)[j] = (*servers)[j], (*servers)[i] }) } } + +func getBastionHosts(bastions []Bastion, splitOn string) string { + if len(bastions) == 1 { + return bastions[0].GetPrint() + } + output := "" + for i, bastion := range bastions { + b := bastion.GetPrint() + + if i < len(bastions)-1 { + output += fmt.Sprintf("%s%s", b, splitOn) + } else { + output += b + } + } + + return output +} diff --git a/core/errors.go b/core/errors.go index 16b235a..17e8393 100644 --- a/core/errors.go +++ b/core/errors.go @@ -178,6 +178,14 @@ func (c *ServerMultipleDef) Error() string { return fmt.Sprintf("can only define one of the following for server `%s`: host, hosts", c.Name) } +type ServerBastionMultipleDef struct { + Name string +} + +func (c *ServerBastionMultipleDef) Error() string { + return fmt.Sprintf("can only define one of the following for server `%s`: bastion, bastions", c.Name) +} + type TaskRefMultipleDef struct { Name string } diff --git a/core/print/print_block.go b/core/print/print_block.go index fd94a02..4ab255b 100644 --- a/core/print/print_block.go +++ b/core/print/print_block.go @@ -48,7 +48,10 @@ func PrintServerBlocks(servers []dao.Server) { output += printStringField("user", server.User, false) output += printStringField("host", server.Host, false) output += printNumberField("port", int(server.Port), false) - output += printStringField("bastion", server.BastionHost, false) + if len(server.Bastions) > 0 { + output += printBastion(server.Bastions) + } + output += printBoolField("local", server.Local, false) output += printStringField("shell", server.Shell, false) output += printStringField("work_dir", server.WorkDir, false) @@ -224,6 +227,18 @@ func printEnv(env []string) { } } +func printBastion(bastions []dao.Bastion) string { + if len(bastions) == 1 { + return fmt.Sprintf("bastion: %s\n", bastions[0].GetPrint()) + } + + output := "bastions: \n" + for _, bastion := range bastions { + output += fmt.Sprintf("%4s- %s\n", " ", bastion.GetPrint()) + } + return output +} + func printStringField(key string, value string, indent bool) string { if value != "" { if indent { diff --git a/core/run/exec.go b/core/run/exec.go index ebe7929..f7fe3d5 100644 --- a/core/run/exec.go +++ b/core/run/exec.go @@ -4,7 +4,6 @@ import ( "bufio" "errors" "fmt" - "golang.org/x/crypto/ssh" "math" "os" "os/signal" @@ -13,6 +12,8 @@ import ( "strings" "sync" + "golang.org/x/crypto/ssh" + "github.com/jedib0t/go-pretty/v6/text" "github.com/alajmo/sake/core" @@ -313,22 +314,37 @@ func (run *Run) SetClients( remote.Sessions = append(remote.Sessions, SSHSession{}) } - var bastion *SSHClient - if server.BastionHost != "" { - bastion = &SSHClient{ - Name: "Bastion", - Host: server.BastionHost, - User: server.BastionUser, - Port: server.BastionPort, - AuthMethod: authMethod, - } - // Connect to bastion - if err := bastion.Connect(ssh.Dial, run.Config.DisableVerifyHost, run.Config.KnownHostsFile, run.Config.DefaultTimeout, mu); err != nil { - errCh <- *err - return - } + if len(server.Bastions) > 0 { + var bastion *SSHClient + for _, bastionServer := range server.Bastions { + if bastion == nil { + bastion = &SSHClient{ + Name: "Bastion", + Host: bastionServer.Host, + User: bastionServer.User, + Port: bastionServer.Port, + AuthMethod: authMethod, + } + if err := bastion.Connect(ssh.Dial, run.Config.DisableVerifyHost, run.Config.KnownHostsFile, run.Config.DefaultTimeout, mu); err != nil { + errCh <- *err + return + } + } else { + bastt := &SSHClient{ + Name: "Bastion", + Host: bastionServer.Host, + User: bastionServer.User, + Port: bastionServer.Port, + AuthMethod: authMethod, + } - // Connect to server through bastion + if err := bastt.Connect(bastion.DialThrough, run.Config.DisableVerifyHost, run.Config.KnownHostsFile, run.Config.DefaultTimeout, mu); err != nil { + errCh <- *err + return + } + bastion = bastt + } + } if err := remote.Connect(bastion.DialThrough, run.Config.DisableVerifyHost, run.Config.KnownHostsFile, run.Config.DefaultTimeout, mu); err != nil { errCh <- *err return @@ -494,16 +510,19 @@ func ParseServers( var errConnects []ErrConnect for i := range *servers { serv := cfg[(*servers)[i].Host] - - // Bastion resolve: + // Bastion resolve, for instance, host: server-1 has an entry in ssh config + // that has ProxyJump, ProxyJump alias or if in sake it has a bastion: server-1 // 1. proxyjump alias // 2. proxyjump - // 3. bastion alias - if proxyJump := serv.ProxyJump; proxyJump != "" { + // 3. bastion alias, in this case we need to handle multiple bastions + // In-case sake has bastions defined, then skip resolving + // TODO: Refactor this part + if proxyJump := serv.ProxyJump; proxyJump != "" && len((*servers)[i].Bastions) == 0 { if hostName := cfg[proxyJump].HostName; hostName != "" { // 1. proxyjump alias - (*servers)[i].BastionHost = hostName - + bastionHost := hostName + bastionPort := (*servers)[i].Port + bastionUser := (*servers)[i].User port := cfg[proxyJump].Port if port != "" { p, err := strconv.ParseUint(port, 10, 16) @@ -518,64 +537,76 @@ func ParseServers( errConnects = append(errConnects, *errConnect) continue } - (*servers)[i].BastionPort = uint16(p) - } else { - (*servers)[i].BastionPort = (*servers)[i].Port + bastionPort = uint16(p) } user := cfg[proxyJump].User if user != "" { - (*servers)[i].BastionUser = user - } else { - (*servers)[i].BastionUser = (*servers)[i].User + bastionUser = user } + + bastion := dao.Bastion{ + Host: bastionHost, + User: bastionUser, + Port: bastionPort, + } + + (*servers)[i].Bastions = append((*servers)[i].Bastions, bastion) } else { // 2. proxyjump - user, host, port, err := core.ParseHostName(proxyJump, (*servers)[i].User, (*servers)[i].Port) - if err != nil { - errConnect := &ErrConnect{ - Name: (*servers)[i].Name, - User: (*servers)[i].User, - Host: (*servers)[i].Host, - Port: (*servers)[i].Port, - Reason: err.Error(), + + for _, proxy := range strings.Split(proxyJump, ",") { + user, host, port, err := core.ParseHostName(proxy, (*servers)[i].User, (*servers)[i].Port) + if err != nil { + errConnect := &ErrConnect{ + Name: (*servers)[i].Name, + User: (*servers)[i].User, + Host: (*servers)[i].Host, + Port: (*servers)[i].Port, + Reason: err.Error(), + } + errConnects = append(errConnects, *errConnect) + continue } - errConnects = append(errConnects, *errConnect) - continue - } - (*servers)[i].BastionHost = host - (*servers)[i].BastionPort = port - (*servers)[i].BastionUser = user - } - } else if bastionHost := cfg[(*servers)[i].BastionHost].HostName; bastionHost != "" { - // 3. bastion alias - (*servers)[i].BastionHost = bastionHost - - port := cfg[(*servers)[i].BastionHost].Port - if port != "" { - p, err := strconv.ParseUint(port, 10, 16) - if err != nil { - errConnect := &ErrConnect{ - Name: (*servers)[i].Name, - User: (*servers)[i].User, - Host: (*servers)[i].Host, - Port: (*servers)[i].Port, - Reason: err.Error(), + bastion := dao.Bastion{ + Host: host, + User: user, + Port: port, } - errConnects = append(errConnects, *errConnect) - continue + (*servers)[i].Bastions = append((*servers)[i].Bastions, bastion) } - (*servers)[i].BastionPort = uint16(p) - } else { - (*servers)[i].BastionPort = (*servers)[i].Port } + } else { + // 3. bastion alias + for j, bastion := range (*servers)[i].Bastions { + if bastionHost := cfg[bastion.Host].HostName; bastionHost != "" { + bastionPort := (*servers)[i].Port + bastionUser := (*servers)[i].User + if cfg[bastion.Host].Port != "" { + p, err := strconv.ParseUint(cfg[bastion.Host].Port, 10, 16) + if err != nil { + errConnect := &ErrConnect{ + Name: (*servers)[i].Name, + User: (*servers)[i].User, + Host: (*servers)[i].Host, + Port: (*servers)[i].Port, + Reason: err.Error(), + } + errConnects = append(errConnects, *errConnect) + continue + } + bastionPort = uint16(p) + } - user := cfg[(*servers)[i].BastionHost].User - if user != "" { - (*servers)[i].BastionUser = user - } else { - (*servers)[i].BastionUser = (*servers)[i].User + if cfg[bastion.Host].User != "" { + bastionUser = cfg[bastion.Host].User + } + + (*servers)[i].Bastions[j].Host = bastionHost + (*servers)[i].Bastions[j].User = bastionUser + (*servers)[i].Bastions[j].Port = bastionPort + } } } diff --git a/core/run/unix.go b/core/run/unix.go index 3122ad3..248ecc9 100644 --- a/core/run/unix.go +++ b/core/run/unix.go @@ -5,10 +5,12 @@ package run import ( "fmt" - "github.com/alajmo/sake/core/dao" - "golang.org/x/sys/unix" "os" "os/exec" + "strings" + + "github.com/alajmo/sake/core/dao" + "golang.org/x/sys/unix" ) // func SSHToServer(host string, user string, port uint16, bastion string, disableVerifyHost bool, knownHostFile string) error { @@ -28,9 +30,14 @@ func SSHToServer(server dao.Server, disableVerifyHost bool, knownHostFile string args = append(args, fmt.Sprintf("-o UserKnownHostsFile=%s", knownHostFile)) } - if server.BastionHost != "" { - jumphost := fmt.Sprintf("%s@%s:%d", server.BastionUser, server.BastionHost, server.BastionPort) - args = append(args, fmt.Sprintf("-J %s", jumphost)) + // TODO: + if len(server.Bastions) > 0 { + jumphosts := []string{} + for _, bastion := range server.Bastions { + jumphosts = append(jumphosts, fmt.Sprintf("%s@%s:%d", bastion.User, bastion.Host, bastion.Port)) + } + + args = append(args, fmt.Sprintf("-J %s", strings.Join(jumphosts, ","))) } err = unix.Exec(sshBin, args, os.Environ()) diff --git a/core/sake.1 b/core/sake.1 index 1e1f193..17830a1 100644 --- a/core/sake.1 +++ b/core/sake.1 @@ -1,4 +1,4 @@ -.TH "SAKE" "1" "2023-01-04T23:23:54CET" "v0.14.0" "Sake Manual" "sake" +.TH "SAKE" "1" "2023-06-10T21:39:07CEST" "v0.14.0" "Sake Manual" "sake" .SH NAME sake - sake is a task runner for local and remote hosts @@ -585,7 +585,10 @@ Below is a config file detailing all of the available options and their defaults # inventory: echo samir@192.168.0.1:22 samir@192.168.1.1:22 # Bastion [optional] - bastion: samir@192.168.1.1:2222 + bastion: samir@192.168.1.1:2222 + + # Bastions [optional] + # bastions: [samir@192.168.1.1:2222, samir@192.168.1.2:3333] # User to connect as. It defaults to the current user [optional] user: samir diff --git a/core/utils.go b/core/utils.go index efeb01f..85c781e 100644 --- a/core/utils.go +++ b/core/utils.go @@ -160,7 +160,7 @@ func DebugPrint(data any) { fmt.Println() } -// Parse host, for instance : user@hostname +// Parse host, for instance : user@hostname:22 func ParseHostName(hostname string, defaultUser string, defaultPort uint16) (string, string, uint16, error) { if strings.Contains(hostname, "/") { return "", "", 22, fmt.Errorf("unexpected slash in the host %s", hostname) diff --git a/docs/changelog.md b/docs/changelog.md index 550dbf9..6c8fda5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,19 @@ # Changelog +## 0.15.0 + +### Features + +- Add support for multiple bastions + +### Misc + +- Update to go 1.20 + +### Fixes + +- Fix issue where user/port was not set correctly when using shorthand format for host/user/port definition + ## 0.14.0 ### Features diff --git a/docs/config-reference.md b/docs/config-reference.md index db60437..8f2cb4a 100644 --- a/docs/config-reference.md +++ b/docs/config-reference.md @@ -61,6 +61,9 @@ servers: # Bastion [optional] bastion: samir@192.168.1.1:2222 + # Bastions [optional] + # bastions: [samir@192.168.1.1:2222, samir@192.168.1.2:3333] + # User to connect as. It defaults to the current user [optional] user: samir diff --git a/go.mod b/go.mod index f2adbdf..acf0704 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/alajmo/sake -go 1.19 +go 1.20 require ( github.com/gobwas/glob v0.2.3 diff --git a/test/Dockerfile b/test/Dockerfile index f44fc0d..329ec5c 100644 --- a/test/Dockerfile +++ b/test/Dockerfile @@ -1,6 +1,6 @@ FROM ubuntu:latest -RUN apt-get update && apt-get install openssh-server sudo -y +RUN apt-get update && apt-get install openssh-server vim sudo -y RUN useradd --user-group --system --create-home -s /bin/bash --no-log-init test @@ -22,4 +22,4 @@ RUN service ssh start WORKDIR /home/test -CMD [ "/usr/sbin/sshd", "-D" ] +ENTRYPOINT [ "/usr/sbin/sshd", "-D" ] diff --git a/test/docker-compose.yaml b/test/docker-compose.yaml index 16b8964..18993e6 100644 --- a/test/docker-compose.yaml +++ b/test/docker-compose.yaml @@ -1,30 +1,34 @@ --- -version: "3.9" +version: '3.9' -services: - bastion: - container_name: "bastion" - build: . - ports: - - "221:22" - networks: - sake: - ipv4_address: 172.24.2.99 +networks: + sake: + name: sake + enable_ipv6: true + ipam: + driver: default + config: + - subnet: 172.24.2.0/16 + gateway: 172.24.2.1 + + - subnet: 2001:3984:3989::/64 + gateway: 2001:3984:3989::1 +services: server-1: - container_name: "server-1" + container_name: 'server-1' build: . ports: - - "222:22" + - '222:22' networks: sake: ipv4_address: 172.24.2.2 server-2: - container_name: "server-2" + container_name: 'server-2' build: . ports: - - "223:33" + - '223:33' networks: sake: ipv4_address: 172.24.2.3 @@ -32,94 +36,134 @@ services: - /usr/sbin/sshd - -D - -p - - "33" + - '33' server-3: - container_name: "server-3" + container_name: 'server-3' build: . ports: - - "224:22" + - '224:22' networks: sake: ipv4_address: 172.24.2.4 server-4: - container_name: "server-4" + container_name: 'server-4' build: . ports: - - "225:22" + - '225:22' networks: sake: ipv4_address: 172.24.2.5 server-5: - container_name: "server-5" + container_name: 'server-5' build: . ports: - - "226:22" + - '226:22' networks: sake: ipv4_address: 172.24.2.6 server-6: - container_name: "server-6" + container_name: 'server-6' build: . ports: - - "227:22" + - '227:22' networks: sake: ipv4_address: 172.24.2.7 server-7: - container_name: "server-7" + container_name: 'server-7' build: . ports: - - "228:22" + - '228:22' networks: sake: ipv4_address: 172.24.2.8 server-8: - container_name: "server-8" + container_name: 'server-8' build: . ports: - - "229:22" + - '229:22' networks: sake: ipv4_address: 172.24.2.9 server-9: - container_name: "server-9" + container_name: 'server-9' build: . ports: - - "230:22" + - '230:22' networks: sake: ipv6_address: 2001:3984:3989::10 - server-10: - container_name: "server-10" + server-10: # Only accesible via bastion-1 + container_name: 'server-10' build: . + entrypoint: + - /bin/sh + - -c + - | + echo 'PasswordAuthentication no' >> /etc/ssh/sshd_config + echo 'PubkeyAuthentication no' >> /etc/ssh/sshd_config + echo 'Match Address 172.24.2.98' >> /etc/ssh/sshd_config + echo ' PasswordAuthentication yes' >> /etc/ssh/sshd_config + echo ' PubkeyAuthentication yes' >> /etc/ssh/sshd_config + /usr/sbin/sshd -D ports: - - "231:33" + - '231:22' networks: sake: - ipv6_address: 2001:3984:3989::11 + ipv4_address: 172.24.2.10 + + server-11: # Only accesible via bastion-2 + container_name: 'server-11' + build: . entrypoint: - - /usr/sbin/sshd - - -D - - -p - - "33" + - /bin/sh + - -c + - | + echo 'PasswordAuthentication no' >> /etc/ssh/sshd_config + echo 'PubkeyAuthentication no' >> /etc/ssh/sshd_config + echo 'Match Address 172.24.2.99' >> /etc/ssh/sshd_config + echo ' PasswordAuthentication yes' >> /etc/ssh/sshd_config + echo ' PubkeyAuthentication yes' >> /etc/ssh/sshd_config + /usr/sbin/sshd -D + ports: + - '232:22' + networks: + sake: + ipv4_address: 172.24.2.11 -networks: - sake: - name: sake - enable_ipv6: true - ipam: - driver: default - config: - - subnet: 172.24.2.0/16 - gateway: 172.24.2.1 + bastion-1: + container_name: 'bastion-1' + build: . + privileged: true + ports: + - '233:22' + networks: + sake: + ipv4_address: 172.24.2.98 - - subnet: 2001:3984:3989::/64 - gateway: 2001:3984:3989::1 + bastion-2: # Only accesible via bastion-1 + container_name: 'bastion-2' + build: . + entrypoint: + - /bin/sh + - -c + - | + echo 'PasswordAuthentication no' >> /etc/ssh/sshd_config + echo 'PubkeyAuthentication no' >> /etc/ssh/sshd_config + echo 'Match Address 172.24.2.98' >> /etc/ssh/sshd_config + echo ' PasswordAuthentication yes' >> /etc/ssh/sshd_config + echo ' PubkeyAuthentication yes' >> /etc/ssh/sshd_config + /usr/sbin/sshd -D + ports: + - '234:22' + networks: + sake: + ipv4_address: 172.24.2.99 diff --git a/test/integration/golden/golden-0.stdout b/test/integration/golden/golden-0.stdout index c1b3174..bd6da96 100755 --- a/test/integration/golden/golden-0.stdout +++ b/test/integration/golden/golden-0.stdout @@ -38,6 +38,8 @@ WantErr: false | server-7 | server-8 | server-9 + | server-10 + | server-11 prod | list-0 | list-1 | range-0 @@ -57,4 +59,6 @@ WantErr: false | server-9 sandbox | server-5 | server-6 + bastion | server-10 + | server-11 diff --git a/test/integration/golden/golden-10.stdout b/test/integration/golden/golden-10.stdout index df43561..b308363 100755 --- a/test/integration/golden/golden-10.stdout +++ b/test/integration/golden/golden-10.stdout @@ -18,7 +18,7 @@ tags: local, reachable name: unreachable user: test -host: unreachable.lan +host: 172.24.2.50 port: 22 tags: unreachable @@ -103,7 +103,6 @@ desc: server-1 user: test host: 172.24.2.2 port: 22 -bastion: 172.24.2.99 work_dir: /home/test tags: remote, prod, reachable env: @@ -181,3 +180,27 @@ host: 2001:3984:3989::10 port: 22 tags: remote, demo, reachable +-- + +name: server-10 +desc: server-10 desc +user: test +host: 172.24.2.10 +port: 22 +bastion: test@172.24.2.98:22 +work_dir: /tmp +tags: remote, bastion + +-- + +name: server-11 +desc: server-11 desc +user: test +host: 172.24.2.11 +port: 22 +bastions: + - test@172.24.2.98:22 + - test@172.24.2.99:22 +work_dir: /tmp +tags: remote, bastion + diff --git a/test/integration/golden/golden-23.stdout b/test/integration/golden/golden-23.stdout index defa7ff..56d7e43 100755 --- a/test/integration/golden/golden-23.stdout +++ b/test/integration/golden/golden-23.stdout @@ -25,6 +25,8 @@ TASKS 172.24.2.8 | pong 172.24.2.9 | pong 2001:3984:3989::10 | pong + 172.24.2.10 | pong + 172.24.2.11 | pong localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 @@ -42,6 +44,8 @@ TASKS 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.11 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ - Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 + Total ok=18 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-37.stdout b/test/integration/golden/golden-37.stdout index fa90b42..7d36d1e 100755 --- a/test/integration/golden/golden-37.stdout +++ b/test/integration/golden/golden-37.stdout @@ -8,8 +8,8 @@ WantErr: true Unreachable Hosts - server | host | user | port | error --------------+-----------------+------+------+------------------------------------------------ - unreachable | unreachable.lan | test | 22 | dial tcp: lookup unreachable.lan: no such host + server | host | user | port | error +-------------+-------------+------+------+---------------------------------------------------- + unreachable | 172.24.2.50 | test | 22 | dial tcp 172.24.2.50:22: connect: no route to host exit status 4 diff --git a/test/integration/golden/golden-38.stdout b/test/integration/golden/golden-38.stdout index 67e165d..547a5f5 100755 --- a/test/integration/golden/golden-38.stdout +++ b/test/integration/golden/golden-38.stdout @@ -8,9 +8,9 @@ WantErr: false Unreachable Hosts - server | host | user | port | error --------------+-----------------+------+------+------------------------------------------------ - unreachable | unreachable.lan | test | 22 | dial tcp: lookup unreachable.lan: no such host + server | host | user | port | error +-------------+-------------+------+------+---------------------------------------------------- + unreachable | 172.24.2.50 | test | 22 | dial tcp 172.24.2.50:22: connect: no route to host TASKS @@ -33,6 +33,8 @@ TASKS 172.24.2.8 | 123 172.24.2.9 | 123 2001:3984:3989::10 | 123 + 172.24.2.10 | 123 + 172.24.2.11 | 123 localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 @@ -50,7 +52,9 @@ TASKS 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - unreachable.lan ok=0 unreachable=1 ignored=0 failed=0 skipped=0 + 172.24.2.10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.11 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + 172.24.2.50 ok=0 unreachable=1 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ - Total ok=16 unreachable=1 ignored=0 failed=0 skipped=0 + Total ok=18 unreachable=1 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-4.stdout b/test/integration/golden/golden-4.stdout index 8e59c2d..1bc3ef5 100755 --- a/test/integration/golden/golden-4.stdout +++ b/test/integration/golden/golden-4.stdout @@ -8,7 +8,7 @@ WantErr: false server | host | tags | desc -------------+--------------------+-----------------------------+---------------------------- localhost | localhost | local,reachable | localhost - unreachable | unreachable.lan | unreachable | + unreachable | 172.24.2.50 | unreachable | list-0 | 172.24.2.2 | remote,prod,list,reachable | many hosts using list list-1 | 172.24.2.4 | remote,prod,list,reachable | many hosts using list range-0 | 172.24.2.2 | remote,prod,range,reachable | many hosts using range @@ -24,4 +24,6 @@ WantErr: false server-7 | 172.24.2.8 | remote,demo,reachable | server-7 server-8 | 172.24.2.9 | remote,demo,reachable | server-8 server-9 | 2001:3984:3989::10 | remote,demo,reachable | server-9 + server-10 | 172.24.2.10 | remote,bastion | server-10 desc + server-11 | 172.24.2.11 | remote,bastion | server-11 desc diff --git a/test/playground/sake.yaml b/test/playground/sake.yaml index 8c99673..58b0545 100644 --- a/test/playground/sake.yaml +++ b/test/playground/sake.yaml @@ -31,7 +31,7 @@ servers: user: test identity_file: ../keys/id_ed25519_pem password: testing - tags: [remote, pi, many, list, "hej san"] + tags: [remote, pi, many, list, 'hej san'] env: hello: world host: 172.24.2.4 @@ -84,6 +84,63 @@ servers: password: testing tags: [remote, pi, pihole] + server-5: + desc: server-5 desc + user: test + host: test@172.24.2.10:22 + bastion: 172.24.2.98:22 + identity_file: ../keys/id_ed25519_pem + password: testing + tags: [remote, bastion] + work_dir: /tmp + env: + hello: world + + server-6: + desc: server-6 desc + user: test + host: 172.24.2.11 + bastions: ['172.24.2.98', '172.24.2.99'] + identity_file: ../keys/id_ed25519_pem + password: testing + tags: [remote, bastion] + work_dir: /tmp + env: + hello: world + + server-9: + desc: server-9 desc + user: test + host: 172.24.2.10 + bastions: [sake-bastion] + identity_file: ../keys/id_ed25519_pem + password: testing + tags: [remote, bastion] + work_dir: /tmp + env: + hello: world + + server-10: + desc: server-10 desc + user: test + host: sake-r + # host: test@172.24.2.10:22 + identity_file: ../keys/id_ed25519_pem + password: testing + tags: [remote, bastion] + work_dir: /tmp + env: + hello: world + + server-11: + desc: server-11 desc + host: sake-rr + user: test + identity_file: ../keys/id_ed25519_pem + password: testing + tags: [remote, bastion] + work_dir: /tmp + ip6-1: desc: ip6-1 desc host: test@[2001:3984:3989::10]:22 @@ -213,12 +270,12 @@ tasks: ignore_errors: true # strategy: free # desc: ping server - # cmd: echo pong - tty: true - tasks: - - cmd: | - ps -p $$ - - cmd: pwd + cmd: echo $S_BASTION + # tty: true + # tasks: + # - cmd: | + # ps -p $$ + # - cmd: pwd exit: # name: Exit diff --git a/test/servers.yaml b/test/servers.yaml index d95d4d7..edc993a 100644 --- a/test/servers.yaml +++ b/test/servers.yaml @@ -8,10 +8,10 @@ servers: user: test local: true work_dir: /tmp - tags: [local,reachable] + tags: [local, reachable] unreachable: - host: unreachable.lan + host: 172.24.2.50 user: test tags: [unreachable] @@ -49,12 +49,11 @@ servers: server-1: desc: server-1 host: 172.24.2.2 - bastion: test@172.24.2.99:22 user: test work_dir: /home/test identity_file: keys/id_ed25519_pem password: testing - tags: [remote,prod,reachable] + tags: [remote, prod, reachable] env: host: 172.24.2.2 @@ -64,7 +63,7 @@ servers: user: test port: 33 identity_file: keys/id_ed25519_pem_no - tags: [remote,prod,reachable] + tags: [remote, prod, reachable] server-3: desc: server-3 @@ -72,14 +71,14 @@ servers: user: test identity_file: keys/id_ed25519_rfc password: testing - tags: [remote,demo,reachable] + tags: [remote, demo, reachable] server-4: desc: server-4 host: 172.24.2.5 user: test identity_file: keys/id_ed25519_rfc_no - tags: [remote,demo,reachable] + tags: [remote, demo, reachable] server-5: desc: server-5 @@ -87,32 +86,50 @@ servers: user: test identity_file: keys/id_rsa_pem password: testing - tags: [remote,sandbox,reachable] + tags: [remote, sandbox, reachable] server-6: desc: server-6 host: 172.24.2.7 user: test identity_file: keys/id_rsa_pem_no - tags: [remote,sandbox,reachable] + tags: [remote, sandbox, reachable] server-7: desc: server-7 host: test@172.24.2.8:22 identity_file: keys/id_rsa_rfc password: testing - tags: [remote,demo,reachable] + tags: [remote, demo, reachable] server-8: desc: server-8 host: 172.24.2.9 user: test identity_file: keys/id_rsa_rfc_no - tags: [remote,demo,reachable] + tags: [remote, demo, reachable] server-9: desc: server-9 host: 2001:3984:3989::10 user: test password: test - tags: [remote,demo,reachable] + tags: [remote, demo, reachable] + + server-10: + desc: server-10 desc + host: test@172.24.2.10:22 + bastion: test@172.24.2.98:22 + identity_file: keys/id_rsa_rfc + password: testing + tags: [remote, bastion] + work_dir: /tmp + + server-11: + desc: server-11 desc + host: test@172.24.2.11:22 + bastions: ['test@172.24.2.98:22', 'test@172.24.2.99:22'] + identity_file: keys/id_rsa_rfc + password: testing + tags: [remote, bastion] + work_dir: /tmp From 4096e51dec86445c58428bfb6a8616e65d594dc1 Mon Sep 17 00:00:00 2001 From: Henrique Taunay Date: Mon, 25 Sep 2023 10:08:43 +0200 Subject: [PATCH 12/18] Add support for Mac ARM architecture on install script (#57) --- install.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/install.sh b/install.sh index dd32c9a..ba2a36a 100755 --- a/install.sh +++ b/install.sh @@ -5,6 +5,8 @@ set -e if [ "$(uname -s)" = "Darwin" ] && [ "$(uname -m)" = "x86_64" ]; then target="darwin_amd64" +elif [ "$(uname -s)" = "Darmin" ] && [ "$(uname -m)" = "arm64" ]; then + target="darmin_arm64" elif [ "$(uname -s)" = "Linux" ] && [ "$(uname -m)" = "x86_64" ]; then target="linux_amd64" elif [ "$(uname -s)" = "Linux" ] && ( uname -m | grep -q -e '^arm' -e '^aarch' ); then From 72850d13cf296bf4bdcdd3e827b23115946cf5e0 Mon Sep 17 00:00:00 2001 From: Samir Alajmovic <5246600+alajmo@users.noreply.github.com> Date: Mon, 25 Sep 2023 11:54:39 +0200 Subject: [PATCH 13/18] resolve tilde path correctly for identityfile and fix pubfile (#58) --- Makefile | 2 +- cmd/ssh.go | 2 +- core/run/client.go | 2 +- core/run/exec.go | 43 ++++++++++++++++++++++++++++++++++++++++++- core/sake.1 | 2 +- core/ssh_config.go | 6 +++--- docs/changelog.md | 7 +++++++ 7 files changed, 56 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 8e12f4b..ddeff85 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ NAME := sake PACKAGE := github.com/alajmo/$(NAME) DATE := $(shell date +%FT%T%Z) GIT := $(shell [ -d .git ] && git rev-parse --short HEAD) -VERSION := v0.15.0 +VERSION := v0.15.1 default: build diff --git a/cmd/ssh.go b/cmd/ssh.go index 1c569b6..eec127e 100644 --- a/cmd/ssh.go +++ b/cmd/ssh.go @@ -52,6 +52,6 @@ func ssh(args []string, config *dao.Config, runFlags *core.RunFlags) { } core.CheckIfError(err) - err = run.SSHToServer(*server, config.DisableVerifyHost, config.KnownHostsFile) + err = run.SSHToServer(servers[0], config.DisableVerifyHost, config.KnownHostsFile) core.CheckIfError(err) } diff --git a/core/run/client.go b/core/run/client.go index 5c0601b..d0b20f3 100644 --- a/core/run/client.go +++ b/core/run/client.go @@ -31,5 +31,5 @@ type ErrConnect struct { } func (e *ErrConnect) Error() string { - return "" + return e.Reason } diff --git a/core/run/exec.go b/core/run/exec.go index f7fe3d5..d721721 100644 --- a/core/run/exec.go +++ b/core/run/exec.go @@ -612,7 +612,48 @@ func ParseServers( // IdentityFile if len(serv.IdentityFiles) > 0 { - (*servers)[i].IdentityFile = &serv.IdentityFiles[0] + iFile, err := core.ExpandPath(serv.IdentityFiles[0]) + if err != nil { + errConnect := &ErrConnect{ + Name: (*servers)[i].Name, + User: (*servers)[i].User, + Host: (*servers)[i].Host, + Port: (*servers)[i].Port, + Reason: err.Error(), + } + errConnects = append(errConnects, *errConnect) + continue + } + + (*servers)[i].IdentityFile = &iFile + + // TODO: Update PubFile as well + if _, err := os.Stat(*(*servers)[i].IdentityFile); errors.Is(err, os.ErrNotExist) { + errConnect := &ErrConnect{ + Name: (*servers)[i].Name, + User: (*servers)[i].User, + Host: (*servers)[i].Host, + Port: (*servers)[i].Port, + Reason: err.Error(), + } + errConnects = append(errConnects, *errConnect) + continue + } + + pubFile := *(*servers)[i].IdentityFile + ".pub" + if _, err := os.Stat(pubFile); errors.Is(err, os.ErrNotExist) { + errConnect := &ErrConnect{ + Name: (*servers)[i].Name, + User: (*servers)[i].User, + Host: (*servers)[i].Host, + Port: (*servers)[i].Port, + Reason: err.Error(), + } + errConnects = append(errConnects, *errConnect) + continue + } else { + *(*servers)[i].PubFile = pubFile + } } // HostName diff --git a/core/sake.1 b/core/sake.1 index 17830a1..e271e92 100644 --- a/core/sake.1 +++ b/core/sake.1 @@ -1,4 +1,4 @@ -.TH "SAKE" "1" "2023-06-10T21:39:07CEST" "v0.14.0" "Sake Manual" "sake" +.TH "SAKE" "1" "2023-09-25T11:47:19CEST" "v0.15.1" "Sake Manual" "sake" .SH NAME sake - sake is a task runner for local and remote hosts diff --git a/core/ssh_config.go b/core/ssh_config.go index cc50636..9a493ce 100644 --- a/core/ssh_config.go +++ b/core/ssh_config.go @@ -14,7 +14,7 @@ import ( "github.com/kevinburke/ssh_config" ) -// PraseFile reads and parses the file in the given path. +// Parse reads and parses the file in the given path. func ParseSSHConfig(path string) (map[string](Endpoint), error) { f, err := os.Open(path) if err != nil { @@ -243,7 +243,7 @@ func parseInternal(r io.Reader, cfg string) (*hostinfoMap, error) { continue } - path, err := expandPath(value) + path, err := ExpandPath(value) if err != nil { return nil, err } @@ -340,7 +340,7 @@ func parseFileInternal(path string) (*hostinfoMap, error) { return parseInternal(f, path) } -func expandPath(p string) (string, error) { +func ExpandPath(p string) (string, error) { if !strings.HasPrefix(p, "~/") { return p, nil } diff --git a/docs/changelog.md b/docs/changelog.md index 6c8fda5..f62ad36 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 0.15.1 + +### Fixes + +- Fix resolving identity file in ssh config correctly when ~ is used. +- Fix public file not found when using ssh config + ## 0.15.0 ### Features From 5b7c4b8f6f4ba4f53166891835d07267f101f2a6 Mon Sep 17 00:00:00 2001 From: samiralajmovic Date: Sat, 14 Dec 2024 07:26:38 +0100 Subject: [PATCH 14/18] Added support for nvim to edit sub-command --- Makefile | 1 + core/dao/config.go | 2 ++ core/dao/server.go | 21 ------------------- core/dao/server_test.go | 1 - core/dao/task.go | 9 -------- core/errors.go | 8 ------- core/hostname-gen.go | 13 ------------ core/man_gen.go | 12 +++++------ core/print/print_table.go | 17 +++++++-------- core/print/report.go | 18 +++++++--------- core/run/exec.go | 12 ----------- core/run/ssh.go | 15 -------------- core/run/table.go | 20 ++++++------------ core/run/text.go | 39 ++++++++++++++--------------------- core/spinner.go | 12 ----------- test/integration/main_test.go | 2 +- 16 files changed, 46 insertions(+), 156 deletions(-) diff --git a/Makefile b/Makefile index ddeff85..185b2c4 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,7 @@ gofmt: lint: golangci-lint run ./cmd/... ./core/... ./test/... + deadcode . benchmark: cd test && ./benchmark.sh diff --git a/core/dao/config.go b/core/dao/config.go index 988617d..42428fe 100644 --- a/core/dao/config.go +++ b/core/dao/config.go @@ -271,6 +271,8 @@ func openEditor(path string, lineNr int) error { if lineNr > 0 { switch editor { + case "nvim": + args = []string{fmt.Sprintf("+%v", lineNr), path} case "vim": args = []string{fmt.Sprintf("+%v", lineNr), path} case "vi": diff --git a/core/dao/server.go b/core/dao/server.go index 82ef2ca..a8e1aa6 100644 --- a/core/dao/server.go +++ b/core/dao/server.go @@ -155,9 +155,6 @@ func (c *ConfigYAML) ParseServersYAML() ([]Server, []ResourceErrors[Server]) { if serverYAML.User == "" { user, err := user.Current() - if err != nil { - panic(err) - } if err != nil { serverErrors[j].Errors = append(serverErrors[j].Errors, err) @@ -500,15 +497,6 @@ func getServerHostDefinition(serverYAML *ServerYAML) (string, error) { return hostDef, nil } -func ServerInSlice(name string, list []Server) bool { - for _, s := range list { - if s.Name == name { - return true - } - } - return false -} - // FilterServers returns servers matching filters, it does a union select. func (c *Config) FilterServers( allServersFlag bool, @@ -915,15 +903,6 @@ func (c *Config) GetServersByTags(tags []string) ([]Server, error) { return servers, nil } -func (c *Config) GetServerNames() []string { - names := []string{} - for _, server := range c.Servers { - names = append(names, server.Name) - } - - return names -} - func GetIntersectionServers(s ...[]Server) []Server { var count int for _, part := range s { diff --git a/core/dao/server_test.go b/core/dao/server_test.go index 5cafb10..09224f2 100644 --- a/core/dao/server_test.go +++ b/core/dao/server_test.go @@ -116,7 +116,6 @@ func TestGetTagAssocations(t *testing.T) { test.CheckErr(t, err) wanted := Tag{ - Name: "t1", Servers: []string{"s1", "s5"}, } test.CheckEqualStringArr(t, ss[0].Servers, wanted.Servers) diff --git a/core/dao/task.go b/core/dao/task.go index f99960d..36be344 100644 --- a/core/dao/task.go +++ b/core/dao/task.go @@ -462,15 +462,6 @@ func (c *Config) GetTasksByIDs(ids []string) ([]Task, error) { return filteredTasks, nil } -func (c *Config) GetTaskNames() []string { - taskNames := []string{} - for _, task := range c.Tasks { - taskNames = append(taskNames, task.Name) - } - - return taskNames -} - func (c *Config) GetTaskIDAndDesc() []string { taskNames := []string{} for _, task := range c.Tasks { diff --git a/core/errors.go b/core/errors.go index 17e8393..e8ce12d 100644 --- a/core/errors.go +++ b/core/errors.go @@ -86,14 +86,6 @@ func (c *TaskNotFound) Error() string { return fmt.Sprintf("cannot find tasks %s", tasks) } -type OutputFormatNotFound struct { - Name string -} - -func (c *OutputFormatNotFound) Error() string { - return fmt.Sprintf("output option `%s` not found", c.Name) -} - type TaskMultipleDef struct { Name string } diff --git a/core/hostname-gen.go b/core/hostname-gen.go index 295fc6b..d3a6a10 100644 --- a/core/hostname-gen.go +++ b/core/hostname-gen.go @@ -147,19 +147,6 @@ const ( Step = 2 ) -func (s RangeState) String() string { - switch s { - case Start: - return "Start" - case End: - return "End" - case Step: - return "Step" - } - - return "unknown" -} - func readRange(input string, i int) (HostRange, int, error) { r := HostRange{ Start: "", diff --git a/core/man_gen.go b/core/man_gen.go index 878a3c3..258adbb 100644 --- a/core/man_gen.go +++ b/core/man_gen.go @@ -69,7 +69,7 @@ func CreateManPage(desc string, version string, date string, rootCmd *cobra.Comm return nil } -func manPreamble(buf io.StringWriter, header *genManHeaders, cmd *cobra.Command, dashedName string) { +func manPreamble(buf io.StringWriter, header *genManHeaders, cmd *cobra.Command) { preamble := `.TH "%s" "%s" "%s" "%s" "%s" "%s"` cobra.WriteStringAndCheck(buf, fmt.Sprintf(preamble, header.Title, header.Section, header.Date, header.Version, header.Source, header.Manual)) @@ -90,7 +90,7 @@ func manPreamble(buf io.StringWriter, header *genManHeaders, cmd *cobra.Command, cobra.WriteStringAndCheck(buf, header.Desc+"\n\n") } -func manCommand(buf io.StringWriter, cmd *cobra.Command, dashedName string) { +func manCommand(buf io.StringWriter, cmd *cobra.Command) { cobra.WriteStringAndCheck(buf, ".TP\n") cobra.WriteStringAndCheck(buf, fmt.Sprintf(`.B %s`, cmd.UseLine())) cobra.WriteStringAndCheck(buf, "\n") @@ -157,7 +157,7 @@ func genMan(header *genManHeaders, cmd *cobra.Command, cmds ...*cobra.Command) [ buf := new(bytes.Buffer) // PREAMBLE - manPreamble(buf, header, cmd, cmd.CommandPath()) + manPreamble(buf, header, cmd) flags := cmd.NonInheritedFlags() // OPTIONS @@ -170,19 +170,17 @@ func genMan(header *genManHeaders, cmd *cobra.Command, cmds ...*cobra.Command) [ // COMMANDS for _, c := range cmds { - dashCommandName := c.CommandPath() - cbuf := new(bytes.Buffer) if !StringInSlice(c.Name(), []string{"list", "describe"}) { - manCommand(cbuf, c, dashCommandName) + manCommand(cbuf, c) } if len(c.Commands()) > 0 { for _, cc := range c.Commands() { // Don't include help command if cc.Name() != "help" { - manCommand(cbuf, cc, dashCommandName) + manCommand(cbuf, cc) } } } diff --git a/core/print/print_table.go b/core/print/print_table.go index fcd311a..afd1053 100644 --- a/core/print/print_table.go +++ b/core/print/print_table.go @@ -33,15 +33,15 @@ func PrintTable[T dao.Items]( case "table", "table-1": return table1(data, options, headers, footers, padTop, padBottom) case "table-2": - return table2(data, options, headers, footers, padTop, padBottom) + return table2(data, options, headers, padTop, padBottom) case "table-3": - return table3(data, options, headers, footers, padTop, padBottom) + return table3(data, options, headers, padTop, padBottom) case "table-4": - return table4(data, options, headers, footers, padTop, padBottom) + return table4(data, options, headers, padTop, padBottom) case "csv": - return printCSV(data, options, headers, footers) + return printCSV(data, options, headers) case "json": - return printJSON(data, options, headers) + return printJSON(data, headers) default: return table1(data, options, headers, footers, padTop, padBottom) } @@ -141,7 +141,6 @@ func table2[T dao.Items]( data []T, options PrintTableOptions, headers []string, - footers []string, padTop bool, padBottom bool, ) error { @@ -204,7 +203,6 @@ func table3[T dao.Items]( data []T, options PrintTableOptions, headers []string, - footers []string, padTop bool, padBottom bool, ) error { @@ -289,7 +287,6 @@ func table4[T dao.Items]( data []T, options PrintTableOptions, headers []string, - footers []string, padTop bool, padBottom bool, ) error { @@ -332,7 +329,7 @@ server,host ip6-1,2001:3984:3989::10 ip6-2,2001:3984:3989::11 */ -func printCSV[T dao.Items](data []T, options PrintTableOptions, headers []string, footers []string) error { +func printCSV[T dao.Items](data []T, options PrintTableOptions, headers []string) error { t := CreateTable(options, headers) // Headers @@ -396,7 +393,7 @@ func printCSV[T dao.Items](data []T, options PrintTableOptions, headers []string ] */ -func printJSON[T dao.Items](data []T, options PrintTableOptions, headers []string) error { +func printJSON[T dao.Items](data []T, headers []string) error { var out []map[string]any for _, v := range data { m := make(map[string]any) diff --git a/core/print/report.go b/core/print/report.go index bffec5e..0864398 100644 --- a/core/print/report.go +++ b/core/print/report.go @@ -122,7 +122,6 @@ func PrintExitReport( ) error { theme.Table.Options.SeparateFooter = core.Ptr(false) var data dao.TableOutput - data.Headers = reportData.Headers for i := range reportData.Tasks { name := getStatusName(reportData.Tasks[i].Name, reportData.Tasks[i].Status) data.Rows = append(data.Rows, dao.Row{Columns: []string{name}}) @@ -133,9 +132,9 @@ func PrintExitReport( } else { v := strconv.Itoa(t.ReturnCode) if t.ReturnCode > 0 { - v = FailedPrint.Sprintf(v) + v = FailedPrint.Sprint(v) } else { - v = OkPrint.Sprintf(v) + v = OkPrint.Sprint(v) } data.Rows[i].Columns = append(data.Rows[i].Columns, v) } @@ -234,7 +233,6 @@ func PrintTaskReport( ) error { theme.Table.Options.SeparateFooter = core.Ptr(false) var data dao.TableOutput - data.Headers = reportData.Headers for i := range reportData.Tasks { name := getStatusName(reportData.Tasks[i].Name, reportData.Tasks[i].Status) data.Rows = append(data.Rows, dao.Row{Columns: []string{name}}) @@ -243,15 +241,15 @@ func PrintTaskReport( var v string switch t.Status { case dao.Ok: - v = OkPrint.Sprintf(t.Status.String()) + v = OkPrint.Sprint(t.Status.String()) case dao.Skipped: - v = SkippedPrint.Sprintf(t.Status.String()) + v = SkippedPrint.Sprint(t.Status.String()) case dao.Ignored: - v = IgnoredPrint.Sprintf(t.Status.String()) + v = IgnoredPrint.Sprint(t.Status.String()) case dao.Failed: - v = FailedPrint.Sprintf(t.Status.String()) + v = FailedPrint.Sprint(t.Status.String()) case dao.Unreachable: - v = UnreachablePrint.Sprintf(t.Status.String()) + v = UnreachablePrint.Sprint(t.Status.String()) } data.Rows[i].Columns = append(data.Rows[i].Columns, v) @@ -333,7 +331,7 @@ func PrintSummaryReport( } func printRecapHeader(h string, filler string) { - hh := text.Bold.Sprintf(h) + hh := text.Bold.Sprint(h) width, _, _ := term.GetSize(0) headerLength := len(core.Strip(hh)) if width > 0 { diff --git a/core/run/exec.go b/core/run/exec.go index d721721..45ef159 100644 --- a/core/run/exec.go +++ b/core/run/exec.go @@ -231,10 +231,6 @@ func (run *Run) RunTask( return err } - if err != nil { - return err - } - if derr != nil { return derr } @@ -1099,14 +1095,6 @@ func getAuthMethod(server dao.Server, signers *Signers) []ssh.AuthMethod { return authMethods } -func CalcFreeForks(batch int, tasks int, forks uint32) int { - tot := batch * tasks - if tot < int(forks) { - return tot - } - return int(forks) -} - func CalcForks(batch int, forks uint32) int { if batch < int(forks) { return batch diff --git a/core/run/ssh.go b/core/run/ssh.go index 2cfab2b..403e08f 100644 --- a/core/run/ssh.go +++ b/core/run/ssh.go @@ -13,7 +13,6 @@ import ( "syscall" "time" - "crypto/sha256" "github.com/kevinburke/ssh_config" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" @@ -312,11 +311,6 @@ func CheckKnownHost(host string, remote net.Addr, key ssh.PublicKey, knownFile s return true, keyErr } - // Some other error occurred and safest way to handle is to pass it back to user - if err != nil { - return false, err - } - // Key not found in file and is therefor not trusted return false, nil } @@ -406,12 +400,6 @@ func AsExport(env []string) string { return exports } -func FingerprintSHA256(b []byte) string { - sha256sum := sha256.Sum256(b) - hash := base64.RawStdEncoding.EncodeToString(sha256sum[:]) - return "SHA256:" + hash -} - func GetSSHAgentSigners() ([]ssh.Signer, error) { // Load keys from SSH Agent if it's running sockPath, found := os.LookupEnv("SSH_AUTH_SOCK") @@ -482,9 +470,6 @@ func GetSigner(identityFile string) (ssh.Signer, error) { if err != nil { return nil, err } - if err != nil { - return nil, err - } signer, err = ssh.ParsePrivateKey(data) if err != nil { diff --git a/core/run/table.go b/core/run/table.go index 879c2f7..b7d3b73 100644 --- a/core/run/table.go +++ b/core/run/table.go @@ -33,7 +33,6 @@ func (run *Run) Table(dryRun bool) (dao.TableOutput, dao.ReportData, error) { // TODO: data, reportData should be pointer? var data dao.TableOutput var reportData dao.ReportData - var dataMutex = sync.RWMutex{} data.Headers = append(reportData.Headers, "host") reportData.Headers = append(reportData.Headers, "host") // Append Command names if set @@ -76,11 +75,11 @@ func (run *Run) Table(dryRun bool) (dao.TableOutput, dao.ReportData, error) { var err error switch task.Spec.Strategy { case "free": - err = run.free(&run.Config, data, reportData, &dataMutex, dryRun) + err = run.free(data, reportData, dryRun) case "host_pinned": - err = run.hostPinned(&run.Config, data, reportData, &dataMutex, dryRun) + err = run.hostPinned(data, reportData, dryRun) default: - err = run.linear(&run.Config, data, reportData, &dataMutex, dryRun) + err = run.linear(data, reportData, dryRun) } reportData.Status = make(map[dao.TaskStatus]int, 5) @@ -115,10 +114,8 @@ func (run *Run) Table(dryRun bool) (dao.TableOutput, dao.ReportData, error) { } func (run *Run) free( - config *dao.Config, data dao.TableOutput, reportData dao.ReportData, - dataMutex *sync.RWMutex, dryRun bool, ) error { serverLen := len(run.Servers) @@ -197,7 +194,7 @@ func (run *Run) free( } } - err := run.tableWork(r, r.j, register, data, reportData, dataMutex, dryRun) + err := run.tableWork(r, r.j, register, data, reportData, dryRun) <-waitChan if err != nil { errCh <- err @@ -221,10 +218,8 @@ func (run *Run) free( } func (run *Run) linear( - config *dao.Config, data dao.TableOutput, reportData dao.ReportData, - dataMutex *sync.RWMutex, dryRun bool, ) error { serverLen := len(run.Servers) @@ -317,7 +312,7 @@ func (run *Run) linear( ) { defer wg.Done() - err := run.tableWork(r, 0, register, data, reportData, dataMutex, dryRun) + err := run.tableWork(r, 0, register, data, reportData, dryRun) <-waitChan if err != nil { errCh <- err @@ -358,10 +353,8 @@ func (run *Run) linear( } func (run *Run) hostPinned( - config *dao.Config, data dao.TableOutput, reportData dao.ReportData, - dataMutex *sync.RWMutex, dryRun bool, ) error { serverLen := len(run.Servers) @@ -442,7 +435,7 @@ func (run *Run) hostPinned( } } - err := run.tableWork(j, 0, register[j.Server.Name], data, reportData, dataMutex, dryRun) + err := run.tableWork(j, 0, register[j.Server.Name], data, reportData, dryRun) <-waitChan if err != nil { errCh <- err @@ -473,7 +466,6 @@ func (run *Run) tableWork( register map[string]string, data dao.TableOutput, reportData dao.ReportData, - dataMutex *sync.RWMutex, dryRun bool, ) error { var wg sync.WaitGroup diff --git a/core/run/text.go b/core/run/text.go index 9953554..4a6fbcb 100644 --- a/core/run/text.go +++ b/core/run/text.go @@ -4,10 +4,6 @@ import ( "bufio" "bytes" "fmt" - "github.com/jedib0t/go-pretty/v6/text" - "golang.org/x/crypto/ssh" - "golang.org/x/exp/slices" - "golang.org/x/term" "io" "math" "os" @@ -17,6 +13,11 @@ import ( "text/template" "time" + "github.com/jedib0t/go-pretty/v6/text" + "golang.org/x/crypto/ssh" + "golang.org/x/exp/slices" + "golang.org/x/term" + "github.com/alajmo/sake/core" "github.com/alajmo/sake/core/dao" "github.com/alajmo/sake/core/print" @@ -34,7 +35,6 @@ func (run *Run) Text(dryRun bool) (dao.ReportData, error) { // TODO: reportData should be pointer? var reportData dao.ReportData - var dataMutex = sync.RWMutex{} reportData.Headers = append(reportData.Headers, "server") // Append Command names if set for _, subTask := range task.Tasks { @@ -59,11 +59,11 @@ func (run *Run) Text(dryRun bool) (dao.ReportData, error) { var err error switch task.Spec.Strategy { case "free": - err = run.freeText(&run.Config, prefixMaxLen, reportData, &dataMutex, dryRun) + err = run.freeText(prefixMaxLen, reportData, dryRun) case "host_pinned": - err = run.hostPinnedText(&run.Config, prefixMaxLen, reportData, &dataMutex, dryRun) + err = run.hostPinnedText(prefixMaxLen, reportData, dryRun) default: // linear - err = run.linearText(&run.Config, prefixMaxLen, reportData, &dataMutex, dryRun) + err = run.linearText(prefixMaxLen, reportData, dryRun) } reportData.Status = make(map[dao.TaskStatus]int, 5) @@ -98,10 +98,8 @@ func (run *Run) Text(dryRun bool) (dao.ReportData, error) { } func (run *Run) freeText( - config *dao.Config, prefixMaxLen int, reportData dao.ReportData, - dataMutex *sync.RWMutex, dryRun bool, ) error { serverLen := len(run.Servers) @@ -182,7 +180,7 @@ func (run *Run) freeText( } } - err := run.textWork(r, r.j, register, prefixMaxLen, reportData, dataMutex, dryRun, batch) + err := run.textWork(r, r.j, register, prefixMaxLen, reportData, dryRun, batch) <-waitChan if err != nil { errCh <- err @@ -206,10 +204,8 @@ func (run *Run) freeText( } func (run *Run) linearText( - config *dao.Config, prefixMaxLen int, reportData dao.ReportData, - dataMutex *sync.RWMutex, dryRun bool, ) error { serverLen := len(run.Servers) @@ -315,7 +311,7 @@ func (run *Run) linearText( ) { defer wg.Done() - err := run.textWork(r, 0, register, prefixMaxLen, reportData, dataMutex, dryRun, batch) + err := run.textWork(r, 0, register, prefixMaxLen, reportData, dryRun, batch) <-waitChan if err != nil { errCh <- err @@ -356,10 +352,8 @@ func (run *Run) linearText( } func (run *Run) hostPinnedText( - config *dao.Config, prefixMaxLen int, reportData dao.ReportData, - dataMutex *sync.RWMutex, dryRun bool, ) error { serverLen := len(run.Servers) @@ -455,7 +449,7 @@ func (run *Run) hostPinnedText( } } - err := run.textWork(j, 0, register[j.Server.Name], prefixMaxLen, reportData, dataMutex, dryRun, batch) + err := run.textWork(j, 0, register[j.Server.Name], prefixMaxLen, reportData, dryRun, batch) <-waitChan if err != nil { errCh <- err @@ -486,7 +480,6 @@ func (run *Run) textWork( register map[string]string, prefixMaxLen int, reportData dao.ReportData, - dataMutex *sync.RWMutex, dryRun bool, batch int, ) error { @@ -741,7 +734,7 @@ func (h HeaderData) Style(s any, args ...string) string { } } - return colors.Sprintf(v) + return colors.Sprint(v) } func PrintHeader(value string, ts dao.Text, padding bool) { @@ -750,9 +743,9 @@ func PrintHeader(value string, ts dao.Text, padding bool) { headerName := text.Colors{text.Reset, text.Bold} var header string if ts.HeaderFiller != "" { - header = fmt.Sprintf("\n%s%s\n", headerName.Sprintf(value), strings.Repeat(ts.HeaderFiller, width-headerLength-1)) + header = fmt.Sprintf("\n%s%s\n", headerName.Sprint(value), strings.Repeat(ts.HeaderFiller, width-headerLength-1)) } else { - header = fmt.Sprintf("\n%s\n", headerName.Sprintf(value)) + header = fmt.Sprintf("\n%s\n", headerName.Sprint(value)) } if padding { @@ -828,7 +821,7 @@ func (h PrefixData) Style(s any, args ...string) string { } } - return colors.Sprintf(v) + return colors.Sprint(v) } func getPrefixer(client Client, i int, prefixMaxLen int, ts dao.Text, batch int) (string, error) { @@ -868,7 +861,7 @@ func getPrefixer(client Client, i int, prefixMaxLen int, ts dao.Text, batch int) } if prefixColor != nil { - prefix = prefixColor.Sprintf(prefixString) + prefix = prefixColor.Sprint(prefixString) } else { prefix = prefixString } diff --git a/core/spinner.go b/core/spinner.go index 3f957da..7c18a73 100644 --- a/core/spinner.go +++ b/core/spinner.go @@ -100,18 +100,6 @@ func (s *Loader) Stop() { } } -func (s *Loader) Disable() { - s.disabled = true -} - func (s *Loader) Enable() { s.disabled = false } - -func (s *Loader) Message(msg string) { - s.spinner.Message(msg) -} - -func (s *Loader) Status() yacspin.SpinnerStatus { - return s.spinner.Status() -} diff --git a/test/integration/main_test.go b/test/integration/main_test.go index d2e4bc1..9696904 100644 --- a/test/integration/main_test.go +++ b/test/integration/main_test.go @@ -147,7 +147,7 @@ func Run(t *testing.T, tt TemplateTest) { fmt.Println(string(actual)) fmt.Println("--------------------->") - t.Fatalf("\nfile: %v\ndiff: %v", text.FgBlue.Sprintf(goldenFilePath), diff(expected, actual)) + t.Fatalf("\nfile: %v\ndiff: %v", text.FgBlue.Sprint(goldenFilePath), diff(expected, actual)) } if err != nil { From 6b2e1ccdaad0fdc715940ffcc5b231b974bac631 Mon Sep 17 00:00:00 2001 From: samiralajmovic Date: Fri, 18 Apr 2025 23:19:47 +0200 Subject: [PATCH 15/18] Update README --- README.md | 10 ++-- docs/{command-reference.md => command.md} | 0 docs/{config-reference.md => config.md} | 2 +- docs/development.md | 2 +- docs/error-handling.md | 2 +- docs/{output.md => output-format.md} | 0 go.mod | 25 ++++----- go.sum | 62 +++++++++++------------ 8 files changed, 49 insertions(+), 54 deletions(-) rename docs/{command-reference.md => command.md} (100%) rename docs/{config-reference.md => config.md} (99%) rename docs/{output.md => output-format.md} (100%) diff --git a/README.md b/README.md index 620b8e7..50a5fde 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,11 @@ `sake` is a command runner for local and remote hosts. You define servers and tasks in `sake.yaml` file and then run the tasks on the servers. -This readme is also accessible on [sakecli.com](https://sakecli.com/). +Interested in managing your git repositories in a similar way? Check out [mani](https://github.com/alajmo/mani)! + +![demo](res/output.gif) -`sake` has tons of features: +## Features - auto-completion of tasks, servers and tags - SSH into servers or docker containers `sake ssh ` @@ -38,10 +40,6 @@ This readme is also accessible on [sakecli.com](https://sakecli.com/). - import other `sake.yaml` configs - and [many more!](docs/recipes.md) -![demo](res/output.gif) - -Interested in managing your git repositories in a similar way? Check out [mani](https://github.com/alajmo/mani)! - ## Table of Contents - [Installation](#installation) diff --git a/docs/command-reference.md b/docs/command.md similarity index 100% rename from docs/command-reference.md rename to docs/command.md diff --git a/docs/config-reference.md b/docs/config.md similarity index 99% rename from docs/config-reference.md rename to docs/config.md index 8f2cb4a..dbfce64 100644 --- a/docs/config-reference.md +++ b/docs/config.md @@ -1,4 +1,4 @@ -# Config Reference +# Config The sake.yaml config is based on the following concepts: diff --git a/docs/development.md b/docs/development.md index 57840d4..dae9047 100644 --- a/docs/development.md +++ b/docs/development.md @@ -50,7 +50,7 @@ The following workflow is used for releasing a new `sake` version: - `make unit-test` 4. Run benchmarks and profiler to check performance - `make benchmark` -5. Update `config-reference.md` and `config.man` if any config changes and generate manpage +5. Update `config.md` and `config.man` if any config changes and generate manpage - `make gen-man` 6. Update `Makefile` and `CHANGELOG.md` with correct version, and add all changes to `CHANGELOG.md` 7. Squash-merge to main with `Release vx.y.z` and description of changes diff --git a/docs/error-handling.md b/docs/error-handling.md index e7244bc..775818e 100644 --- a/docs/error-handling.md +++ b/docs/error-handling.md @@ -119,7 +119,7 @@ Sometimes you want to ignore remote hosts which are unreachable, for instance if list-1 | 172.24.2.222 | test | 22 | dial tcp 172.24.2.222:22: connect: no route to host ``` -- `ignore-unreachable` set to false +- `ignore-unreachable` set to true ```bash $ sake run unreachable --ignore-unreachable=true diff --git a/docs/output.md b/docs/output-format.md similarity index 100% rename from docs/output.md rename to docs/output-format.md diff --git a/go.mod b/go.mod index acf0704..aa5439d 100644 --- a/go.mod +++ b/go.mod @@ -1,31 +1,32 @@ module github.com/alajmo/sake -go 1.20 +go 1.23 require ( github.com/gobwas/glob v0.2.3 - github.com/jedib0t/go-pretty/v6 v6.4.6 + github.com/jedib0t/go-pretty/v6 v6.6.5 github.com/kevinburke/ssh_config v1.2.0 github.com/kr/pretty v0.2.1 - github.com/spf13/cobra v1.7.0 + github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/theckman/yacspin v0.13.12 - golang.org/x/crypto v0.9.0 - golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 - golang.org/x/sys v0.8.0 - golang.org/x/term v0.8.0 + golang.org/x/crypto v0.31.0 + golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 + golang.org/x/sys v0.28.0 + golang.org/x/term v0.27.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect - github.com/fatih/color v1.15.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/text v0.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect - github.com/rivo/uniseg v0.4.4 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + golang.org/x/text v0.21.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/go.sum b/go.sum index fa898f7..d90a60a 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,18 @@ -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= -github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jedib0t/go-pretty/v6 v6.4.6 h1:v6aG9h6Uby3IusSSEjHaZNXpHFhzqMmjXcPq1Rjl9Jw= -github.com/jedib0t/go-pretty/v6 v6.4.6/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs= +github.com/jedib0t/go-pretty/v6 v6.6.5 h1:9PgMJOVBedpgYLI56jQRJYqngxYAAzfEUua+3NgSqAo= +github.com/jedib0t/go-pretty/v6 v6.6.5/go.mod h1:Uq/HrbhuFty5WSVNfjpQQe47x16RwVGXIveNGEyGtHs= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= @@ -22,44 +23,39 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM= -github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4= github.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= -golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= +golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 5fe6564b08b97564c4f0603a4ebd6405218daab7 Mon Sep 17 00:00:00 2001 From: samiralajmovic Date: Fri, 18 Apr 2025 23:20:09 +0200 Subject: [PATCH 16/18] Update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 50a5fde..abbd75a 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,10 @@ `sake` is a command runner for local and remote hosts. You define servers and tasks in `sake.yaml` file and then run the tasks on the servers. -Interested in managing your git repositories in a similar way? Check out [mani](https://github.com/alajmo/mani)! - ![demo](res/output.gif) +Interested in managing your git repositories in a similar way? Check out [mani](https://github.com/alajmo/mani)! + ## Features - auto-completion of tasks, servers and tags From 14232a2d622faf9f0d27be2fbb3e2341c813285f Mon Sep 17 00:00:00 2001 From: samiralajmovic Date: Fri, 18 Apr 2025 23:33:40 +0200 Subject: [PATCH 17/18] Update README --- README.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index abbd75a..ce0ed52 100644 --- a/README.md +++ b/README.md @@ -32,13 +32,17 @@ Interested in managing your git repositories in a similar way? Check out [mani]( ## Features -- auto-completion of tasks, servers and tags +- Auto-completion of tasks, servers and tags - SSH into servers or docker containers `sake ssh ` -- list servers/tasks via `sake list servers|tasks` -- present task output in a compact table format `sake run --output table` -- open task/server in your preferred editor `sake edit task ` -- import other `sake.yaml` configs -- and [many more!](docs/recipes.md) +- List servers/tasks via `sake list servers|tasks` +- Present task output in a compact table format `sake run --output table` +- Open task/server in your preferred editor `sake edit task ` +- Import other `sake.yaml` configs +- [Many more!](docs/recipes.md) + +## Sponsors + +Mani is an MIT-licensed open source project with ongoing development. If you'd like to support their efforts, check out [Tabify](https://chromewebstore.google.com/detail/tabify/bokfkclamoepkmhjncgkdldmhfpgfdmo) - a Chrome extension that enhances your browsing experience with powerful window and tab management, focus-improving site blocking, and numerous features to optimize your browser workflow. ## Table of Contents From 9547a4fff6ab53e2b3abec4fe8a2da70629b199f Mon Sep 17 00:00:00 2001 From: samiralajmovic Date: Fri, 18 Apr 2025 23:34:23 +0200 Subject: [PATCH 18/18] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ce0ed52..5dc0f84 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Interested in managing your git repositories in a similar way? Check out [mani]( ## Sponsors -Mani is an MIT-licensed open source project with ongoing development. If you'd like to support their efforts, check out [Tabify](https://chromewebstore.google.com/detail/tabify/bokfkclamoepkmhjncgkdldmhfpgfdmo) - a Chrome extension that enhances your browsing experience with powerful window and tab management, focus-improving site blocking, and numerous features to optimize your browser workflow. +Sake is an MIT-licensed open source project with ongoing development. If you'd like to support their efforts, check out [Tabify](https://chromewebstore.google.com/detail/tabify/bokfkclamoepkmhjncgkdldmhfpgfdmo) - a Chrome extension that enhances your browsing experience with powerful window and tab management, focus-improving site blocking, and numerous features to optimize your browser workflow. ## Table of Contents