├── .github └── workflows │ ├── goreleaser.yml │ └── main.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── Makefile ├── OWNERS ├── README.md ├── VERSION ├── cmd └── client │ └── main.go ├── go.mod ├── go.sum ├── pkg └── db │ ├── inventory.go │ ├── inventory_test.go │ ├── util.go │ ├── vault.go │ └── vault_test.go └── testdata └── inventory ├── hosts ├── hosts2 ├── hosts3 ├── hosts4 ├── hosts5 ├── hosts6 ├── vault.key └── vault.yml /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: {} 5 | push: 6 | tags: 7 | - 'v*' 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | goreleaser: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - run: git fetch --force --tags 20 | - uses: actions/setup-go@v3 21 | with: 22 | go-version: '>=1.20.7' 23 | cache: true 24 | - uses: goreleaser/goreleaser-action@v4 25 | with: 26 | distribution: goreleaser 27 | version: latest 28 | args: release --rm-dist 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | GOPATH: /home/runner/go 32 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: build 3 | 4 | on: 5 | workflow_dispatch: {} 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | core: 15 | strategy: 16 | matrix: 17 | go-version: [1.20.x] 18 | platform: [ubuntu-latest] 19 | name: Build 20 | runs-on: ${{ matrix.platform }} 21 | env: 22 | GOBIN: /home/runner/.local/bin 23 | steps: 24 | - name: Install Go 25 | uses: actions/setup-go@v3 26 | with: 27 | go-version: ${{ matrix.go-version }} 28 | id: go 29 | - name: Check out code into the Go module directory 30 | uses: actions/checkout@v3 31 | - name: Amend Environment Path 32 | run: | 33 | mkdir -p /home/runner/.local/bin 34 | echo "/home/runner/.local/bin" >> $GITHUB_PATH 35 | - name: Setup Environment 36 | run: | 37 | mkdir -p .coverage 38 | echo "*** Current Directory ***" 39 | pwd 40 | echo "*** Environment Variables ***" 41 | env | sort 42 | echo "*** Executable Path ***" 43 | echo "$PATH" | tr ':' '\n' 44 | echo "*** Workspace Files ***" 45 | find . 46 | which make 47 | - name: Install prerequisites 48 | run: | 49 | sudo apt-get --assume-yes install make 50 | sudo apt-get --assume-yes install libnss3-tools 51 | pip3 install --upgrade pip setuptools wheel 52 | sudo apt-get update 53 | - name: Install Go modules 54 | run: | 55 | make dep 56 | - name: Validate prerequisites 57 | run: | 58 | echo "*** Local binaries ***" 59 | find /home/runner/.local/bin 60 | - name: Run tests 61 | run: | 62 | make 63 | make test 64 | - name: Generate coverage report 65 | run: make coverage 66 | - name: Upload coverage report 67 | uses: actions/upload-artifact@v3 68 | with: 69 | name: Test Coverage Report 70 | path: .coverage/coverage.html 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/** 2 | bin/** 3 | hidden* 4 | .doc/** 5 | 6 | # Binaries for programs and plugins 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | 13 | # Test binary, build with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: go-ansible-db-client 2 | 3 | release: 4 | github: 5 | owner: greenpau 6 | name: go-ansible-db 7 | draft: false 8 | prerelease: auto 9 | 10 | checksum: 11 | name_template: "{{.ProjectName}}_{{.Version}}_SHA256SUMS" 12 | 13 | builds: 14 | - env: 15 | - CGO_ENABLED=0 16 | - GO111MODULE=on 17 | goos: 18 | - linux 19 | - windows 20 | - darwin 21 | goarch: 22 | - amd64 23 | - arm64 24 | main: ./cmd/client 25 | binary: ./bin/go-ansible-db-client 26 | flags: 27 | - -trimpath 28 | - -mod=readonly 29 | asmflags: 30 | - all=-trimpath={{.Env.GOPATH}} 31 | gcflags: 32 | - all=-trimpath={{.Env.GOPATH}} 33 | ldflags: -s -w 34 | 35 | 36 | nfpms: 37 | - id: go-ansible-db-client 38 | maintainer: "Paul Greenberg " 39 | file_name_template: >- 40 | {{ .ProjectName }}_ 41 | {{- title .Os }}_ 42 | {{- if eq .Arch "amd64" }}x86_64 43 | {{- else if eq .Arch "386" }}i386 44 | {{- else }}{{ .Arch }}{{ end }} 45 | 46 | changelog: 47 | sort: asc 48 | filters: 49 | exclude: 50 | - '^docs?:' 51 | - '^readme:' 52 | - '^tests?:' 53 | # - '^\w+\s+' # a hack to remove commit messages without colons thus don't correspond to a package 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Paul Greenberg (greenpau@outlook.com) 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test ctest covdir coverage docs linter qtest clean dep 2 | APP_VERSION:=$(shell cat VERSION | head -1) 3 | GIT_COMMIT:=$(shell git describe --dirty --always) 4 | GIT_BRANCH:=$(shell git rev-parse --abbrev-ref HEAD -- | head -1) 5 | BUILD_USER:=$(shell whoami) 6 | BUILD_DATE:=$(shell date +"%Y-%m-%d") 7 | PROJECT:="github.com/greenpau/go-ansible-db" 8 | BINARY:="go-ansible-db-client" 9 | VERBOSE:=-v 10 | ifdef TEST 11 | TEST:="-run ${TEST}" 12 | endif 13 | 14 | all: 15 | @echo "Version: $(APP_VERSION), Branch: $(GIT_BRANCH), Revision: $(GIT_COMMIT)" 16 | @echo "Build on $(BUILD_DATE) by $(BUILD_USER)" 17 | @mkdir -p bin/ 18 | @CGO_ENABLED=0 go build -o bin/$(BINARY) $(VERBOSE) \ 19 | -ldflags="-w -s \ 20 | -X main.appName=$(BINARY) \ 21 | -X main.appVersion=$(APP_VERSION) \ 22 | -X main.gitBranch=$(GIT_BRANCH) \ 23 | -X main.gitCommit=$(GIT_COMMIT) \ 24 | -X main.buildUser=$(BUILD_USER) \ 25 | -X main.buildDate=$(BUILD_DATE)" \ 26 | -gcflags="all=-trimpath=$(GOPATH)/src" \ 27 | -asmflags="all=-trimpath $(GOPATH)/src" cmd/client/* 28 | @echo "Done!" 29 | 30 | linter: 31 | @golint pkg/db/*.go 32 | @golint cmd/client/*.go 33 | @echo "PASS: golint" 34 | 35 | test: covdir linter 36 | @go test $(VERBOSE) -coverprofile=.coverage/coverage.out ./pkg/db/*.go 37 | @bin/$(BINARY) -log.level debug -inventory ./testdata/inventory/hosts \ 38 | -vault ./testdata/inventory/vault.yml -vault.key.file ./testdata/inventory/vault.key 39 | 40 | ctest: covdir linter 41 | @#richgo version || go get -u github.com/kyoh86/richgo 42 | @time richgo test $(VERBOSE) -coverprofile=.coverage/coverage.out ./pkg/db/*.go 43 | 44 | covdir: 45 | @mkdir -p .coverage 46 | 47 | coverage: 48 | @go tool cover -html=.coverage/coverage.out -o .coverage/coverage.html 49 | 50 | docs: 51 | @rm -rf .doc/ 52 | @mkdir -p .doc/ 53 | @godoc -html $(PROJECT)/pkg/db > .doc/index.html 54 | @echo "Run to serve docs:" 55 | @echo " godoc -goroot .doc/ -html -http \":8080\"" 56 | 57 | clean: 58 | @rm -rf .doc 59 | @rm -rf .coverage 60 | @rm -rf bin/ 61 | 62 | qtest: 63 | @#go test -v -run TestNewInventory ./pkg/db/*.go 64 | @#go test -v -run TestNewVault ./pkg/db/*.go 65 | @#go test -v -run TestGetHost ./pkg/db/*.go 66 | @richgo test -v -run GetHostsWithFilter ./pkg/... 67 | 68 | dep: 69 | @echo "Making dependencies check ..." 70 | @go install golang.org/x/lint/golint@latest 71 | @go install github.com/kyoh86/richgo@latest 72 | @go install github.com/greenpau/versioned/cmd/versioned@latest 73 | @go install github.com/greenpau/gorpm/cmd/gorpm@latest 74 | @pip3 install yamllint --user 75 | @pip3 install yq --user 76 | 77 | release: 78 | @echo "Making release" 79 | @go mod tidy 80 | @go mod verify 81 | @if [ $(GIT_BRANCH) != "main" ]; then echo "cannot release to non-main branch $(GIT_BRANCH)" && false; fi 82 | @git diff-index --quiet HEAD -- || ( echo "git directory is dirty, commit changes first" && git status && false ) 83 | @versioned -patch 84 | @echo "Patched version" 85 | @git add VERSION 86 | @git commit -m "released v`cat VERSION | head -1`" 87 | @git tag -a v`cat VERSION | head -1` -m "v`cat VERSION | head -1`" 88 | @git push 89 | @git push --tags 90 | @@echo "If necessary, run the following commands:" 91 | @echo " git push --delete origin v$(APP_VERSION)" 92 | @echo " git tag --delete v$(APP_VERSION)" 93 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | reviewers: 2 | - greenpau 3 | 4 | approvers: 5 | - greenpau 6 | 7 | features: 8 | - comments 9 | - reviewers 10 | - aliases 11 | - branches 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-ansible-db 2 | 3 | 4 | 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/greenpau/go-ansible-db)](https://goreportcard.com/report/github.com/greenpau/go-ansible-db) 6 | 7 | Ansible Inventory and Vault management client library written in Go. 8 | 9 | 10 | ## Table of Contents 11 | 12 | * [Overview](#overview) 13 | * [Getting Started](#getting-started) 14 | * [Inventory Search](#inventory-search) 15 | 16 | 17 | 18 | ## Overview 19 | 20 | Ansible inventory and secrets management is being handled well by native 21 | Ansible tools. The inventory format is well defined and the vault usage 22 | is well understood. Ansible is written in Python and therefore integrates 23 | nicely with Python code. 24 | 25 | What happens when a user wants to read inventory and secrets for use in 26 | Go applications? 27 | 28 | This library allows: 29 | * Reading Ansible ini-style inventory files 30 | * Reading Ansible vault files 31 | * Getting Ansible variables for a host or a group of hosts 32 | * Getting Ansible secrets (credentials) for a host or a group of hosts 33 | 34 | ## Getting Started 35 | 36 | To demonstrate the use of the library, please consider the following files: 37 | 38 | * `assets/inventory/hosts`: Ansible inventory file 39 | * `assets/inventory/vault.yml`: Ansible vault file 40 | * `assets/inventory/vault.key`: The file with the password for the vault 41 | 42 | The following code snippet would load the inventory and vault content. 43 | 44 | ```golang 45 | invFile := "../../assets/inventory/hosts" 46 | vltFile := "../../assets/inventory/vault.yml" 47 | vltKeyFile := "../../assets/inventory/vault.key" 48 | 49 | // Create a new inventory file. 50 | inv := NewInventory() 51 | // Load the contents of the inventory from an input file. 52 | if err := inv.LoadFromFile(invFile); err != nil { 53 | t.Fatalf("error reading inventory: %s", err) 54 | } 55 | 56 | // Create a new vault file. 57 | vlt := NewVault() 58 | // Read the password for the vault file from an input file. 59 | if err := vlt.LoadPasswordFromFile(vltKeyFile); err != nil { 60 | t.Fatalf("error reading vault key file: %s", err) 61 | } 62 | // Load the contents of the vault from an input file. 63 | if err := vlt.LoadFromFile(vltFile); err != nil { 64 | t.Fatalf("error reading vault: %s", err) 65 | } 66 | ``` 67 | 68 | ## Inventory Search 69 | 70 | After that, the code retrieves the inventory record for `ny-sw01` and makes 71 | a subsequent call to retrieve the credentials for accessing `ny-sw01`. 72 | 73 | ```golang 74 | h := "ny-sw01" 75 | host, err := inv.GetHost(h) 76 | if err != nil { 77 | t.Fatalf("error getting host %s from inventory: %s", h, err) 78 | } 79 | creds, err := vlt.GetCredentials(host.Name) 80 | if err != nil { 81 | t.Fatalf("error getting credentials for host %s: %s", host.Name, err) 82 | } 83 | ``` 84 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.9 -------------------------------------------------------------------------------- /cmd/client/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Paul Greenberg (greenpau@outlook.com) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "flag" 19 | "fmt" 20 | "github.com/greenpau/go-ansible-db/pkg/db" 21 | log "github.com/sirupsen/logrus" 22 | "os" 23 | ) 24 | 25 | var ( 26 | appName = "go-ansible-db-client" 27 | appVersion = "[untracked]" 28 | appDocs = "https://github.com/greenpau/go-ansible-db/" 29 | appDescription = "Ansible DB (Inventory and Vault) client" 30 | gitBranch string 31 | gitCommit string 32 | buildUser string // whoami 33 | buildDate string // date -u 34 | ) 35 | 36 | func main() { 37 | var logLevel string 38 | var isShowVersion bool 39 | 40 | var inputInventoryFile string 41 | var inputVaultFile string 42 | var inputVaultPassword string 43 | var inputVaultPasswordFile string 44 | 45 | flag.StringVar(&inputInventoryFile, "inventory", "hosts", "ansible inventory file") 46 | flag.StringVar(&inputVaultFile, "vault", "", "ansible vault file") 47 | flag.StringVar(&inputVaultPassword, "vault.key", "", "ansible vault password") 48 | flag.StringVar(&inputVaultPasswordFile, "vault.key.file", "", "ansible vault password file") 49 | flag.StringVar(&logLevel, "log.level", "info", "logging severity level") 50 | flag.BoolVar(&isShowVersion, "version", false, "version information") 51 | flag.Usage = func() { 52 | fmt.Fprintf(os.Stderr, "\n%s - %s\n\n", appName, appDescription) 53 | fmt.Fprintf(os.Stderr, "Usage: %s [arguments]\n\n", appName) 54 | flag.PrintDefaults() 55 | fmt.Fprintf(os.Stderr, "\nDocumentation: %s\n\n", appDocs) 56 | } 57 | flag.Parse() 58 | if isShowVersion { 59 | fmt.Fprintf(os.Stdout, "%s %s", appName, appVersion) 60 | if gitBranch != "" { 61 | fmt.Fprintf(os.Stdout, ", branch: %s", gitBranch) 62 | } 63 | if gitCommit != "" { 64 | fmt.Fprintf(os.Stdout, ", commit: %s", gitCommit) 65 | } 66 | if buildUser != "" && buildDate != "" { 67 | fmt.Fprintf(os.Stdout, ", build on %s by %s", buildDate, buildUser) 68 | } 69 | fmt.Fprint(os.Stdout, "\n") 70 | os.Exit(0) 71 | } 72 | if level, err := log.ParseLevel(logLevel); err == nil { 73 | log.SetLevel(level) 74 | } else { 75 | log.Errorf(err.Error()) 76 | os.Exit(1) 77 | } 78 | 79 | inv := db.NewInventory() 80 | if err := inv.LoadFromFile(inputInventoryFile); err != nil { 81 | log.Fatalf("argument '-inventory %s': %s", inputInventoryFile, err) 82 | } 83 | log.Debugf("inventory file: %s", inputInventoryFile) 84 | hosts, err := inv.GetHosts() 85 | if err != nil { 86 | log.Fatalf("GetHosts() failed: %s", err) 87 | } 88 | for _, h := range hosts { 89 | fmt.Fprintf(os.Stdout, "%s", h.Name) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/greenpau/go-ansible-db 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/sirupsen/logrus v1.9.3 7 | golang.org/x/crypto v0.13.0 8 | gopkg.in/yaml.v2 v2.4.0 9 | ) 10 | 11 | require golang.org/x/sys v0.12.0 // indirect 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 7 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 10 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 11 | golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= 12 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 13 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 14 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 15 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 18 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 19 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 20 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 21 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 22 | -------------------------------------------------------------------------------- /pkg/db/inventory.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Paul Greenberg (greenpau@outlook.com) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package db 16 | 17 | import ( 18 | "fmt" 19 | //"github.com/davecgh/go-spew/spew" 20 | "io/ioutil" 21 | "regexp" 22 | "strings" 23 | "sync/atomic" 24 | ) 25 | 26 | // Inventory is the contents of Ansible inventory file. 27 | type Inventory struct { 28 | Raw []byte 29 | HostsRef map[string]string `json:"host_refs,omitempty" yaml:"host_refs,omitempty"` 30 | GroupsRef map[string]bool `json:"group_refs,omitempty" yaml:"group_refs,omitempty"` 31 | Hosts []*InventoryHost `json:"hosts,omitempty" yaml:"hosts,omitempty"` 32 | Groups []*InventoryGroup `json:"groups,omitempty" yaml:"groups,omitempty"` 33 | } 34 | 35 | // InventoryHost is a host in Ansible inventory 36 | type InventoryHost struct { 37 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 38 | Parent string `json:"parent_group,omitempty" yaml:"parent_group,omitempty"` 39 | Variables map[string]string `json:"variables,omitempty" yaml:"variables,omitempty"` 40 | Groups []string `json:"groups,omitempty" yaml:"groups,omitempty"` 41 | GroupChains []string `json:"group_chains,omitempty" yaml:"group_chains,omitempty"` 42 | } 43 | 44 | // InventoryGroup is an group of InventoryHost instances. 45 | type InventoryGroup struct { 46 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 47 | Ancestors []string `json:"parent_groups,omitempty" yaml:"parent_groups,omitempty"` 48 | Variables map[string]string `json:"variables,omitempty" yaml:"variables,omitempty"` 49 | Counters InventoryGroupCounters `json:"counters,omitempty" yaml:"counters,omitempty"` 50 | } 51 | 52 | // InventoryGroupCounters are counters associated with InventoryGroup 53 | type InventoryGroupCounters struct { 54 | Hosts uint64 `json:"hosts,omitempty" yaml:"hosts,omitempty"` 55 | Groups uint64 `json:"groups,omitempty" yaml:"groups,omitempty"` 56 | } 57 | 58 | // NewInventory returns a pointer to Inventory. 59 | func NewInventory() *Inventory { 60 | g := &InventoryGroup{ 61 | Name: "all", 62 | Variables: make(map[string]string), 63 | Ancestors: []string{}, 64 | } 65 | inv := &Inventory{ 66 | HostsRef: make(map[string]string), 67 | GroupsRef: make(map[string]bool), 68 | } 69 | inv.GroupsRef["all"] = true 70 | inv.Groups = append(inv.Groups, g) 71 | return inv 72 | } 73 | 74 | // Size returns the number of hosts in the Inventory. 75 | func (inv *Inventory) Size() uint64 { 76 | return uint64(len(inv.Hosts)) 77 | } 78 | 79 | func (inv *Inventory) parseString(s string) error { 80 | // Sections are default (0), group (1), children (2), and variables (3) 81 | var sectionType int 82 | groupName := "all" 83 | lines := strings.Split(s, "\n") 84 | for lc, line := range lines { 85 | orig := line 86 | line = strings.TrimSpace(line) 87 | if line == "" { 88 | continue 89 | } 90 | if strings.HasPrefix(line, "#") { 91 | continue 92 | } 93 | if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { 94 | line = strings.TrimRight(line, "]") 95 | line = strings.TrimLeft(line, "[") 96 | kv := strings.Split(line, ":") 97 | if len(kv) > 2 { 98 | return fmt.Errorf("invalid section: %s", orig) 99 | } 100 | groupName = kv[0] 101 | if err := inv.AddGroup(groupName, "all"); err != nil { 102 | return fmt.Errorf("AddGroup() failed: %s, line: %d", err, lc) 103 | } 104 | if len(kv) == 1 { 105 | sectionType = 1 106 | continue 107 | } 108 | switch kv[1] { 109 | case "children": 110 | sectionType = 2 111 | case "vars": 112 | sectionType = 3 113 | default: 114 | return fmt.Errorf("invalid section: %s", orig) 115 | } 116 | continue 117 | } 118 | 119 | switch sectionType { 120 | case 0: 121 | // default, group all 122 | if err := inv.AddHost(line, "all"); err != nil { 123 | return fmt.Errorf("AddHost() failed: %s, line: %d", err, lc) 124 | } 125 | case 1: 126 | // group section, contains individual hosts 127 | if err := inv.AddHost(line, groupName); err != nil { 128 | return fmt.Errorf("AddHost() failed: %s, line: %d", err, lc) 129 | } 130 | case 2: 131 | // children section 132 | if err := inv.AddGroup(line, groupName); err != nil { 133 | return fmt.Errorf("AddGroup() failed: %s, line: %d", err, lc) 134 | } 135 | case 3: 136 | // group variables 137 | if err := inv.AddVariable(line, groupName); err != nil { 138 | return fmt.Errorf("AddVariable() failed: %s, line: %d", err, lc) 139 | } 140 | default: 141 | return fmt.Errorf("invalid section type: %d", sectionType) 142 | } 143 | } 144 | 145 | for _, h := range inv.Hosts { 146 | groupChains, groups, err := inv.GetParentGroupChains(h.Parent) 147 | if err != nil { 148 | return fmt.Errorf("the search for parent group chains for host '%s' erred: %s", h.Name, err) 149 | } 150 | if len(groupChains) < 1 { 151 | return fmt.Errorf("parent group for host '%s' not found", h.Name) 152 | } 153 | for _, g := range groups { 154 | if err := inv.AddGroupMemberCounter("host", g); err != nil { 155 | return fmt.Errorf("failed updating counters for the parent group '%s' of host '%s': %s", g, h.Name, err) 156 | } 157 | } 158 | h.GroupChains = groupChains 159 | h.Groups = groups 160 | } 161 | 162 | for _, g := range inv.Groups { 163 | if g.Counters.Hosts < 1 { 164 | return fmt.Errorf("inventory group '%s' has no hosts", g.Name) 165 | } 166 | for _, a := range g.Ancestors { 167 | if err := inv.AddGroupMemberCounter("group", a); err != nil { 168 | return fmt.Errorf("failed updating counters for '%s' group: %s", a, err) 169 | } 170 | } 171 | } 172 | 173 | // inherit variables from parent groups 174 | for _, h := range inv.Hosts { 175 | m := make(map[string]string) 176 | for _, g := range h.Groups { 177 | group, err := inv.GetGroup(g) 178 | if err != nil { 179 | return err 180 | } 181 | for k, v := range group.Variables { 182 | m[k] = v 183 | } 184 | } 185 | for k, v := range m { 186 | if _, exists := h.Variables[k]; !exists { 187 | h.Variables[k] = v 188 | } 189 | } 190 | } 191 | 192 | return nil 193 | } 194 | 195 | // AddGroupMemberCounter increments group membership counters for hosts and 196 | // sub-groups. 197 | func (inv *Inventory) AddGroupMemberCounter(counterType, groupName string) error { 198 | if _, exists := inv.GroupsRef[groupName]; !exists { 199 | return fmt.Errorf("group %s does not exist", groupName) 200 | } 201 | for _, g := range inv.Groups { 202 | if g.Name == groupName { 203 | if counterType == "host" { 204 | atomic.AddUint64(&g.Counters.Hosts, 1) 205 | return nil 206 | } 207 | atomic.AddUint64(&g.Counters.Groups, 1) 208 | return nil 209 | } 210 | } 211 | return fmt.Errorf("group %s was not found", groupName) 212 | } 213 | 214 | // LoadFromBytes loads inventory data from an array of bytes. 215 | func (inv *Inventory) LoadFromBytes(b []byte) error { 216 | s := string(b[:]) 217 | return inv.parseString(s) 218 | } 219 | 220 | // LoadFromFile loads inventory data from a file. 221 | func (inv *Inventory) LoadFromFile(fp string) error { 222 | fp = expandFilePath(fp) 223 | b, err := ioutil.ReadFile(fp) 224 | if err != nil { 225 | return err 226 | } 227 | s := string(b[:]) 228 | return inv.parseString(s) 229 | } 230 | 231 | // GetHosts returns a list of InventoryHost instances. 232 | func (inv *Inventory) GetHosts() ([]*InventoryHost, error) { 233 | return inv.Hosts, nil 234 | } 235 | 236 | // AddGroup adds a group to the Inventory. 237 | func (inv *Inventory) AddGroup(s, p string) error { 238 | for _, g := range inv.Groups { 239 | if g.Name == s { 240 | for _, a := range g.Ancestors { 241 | if a == p { 242 | return nil 243 | } 244 | } 245 | g.Ancestors = append(g.Ancestors, p) 246 | return nil 247 | } 248 | } 249 | g := &InventoryGroup{ 250 | Name: s, 251 | Variables: make(map[string]string), 252 | } 253 | g.Ancestors = append(g.Ancestors, p) 254 | inv.Groups = append(inv.Groups, g) 255 | inv.GroupsRef[s] = true 256 | return nil 257 | } 258 | 259 | func getKeyValuePairs(s string) (map[string]string, error) { 260 | s = strings.TrimSpace(s) 261 | m := make(map[string]string) 262 | size := len(s) 263 | var k string 264 | 265 | x := 10000 266 | for { 267 | x-- 268 | if x == 0 { 269 | break 270 | } 271 | i := strings.Index(s, "=") 272 | if i < 0 { 273 | break 274 | } 275 | k = s[:i] 276 | if i >= size { 277 | break 278 | } 279 | s = s[i+1:] 280 | nx := strings.Index(s, "=") 281 | if nx < 0 { 282 | // no more key value pairs 283 | m[k] = strings.TrimSpace(s) 284 | break 285 | } else { 286 | v := s[:nx] 287 | vIndex := strings.LastIndex(v, " ") 288 | v = s[:vIndex] 289 | m[k] = strings.TrimSpace(v) 290 | s = s[vIndex+1:] 291 | } 292 | } 293 | return m, nil 294 | } 295 | 296 | // AddHost adds a host to the Inventory. 297 | func (inv *Inventory) AddHost(s, groupName string) error { 298 | if _, exists := inv.GroupsRef[groupName]; !exists { 299 | return fmt.Errorf("the group %s for host %s does not exist", groupName, s) 300 | } 301 | n := strings.Split(s, " ")[0] 302 | kv, err := getKeyValuePairs(s[len(n):]) 303 | if err != nil { 304 | return err 305 | } 306 | if g, exists := inv.HostsRef[n]; exists { 307 | if g != groupName { 308 | return fmt.Errorf("host %s exist in multiple groups: %s, %s", n, g, groupName) 309 | } 310 | } 311 | h := &InventoryHost{ 312 | Name: n, 313 | Parent: groupName, 314 | Variables: kv, 315 | } 316 | inv.HostsRef[n] = groupName 317 | inv.Hosts = append(inv.Hosts, h) 318 | return nil 319 | } 320 | 321 | // AddVariable adds a variable to an InventoryGroup. 322 | func (inv *Inventory) AddVariable(s, groupName string) error { 323 | if _, exists := inv.GroupsRef[groupName]; !exists { 324 | return fmt.Errorf("the group %s does not exist", groupName) 325 | } 326 | kvPairs, err := getKeyValuePairs(s) 327 | if err != nil { 328 | return err 329 | } 330 | for _, g := range inv.Groups { 331 | if g.Name == groupName { 332 | for k, v := range kvPairs { 333 | g.Variables[k] = v 334 | } 335 | break 336 | } 337 | } 338 | return nil 339 | } 340 | 341 | // GetParentGroupChains gets parent inventory groups recursively for the provided one. 342 | func (inv *Inventory) GetParentGroupChains(s string) ([]string, []string, error) { 343 | var x, max int 344 | outputs := make(map[string]bool) 345 | groups := make(map[string]bool) 346 | groups[s] = false 347 | max = 10000 348 | x = max 349 | for { 350 | x-- 351 | if x == 0 { 352 | return []string{}, []string{}, fmt.Errorf("failed to get parent groups: exceeded %d (max) iterations", max) 353 | } 354 | breakOut := true 355 | for k, completed := range groups { 356 | if completed { 357 | continue 358 | } 359 | parentGroups, err := inv.GetParentGroup(k) 360 | if err != nil { 361 | return []string{}, []string{}, err 362 | } 363 | groups[k] = true 364 | for _, g := range parentGroups { 365 | if _, exists := groups[g]; !exists { 366 | groups[g] = false 367 | breakOut = false 368 | } 369 | if g != "all" { 370 | out := fmt.Sprintf("%s,%s", g, k) 371 | if _, exists := outputs[out]; !exists { 372 | outputs[out] = true 373 | } 374 | } 375 | } 376 | } 377 | if breakOut { 378 | break 379 | } 380 | } 381 | 382 | max = 10000 383 | x = max 384 | for { 385 | x-- 386 | if x == 0 { 387 | return []string{}, []string{}, fmt.Errorf("failed to assemble group chains: exceeded %d (max) iterations", max) 388 | } 389 | delElements := []string{} 390 | continueNow := false 391 | for g1 := range outputs { 392 | g1arr := strings.Split(g1, ",") 393 | for g2 := range outputs { 394 | if g1 == g2 { 395 | continue 396 | } 397 | g2arr := strings.Split(g2, ",") 398 | // check whether the first element is last in the other outputs 399 | if g1arr[0] == g2arr[len(g2arr)-1] { 400 | var output string 401 | if g2arr[len(g2arr)-1] == g1arr[1] { 402 | output = fmt.Sprintf("%s,%s", g2arr[len(g2arr)-1], g1arr[1]) 403 | } else { 404 | output = fmt.Sprintf("%s,%s", g2, g1arr[1]) 405 | } 406 | delElements = append(delElements, g2) 407 | outputs[output] = true 408 | continueNow = true 409 | break 410 | } 411 | } 412 | if continueNow { 413 | break 414 | } 415 | } 416 | if len(delElements) == 0 { 417 | break 418 | } 419 | for _, e := range delElements { 420 | delete(outputs, e) 421 | } 422 | } 423 | 424 | chains := []string{} 425 | chains = append(chains, "all") 426 | for g := range outputs { 427 | // skip the group if the first element is not a top one or that the last 428 | // element is not a leaf 429 | groups := strings.Split(g, ",") 430 | fg, err := inv.GetGroup(groups[0]) 431 | if err != nil { 432 | return []string{}, []string{}, err 433 | } 434 | if len(fg.Ancestors) > 1 { 435 | continue 436 | } 437 | chains = append(chains, g) 438 | } 439 | 440 | // sort the array such that group chains with the most members appear last. 441 | rc := []string{} 442 | max = 1000 443 | x = max 444 | for { 445 | x-- 446 | if x == 0 { 447 | return []string{}, []string{}, fmt.Errorf("failed to sort group chains: exceeded %d (max) iterations", max) 448 | } 449 | k := 0 450 | v := 10000 451 | for i, chain := range chains { 452 | j := len(strings.Split(chain, ",")) 453 | if j < v { 454 | k = i 455 | v = j 456 | } 457 | } 458 | rc = append(rc, chains[k]) 459 | chains[k] = chains[len(chains)-1] 460 | chains[len(chains)-1] = "" 461 | chains = chains[:len(chains)-1] 462 | if len(chains) == 0 { 463 | break 464 | } 465 | } 466 | 467 | // create a list of unique groups 468 | groupChains := make([]string, len(rc)) 469 | copy(groupChains, rc) 470 | processedGroups := make(map[string]float64) 471 | max = 10000 472 | x = max 473 | for { 474 | x-- 475 | if x == 0 { 476 | return []string{}, []string{}, fmt.Errorf("failed create a list of unique groups: exceeded %d (max) iterations", max) 477 | } 478 | for i, chain := range groupChains { 479 | groups := strings.Split(chain, ",") 480 | if len(groups) < 2 && groups[0] == "" { 481 | continue 482 | } 483 | processedGroups[groups[0]] = float64(x) 484 | if groupChains[i] == "" { 485 | continue 486 | } 487 | x-- 488 | groupChains[i] = strings.Join(groups[1:], ",") 489 | } 490 | 491 | isEmpty := true 492 | for _, chain := range groupChains { 493 | if chain != "" { 494 | isEmpty = false 495 | break 496 | } 497 | } 498 | if isEmpty { 499 | break 500 | } 501 | } 502 | 503 | rg := sortStringFloatMap(processedGroups) 504 | 505 | if len(rg) == 1 { 506 | if rg[0] == "all" && s != "all" { 507 | rg = append(rg, s) 508 | rc = append(rc, s) 509 | } 510 | } 511 | return rc, rg, nil 512 | } 513 | 514 | // GetParentGroup gets parent inventory groups for the provided one. 515 | func (inv *Inventory) GetParentGroup(s string) ([]string, error) { 516 | groups := make(map[string]bool) 517 | if _, exists := inv.GroupsRef[s]; !exists { 518 | return []string{}, fmt.Errorf("group %s does not exist in the inventory", s) 519 | } 520 | for _, g := range inv.Groups { 521 | if g.Name == s { 522 | for _, a := range g.Ancestors { 523 | if _, exists := groups[a]; !exists { 524 | groups[a] = false 525 | } 526 | } 527 | break 528 | } 529 | } 530 | r := []string{} 531 | for g := range groups { 532 | r = append(r, g) 533 | } 534 | return r, nil 535 | } 536 | 537 | // GetHost returns an instance of InventoryHost. 538 | func (inv *Inventory) GetHost(s string) (*InventoryHost, error) { 539 | if _, exists := inv.HostsRef[s]; !exists { 540 | return nil, fmt.Errorf("host %s does not exist in the inventory", s) 541 | } 542 | for _, h := range inv.Hosts { 543 | if h.Name == s { 544 | return h, nil 545 | } 546 | } 547 | return nil, fmt.Errorf("host %s not found", s) 548 | } 549 | 550 | // GetGroup returns an instance of InventoryGroup. 551 | func (inv *Inventory) GetGroup(s string) (*InventoryGroup, error) { 552 | if _, exists := inv.GroupsRef[s]; !exists { 553 | return nil, fmt.Errorf("Group %s does not exist in the inventory", s) 554 | } 555 | for _, g := range inv.Groups { 556 | if g.Name == s { 557 | return g, nil 558 | } 559 | } 560 | return nil, fmt.Errorf("Group %s not found", s) 561 | } 562 | 563 | // GetHostsWithFilter returns a list of InventoryHost instances filtered by 564 | // input host and group patterns. Returns the host matching the patterns only. 565 | func (inv *Inventory) GetHostsWithFilter(hostFilter, groupFilter interface{}) ([]*InventoryHost, error) { 566 | if hostFilter == nil && groupFilter == nil { 567 | return inv.Hosts, nil 568 | } 569 | hosts := []*InventoryHost{} 570 | for _, host := range inv.Hosts { 571 | hostMatched := false 572 | if hostFilter != nil { 573 | var filters []string 574 | // see if a host matches the pattern or patterns 575 | switch hostFilter.(type) { 576 | case string: 577 | filters = append(filters, hostFilter.(string)) 578 | case []string: 579 | filters = hostFilter.([]string) 580 | default: 581 | return hosts, fmt.Errorf("unsupporter host filter type: %T", hostFilter) 582 | } 583 | for _, filter := range filters { 584 | filterPattern, err := regexp.Compile(filter) 585 | if err != nil { 586 | return hosts, fmt.Errorf("filter contains invalid pattern: %s, error: %s", filter, err) 587 | } 588 | if filterPattern.MatchString(host.Name) { 589 | hostMatched = true 590 | break 591 | } 592 | } 593 | } 594 | 595 | if groupFilter != nil { 596 | var filters []string 597 | switch groupFilter.(type) { 598 | case string: 599 | filters = append(filters, groupFilter.(string)) 600 | case []string: 601 | filters = groupFilter.([]string) 602 | default: 603 | return hosts, fmt.Errorf("unsupporter group filter type: %T", groupFilter) 604 | } 605 | for _, filter := range filters { 606 | if hostMatched { 607 | break 608 | } 609 | filterPattern, err := regexp.Compile(filter) 610 | if err != nil { 611 | return hosts, fmt.Errorf("filter contains invalid pattern: %s, error: %s", filter, err) 612 | } 613 | 614 | for _, group := range host.Groups { 615 | if filterPattern.MatchString(group) { 616 | hostMatched = true 617 | break 618 | } 619 | } 620 | } 621 | } 622 | 623 | if hostMatched { 624 | hosts = append(hosts, host) 625 | } 626 | } 627 | return hosts, nil 628 | } 629 | -------------------------------------------------------------------------------- /pkg/db/inventory_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Paul Greenberg (greenpau@outlook.com) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package db 16 | 17 | import ( 18 | //"fmt" 19 | //"io/ioutil" 20 | "testing" 21 | ) 22 | 23 | func TestNewInventory(t *testing.T) { 24 | testFailed := 0 25 | for i, test := range []struct { 26 | input []byte 27 | inputFile string 28 | host string 29 | size uint64 30 | shouldFail bool 31 | shouldErr bool 32 | }{ 33 | { 34 | input: []byte(`ny-sw01 os=cisco_nxos`), 35 | host: "ny-sw01", 36 | size: 1, 37 | shouldFail: false, 38 | shouldErr: false, 39 | }, 40 | { 41 | input: []byte(`ny-sw02 os=cisco_nxos`), 42 | host: "ny-sw03", 43 | size: 1, 44 | shouldFail: true, 45 | shouldErr: false, 46 | }, 47 | { 48 | inputFile: "../../testdata/inventory/hosts", 49 | host: "ny-sw05", 50 | size: 1, 51 | shouldFail: true, 52 | shouldErr: false, 53 | }, 54 | { 55 | inputFile: "../../testdata/inventory/hosts", 56 | host: "ny-sw01", 57 | size: 5, 58 | shouldFail: false, 59 | shouldErr: false, 60 | }, 61 | { 62 | inputFile: "../../testdata/inventory/hosts5", 63 | host: "ny-sw10", 64 | size: 1, 65 | shouldFail: false, 66 | shouldErr: false, 67 | }, 68 | } { 69 | inv := NewInventory() 70 | var err error 71 | if test.inputFile == "" { 72 | err = inv.LoadFromBytes(test.input) 73 | } else { 74 | err = inv.LoadFromFile(test.inputFile) 75 | } 76 | if err != nil { 77 | if !test.shouldErr { 78 | t.Logf("FAIL: Test %d: expected to pass, but threw error: %v", i, err) 79 | testFailed++ 80 | continue 81 | } 82 | } else { 83 | if test.shouldErr { 84 | t.Logf("FAIL: Test %d: expected to throw error, but passed", i) 85 | testFailed++ 86 | continue 87 | } 88 | } 89 | 90 | if (inv.Size() != test.size) && !test.shouldFail { 91 | t.Logf("FAIL: Test %d: expected to pass, but failed due to inventory size: '%d' (actual) vs. '%d' (expected)", 92 | i, inv.Size(), test.size) 93 | testFailed++ 94 | continue 95 | } 96 | 97 | if host, err := inv.GetHost(test.host); err != nil { 98 | if !test.shouldFail { 99 | t.Logf("FAIL: Test %d: host %s: expected to pass, but failed due to: %s", i, test.host, err) 100 | testFailed++ 101 | continue 102 | } 103 | } else { 104 | if test.shouldFail { 105 | t.Logf("FAIL: Test %d: host %s: expected to fail, but passed", i, test.host) 106 | testFailed++ 107 | continue 108 | } 109 | t.Logf("INFO: Test %d: host %s\n%s", i, test.host, host) 110 | } 111 | 112 | if test.shouldFail { 113 | t.Logf("PASS: Test %d: host '%s', expected to fail, failed", i, test.host) 114 | } else { 115 | t.Logf("PASS: Test %d: host '%s', expected to pass, passed", i, test.host) 116 | } 117 | } 118 | if testFailed > 0 { 119 | t.Fatalf("Failed %d tests", testFailed) 120 | } 121 | } 122 | 123 | func TestGetHost(t *testing.T) { 124 | invFile := "../../testdata/inventory/hosts" 125 | vltFile := "../../testdata/inventory/vault.yml" 126 | vltKeyFile := "../../testdata/inventory/vault.key" 127 | // Create a new inventory file. 128 | inv := NewInventory() 129 | // Load the contents of the inventory from an input file. 130 | if err := inv.LoadFromFile(invFile); err != nil { 131 | t.Fatalf("error reading inventory: %s", err) 132 | } 133 | // Create a new vault file. 134 | vlt := NewVault() 135 | // Read the password for the vault file from an input file. 136 | if err := vlt.LoadPasswordFromFile(vltKeyFile); err != nil { 137 | t.Fatalf("error reading vault key file: %s", err) 138 | } 139 | // Load the contents of the vault from an input file. 140 | if err := vlt.LoadFromFile(vltFile); err != nil { 141 | t.Fatalf("error reading vault: %s", err) 142 | } 143 | 144 | for i, test := range []struct { 145 | host string 146 | vars int 147 | groups int 148 | groupChains int 149 | credentials int 150 | }{ 151 | { 152 | host: "ny-sw01", 153 | vars: 7, 154 | groups: 6, 155 | groupChains: 3, 156 | credentials: 4, 157 | }, 158 | { 159 | host: "ny-sw02", 160 | vars: 7, 161 | groups: 6, 162 | groupChains: 3, 163 | credentials: 4, 164 | }, 165 | { 166 | host: "ny-sw03", 167 | vars: 7, 168 | groups: 6, 169 | groupChains: 3, 170 | credentials: 4, 171 | }, 172 | { 173 | host: "ny-sw04", 174 | vars: 7, 175 | groups: 6, 176 | groupChains: 3, 177 | credentials: 4, 178 | }, 179 | { 180 | host: "controller", 181 | vars: 2, 182 | groups: 1, 183 | groupChains: 1, 184 | credentials: 2, 185 | }, 186 | } { 187 | // Get host variables for a specific host. 188 | host, err := inv.GetHost(test.host) 189 | if err != nil { 190 | t.Fatalf("error getting host %s from inventory: %s", test.host, err) 191 | } 192 | if len(host.Variables) != test.vars { 193 | t.Fatalf("the number of variables for host %s is not %d, but %d", host.Name, test.vars, len(host.Variables)) 194 | } 195 | // Validate the number of group memberships for a specific host 196 | if len(host.Groups) != test.groups { 197 | t.Fatalf("the number of groups for host %s is not %d, but %d", host.Name, test.groups, len(host.Groups)) 198 | } 199 | // Validate the number of group chains associated with a specific host 200 | if len(host.GroupChains) != test.groupChains { 201 | t.Fatalf("the number of group chains for host %s is not %d, but %d", host.Name, test.groupChains, len(host.GroupChains)) 202 | } 203 | // Get credentials for a specific host. 204 | creds, err := vlt.GetCredentials(host.Name) 205 | if err != nil { 206 | t.Fatalf("error getting credentials for host %s: %s", host.Name, err) 207 | } 208 | if len(creds) != test.credentials { 209 | t.Fatalf("the number of credentials for host %s is not %d, but %d", host.Name, test.credentials, len(creds)) 210 | } 211 | // Display host summary 212 | t.Logf("PASS: Test %d, Host '%s' found, parent group: %s", i, host.Name, host.Parent) 213 | t.Logf("Credentials:") 214 | for _, c := range creds { 215 | t.Logf(" - %s", c) 216 | } 217 | t.Logf("Variables:") 218 | for k, v := range host.Variables { 219 | t.Logf(" - %s: %s", k, v) 220 | } 221 | t.Logf("Groups:") 222 | for _, g := range host.Groups { 223 | t.Logf(" - %s", g) 224 | } 225 | t.Logf("Group Chains:") 226 | for _, g := range host.GroupChains { 227 | t.Logf(" - %s", g) 228 | } 229 | } 230 | } 231 | 232 | func TestGetHostsWithFilter(t *testing.T) { 233 | invFile := "../../testdata/inventory/hosts" 234 | vltFile := "../../testdata/inventory/vault.yml" 235 | vltKeyFile := "../../testdata/inventory/vault.key" 236 | // Create a new inventory file. 237 | inv := NewInventory() 238 | // Load the contents of the inventory from an input file. 239 | if err := inv.LoadFromFile(invFile); err != nil { 240 | t.Fatalf("error reading inventory: %s", err) 241 | } 242 | // Create a new vault file. 243 | vlt := NewVault() 244 | // Read the password for the vault file from an input file. 245 | if err := vlt.LoadPasswordFromFile(vltKeyFile); err != nil { 246 | t.Fatalf("error reading vault key file: %s", err) 247 | } 248 | // Load the contents of the vault from an input file. 249 | if err := vlt.LoadFromFile(vltFile); err != nil { 250 | t.Fatalf("error reading vault: %s", err) 251 | } 252 | 253 | for i, test := range []struct { 254 | hostFilter interface{} 255 | groupFilter interface{} 256 | count int 257 | }{ 258 | { 259 | hostFilter: "ny-sw01", 260 | count: 1, 261 | }, 262 | { 263 | hostFilter: "ny-sw0[12]", 264 | count: 2, 265 | }, 266 | { 267 | hostFilter: []string{"ny-sw01", "ny-sw02"}, 268 | count: 2, 269 | }, 270 | { 271 | hostFilter: "ny-sw01", 272 | groupFilter: "arista", 273 | count: 3, 274 | }, 275 | } { 276 | // Get host variables for a specific host. 277 | hosts, err := inv.GetHostsWithFilter(test.hostFilter, test.groupFilter) 278 | if err != nil { 279 | t.Fatalf("FAIL: Test %d, error getting hosts from inventory: %s", i, err) 280 | } 281 | if len(hosts) != test.count { 282 | t.Fatalf("FAIL: Test %d, filtered hosts count mismatch: %d (expected) vs. %d (received)", i, test.count, len(hosts)) 283 | } 284 | t.Logf("PASS: Test %d, host filters: %v, group filters: %v, hosts: %d", i, test.hostFilter, test.groupFilter, len(hosts)) 285 | 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /pkg/db/util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Paul Greenberg (greenpau@outlook.com) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package db 16 | 17 | import ( 18 | "os/user" 19 | "sort" 20 | "strings" 21 | ) 22 | 23 | type stringFloatMap struct { 24 | m map[string]float64 25 | s []string 26 | } 27 | 28 | func (sfm *stringFloatMap) Len() int { 29 | return len(sfm.m) 30 | } 31 | 32 | func (sfm *stringFloatMap) Less(i, j int) bool { 33 | return sfm.m[sfm.s[i]] > sfm.m[sfm.s[j]] 34 | } 35 | 36 | func (sfm *stringFloatMap) Swap(i, j int) { 37 | sfm.s[i], sfm.s[j] = sfm.s[j], sfm.s[i] 38 | } 39 | 40 | func sortStringFloatMap(m map[string]float64) []string { 41 | sfm := new(stringFloatMap) 42 | sfm.m = m 43 | sfm.s = make([]string, len(m)) 44 | i := 0 45 | for k := range m { 46 | sfm.s[i] = k 47 | i++ 48 | } 49 | sort.Sort(sfm) 50 | return sfm.s 51 | } 52 | 53 | func expandFilePath(s string) string { 54 | if strings.HasPrefix(s, "~/") { 55 | usr, err := user.Current() 56 | if err != nil { 57 | return s 58 | } 59 | s = strings.Replace(s, "~", usr.HomeDir, 1) 60 | } 61 | return s 62 | } 63 | -------------------------------------------------------------------------------- /pkg/db/vault.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Paul Greenberg (greenpau@outlook.com) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package db 16 | 17 | import ( 18 | "crypto/aes" 19 | "crypto/cipher" 20 | "crypto/hmac" 21 | "crypto/sha256" 22 | "encoding/hex" 23 | "fmt" 24 | //"github.com/davecgh/go-spew/spew" 25 | "golang.org/x/crypto/pbkdf2" 26 | "gopkg.in/yaml.v2" 27 | "io/ioutil" 28 | "regexp" 29 | "sort" 30 | "strconv" 31 | "strings" 32 | ) 33 | 34 | const ( 35 | vaultOperations = 10000 36 | vaultKeyLength = 32 37 | vaultInitializationVectorLength = 16 38 | vaultSaltLength = 32 39 | ) 40 | 41 | // Vault is the contents of Ansible vault file. 42 | type Vault struct { 43 | Header VaultHeader `xml:"-" json:"-" yaml:"-"` 44 | Body VaultBody `xml:"-" json:"-" yaml:"-"` 45 | Key VaultKey `xml:"-" json:"-" yaml:"-"` 46 | Password []byte `xml:"-" json:"-" yaml:"-"` 47 | Payload []byte `xml:"-" json:"-" yaml:"-"` 48 | Credentials []*VaultCredential `xml:"credentials" json:"credentials" yaml:"credentials"` 49 | } 50 | 51 | // VaultHeader is the header of a Vault. 52 | type VaultHeader struct { 53 | Format string `xml:"-" json:"-" yaml:"-"` 54 | Version string `xml:"-" json:"-" yaml:"-"` 55 | Cipher string `xml:"-" json:"-" yaml:"-"` 56 | } 57 | 58 | // VaultBody is the body of a Vault. 59 | type VaultBody struct { 60 | Salt []byte `xml:"-" json:"-" yaml:"-"` 61 | HMAC []byte `xml:"-" json:"-" yaml:"-"` 62 | Data []byte `xml:"-" json:"-" yaml:"-"` 63 | } 64 | 65 | // VaultKey is the key for a Vault 66 | type VaultKey struct { 67 | Cipher []byte `xml:"-" json:"-" yaml:"-"` 68 | HMAC []byte `xml:"-" json:"-" yaml:"-"` 69 | InitializationVector []byte `xml:"-" json:"-" yaml:"-"` 70 | } 71 | 72 | // VaultCredential is a decoded credential from a Vault. 73 | type VaultCredential struct { 74 | Description string `xml:"description,omitempty" json:"description,omitempty" yaml:"description,omitempty"` 75 | Regex string `xml:"regex,omitempty" json:"regex,omitempty" yaml:"regex,omitempty"` 76 | Username string `xml:"username,omitempty" json:"username,omitempty" yaml:"username,omitempty"` 77 | Password string `xml:"password,omitempty" json:"password,omitempty" yaml:"password,omitempty"` 78 | EnabledPassword string `xml:"password_enable,omitempty" json:"password_enable,omitempty" yaml:"password_enable,omitempty"` 79 | Priority int `xml:"priority,omitempty" json:"priority,omitempty" yaml:"priority,omitempty"` 80 | Default bool `xml:"default,omitempty" json:"default,omitempty" yaml:"default,omitempty"` 81 | } 82 | 83 | // NewVault returns a pointer to Vault. 84 | func NewVault() *Vault { 85 | v := &Vault{} 86 | return v 87 | } 88 | 89 | func (v *Vault) readVault(b []byte) error { 90 | if v.Password == nil { 91 | return fmt.Errorf("vault password not found") 92 | } 93 | lines := strings.Split(string(b[:]), "\n") 94 | if len(lines) < 2 { 95 | return fmt.Errorf("invalid vault payload") 96 | } 97 | header := strings.Split(strings.TrimSpace(lines[0]), ";") 98 | if len(header) != 3 { 99 | return fmt.Errorf("invalid vault header: %s", lines[0]) 100 | } 101 | // Capture vault header 102 | v.Header.Format = header[0] 103 | v.Header.Version = header[1] 104 | v.Header.Cipher = header[2] 105 | if v.Header.Version != "1.1" { 106 | return fmt.Errorf("unsupported vault version: %s", v.Header.Version) 107 | } 108 | 109 | if v.Header.Cipher != "AES256" { 110 | return fmt.Errorf("unsupported vault cipher: %s", v.Header.Cipher) 111 | } 112 | // Capture vault body 113 | var bb strings.Builder 114 | for _, line := range lines[1:] { 115 | bb.WriteString(strings.TrimSpace(line)) 116 | } 117 | body, err := hex.DecodeString(bb.String()) 118 | if err != nil { 119 | return fmt.Errorf("vault hex decoding error: %s", err) 120 | } 121 | // Split the body into 3 parts: Salt, HMAC, and Data 122 | parts := strings.SplitN(string(body[:]), "\n", 3) 123 | if len(parts) != 3 { 124 | return fmt.Errorf("invalid vault body") 125 | } 126 | saltPart, err := hex.DecodeString(parts[0]) 127 | if err != nil { 128 | return fmt.Errorf("invalid vault body (salt): %s", err) 129 | } 130 | v.Body.Salt = saltPart 131 | hmacPart, err := hex.DecodeString(parts[1]) 132 | if err != nil { 133 | return fmt.Errorf("invalid vault body (hmac): %s", err) 134 | } 135 | v.Body.HMAC = hmacPart 136 | dataPart, err := hex.DecodeString(parts[2]) 137 | if err != nil { 138 | return fmt.Errorf("invalid vault body (data): %s", err) 139 | } 140 | v.Body.Data = dataPart 141 | // Generate a decryption key 142 | key := pbkdf2.Key(v.Password, v.Body.Salt, vaultOperations, 2*vaultKeyLength*vaultInitializationVectorLength, sha256.New) 143 | v.Key.Cipher = key[:vaultKeyLength] 144 | v.Key.HMAC = key[vaultKeyLength:(vaultKeyLength * 2)] 145 | v.Key.InitializationVector = key[(vaultKeyLength * 2) : (vaultKeyLength*2)+vaultInitializationVectorLength] 146 | // Valudate the password 147 | keyHash := hmac.New(sha256.New, v.Key.HMAC) 148 | keyHash.Write(v.Body.Data) 149 | if !hmac.Equal(keyHash.Sum(nil), v.Body.HMAC) { 150 | return fmt.Errorf("invalid vault vault password") 151 | } 152 | // Decrypt the vault 153 | cphr, err := aes.NewCipher(v.Key.Cipher) 154 | if err != nil { 155 | return fmt.Errorf("error opening the vault: %s", err) 156 | } 157 | plainText := make([]byte, len(v.Body.Data)) 158 | encrBlock := cipher.NewCTR(cphr, v.Key.InitializationVector) 159 | encrBlock.XORKeyStream(plainText, v.Body.Data) 160 | output, err := unpadBytes(plainText) 161 | if err != nil { 162 | return fmt.Errorf("error opening the vault: %s", err) 163 | } 164 | v.Payload = output 165 | tv := &Vault{} 166 | if err := yaml.Unmarshal(output, tv); err != nil { 167 | return fmt.Errorf("error parsing YAML content of the vault: %s", err) 168 | } 169 | // Check regular expressions for their validity 170 | for _, c := range tv.Credentials { 171 | if !c.Default && c.Regex == "" { 172 | return fmt.Errorf("invalid vault entry, non-default and empty regex pattern") 173 | } 174 | if c.Default && c.Regex != "" { 175 | return fmt.Errorf("invalid vault entry, default and non-empty regex pattern") 176 | } 177 | if c.Default { 178 | continue 179 | } 180 | if _, err := regexp.Compile(c.Regex); err != nil { 181 | return fmt.Errorf("invalid vault entry, regex compilation for '%s', failed: %s", c.Regex, err) 182 | } 183 | } 184 | v.Credentials = tv.Credentials 185 | return nil 186 | } 187 | 188 | func unpadBytes(b []byte) ([]byte, error) { 189 | length := len(b) 190 | paddingLength := int(b[length-1]) 191 | if paddingLength > length { 192 | return nil, fmt.Errorf("invalid padding") 193 | } 194 | return b[:(length - paddingLength)], nil 195 | } 196 | 197 | // LoadFromBytes loads vault data from an array of bytes. 198 | func (v *Vault) LoadFromBytes(b []byte) error { 199 | return v.readVault(b) 200 | } 201 | 202 | // LoadFromFile loads vault data from a file. 203 | func (v *Vault) LoadFromFile(fp string) error { 204 | fp = expandFilePath(fp) 205 | b, err := ioutil.ReadFile(fp) 206 | if err != nil { 207 | return err 208 | } 209 | return v.readVault(b) 210 | } 211 | 212 | // LoadPasswordFromFile loads unlock password for the vault from a file. 213 | func (v *Vault) LoadPasswordFromFile(fp string) error { 214 | fp = expandFilePath(fp) 215 | b, err := ioutil.ReadFile(fp) 216 | if err != nil { 217 | return err 218 | } 219 | v.Password = []byte(strings.TrimSpace(strings.Split(string(b[:]), "\n")[0])) 220 | return nil 221 | } 222 | 223 | // SetPassword sets unlock password for the vault. 224 | func (v *Vault) SetPassword(s string) error { 225 | if s == "" { 226 | return fmt.Errorf("empty password is unsupported") 227 | } 228 | v.Password = []byte(strings.TrimSpace(s)) 229 | return nil 230 | } 231 | 232 | // GetCredentials returns a list of credential applicable to the provided 233 | // host name. 234 | func (v *Vault) GetCredentials(s string) ([]*VaultCredential, error) { 235 | cv := []*VaultCredential{} 236 | for _, c := range v.Credentials { 237 | if c.Default { 238 | continue 239 | } 240 | r, err := regexp.Compile(c.Regex) 241 | if err != nil { 242 | continue 243 | } 244 | if r.MatchString(s) == true { 245 | cv = append(cv, c) 246 | } 247 | } 248 | sort.SliceStable(cv, func(i, j int) bool { 249 | return cv[i].Priority < cv[j].Priority 250 | }) 251 | dcv := []*VaultCredential{} 252 | for _, c := range v.Credentials { 253 | if !c.Default { 254 | continue 255 | } 256 | dcv = append(dcv, c) 257 | } 258 | sort.SliceStable(dcv, func(i, j int) bool { 259 | return dcv[i].Priority < dcv[j].Priority 260 | }) 261 | for _, c := range dcv { 262 | cv = append(cv, c) 263 | } 264 | return cv, nil 265 | } 266 | 267 | func (c *VaultCredential) String() string { 268 | var s strings.Builder 269 | s.WriteString("username=" + c.Username) 270 | s.WriteString(", password=" + c.Password) 271 | s.WriteString(", enabled_password=" + c.EnabledPassword) 272 | s.WriteString(", priority=" + strconv.Itoa(c.Priority)) 273 | s.WriteString(", default=" + strconv.FormatBool(c.Default)) 274 | s.WriteString(", description=" + c.Description) 275 | return s.String() 276 | } 277 | -------------------------------------------------------------------------------- /pkg/db/vault_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Paul Greenberg (greenpau@outlook.com) 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package db 16 | 17 | import ( 18 | //"fmt" 19 | //"io/ioutil" 20 | "testing" 21 | ) 22 | 23 | func TestNewVault(t *testing.T) { 24 | testFailed := 0 25 | for i, test := range []struct { 26 | host string 27 | inputFile string 28 | key string 29 | size int 30 | keyFile string 31 | shouldFail bool 32 | shouldErr bool 33 | }{ 34 | { 35 | host: "ny-sw01", 36 | inputFile: "../../testdata/inventory/vault.yml", 37 | keyFile: "../../testdata/inventory/vault.key", 38 | size: 4, 39 | shouldFail: false, 40 | shouldErr: false, 41 | }, 42 | { 43 | host: "ny-sw01", 44 | inputFile: "../../testdata/inventory/vault.yml", 45 | key: "7f017fde-e88b-42c5-89df-a7c8f9de981d", 46 | size: 4, 47 | shouldFail: false, 48 | shouldErr: false, 49 | }, 50 | } { 51 | vlt := NewVault() 52 | var err error 53 | if test.key == "" { 54 | err = vlt.LoadPasswordFromFile(test.keyFile) 55 | } else { 56 | err = vlt.SetPassword(test.key) 57 | } 58 | 59 | if err != nil { 60 | if !test.shouldErr { 61 | t.Logf("FAIL: Test %d: expected to pass, but threw error: %v", i, err) 62 | testFailed++ 63 | continue 64 | } 65 | } else { 66 | if test.shouldErr { 67 | t.Logf("FAIL: Test %d: expected to throw error, but passed", i) 68 | testFailed++ 69 | continue 70 | } 71 | } 72 | 73 | err = vlt.LoadFromFile(test.inputFile) 74 | if err != nil { 75 | if !test.shouldErr { 76 | t.Logf("FAIL: Test %d: expected to pass, but threw error: %v", i, err) 77 | testFailed++ 78 | continue 79 | } 80 | } else { 81 | if test.shouldErr { 82 | t.Logf("FAIL: Test %d: expected to throw error, but passed", i) 83 | testFailed++ 84 | continue 85 | } 86 | } 87 | 88 | hostCredentials, err := vlt.GetCredentials(test.host) 89 | if err != nil { 90 | if !test.shouldFail { 91 | t.Logf("FAIL: Test %d: host %s: expected to pass, but failed due to: %s", i, test.host, err) 92 | testFailed++ 93 | continue 94 | } 95 | } else { 96 | if test.shouldFail { 97 | t.Logf("FAIL: Test %d: host %s: expected to fail, but passed", i, test.host) 98 | testFailed++ 99 | continue 100 | } 101 | for _, c := range hostCredentials { 102 | t.Logf("INFO: Test %d: host %s, credential: %v", i, test.host, c) 103 | } 104 | } 105 | 106 | if len(hostCredentials) != test.size { 107 | if !test.shouldFail { 108 | t.Logf("FAIL: Test %d: host %s: expected to pass, but failed due to len(credentials) mismatch %d (actual) vs. %d (expected)", 109 | i, test.host, len(hostCredentials), test.size) 110 | testFailed++ 111 | continue 112 | } 113 | } 114 | 115 | if test.shouldFail { 116 | t.Logf("PASS: Test %d: host '%s', expected to fail, failed", i, test.host) 117 | } else { 118 | t.Logf("PASS: Test %d: host '%s', expected to pass, passed", i, test.host) 119 | } 120 | } 121 | if testFailed > 0 { 122 | t.Fatalf("Failed %d tests", testFailed) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /testdata/inventory/hosts: -------------------------------------------------------------------------------- 1 | # 2 | # Managed devices 3 | # 4 | 5 | controller ansible_connection=local 6 | 7 | [us:children] 8 | ny 9 | 10 | [ny:children] 11 | ny4 12 | ny5 13 | 14 | [ny4:children] 15 | ny4-cisco 16 | ny4-arista 17 | 18 | [ny4:vars] 19 | datacenter=ny4 20 | 21 | [ny5:children] 22 | ny5-cisco 23 | ny5-arista 24 | 25 | [ny5:vars] 26 | datacenter=ny5 27 | 28 | [cisco:children] 29 | ny4-cisco 30 | ny5-cisco 31 | 32 | [cisco:vars] 33 | vendor=Cisco Systems 34 | 35 | [arista:children] 36 | ny4-arista 37 | ny5-arista 38 | 39 | [arista:vars] 40 | vendor=Arista Networks 41 | 42 | [ny4-cisco] 43 | ny-sw01 os=cisco_nxos host_overwrite=localhost host_port=8224 44 | 45 | [ny4-arista] 46 | ny-sw02 os=arista_eos host_overwrite=localhost host_port=8225 47 | 48 | [ny5-arista] 49 | ny-sw03 os=arista_eos host_overwrite=localhost host_port=8226 50 | 51 | [ny5-cisco] 52 | ny-sw04 os=cisco_nxos host_overwrite=localhost host_port=8227 53 | 54 | [all:vars] 55 | ansible_connection=local 56 | contact_person=Paul Greenberg @greenpau 57 | -------------------------------------------------------------------------------- /testdata/inventory/hosts2: -------------------------------------------------------------------------------- 1 | mail.example.com 2 | 3 | [webservers] 4 | foo.example.com 5 | bar.example.com 6 | 7 | [dbservers] 8 | one.example.com 9 | two.example.com 10 | three.example.com 11 | -------------------------------------------------------------------------------- /testdata/inventory/hosts3: -------------------------------------------------------------------------------- 1 | [atlanta] 2 | host1 http_port=80 maxRequestsPerChild=808 3 | host2 http_port=303 maxRequestsPerChild=909 4 | -------------------------------------------------------------------------------- /testdata/inventory/hosts4: -------------------------------------------------------------------------------- 1 | [targets] 2 | 3 | localhost ansible_connection=local 4 | other1.example.com ansible_connection=ssh ansible_user=mpdehaan 5 | other2.example.com ansible_connection=ssh ansible_user=mdehaan 6 | -------------------------------------------------------------------------------- /testdata/inventory/hosts5: -------------------------------------------------------------------------------- 1 | [devnet] 2 | ny-sw10 os=cisco_nxos host_overwrite=10.10.10.10 3 | -------------------------------------------------------------------------------- /testdata/inventory/hosts6: -------------------------------------------------------------------------------- 1 | [pdc1:children] 2 | dc1 3 | dc2 4 | dc12 5 | dc21 6 | dc22 7 | pdc1-aerospike_cluster 8 | 9 | [aerospike_cluster:children] 10 | pdc1-aerospike_cluster 11 | 12 | [pdc1-aerospike_cluster] 13 | p1p31.storagebozotron.com rack_id=1 14 | p1p32.storagebozotron.com rack_id=1 15 | p1p33.storagebozotron.com rack_id=1 16 | p1p34.storagebozotron.com rack_id=2 17 | p1p35.storagebozotron.com rack_id=2 18 | p1p36.storagebozotron.com rack_id=2 19 | p1p37.storagebozotron.com rack_id=1 20 | p1p38.storagebozotron.com rack_id=1 21 | p1p39.storagebozotron.com rack_id=1 22 | p1p40.storagebozotron.com rack_id=2 23 | p1p41.storagebozotron.com rack_id=2 24 | p1p42.storagebozotron.com rack_id=2 25 | p1p43.storagebozotron.com rack_id=1 26 | p1p44.storagebozotron.com rack_id=1 27 | p1p45.storagebozotron.com rack_id=1 28 | p1p46.storagebozotron.com rack_id=2 29 | p1p47.storagebozotron.com rack_id=2 30 | p1p49.storagebozotron.com rack_id=1 31 | p1p52.storagebozotron.com rack_id=2 32 | p1p53.storagebozotron.com rack_id=2 33 | 34 | [aerospike_cluster:vars] 35 | ansible_user=root 36 | 37 | [aerospike-xdr-pdc2bozotron.com] 38 | 39 | [aerospike-xdr-pdc3bozotron.com] 40 | 41 | [aerospike-xdr-pdc4bozotron.com] 42 | 43 | [aerospike-xdr-pdc6bozotron.com] 44 | 45 | [aerospike-xdr-pdc7bozotron.com] 46 | 47 | [aerospike-xdr-pdc8bozotron.com] 48 | 49 | [kafka-nodes:children] 50 | stats 51 | 52 | # Not all DCSTATS (Zookeeper is slower in writing with more nodes) 53 | [zookeeper-nodes] 54 | p1s1.dcauxbozotron.com 55 | p1s10.dcauxbozotron.com 56 | p1s19.dcauxbozotron.com 57 | p1s27.dcauxbozotron.com 58 | p1s35.dcauxbozotron.com 59 | p1s43.dcauxbozotron.com 60 | p1s51.dcauxbozotron.com 61 | 62 | [as:children] 63 | dc1-as 64 | dc2-as 65 | dc12-as 66 | dc21-as 67 | dc22-as 68 | 69 | [stats:children] 70 | dc1-stats 71 | dc2-stats 72 | dc12-stats 73 | dc21-stats 74 | dc22-stats 75 | 76 | [dcapp:children] 77 | dc1-dcapp 78 | dc2-dcapp 79 | dc12-dcapp 80 | dc21-dcapp 81 | dc22-dcapp 82 | 83 | [dc1:children] 84 | dc1-stats 85 | dc1-as 86 | dc1-dcapp 87 | 88 | [dc2:children] 89 | dc2-stats 90 | dc2-as 91 | dc2-dcapp 92 | 93 | [dc12:children] 94 | dc12-stats 95 | dc12-as 96 | dc12-dcapp 97 | 98 | [dc21:children] 99 | dc21-stats 100 | dc21-as 101 | dc21-dcapp 102 | 103 | [dc22:children] 104 | dc22-stats 105 | dc22-as 106 | dc22-dcapp 107 | 108 | [dc1-as] 109 | a481.bozotron.com 110 | a482.bozotron.com 111 | a483.bozotron.com 112 | a484.bozotron.com 113 | a485.bozotron.com 114 | a486.bozotron.com 115 | a487.bozotron.com 116 | a488.bozotron.com 117 | a489.bozotron.com 118 | a490.bozotron.com 119 | a491.bozotron.com 120 | a492.bozotron.com 121 | a493.bozotron.com 122 | a494.bozotron.com 123 | a495.bozotron.com 124 | a496.bozotron.com 125 | a497.bozotron.com 126 | a498.bozotron.com 127 | a499.bozotron.com 128 | a500.bozotron.com 129 | a501.bozotron.com 130 | a502.bozotron.com 131 | a503.bozotron.com 132 | a504.bozotron.com 133 | a505.bozotron.com 134 | a506.bozotron.com 135 | a507.bozotron.com 136 | a508.bozotron.com 137 | a509.bozotron.com 138 | a510.bozotron.com 139 | a511.bozotron.com 140 | a512.bozotron.com 141 | a513.bozotron.com 142 | a514.bozotron.com 143 | a515.bozotron.com 144 | a516.bozotron.com 145 | a517.bozotron.com 146 | a518.bozotron.com 147 | a519.bozotron.com 148 | a520.bozotron.com 149 | a521.bozotron.com 150 | a522.bozotron.com 151 | a523.bozotron.com 152 | a524.bozotron.com 153 | a525.bozotron.com 154 | a526.bozotron.com 155 | a527.bozotron.com 156 | a528.bozotron.com 157 | a529.bozotron.com 158 | a530.bozotron.com 159 | a531.bozotron.com 160 | a532.bozotron.com 161 | a533.bozotron.com 162 | a534.bozotron.com 163 | a535.bozotron.com 164 | a536.bozotron.com 165 | a537.bozotron.com 166 | a538.bozotron.com 167 | a539.bozotron.com 168 | a540.bozotron.com 169 | a541.bozotron.com 170 | a542.bozotron.com 171 | a543.bozotron.com 172 | a544.bozotron.com 173 | a545.bozotron.com 174 | a546.bozotron.com 175 | a547.bozotron.com 176 | a548.bozotron.com 177 | a549.bozotron.com 178 | a550.bozotron.com 179 | a551.bozotron.com 180 | a552.bozotron.com 181 | a553.bozotron.com 182 | a554.bozotron.com 183 | a555.bozotron.com 184 | a556.bozotron.com 185 | a557.bozotron.com 186 | a558.bozotron.com 187 | a559.bozotron.com 188 | a560.bozotron.com 189 | a981.bozotron.com 190 | a982.bozotron.com 191 | a983.bozotron.com 192 | a984.bozotron.com 193 | a985.bozotron.com 194 | a986.bozotron.com 195 | a987.bozotron.com 196 | a988.bozotron.com 197 | a989.bozotron.com 198 | a990.bozotron.com 199 | a991.bozotron.com 200 | a992.bozotron.com 201 | a993.bozotron.com 202 | a994.bozotron.com 203 | a995.bozotron.com 204 | a996.bozotron.com 205 | a997.bozotron.com 206 | a998.bozotron.com 207 | a999.bozotron.com 208 | a1000.bozotron.com 209 | a1001.bozotron.com 210 | a1002.bozotron.com 211 | a1003.bozotron.com 212 | a1004.bozotron.com 213 | a1005.bozotron.com 214 | a1006.bozotron.com 215 | a1007.bozotron.com 216 | a1008.bozotron.com 217 | a1009.bozotron.com 218 | a1010.bozotron.com 219 | a1011.bozotron.com 220 | a1012.bozotron.com 221 | a1013.bozotron.com 222 | a1014.bozotron.com 223 | a1015.bozotron.com 224 | a1016.bozotron.com 225 | a1017.bozotron.com 226 | a1018.bozotron.com 227 | a1019.bozotron.com 228 | a1020.bozotron.com 229 | 230 | [dc2-as:children] 231 | vip-dc2-1 232 | vip-dc2-2 233 | 234 | [vip-dc2-1] 235 | a1221.bozotron.com 236 | a1222.bozotron.com 237 | a1223.bozotron.com 238 | a1224.bozotron.com 239 | a1225.bozotron.com 240 | a1226.bozotron.com 241 | a1227.bozotron.com 242 | a1228.bozotron.com 243 | a1229.bozotron.com 244 | a1230.bozotron.com 245 | a1231.bozotron.com 246 | a1232.bozotron.com 247 | a1233.bozotron.com 248 | a1234.bozotron.com 249 | a1235.bozotron.com 250 | a1236.bozotron.com 251 | a1237.bozotron.com 252 | a1238.bozotron.com 253 | a1239.bozotron.com 254 | a1240.bozotron.com 255 | a1241.bozotron.com 256 | a1242.bozotron.com 257 | a1243.bozotron.com 258 | a1244.bozotron.com 259 | a1245.bozotron.com 260 | a1246.bozotron.com 261 | a1247.bozotron.com 262 | a1248.bozotron.com 263 | a1249.bozotron.com 264 | a1250.bozotron.com 265 | a1251.bozotron.com 266 | a1252.bozotron.com 267 | a1253.bozotron.com 268 | a1254.bozotron.com 269 | a1255.bozotron.com 270 | a1256.bozotron.com 271 | a1257.bozotron.com 272 | a1258.bozotron.com 273 | a1259.bozotron.com 274 | a1260.bozotron.com 275 | a1261.bozotron.com 276 | a1262.bozotron.com 277 | a1263.bozotron.com 278 | a1264.bozotron.com 279 | a1265.bozotron.com 280 | a1266.bozotron.com 281 | a1267.bozotron.com 282 | a1268.bozotron.com 283 | a1269.bozotron.com 284 | a1270.bozotron.com 285 | a1271.bozotron.com 286 | a1272.bozotron.com 287 | a1273.bozotron.com 288 | a1274.bozotron.com 289 | a1275.bozotron.com 290 | a1276.bozotron.com 291 | a1277.bozotron.com 292 | a1278.bozotron.com 293 | a1279.bozotron.com 294 | a1280.bozotron.com 295 | a1281.bozotron.com 296 | a1282.bozotron.com 297 | a1283.bozotron.com 298 | a1284.bozotron.com 299 | a1285.bozotron.com 300 | a1286.bozotron.com 301 | a1287.bozotron.com 302 | a1288.bozotron.com 303 | a1289.bozotron.com 304 | a1290.bozotron.com 305 | a1291.bozotron.com 306 | a1292.bozotron.com 307 | a1293.bozotron.com 308 | a1294.bozotron.com 309 | a1295.bozotron.com 310 | a1296.bozotron.com 311 | a1297.bozotron.com 312 | a1298.bozotron.com 313 | a1299.bozotron.com 314 | a1300.bozotron.com 315 | a1301.bozotron.com 316 | a1302.bozotron.com 317 | a1303.bozotron.com 318 | a1304.bozotron.com 319 | a1305.bozotron.com 320 | a1306.bozotron.com 321 | a1307.bozotron.com 322 | a1308.bozotron.com 323 | a1309.bozotron.com 324 | a1310.bozotron.com 325 | a1311.bozotron.com 326 | a1312.bozotron.com 327 | a1313.bozotron.com 328 | a1314.bozotron.com 329 | a1315.bozotron.com 330 | a1316.bozotron.com 331 | a1317.bozotron.com 332 | a1318.bozotron.com 333 | a1319.bozotron.com 334 | a1320.bozotron.com 335 | a1321.bozotron.com 336 | a1322.bozotron.com 337 | a1323.bozotron.com 338 | a1324.bozotron.com 339 | a1325.bozotron.com 340 | a1326.bozotron.com 341 | a1327.bozotron.com 342 | a1328.bozotron.com 343 | a1329.bozotron.com 344 | a1330.bozotron.com 345 | a1331.bozotron.com 346 | a1332.bozotron.com 347 | a1333.bozotron.com 348 | a1334.bozotron.com 349 | a1335.bozotron.com 350 | a1336.bozotron.com 351 | a1337.bozotron.com 352 | a1338.bozotron.com 353 | a1339.bozotron.com 354 | a1340.bozotron.com 355 | 356 | [vip-dc2-2] 357 | a1341.bozotron.com 358 | a1342.bozotron.com 359 | a1343.bozotron.com 360 | a1344.bozotron.com 361 | a1345.bozotron.com 362 | a1346.bozotron.com 363 | a1347.bozotron.com 364 | a1348.bozotron.com 365 | a1349.bozotron.com 366 | a1350.bozotron.com 367 | a1351.bozotron.com 368 | a1352.bozotron.com 369 | a1353.bozotron.com 370 | a1354.bozotron.com 371 | a1355.bozotron.com 372 | a1356.bozotron.com 373 | a1357.bozotron.com 374 | a1358.bozotron.com 375 | a1359.bozotron.com 376 | a1360.bozotron.com 377 | a1361.bozotron.com 378 | a1362.bozotron.com 379 | a1363.bozotron.com 380 | a1364.bozotron.com 381 | a1365.bozotron.com 382 | a1366.bozotron.com 383 | a1367.bozotron.com 384 | a1368.bozotron.com 385 | a1369.bozotron.com 386 | a1370.bozotron.com 387 | a1371.bozotron.com 388 | a1372.bozotron.com 389 | a1373.bozotron.com 390 | a1374.bozotron.com 391 | a1375.bozotron.com 392 | a1376.bozotron.com 393 | a1377.bozotron.com 394 | a1378.bozotron.com 395 | a1379.bozotron.com 396 | a1380.bozotron.com 397 | a1381.bozotron.com 398 | a1382.bozotron.com 399 | a1383.bozotron.com 400 | a1384.bozotron.com 401 | a1385.bozotron.com 402 | a1386.bozotron.com 403 | a1387.bozotron.com 404 | a1388.bozotron.com 405 | a1389.bozotron.com 406 | a1390.bozotron.com 407 | a1391.bozotron.com 408 | a1392.bozotron.com 409 | a1393.bozotron.com 410 | a1394.bozotron.com 411 | a1395.bozotron.com 412 | a1396.bozotron.com 413 | a1397.bozotron.com 414 | a1398.bozotron.com 415 | a1399.bozotron.com 416 | a1400.bozotron.com 417 | a1401.bozotron.com 418 | a1402.bozotron.com 419 | a1403.bozotron.com 420 | a1404.bozotron.com 421 | a1405.bozotron.com 422 | a1406.bozotron.com 423 | a1407.bozotron.com 424 | a1408.bozotron.com 425 | a1409.bozotron.com 426 | a1410.bozotron.com 427 | a1411.bozotron.com 428 | a1412.bozotron.com 429 | a1413.bozotron.com 430 | a1414.bozotron.com 431 | a1415.bozotron.com 432 | a1416.bozotron.com 433 | a1417.bozotron.com 434 | a1418.bozotron.com 435 | a1419.bozotron.com 436 | a1420.bozotron.com 437 | a1421.bozotron.com 438 | a1422.bozotron.com 439 | a1423.bozotron.com 440 | a1424.bozotron.com 441 | a1425.bozotron.com 442 | a1426.bozotron.com 443 | a1427.bozotron.com 444 | a1428.bozotron.com 445 | a1429.bozotron.com 446 | a1430.bozotron.com 447 | a1431.bozotron.com 448 | a1432.bozotron.com 449 | a1433.bozotron.com 450 | a1434.bozotron.com 451 | a1435.bozotron.com 452 | a1436.bozotron.com 453 | a1437.bozotron.com 454 | a1438.bozotron.com 455 | a1439.bozotron.com 456 | a1440.bozotron.com 457 | a1441.bozotron.com 458 | a1442.bozotron.com 459 | a1443.bozotron.com 460 | a1444.bozotron.com 461 | a1445.bozotron.com 462 | a1446.bozotron.com 463 | a1447.bozotron.com 464 | a1448.bozotron.com 465 | a1449.bozotron.com 466 | a1450.bozotron.com 467 | a1451.bozotron.com 468 | a1452.bozotron.com 469 | a1453.bozotron.com 470 | a1454.bozotron.com 471 | a1455.bozotron.com 472 | a1456.bozotron.com 473 | a1457.bozotron.com 474 | a1458.bozotron.com 475 | a1459.bozotron.com 476 | a1460.bozotron.com 477 | 478 | [dc12-as] 479 | a010.bozotron.com 480 | a011.bozotron.com 481 | a012.bozotron.com 482 | a013.bozotron.com 483 | a014.bozotron.com 484 | a015.bozotron.com 485 | a016.bozotron.com 486 | a017.bozotron.com 487 | a018.bozotron.com 488 | a019.bozotron.com 489 | a020.bozotron.com 490 | a021.bozotron.com 491 | a022.bozotron.com 492 | a023.bozotron.com 493 | a024.bozotron.com 494 | a025.bozotron.com 495 | a026.bozotron.com 496 | a027.bozotron.com 497 | a028.bozotron.com 498 | a029.bozotron.com 499 | a030.bozotron.com 500 | a031.bozotron.com 501 | a032.bozotron.com 502 | a033.bozotron.com 503 | a034.bozotron.com 504 | a035.bozotron.com 505 | a036.bozotron.com 506 | a037.bozotron.com 507 | a038.bozotron.com 508 | a039.bozotron.com 509 | a040.bozotron.com 510 | a041.bozotron.com 511 | a042.bozotron.com 512 | a043.bozotron.com 513 | a044.bozotron.com 514 | a045.bozotron.com 515 | a046.bozotron.com 516 | a050.bozotron.com 517 | a054.bozotron.com 518 | a058.bozotron.com 519 | a074.bozotron.com 520 | a075.bozotron.com 521 | a088.bozotron.com 522 | a091.bozotron.com 523 | a095.bozotron.com 524 | a096.bozotron.com 525 | a097.bozotron.com 526 | a098.bozotron.com 527 | a099.bozotron.com 528 | a101.bozotron.com 529 | a119.bozotron.com 530 | a121.bozotron.com 531 | a123.bozotron.com 532 | a124.bozotron.com 533 | a126.bozotron.com 534 | a127.bozotron.com 535 | a128.bozotron.com 536 | a129.bozotron.com 537 | a131.bozotron.com 538 | a132.bozotron.com 539 | a133.bozotron.com 540 | a134.bozotron.com 541 | a136.bozotron.com 542 | a137.bozotron.com 543 | a138.bozotron.com 544 | a139.bozotron.com 545 | a140.bozotron.com 546 | a141.bozotron.com 547 | a143.bozotron.com 548 | a144.bozotron.com 549 | a145.bozotron.com 550 | a146.bozotron.com 551 | a147.bozotron.com 552 | a149.bozotron.com 553 | a150.bozotron.com 554 | a151.bozotron.com 555 | a152.bozotron.com 556 | a153.bozotron.com 557 | a156.bozotron.com 558 | a157.bozotron.com 559 | a158.bozotron.com 560 | a159.bozotron.com 561 | a160.bozotron.com 562 | a161.bozotron.com 563 | a166.bozotron.com 564 | a169.bozotron.com 565 | a170.bozotron.com 566 | a172.bozotron.com 567 | a178.bozotron.com 568 | a179.bozotron.com 569 | a182.bozotron.com 570 | a186.bozotron.com 571 | a187.bozotron.com 572 | a188.bozotron.com 573 | a189.bozotron.com 574 | a190.bozotron.com 575 | a192.bozotron.com 576 | a195.bozotron.com 577 | a200.bozotron.com 578 | a201.bozotron.com 579 | a461.bozotron.com 580 | a462.bozotron.com 581 | a463.bozotron.com 582 | a464.bozotron.com 583 | a465.bozotron.com 584 | a466.bozotron.com 585 | a467.bozotron.com 586 | a468.bozotron.com 587 | a469.bozotron.com 588 | a470.bozotron.com 589 | a471.bozotron.com 590 | a472.bozotron.com 591 | a473.bozotron.com 592 | a474.bozotron.com 593 | a475.bozotron.com 594 | a476.bozotron.com 595 | a477.bozotron.com 596 | a478.bozotron.com 597 | a479.bozotron.com 598 | a480.bozotron.com 599 | 600 | [dc21-as] 601 | a3401.bozotron.com 602 | a3402.bozotron.com 603 | a3403.bozotron.com 604 | a3404.bozotron.com 605 | a3405.bozotron.com 606 | a3406.bozotron.com 607 | a3407.bozotron.com 608 | a3408.bozotron.com 609 | a3409.bozotron.com 610 | a3410.bozotron.com 611 | a3411.bozotron.com 612 | a3412.bozotron.com 613 | a3413.bozotron.com 614 | a3414.bozotron.com 615 | a3415.bozotron.com 616 | a3416.bozotron.com 617 | a3417.bozotron.com 618 | a3418.bozotron.com 619 | a3419.bozotron.com 620 | a3420.bozotron.com 621 | a3421.bozotron.com 622 | a3422.bozotron.com 623 | a3423.bozotron.com 624 | a3424.bozotron.com 625 | a3425.bozotron.com 626 | a3426.bozotron.com 627 | a3427.bozotron.com 628 | a3428.bozotron.com 629 | a3429.bozotron.com 630 | a3430.bozotron.com 631 | a3431.bozotron.com 632 | a3432.bozotron.com 633 | a3433.bozotron.com 634 | a3434.bozotron.com 635 | a3435.bozotron.com 636 | a3436.bozotron.com 637 | a3437.bozotron.com 638 | a3438.bozotron.com 639 | a3439.bozotron.com 640 | a3440.bozotron.com 641 | a3441.bozotron.com 642 | a3442.bozotron.com 643 | a3443.bozotron.com 644 | a3444.bozotron.com 645 | a3445.bozotron.com 646 | a3446.bozotron.com 647 | a3447.bozotron.com 648 | a3448.bozotron.com 649 | a3449.bozotron.com 650 | a3450.bozotron.com 651 | a3451.bozotron.com 652 | a3452.bozotron.com 653 | a3453.bozotron.com 654 | a3454.bozotron.com 655 | a3455.bozotron.com 656 | a3456.bozotron.com 657 | a3457.bozotron.com 658 | a3458.bozotron.com 659 | a3459.bozotron.com 660 | a3460.bozotron.com 661 | a3461.bozotron.com 662 | a3462.bozotron.com 663 | a3463.bozotron.com 664 | a3464.bozotron.com 665 | a3465.bozotron.com 666 | a3466.bozotron.com 667 | a3467.bozotron.com 668 | a3468.bozotron.com 669 | a3469.bozotron.com 670 | a3470.bozotron.com 671 | a3471.bozotron.com 672 | a3472.bozotron.com 673 | a3473.bozotron.com 674 | a3474.bozotron.com 675 | a3475.bozotron.com 676 | a3476.bozotron.com 677 | a3477.bozotron.com 678 | a3478.bozotron.com 679 | a3479.bozotron.com 680 | a3480.bozotron.com 681 | a3481.bozotron.com 682 | a3482.bozotron.com 683 | a3483.bozotron.com 684 | a3484.bozotron.com 685 | a3485.bozotron.com 686 | a3486.bozotron.com 687 | a3487.bozotron.com 688 | a3488.bozotron.com 689 | a3489.bozotron.com 690 | a3490.bozotron.com 691 | a3491.bozotron.com 692 | a3492.bozotron.com 693 | a3493.bozotron.com 694 | a3494.bozotron.com 695 | a3495.bozotron.com 696 | a3496.bozotron.com 697 | a3497.bozotron.com 698 | a3498.bozotron.com 699 | a3499.bozotron.com 700 | a3500.bozotron.com 701 | a3501.bozotron.com 702 | a3502.bozotron.com 703 | a3503.bozotron.com 704 | a3504.bozotron.com 705 | a3505.bozotron.com 706 | a3506.bozotron.com 707 | a3507.bozotron.com 708 | a3508.bozotron.com 709 | a3509.bozotron.com 710 | a3510.bozotron.com 711 | a3511.bozotron.com 712 | a3512.bozotron.com 713 | a3513.bozotron.com 714 | a3514.bozotron.com 715 | a3515.bozotron.com 716 | a3516.bozotron.com 717 | a3517.bozotron.com 718 | a3518.bozotron.com 719 | a3519.bozotron.com 720 | a3520.bozotron.com 721 | a3521.bozotron.com 722 | a3522.bozotron.com 723 | a3523.bozotron.com 724 | a3524.bozotron.com 725 | a3525.bozotron.com 726 | a3526.bozotron.com 727 | a3527.bozotron.com 728 | a3528.bozotron.com 729 | a3529.bozotron.com 730 | a3530.bozotron.com 731 | a3531.bozotron.com 732 | a3532.bozotron.com 733 | a3533.bozotron.com 734 | a3534.bozotron.com 735 | a3535.bozotron.com 736 | a3536.bozotron.com 737 | a3537.bozotron.com 738 | a3538.bozotron.com 739 | a3539.bozotron.com 740 | a3540.bozotron.com 741 | a3541.bozotron.com 742 | a3542.bozotron.com 743 | a3543.bozotron.com 744 | a3544.bozotron.com 745 | a3545.bozotron.com 746 | a3546.bozotron.com 747 | a3547.bozotron.com 748 | a3548.bozotron.com 749 | a3549.bozotron.com 750 | a3550.bozotron.com 751 | a3791.bozotron.com 752 | a3792.bozotron.com 753 | a3793.bozotron.com 754 | a3794.bozotron.com 755 | a3795.bozotron.com 756 | a3796.bozotron.com 757 | a3797.bozotron.com 758 | a3798.bozotron.com 759 | a3799.bozotron.com 760 | a3800.bozotron.com 761 | a3801.bozotron.com 762 | a3802.bozotron.com 763 | a3803.bozotron.com 764 | a3804.bozotron.com 765 | a3805.bozotron.com 766 | a3806.bozotron.com 767 | a3807.bozotron.com 768 | a3808.bozotron.com 769 | a3809.bozotron.com 770 | a3810.bozotron.com 771 | a3811.bozotron.com 772 | a3812.bozotron.com 773 | a3813.bozotron.com 774 | a3814.bozotron.com 775 | a3815.bozotron.com 776 | a3816.bozotron.com 777 | a3817.bozotron.com 778 | a3818.bozotron.com 779 | a3819.bozotron.com 780 | a3820.bozotron.com 781 | a3821.bozotron.com 782 | a3822.bozotron.com 783 | a3823.bozotron.com 784 | a3824.bozotron.com 785 | a3825.bozotron.com 786 | a3826.bozotron.com 787 | a3827.bozotron.com 788 | a3828.bozotron.com 789 | a3829.bozotron.com 790 | a3830.bozotron.com 791 | a3831.bozotron.com 792 | a3832.bozotron.com 793 | a3833.bozotron.com 794 | a3834.bozotron.com 795 | a3835.bozotron.com 796 | a3836.bozotron.com 797 | a3837.bozotron.com 798 | a3838.bozotron.com 799 | a3839.bozotron.com 800 | a3840.bozotron.com 801 | a3841.bozotron.com 802 | a3842.bozotron.com 803 | a3843.bozotron.com 804 | a3844.bozotron.com 805 | a3845.bozotron.com 806 | a3846.bozotron.com 807 | a3847.bozotron.com 808 | a3848.bozotron.com 809 | a3849.bozotron.com 810 | a3850.bozotron.com 811 | a3851.bozotron.com 812 | a3852.bozotron.com 813 | a3853.bozotron.com 814 | a3854.bozotron.com 815 | a3855.bozotron.com 816 | a3856.bozotron.com 817 | a3857.bozotron.com 818 | a3858.bozotron.com 819 | a3859.bozotron.com 820 | a3860.bozotron.com 821 | a3861.bozotron.com 822 | a3862.bozotron.com 823 | a3863.bozotron.com 824 | a3864.bozotron.com 825 | a3865.bozotron.com 826 | a3866.bozotron.com 827 | a3867.bozotron.com 828 | a3868.bozotron.com 829 | a3869.bozotron.com 830 | a3870.bozotron.com 831 | a3871.bozotron.com 832 | a3872.bozotron.com 833 | a3873.bozotron.com 834 | a3874.bozotron.com 835 | a3875.bozotron.com 836 | a3876.bozotron.com 837 | a3877.bozotron.com 838 | a3878.bozotron.com 839 | a3879.bozotron.com 840 | a3880.bozotron.com 841 | 842 | [dc22-as:children] 843 | vip-dc22-1 844 | vip-dc22-2 845 | 846 | [vip-dc22-1] 847 | a3551.bozotron.com 848 | a3552.bozotron.com 849 | a3553.bozotron.com 850 | a3554.bozotron.com 851 | a3555.bozotron.com 852 | a3556.bozotron.com 853 | a3557.bozotron.com 854 | a3558.bozotron.com 855 | a3559.bozotron.com 856 | a3560.bozotron.com 857 | a3561.bozotron.com 858 | a3562.bozotron.com 859 | a3563.bozotron.com 860 | a3564.bozotron.com 861 | a3565.bozotron.com 862 | a3566.bozotron.com 863 | a3567.bozotron.com 864 | a3568.bozotron.com 865 | a3569.bozotron.com 866 | a3570.bozotron.com 867 | a3571.bozotron.com 868 | a3572.bozotron.com 869 | a3573.bozotron.com 870 | a3574.bozotron.com 871 | a3575.bozotron.com 872 | a3576.bozotron.com 873 | a3577.bozotron.com 874 | a3578.bozotron.com 875 | a3579.bozotron.com 876 | a3580.bozotron.com 877 | a3581.bozotron.com 878 | a3582.bozotron.com 879 | a3583.bozotron.com 880 | a3584.bozotron.com 881 | a3585.bozotron.com 882 | a3586.bozotron.com 883 | a3587.bozotron.com 884 | a3588.bozotron.com 885 | a3589.bozotron.com 886 | a3590.bozotron.com 887 | a3591.bozotron.com 888 | a3592.bozotron.com 889 | a3593.bozotron.com 890 | a3594.bozotron.com 891 | a3595.bozotron.com 892 | a3596.bozotron.com 893 | a3597.bozotron.com 894 | a3598.bozotron.com 895 | a3599.bozotron.com 896 | a3600.bozotron.com 897 | a3601.bozotron.com 898 | a3602.bozotron.com 899 | a3603.bozotron.com 900 | a3604.bozotron.com 901 | a3605.bozotron.com 902 | a3606.bozotron.com 903 | a3607.bozotron.com 904 | a3608.bozotron.com 905 | a3609.bozotron.com 906 | a3610.bozotron.com 907 | a3611.bozotron.com 908 | a3612.bozotron.com 909 | a3613.bozotron.com 910 | a3614.bozotron.com 911 | a3615.bozotron.com 912 | a3616.bozotron.com 913 | a3617.bozotron.com 914 | a3618.bozotron.com 915 | a3619.bozotron.com 916 | a3620.bozotron.com 917 | a3621.bozotron.com 918 | a3622.bozotron.com 919 | a3623.bozotron.com 920 | a3624.bozotron.com 921 | a3625.bozotron.com 922 | a3626.bozotron.com 923 | a3627.bozotron.com 924 | a3628.bozotron.com 925 | a3629.bozotron.com 926 | a3630.bozotron.com 927 | a3631.bozotron.com 928 | a3632.bozotron.com 929 | a3633.bozotron.com 930 | a3634.bozotron.com 931 | a3635.bozotron.com 932 | a3636.bozotron.com 933 | a3637.bozotron.com 934 | a3638.bozotron.com 935 | a3639.bozotron.com 936 | a3640.bozotron.com 937 | a3641.bozotron.com 938 | a3642.bozotron.com 939 | a3643.bozotron.com 940 | a3644.bozotron.com 941 | a3645.bozotron.com 942 | a3646.bozotron.com 943 | a3647.bozotron.com 944 | a3648.bozotron.com 945 | a3649.bozotron.com 946 | a3650.bozotron.com 947 | a3651.bozotron.com 948 | a3652.bozotron.com 949 | a3653.bozotron.com 950 | a3654.bozotron.com 951 | a3655.bozotron.com 952 | a3656.bozotron.com 953 | a3657.bozotron.com 954 | a3658.bozotron.com 955 | a3659.bozotron.com 956 | a3660.bozotron.com 957 | a3661.bozotron.com 958 | a3662.bozotron.com 959 | a3663.bozotron.com 960 | a3664.bozotron.com 961 | a3665.bozotron.com 962 | a3666.bozotron.com 963 | a3667.bozotron.com 964 | a3668.bozotron.com 965 | a3669.bozotron.com 966 | a3670.bozotron.com 967 | 968 | [vip-dc22-2] 969 | a3671.bozotron.com 970 | a3672.bozotron.com 971 | a3673.bozotron.com 972 | a3674.bozotron.com 973 | a3675.bozotron.com 974 | a3676.bozotron.com 975 | a3677.bozotron.com 976 | a3678.bozotron.com 977 | a3679.bozotron.com 978 | a3680.bozotron.com 979 | a3681.bozotron.com 980 | a3682.bozotron.com 981 | a3683.bozotron.com 982 | a3684.bozotron.com 983 | a3685.bozotron.com 984 | a3686.bozotron.com 985 | a3687.bozotron.com 986 | a3688.bozotron.com 987 | a3689.bozotron.com 988 | a3690.bozotron.com 989 | a3691.bozotron.com 990 | a3692.bozotron.com 991 | a3693.bozotron.com 992 | a3694.bozotron.com 993 | a3695.bozotron.com 994 | a3696.bozotron.com 995 | a3697.bozotron.com 996 | a3698.bozotron.com 997 | a3699.bozotron.com 998 | a3700.bozotron.com 999 | a3701.bozotron.com 1000 | a3702.bozotron.com 1001 | a3703.bozotron.com 1002 | a3704.bozotron.com 1003 | a3705.bozotron.com 1004 | a3706.bozotron.com 1005 | a3707.bozotron.com 1006 | a3708.bozotron.com 1007 | a3709.bozotron.com 1008 | a3710.bozotron.com 1009 | a3711.bozotron.com 1010 | a3712.bozotron.com 1011 | a3713.bozotron.com 1012 | a3714.bozotron.com 1013 | a3715.bozotron.com 1014 | a3716.bozotron.com 1015 | a3717.bozotron.com 1016 | a3718.bozotron.com 1017 | a3719.bozotron.com 1018 | a3720.bozotron.com 1019 | a3721.bozotron.com 1020 | a3722.bozotron.com 1021 | a3723.bozotron.com 1022 | a3724.bozotron.com 1023 | a3725.bozotron.com 1024 | a3726.bozotron.com 1025 | a3727.bozotron.com 1026 | a3728.bozotron.com 1027 | a3729.bozotron.com 1028 | a3730.bozotron.com 1029 | a3731.bozotron.com 1030 | a3732.bozotron.com 1031 | a3733.bozotron.com 1032 | a3734.bozotron.com 1033 | a3735.bozotron.com 1034 | a3736.bozotron.com 1035 | a3737.bozotron.com 1036 | a3738.bozotron.com 1037 | a3739.bozotron.com 1038 | a3740.bozotron.com 1039 | a3741.bozotron.com 1040 | a3742.bozotron.com 1041 | a3743.bozotron.com 1042 | a3744.bozotron.com 1043 | a3745.bozotron.com 1044 | a3746.bozotron.com 1045 | a3747.bozotron.com 1046 | a3748.bozotron.com 1047 | a3749.bozotron.com 1048 | a3750.bozotron.com 1049 | a3751.bozotron.com 1050 | a3752.bozotron.com 1051 | a3753.bozotron.com 1052 | a3754.bozotron.com 1053 | a3755.bozotron.com 1054 | a3756.bozotron.com 1055 | a3757.bozotron.com 1056 | a3758.bozotron.com 1057 | a3759.bozotron.com 1058 | a3760.bozotron.com 1059 | a3761.bozotron.com 1060 | a3762.bozotron.com 1061 | a3763.bozotron.com 1062 | a3764.bozotron.com 1063 | a3765.bozotron.com 1064 | a3766.bozotron.com 1065 | a3767.bozotron.com 1066 | a3768.bozotron.com 1067 | a3769.bozotron.com 1068 | a3770.bozotron.com 1069 | a3771.bozotron.com 1070 | a3772.bozotron.com 1071 | a3773.bozotron.com 1072 | a3774.bozotron.com 1073 | a3775.bozotron.com 1074 | a3776.bozotron.com 1075 | a3777.bozotron.com 1076 | a3778.bozotron.com 1077 | a3779.bozotron.com 1078 | a3780.bozotron.com 1079 | a3781.bozotron.com 1080 | a3782.bozotron.com 1081 | a3783.bozotron.com 1082 | a3784.bozotron.com 1083 | a3785.bozotron.com 1084 | a3786.bozotron.com 1085 | a3787.bozotron.com 1086 | a3788.bozotron.com 1087 | a3789.bozotron.com 1088 | a3790.bozotron.com 1089 | 1090 | [dc1-stats] 1091 | p1s9.dcauxbozotron.com 1092 | p1s10.dcauxbozotron.com 1093 | p1s11.dcauxbozotron.com 1094 | p1s12.dcauxbozotron.com 1095 | p1s13.dcauxbozotron.com 1096 | p1s14.dcauxbozotron.com 1097 | p1s15.dcauxbozotron.com 1098 | p1s16.dcauxbozotron.com 1099 | 1100 | [dc2-stats] 1101 | p1s49.dcauxbozotron.com 1102 | p1s50.dcauxbozotron.com 1103 | p1s51.dcauxbozotron.com 1104 | p1s52.dcauxbozotron.com 1105 | p1s53.dcauxbozotron.com 1106 | p1s54.dcauxbozotron.com 1107 | p1s55.dcauxbozotron.com 1108 | p1s56.dcauxbozotron.com 1109 | p1s57.dcauxbozotron.com 1110 | p1s58.dcauxbozotron.com 1111 | p1s59.dcauxbozotron.com 1112 | p1s60.dcauxbozotron.com 1113 | p1s61.dcauxbozotron.com 1114 | p1s62.dcauxbozotron.com 1115 | p1s63.dcauxbozotron.com 1116 | p1s64.dcauxbozotron.com 1117 | 1118 | [dc12-stats] 1119 | p1s1.dcauxbozotron.com 1120 | p1s2.dcauxbozotron.com 1121 | p1s3.dcauxbozotron.com 1122 | p1s4.dcauxbozotron.com 1123 | p1s5.dcauxbozotron.com 1124 | p1s6.dcauxbozotron.com 1125 | p1s7.dcauxbozotron.com 1126 | p1s8.dcauxbozotron.com 1127 | 1128 | [dc21-stats] 1129 | p1s17.dcauxbozotron.com 1130 | p1s18.dcauxbozotron.com 1131 | p1s19.dcauxbozotron.com 1132 | p1s20.dcauxbozotron.com 1133 | p1s21.dcauxbozotron.com 1134 | p1s22.dcauxbozotron.com 1135 | p1s23.dcauxbozotron.com 1136 | p1s24.dcauxbozotron.com 1137 | p1s25.dcauxbozotron.com 1138 | p1s26.dcauxbozotron.com 1139 | p1s43.dcauxbozotron.com 1140 | p1s44.dcauxbozotron.com 1141 | p1s45.dcauxbozotron.com 1142 | p1s46.dcauxbozotron.com 1143 | p1s47.dcauxbozotron.com 1144 | p1s48.dcauxbozotron.com 1145 | 1146 | [dc22-stats] 1147 | p1s27.dcauxbozotron.com 1148 | p1s28.dcauxbozotron.com 1149 | p1s29.dcauxbozotron.com 1150 | p1s30.dcauxbozotron.com 1151 | p1s31.dcauxbozotron.com 1152 | p1s32.dcauxbozotron.com 1153 | p1s33.dcauxbozotron.com 1154 | p1s34.dcauxbozotron.com 1155 | p1s35.dcauxbozotron.com 1156 | p1s36.dcauxbozotron.com 1157 | p1s37.dcauxbozotron.com 1158 | p1s38.dcauxbozotron.com 1159 | p1s39.dcauxbozotron.com 1160 | p1s40.dcauxbozotron.com 1161 | p1s41.dcauxbozotron.com 1162 | p1s42.dcauxbozotron.com 1163 | 1164 | [dc1-dcapp] 1165 | p1a1.dcauxbozotron.com #app1dc01.bozotron.com 1166 | 1167 | [dc2-dcapp] 1168 | p1a2.dcauxbozotron.com #app1dc02.bozotron.com 1169 | 1170 | [dc12-dcapp] 1171 | p1a1.dcauxbozotron.com #app1dc12.bozotron.com 1172 | 1173 | [dc21-dcapp] 1174 | p1a4.dcauxbozotron.com 1175 | 1176 | [dc22-dcapp] 1177 | p1a4.dcauxbozotron.com 1178 | -------------------------------------------------------------------------------- /testdata/inventory/vault.key: -------------------------------------------------------------------------------- 1 | 7f017fde-e88b-42c5-89df-a7c8f9de981d 2 | -------------------------------------------------------------------------------- /testdata/inventory/vault.yml: -------------------------------------------------------------------------------- 1 | $ANSIBLE_VAULT;1.1;AES256 2 | 32326463356266366337623135303463636434316263643961616531303139666333623463636665 3 | 3561383561623735303533373734626234303165383538620a626637353365613633303439306136 4 | 62336638326562336538396165396662356465396633646664313263636164636437386137356461 5 | 6165316437396133640a303261373437356539666638613333373035663961393135613939316165 6 | 35653032316630383935653963366265313136663933333732653233373633383138643661616538 7 | 36626461633266626561333437623132633461666537303833313931306366633164623339363237 8 | 31666133326431383536643639333562333938356535363330613565616230623031323830626564 9 | 35346533643066653231356662303134623266356631386331303939663964316639303362343739 10 | 30613862663935623036396536363739633966353136633161313738376662653239643964316631 11 | 61616137666438643363363836393663396130343562316638613331623464613532336333306639 12 | 63343938313862623765303838383633646135313866656664616336623734343461653361333531 13 | 36663564363364663332613030383161366666383830383730613763396665353633623863633439 14 | 63336632333566646138656637636265626531373536376330393161313131656261653439633338 15 | 33313534323733323739613537656462323031393731383864323965353933643262366239626362 16 | 39656630336563616463643135333065303232633363373932376539316562323934633939383864 17 | 36363136366636663165303837653138383932653436343238333066313663636463396163613464 18 | 30333133663537303339373236326438396638373962646133623561383663646436373763383836 19 | 65633263326565373834626532656666633837363236666137663730396161303965663532323036 20 | 61313131656332393939333761333535653162353538376332626665343538633865313139613836 21 | 63643163633338613438643935316335396535626363623737633534663632366330613135643639 22 | 36633765363430663036656263393162333336383266366234396661373130363939373862656139 23 | 64643862306431396533663439633166626132343530363739646331666235313337633237613638 24 | 66386237633935343264643832313432636361316366313232663236393736323862373034376264 25 | 33353865376161636265666235626636666532623335386531313266393532653138636532373339 26 | 38303332633933633263323862303136356365636330373636346162353361356163303861386165 27 | 62316361393533363239323262666434346338393437643533303138343134396463303631386433 28 | 34353463303835376632333238386665356637383131373733613830613266386362323463393737 29 | 36323537646432306131616530313566323236663430643561343465333861363930633563343135 30 | 38633562643234353764663461303131656230633638373034303436356630633066613563333637 31 | 30636338346539313536346431373037636634353465663732363536623664303230666565376434 32 | 30356132363731646534626662373039643665313532346531396166333065643562623439313434 33 | 34386238363336663563633337643834633639653035303134303233333939323632333532333365 34 | 3831 35 | --------------------------------------------------------------------------------