Compare commits
392 Commits
threading_
...
stable-2.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 611d34acf3 | |||
| e72b41d3eb | |||
| ebae05adf5 | |||
| b08dc0c056 | |||
| 90623b1ffb | |||
| 0b16e9257a | |||
| df14dceb1f | |||
| 6c82b90587 | |||
| 5d0ccb68af | |||
| 672a8b3ec9 | |||
| 4d585c9035 | |||
| d189b668cb | |||
| 55339cd0b4 | |||
| df551246a3 | |||
| 6e3f5404f9 | |||
| 6e474bb995 | |||
| 9adfb2d6f5 | |||
| 0cd22c730c | |||
| 9e6fc2b182 | |||
| f5be18f409 | |||
| da260cb013 | |||
| f0e348f5ee | |||
| f779227859 | |||
| 341ffef08e | |||
| d600266d52 | |||
| 0feb231a9c | |||
| 8899f5e068 | |||
| ad6ccf8ea9 | |||
| 7b6557a23b | |||
| 7cc351a237 | |||
| 34511d1fc9 | |||
| 3f8c56a7e3 | |||
| b12b5c376d | |||
| 792832b53e | |||
| 6ee3a42673 | |||
| 7ff9fa52cf | |||
| 4a852ec287 | |||
| df3e71a83f | |||
| 42afc7dc45 | |||
| 04f65a4d29 | |||
| 9b71a6a0b0 | |||
| 972660968a | |||
| 2273800f7c | |||
| 31300bd36b | |||
| 147c29f0e9 | |||
| de331a9488 | |||
| 36b462ba85 | |||
| b33c41dc5f | |||
| 8b8f0597ed | |||
| 3ba057d28d | |||
| ac4ce95810 | |||
| dae298e429 | |||
| c40d9e63bb | |||
| 75c04b6773 | |||
| 6ca528aea4 | |||
| 1a6e27a6ac | |||
| d45f2d3288 | |||
| e9e39e4fd6 | |||
| 1eda29bfa8 | |||
| 6791061395 | |||
| 694011a897 | |||
| cfd57fcae2 | |||
| 097dbb2daf | |||
| c459f87a42 | |||
| 547dcf4b9e | |||
| f47356f5c7 | |||
| fe3ede881d | |||
| 5dcce0666a | |||
| c92ce0c2ca | |||
| 1262e5fdca | |||
| cb93ecaef9 | |||
| 6176c95838 | |||
| ef24d56c8f | |||
| f1217a9b94 | |||
| 9029547603 | |||
| 81e96c754a | |||
| c4b09cbcb4 | |||
| ef1ecfd2c2 | |||
| 39acdde5aa | |||
| 7b2fcb2d4e | |||
| 4e257fad84 | |||
| 3551de98c6 | |||
| a0104cfe81 | |||
| 5f5d143194 | |||
| 04b2e1fbc0 | |||
| 65ee9d2e46 | |||
| 75de5737f7 | |||
| 21106cc95b | |||
| 89fdca0929 | |||
| 7164956cc6 | |||
| 0240ffe220 | |||
| db22594955 | |||
| 9e9d202d6f | |||
| b2942f1084 | |||
| 26f5e4b32d | |||
| e85f3f4461 | |||
| f4ecbc4c3f | |||
| b12256acf2 | |||
| f649a87a19 | |||
| 2f0dba4f36 | |||
| 6a9572e1e5 | |||
| 5362910000 | |||
| 009ac7b65e | |||
| c6ef74d81b | |||
| 0c153146e3 | |||
| 86eadc5814 | |||
| 4b495d7e43 | |||
| c846e915f8 | |||
| a93e7506a8 | |||
| 49fc0cfe4f | |||
| d1c5a39420 | |||
| 06ed25e788 | |||
| b3daa9dd64 | |||
| 6dee2b21e6 | |||
| cc4634a5e7 | |||
| eb8c26c105 | |||
| 240c388e6c | |||
| 941552d107 | |||
| d8c9b8d347 | |||
| ec84ff6de6 | |||
| 56de9d8ae7 | |||
| 3e2f6e5094 | |||
| 577ea88f78 | |||
| 7e10994b6d | |||
| f6a3c4f071 | |||
| 8439d1813e | |||
| e223349edc | |||
| b25c06725a | |||
| 22f7ca8c97 | |||
| a0a2392c87 | |||
| 3c4ac877f6 | |||
| 38bf7ab71f | |||
| 47e16bef08 | |||
| e05222a4cc | |||
| 32dad09d2e | |||
| a04d0f485b | |||
| 7683715caf | |||
| 3c7987f3a4 | |||
| 3e72e0c173 | |||
| 10a3053525 | |||
| 29762c87c8 | |||
| 477043c422 | |||
| f89abc705b | |||
| 2d8ebbfe8c | |||
| e1b459470d | |||
| 1bdf25561a | |||
| b1e44d1195 | |||
| 8ad67b44dc | |||
| cfbb58adae | |||
| f90a6439c4 | |||
| d8449b3013 | |||
| 1168524f22 | |||
| 99472c42e3 | |||
| 00378515e2 | |||
| 2639016847 | |||
| be07fcc6d9 | |||
| 268645c17a | |||
| e715221a66 | |||
| fe33c937c4 | |||
| cbf1f23e7e | |||
| 0db1c77041 | |||
| ddc8d3d988 | |||
| 3beac89893 | |||
| abc0eeac02 | |||
| 886f8d224e | |||
| 76be9aa693 | |||
| acad2ba246 | |||
| 47cd4867a1 | |||
| 69301f2823 | |||
| 6025e97d13 | |||
| 01fa3d3024 | |||
| 18aba3ebec | |||
| b15e1b743f | |||
| 97444c5e59 | |||
| 4ef8493a11 | |||
| 4854705267 | |||
| ff60245e2b | |||
| a7abe4be19 | |||
| faaabec397 | |||
| 288f6684cf | |||
| b878e8f0f0 | |||
| 4d7760c0b1 | |||
| b9a1b2836a | |||
| 8dee7f3138 | |||
| f4a2332d48 | |||
| a60a7279d0 | |||
| f1f6752686 | |||
| 42e0efbbbf | |||
| ac076dfc12 | |||
| 0bed5d4d85 | |||
| b39f48121d | |||
| bf8902f371 | |||
| 27be8a1022 | |||
| f62224497e | |||
| 529adb574a | |||
| 528426ce0c | |||
| a34793d7fb | |||
| a91788e25a | |||
| 74bb122598 | |||
| 66779698f0 | |||
| d4e8cdc84f | |||
| ead92bee3d | |||
| 23b5764b57 | |||
| 3ce19f4c58 | |||
| 585c57fca4 | |||
| aed616ab31 | |||
| f6b47c53d7 | |||
| 8696ce9e00 | |||
| d637559825 | |||
| c2f9846278 | |||
| 35e198a616 | |||
| 0871d955fe | |||
| 159399fea4 | |||
| 47d5f0f0a8 | |||
| bc539adddc | |||
| ecbac4cf73 | |||
| 1a247de0b6 | |||
| 30d5d5fa67 | |||
| 39cb6797a3 | |||
| 6f5ec79e91 | |||
| 42fb088807 | |||
| 7602a2a030 | |||
| 413f6ab7f1 | |||
| 832cc5bb5b | |||
| 28883975bf | |||
| c23d99b786 | |||
| 0cde6fdaca | |||
| fad7f1de7c | |||
| 6bd4bec9de | |||
| debfb798dd | |||
| dfad25bd38 | |||
| b482cdcf03 | |||
| c920c8bc3b | |||
| 9499ed5360 | |||
| caba50c778 | |||
| 58d8a0fca0 | |||
| aaedf0bd73 | |||
| 366bfe14c3 | |||
| 780d2c4bc4 | |||
| 1cc26cf7cd | |||
| de7fbd407a | |||
| 9d81ad6423 | |||
| 5bf1269aaf | |||
| 3f25088bf0 | |||
| 09fc911a48 | |||
| 2b9659945c | |||
| 182943f3b3 | |||
| b229898f80 | |||
| 5c4a4703d9 | |||
| 06599f49eb | |||
| 3a577966ba | |||
| 6824b1ea1e | |||
| 00bdada50e | |||
| 32971e8639 | |||
| 70824e06b5 | |||
| e13f3e3c07 | |||
| 6adbc7d64a | |||
| 325bf617e9 | |||
| 2c572ba786 | |||
| 503537eb25 | |||
| cdec853e37 | |||
| 23812ab87d | |||
| 1f80e35312 | |||
| c07f6d1bdd | |||
| d559355b29 | |||
| 219a20277f | |||
| 3de9d8373b | |||
| 9d4ce0a94e | |||
| c5d4134f37 | |||
| 95a8bbdbda | |||
| 1ebc94f290 | |||
| a2df07ade3 | |||
| 6b603b026c | |||
| deb1e3ebc7 | |||
| 79e43925b1 | |||
| 0eb23f5a86 | |||
| 066a360a36 | |||
| 731422a6dc | |||
| 96d3f06743 | |||
| eafb4043c9 | |||
| 35938b907d | |||
| bab1ac1d5c | |||
| f3fc029726 | |||
| e4ebe721f5 | |||
| 2fa12438dd | |||
| 9d82a3aa0c | |||
| fef9de30d9 | |||
| 5169252641 | |||
| 4d5368e93b | |||
| 02ed599035 | |||
| bf503e4ff2 | |||
| 9022862624 | |||
| 6c118252b6 | |||
| e97a00de9e | |||
| f5240d2953 | |||
| 92c851a894 | |||
| 84485c29ee | |||
| 806fc1ac74 | |||
| b702d3810e | |||
| 8c6d749ad9 | |||
| 8dbc564fc6 | |||
| e8c97768b7 | |||
| 90d3824678 | |||
| 5bd6a9b76c | |||
| a15d3106e9 | |||
| 3ee4effb7a | |||
| 90b06bc8b4 | |||
| b6e51d670a | |||
| dcc6a15ce3 | |||
| 25e4398d5b | |||
| f15ec38788 | |||
| ddbc01dfe5 | |||
| 6e36d1899c | |||
| ba8e1f88a9 | |||
| 1dfa0e06eb | |||
| e9b7d42205 | |||
| d2998f0811 | |||
| fa8f9e9ead | |||
| 257182e46a | |||
| bce9bfce51 | |||
| cc91c34f36 | |||
| a0a4d0e3f4 | |||
| b9e8aa72be | |||
| fb921042db | |||
| 885b218a7b | |||
| c5b155ba1a | |||
| f6295677c9 | |||
| 1e54f424ec | |||
| b373f67368 | |||
| 924e0726df | |||
| 69ff46b8ca | |||
| 19516d8c19 | |||
| 531023ad1f | |||
| 1af1cca59f | |||
| dde882c91f | |||
| 7ee14f466e | |||
| 9a9f767857 | |||
| d4b8178b4b | |||
| 141ed26e02 | |||
| 5bf850568a | |||
| 24d7555d0b | |||
| d3dd82f3cf | |||
| e1101f78bd | |||
| 3d3ebbf98e | |||
| 261013354f | |||
| 15f4b83564 | |||
| 747a5ef791 | |||
| c1a34b5eff | |||
| b8c25d8f70 | |||
| 05dfed7575 | |||
| b6e317c045 | |||
| a94db01b89 | |||
| 6d909bd65c | |||
| ab3d4731a3 | |||
| ed9d0cdf4a | |||
| cb8c28870c | |||
| 2accc28d14 | |||
| d8155cc4fa | |||
| edff94f96d | |||
| 1acd258931 | |||
| 7034a34ce4 | |||
| 4cd32ee1ac | |||
| 9f4a656929 | |||
| 3db274ac21 | |||
| 3a822faeae | |||
| 3c9966d6fc | |||
| 2a7f728fdf | |||
| aafa7ab471 | |||
| f59430aba8 | |||
| 448cac16db | |||
| ede5eb78ab | |||
| 7063ed8ceb | |||
| be6396d5e9 | |||
| e4efe0b2f1 | |||
| dea2cabe94 | |||
| 08b646684b | |||
| 255b9364ab | |||
| 9596b9218c | |||
| 66ebe7a461 | |||
| 6a76d7fbef | |||
| b878c47d5e | |||
| f7d3ed6eb3 | |||
| 8456686f4b | |||
| fb4c0a085f | |||
| 6b85c31fdf | |||
| 767dba8f24 | |||
| 5109d50adb | |||
| f4b1d87ec0 | |||
| ae52943719 | |||
| 6ba009f913 | |||
| 52173e7707 | |||
| 44faad0593 |
27
.coveragerc
27
.coveragerc
@ -1,25 +1,4 @@
|
||||
# This configuration file is used for manual execution of coverage
|
||||
# as well as for tests run through ansible-test.
|
||||
|
||||
[run]
|
||||
branch = True
|
||||
|
||||
# Enable concurrency. This also enables parallel mode, which results in
|
||||
# multiple coverage files being created. Concurrency allows us to collect
|
||||
# results from multiple tests simultaneously, as well as supporting multiple
|
||||
# test runs, such as from integration tests.
|
||||
concurrency = multiprocessing
|
||||
parallel = True
|
||||
|
||||
# When running tests through ansible-test, this option is overridden by
|
||||
# the COVERAGE_FILE environment variable. This option is present for
|
||||
# convenience when running coverage manually from this directory.
|
||||
data_file = test/results/coverage/coverage
|
||||
|
||||
# Don't collect or report code coverage from files matching these patterns.
|
||||
[report]
|
||||
omit =
|
||||
*/python*/dist-packages/*
|
||||
*/python*/site-packages/*
|
||||
*/python*/distutils/*
|
||||
*/pyshared/*
|
||||
*/pytest
|
||||
*/python?.?/*
|
||||
*/site-packages/nose/*
|
||||
|
||||
3
.gitattributes
vendored
3
.gitattributes
vendored
@ -1,4 +1 @@
|
||||
CHANGELOG.md merge=union
|
||||
.github/ export-ignore
|
||||
hacking/ export-ignore
|
||||
ticket-stubs/ export-ignore
|
||||
|
||||
1117
.github/BOTMETA.yml
vendored
1117
.github/BOTMETA.yml
vendored
File diff suppressed because it is too large
Load Diff
27
.github/ISSUE_TEMPLATE.md
vendored
27
.github/ISSUE_TEMPLATE.md
vendored
@ -1,8 +1,14 @@
|
||||
<!---
|
||||
Verify first that your issue/request is not already reported on GitHub.
|
||||
Also test if the latest release, and master branch are affected too.
|
||||
Please do not report issues/requests related to Ansible modules here !!
|
||||
|
||||
Report them to the appropriate modules-core or modules-extras project:
|
||||
- https://github.com/ansible/ansible-modules-core/issues
|
||||
- https://github.com/ansible/ansible-modules-extras/issues
|
||||
|
||||
Also verify first that your issue/request is not already reported in GitHub
|
||||
-->
|
||||
|
||||
|
||||
##### ISSUE TYPE
|
||||
<!--- Pick one below and delete the rest: -->
|
||||
- Bug Report
|
||||
@ -10,29 +16,24 @@ Also test if the latest release, and master branch are affected too.
|
||||
- Documentation Report
|
||||
|
||||
##### COMPONENT NAME
|
||||
<!--- Name of the module/plugin/task/feature -->
|
||||
<!--- Name of the plugin/task/feature -->
|
||||
|
||||
##### ANSIBLE VERSION
|
||||
<!--- Paste verbatim output from "ansible --version" between quotes below -->
|
||||
<!--- Paste verbatim output from “ansible --version” between quotes below -->
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
##### CONFIGURATION
|
||||
<!---
|
||||
If using Ansible 2.4 or above, paste the results of "ansible-config dump --only-changed"
|
||||
|
||||
Otherwise, mention any settings you have changed/added/removed in ansible.cfg
|
||||
Mention any settings you have changed/added/removed in ansible.cfg
|
||||
(or using the ANSIBLE_* environment variables).
|
||||
|
||||
-->
|
||||
|
||||
##### OS / ENVIRONMENT
|
||||
<!---
|
||||
Mention the OS you are running Ansible from, and the OS you are
|
||||
managing, or say "N/A" for anything that is not platform-specific.
|
||||
Also mention the specific version of what you are trying to control,
|
||||
e.g. if this is a network bug the version of firmware on the network device.
|
||||
managing, or say “N/A” for anything that is not platform-specific.
|
||||
-->
|
||||
|
||||
##### SUMMARY
|
||||
@ -40,12 +41,12 @@ e.g. if this is a network bug the version of firmware on the network device.
|
||||
|
||||
##### STEPS TO REPRODUCE
|
||||
<!---
|
||||
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
|
||||
For bugs, show exactly how to reproduce the problem.
|
||||
For new features, show how the feature would be used.
|
||||
-->
|
||||
|
||||
<!--- Paste example playbooks or commands between quotes below -->
|
||||
```yaml
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
|
||||
42
.github/PULL_REQUEST_TEMPLATE.md
vendored
42
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,3 +1,19 @@
|
||||
##### ISSUE TYPE
|
||||
<!--- Pick one below and delete the rest: -->
|
||||
- Feature Pull Request
|
||||
- New Module Pull Request
|
||||
- Bugfix Pull Request
|
||||
- Docs Pull Request
|
||||
|
||||
##### COMPONENT NAME
|
||||
<!--- Name of the plugin/module/task -->
|
||||
|
||||
##### ANSIBLE VERSION
|
||||
<!--- Paste verbatim output from “ansible --version” between quotes below -->
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
##### SUMMARY
|
||||
<!--- Describe the change, including rationale and design decisions -->
|
||||
|
||||
@ -7,31 +23,7 @@ commit message and your description; but you should still explain what
|
||||
the change does.
|
||||
-->
|
||||
|
||||
##### ISSUE TYPE
|
||||
<!--- Pick one below and delete the rest: -->
|
||||
- Feature Pull Request
|
||||
- New Module Pull Request
|
||||
- Bugfix Pull Request
|
||||
- Docs Pull Request
|
||||
|
||||
##### COMPONENT NAME
|
||||
<!--- Name of the module/plugin/module/task -->
|
||||
|
||||
##### ANSIBLE VERSION
|
||||
<!--- Paste verbatim output from "ansible --version" between quotes below -->
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
|
||||
##### ADDITIONAL INFORMATION
|
||||
<!---
|
||||
Include additional information to help people understand the change here.
|
||||
For bugs that don't have a linked bug report, a step-by-step reproduction
|
||||
of the problem is helpful.
|
||||
-->
|
||||
|
||||
<!--- Paste verbatim command output below, e.g. before and after your change -->
|
||||
<!-- Paste verbatim command output below, e.g. before and after your change -->
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
78
.gitignore
vendored
78
.gitignore
vendored
@ -2,10 +2,9 @@
|
||||
*.py[co]
|
||||
build
|
||||
AUTHORS.TXT
|
||||
# Emacs backup and autosave files...
|
||||
# Emacs backup files...
|
||||
*~
|
||||
.\#*
|
||||
\#*
|
||||
# RPM stuff...
|
||||
MANIFEST
|
||||
dist
|
||||
@ -20,37 +19,29 @@ rpm-build
|
||||
# Mac OS X stuff...
|
||||
.DS_Store
|
||||
# manpage build stuff...
|
||||
docs/man/man1/ansible*.1
|
||||
docs/man/man1/ansible*.1.asciidoc.in
|
||||
docs/man/man1/ansible.1
|
||||
docs/man/man1/ansible-doc.1
|
||||
docs/man/man1/ansible-galaxy.1
|
||||
docs/man/man1/ansible-playbook.1
|
||||
docs/man/man1/ansible-pull.1
|
||||
docs/man/man1/ansible-vault.1
|
||||
docs/man/man3/*
|
||||
# Sublime stuff
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
# docsite stuff...
|
||||
docs/api/_build/
|
||||
docs/api/rst/
|
||||
docs/docsite/*.html
|
||||
docs/docsite/_build
|
||||
docs/docsite/_static/*.gif
|
||||
docs/docsite/_static/*.png
|
||||
docs/docsite/_static/websupport.js
|
||||
docs/docsite/htmlout
|
||||
docs/docsite/searchindex.js
|
||||
docs/docsite/rst_warnings
|
||||
docs/docsite/rst/*_module.rst
|
||||
docs/docsite/rst/ansible.rst
|
||||
docs/docsite/rst/ansible-*.rst
|
||||
docs/docsite/rst/community_maintained.rst
|
||||
docs/docsite/rst/config.rst
|
||||
docs/docsite/rst/core_maintained.rst
|
||||
docs/docsite/rst/list_of_*.rst
|
||||
docs/docsite/rst/modules_by_category.rst
|
||||
docs/docsite/rst/network_maintained.rst
|
||||
docs/docsite/rst/plugins_by_category.rst
|
||||
docs/docsite/rst/partner_maintained.rst
|
||||
docs/docsite/rst/playbook_keywords.rst
|
||||
docs/docsite/rst/playbooks_directives.rst
|
||||
docs/docsite/rst/plugins/*/*.rst
|
||||
docsite/rst/modules_by_category.rst
|
||||
docsite/rst/playbooks_directives.rst
|
||||
docsite/rst/list_of_*.rst
|
||||
docsite/rst/*_module.rst
|
||||
docsite/*.html
|
||||
docsite/_static/*.gif
|
||||
docsite/_static/*.png
|
||||
docsite/_static/websupport.js
|
||||
docsite/searchindex.js
|
||||
docsite/htmlout
|
||||
docs-api/rst/
|
||||
docs-api/_build/
|
||||
# deb building stuff...
|
||||
/debian/
|
||||
deb-build
|
||||
@ -61,12 +52,11 @@ credentials.yml
|
||||
# test output
|
||||
*.retry
|
||||
*.out
|
||||
.coverage
|
||||
.tox
|
||||
.cache
|
||||
results.xml
|
||||
coverage.xml
|
||||
/test/units/cover-html
|
||||
/test/integration/targets/*/backup/
|
||||
# Development
|
||||
/test/develop
|
||||
venv
|
||||
@ -74,31 +64,3 @@ Vagrantfile
|
||||
.vagrant
|
||||
ansible.egg-info/
|
||||
/shippable/
|
||||
/test/integration/cloud-config-*.*
|
||||
!/test/integration/cloud-config-*.*.template
|
||||
# Release directory
|
||||
packaging/release/ansible_release
|
||||
/.cache/
|
||||
/test/results/coverage/*=coverage.*
|
||||
/test/results/coverage/coverage*
|
||||
/test/results/reports/coverage*.xml
|
||||
/test/results/reports/coverage*/
|
||||
/test/results/bot/*.json
|
||||
/test/results/junit/*.xml
|
||||
/test/results/logs/*.log
|
||||
/test/results/data/*.json
|
||||
/test/integration/inventory.remote
|
||||
/test/integration/inventory.networking
|
||||
/test/integration/inventory.winrm
|
||||
/test/integration/cloud-config-aws.yml
|
||||
# python 'rope' stuff
|
||||
.ropeproject
|
||||
# local 'ack' config files
|
||||
.ackrc
|
||||
# default 'coverage html' results
|
||||
htmlcov/
|
||||
# default 'coverage' tool data
|
||||
.coverage
|
||||
# ansible-test coverage results
|
||||
test/units/.coverage.*
|
||||
/test/integration/cloud-config-azure.yml
|
||||
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@ -0,0 +1,6 @@
|
||||
[submodule "lib/ansible/modules/core"]
|
||||
path = lib/ansible/modules/core
|
||||
url = https://github.com/ansible/ansible-modules-core
|
||||
[submodule "lib/ansible/modules/extras"]
|
||||
path = lib/ansible/modules/extras
|
||||
url = https://github.com/ansible/ansible-modules-extras
|
||||
|
||||
5
.mailmap
5
.mailmap
@ -28,9 +28,8 @@ Will Thames <will@thames.id.au> willthames <willdthames@gmail.com>
|
||||
Will Thames <will@thames.id.au> willthames <will@musc.org.uk>
|
||||
Will Thames <will@thames.id.au> u348095 <will.thames@suncorp.com.au>
|
||||
Will Thames <will@thames.id.au> willthames <will.thames@suncorp.com.au>
|
||||
Will Thames <will@thames.id.au> Will Thames <will.thames@xvt.com.au>
|
||||
nitzmahone <mdavis@ansible.com> Matt Davis <nitzmahone@users.noreply.github.com>
|
||||
Dag Wieers <dag@wieers.com> Dag Wieërs <dag@wieers.com>
|
||||
Dag Wieers <dag@wieers.com> Dag Wieers <dag.wieers@gmail.com>
|
||||
Rene Moser <mail@renemoser.net> René Moser <mail@renemoser.net>
|
||||
Rene Moser <mail@renemoser.net> Rene Moser <rene.moser@swisstxt.ch>
|
||||
|
||||
|
||||
|
||||
17
.yamllint
17
.yamllint
@ -1,19 +1,22 @@
|
||||
extends: default
|
||||
|
||||
# Disable all cosmetic rules
|
||||
# (see https://github.com/ansible/ansible/pull/15470#issuecomment-214437876)
|
||||
# Only keep 'key-duplicates' and 'new-lines: {type: unix}' checks enabled.
|
||||
|
||||
rules:
|
||||
braces: {max-spaces-inside: 1, level: error}
|
||||
brackets: {max-spaces-inside: 1, level: error}
|
||||
colons: {max-spaces-after: -1, level: error}
|
||||
commas: {max-spaces-after: -1, level: error}
|
||||
braces: disable
|
||||
brackets: disable
|
||||
colons: disable
|
||||
commas: disable
|
||||
comments: disable
|
||||
comments-indentation: disable
|
||||
document-start: disable
|
||||
empty-lines: {max: 3, level: error}
|
||||
hyphens: {level: error}
|
||||
empty-lines: disable
|
||||
hyphens: disable
|
||||
indentation: disable
|
||||
key-duplicates: enable
|
||||
line-length: disable
|
||||
new-line-at-end-of-file: disable
|
||||
new-lines: {type: unix}
|
||||
trailing-spaces: disable
|
||||
truthy: disable
|
||||
|
||||
1251
CHANGELOG.md
1251
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -14,15 +14,15 @@ Language
|
||||
* While not all components of Ansible must be in Python, core contributions to the Ansible repo must be written in Python. This is to maximize the ability of everyone to contribute.
|
||||
* If you want to write non-Python ansible modules or inventory scripts, that's fine, but they are not going to get merged in most likely. Sorry!!
|
||||
|
||||
PEP 8 and basic style checks
|
||||
PEP8 and basic style checks
|
||||
===========================
|
||||
|
||||
* [PEP 8](https://www.python.org/dev/peps/pep-0008/) is a great Python style guide, which you should read.
|
||||
* PEP 8 must not be strictly followed in all aspects, but most of it is good advice
|
||||
* PEP8 is a great Python style guide, which you should read.
|
||||
* PEP8 must not be strictly followed in all aspects, but most of it is good advice
|
||||
* In particular, we don't really care about line lengths. Buy a bigger monitor!
|
||||
* To run checks for things we care about, use [ansible-test](http://docs.ansible.com/ansible/dev_guide/testing_pep8.html#running-locally).
|
||||
* To run checks for things we care about, run "make pep8"
|
||||
* Similarly, additional checks can be made with "make pyflakes"
|
||||
* There is no need to submit code changes for PEP 8 and pyflakes fixes, as these break attribution history. Project leadership will make these periodically.
|
||||
* There is no need to submit code changes for pep8 and pyflake fixes, as these break attribution history. Project leadership will make these periodically.
|
||||
* Do not submit pull requests that simply adjust whitespace in the code
|
||||
|
||||
Testing
|
||||
@ -101,22 +101,22 @@ Use the dict constructor where possible when allocating dictionaries:
|
||||
|
||||
# this:
|
||||
foo = dict(
|
||||
a=12,
|
||||
b=34,
|
||||
a = 12,
|
||||
b = 34
|
||||
)
|
||||
|
||||
Do not line up variables
|
||||
Line up variables
|
||||
|
||||
# not this
|
||||
a = 12
|
||||
foosball = 34
|
||||
xyz = 'dog'
|
||||
|
||||
# this
|
||||
a = 12
|
||||
foosball = 34
|
||||
xyz = 'dog'
|
||||
|
||||
# this
|
||||
a = 12
|
||||
foosball = 34
|
||||
xyz = 'dog'
|
||||
|
||||
Don't use line continuations:
|
||||
|
||||
# no
|
||||
@ -131,13 +131,13 @@ Don't use line continuations:
|
||||
Spacing:
|
||||
|
||||
# no
|
||||
x = [1,2,3]
|
||||
x=[1,2,3]
|
||||
|
||||
# no
|
||||
x = [ 1, 2, 3 ]
|
||||
x = [1,2,3]
|
||||
|
||||
# yes
|
||||
x = [1, 2, 3]
|
||||
x = [ 1, 2, 3 ]
|
||||
|
||||
Spacing continued:
|
||||
|
||||
@ -277,7 +277,7 @@ To test if something is a string, consider that it may be unicode.
|
||||
if type(x) == str:
|
||||
|
||||
# yes
|
||||
from ansible.module_utils.six import string_types
|
||||
from ansible.compat.six import string_types
|
||||
if isinstance(x, string_types):
|
||||
|
||||
Cleverness
|
||||
@ -314,3 +314,7 @@ This was not meant to be a scary document, so we hope it wasn't, but we also hop
|
||||
If you have questions about this document, please ask on the ansible-devel mailing list.
|
||||
|
||||
Thank you!
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
15
MANIFEST.in
15
MANIFEST.in
@ -1,21 +1,24 @@
|
||||
prune v2
|
||||
prune docsite
|
||||
prune ticket_stubs
|
||||
prune packaging
|
||||
prune test
|
||||
prune hacking
|
||||
include README.md COPYING
|
||||
include SYMLINK_CACHE.json
|
||||
include requirements.txt
|
||||
include .coveragerc
|
||||
include .yamllint
|
||||
include examples/hosts
|
||||
include examples/ansible.cfg
|
||||
recursive-include lib/ansible/module_utils/powershell *
|
||||
include lib/ansible/module_utils/powershell.ps1
|
||||
recursive-include lib/ansible/modules *
|
||||
recursive-include lib/ansible/galaxy/data *
|
||||
recursive-include docs *
|
||||
recursive-include packaging *
|
||||
recursive-include test *
|
||||
include Makefile
|
||||
include VERSION
|
||||
include MANIFEST.in
|
||||
include CHANGELOG.md
|
||||
include contrib/README.md
|
||||
recursive-include contrib/inventory *
|
||||
exclude lib/ansible/modules/core/.git*
|
||||
exclude lib/ansible/modules/extras/.git*
|
||||
prune lib/ansible/modules/core/.git
|
||||
prune lib/ansible/modules/extras/.git
|
||||
|
||||
@ -1,70 +0,0 @@
|
||||
# Module Maintainer Guidelines
|
||||
|
||||
Thank you for being a maintainer of one Ansible's community modules! This guide provides module maintainers an overview of their responsibilities, resources for additional information, and links to helpful tools.
|
||||
|
||||
In addition to the information below, module maintainers should be familiar with:
|
||||
* General Ansible community development practices (http://docs.ansible.com/ansible/community.html)
|
||||
* Documentation on module development (http://docs.ansible.com/ansible/developing_modules.html)
|
||||
* Any namespace-specific module guidelines (identified as GUIDELINES.md in the appropriate file tree).
|
||||
|
||||
***
|
||||
|
||||
# Maintainer Responsibilities
|
||||
|
||||
When you contribute a new module to the [ansible/ansible](https://github.com/ansible/ansible) repository, you become the maintainer for that module once it has been merged. Maintainership empowers you with the authority to accept, reject, or request revisions to pull requests on your module -- but as they say, "with great power comes great responsibility."
|
||||
|
||||
Maintainers of Ansible modules are expected to provide feedback, responses, or actions on pull requests or issues to the module(s) they maintain in a reasonably timely manner.
|
||||
|
||||
It is also recommended that you occasionally revisit the [contribution guidelines](https://github.com/ansible/ansible/blob/devel/CONTRIBUTING.md), as they are continually refined. Occasionally, you may be requested to update your module to move it closer to the general accepted standard requirements. We hope for this to be infrequent, and will always be a request with a fair amount of lead time (ie: not by tomorrow!).
|
||||
|
||||
Finally, following the [ansible-devel](https://groups.google.com/forum/#!forum/ansible-devel) mailing list can be a great way to participate in the broader Ansible community, and a place where you can influence the overall direction, quality, and goals of Ansible and its modules. If you're not on this relatively low-volume list, please join us here: https://groups.google.com/forum/#!forum/ansible-devel
|
||||
|
||||
The Ansible community hopes that you will find that maintaining your module is as rewarding for you as having the module is for the wider community.
|
||||
|
||||
***
|
||||
|
||||
# Pull Requests, Issues, and Workflow
|
||||
|
||||
## Pull Requests
|
||||
|
||||
Module pull requests are located in the [main Ansible repository](https://github.com/ansible/ansible/pulls).
|
||||
|
||||
Because of the high volume of pull requests, notification of PRs to specific modules are routed by an automated bot to the appropriate maintainer for handling. It is recommended that you set an appropriate notification process to receive notifications which mention your GitHub ID.
|
||||
|
||||
## Issues
|
||||
|
||||
Issues for modules, including bug reports, documentation bug reports, and feature requests, are tracked in the [ansible repository](https://github.com/ansible/ansible/issues).
|
||||
|
||||
Issues for modules are routed to their maintainers via an automated process. This process is still being refined, and currently depends upon the issue creator to provide adequate details (specifically, providing the proper module name) in order to route it correctly. If you are a maintainer of a specific module, it is recommended that you periodically search module issues for issues which mention your module's name (or some variation on that name), as well as setting an appropriate notification process for receiving notification of mentions of your GitHub ID.
|
||||
|
||||
## PR Workflow
|
||||
|
||||
Automated routing of pull requests is handled by a tool called [Ansibullbot](https://github.com/ansible/ansibullbot). (You could say that he moooo-ves things around.)
|
||||
|
||||
Being moderately familiar with how the workflow behind the bot operates can be helpful to you, and -- should things go awry -- your feedback can be helpful to the folks that continually help Ansibullbot to evolve.
|
||||
|
||||
A detailed explanation of the PR workflow can be seen here: https://github.com/ansible/community/blob/master/PR-FLOW.md
|
||||
|
||||
***
|
||||
|
||||
# Maintainers list
|
||||
|
||||
The full list of maintainers for modules is located here: https://github.com/ansible/ansible/blob/devel/.github/BOTMETA.yml
|
||||
|
||||
## Changing Maintainership
|
||||
|
||||
Communities change over time, and no one maintains a module forever. If you'd like to propose an additional maintainer for your module, please submit a PR to the maintainers file with the Github username of the new maintainer.
|
||||
|
||||
If you'd like to step down as a maintainer, please submit a PR to the maintainers file removing your Github ID from the module in question. If that would leave the module with no maintainers, put "ansible" as the maintainer. This will indicate that the module is temporarily without a maintainer, and the Ansible community team will search for a new maintainer.
|
||||
|
||||
***
|
||||
|
||||
# Tools and other Resources
|
||||
|
||||
## Useful tools
|
||||
* https://ansible.sivel.net/pr/byfile.html -- a full list of all open Pull Requests, organized by file.
|
||||
|
||||
## Other Resources
|
||||
|
||||
* Ansibullbot: https://github.com/ansible/ansibullbot
|
||||
* Triage / pull request workflow and information, including definitions for Labels in GitHub: https://github.com/ansible/community/blob/master/PR-FLOW.md
|
||||
97
Makefile
97
Makefile
@ -1,3 +1,4 @@
|
||||
#!/usr/bin/make
|
||||
# WARN: gmake syntax
|
||||
########################################################
|
||||
# Makefile for Ansible
|
||||
@ -9,7 +10,7 @@
|
||||
# make deb-src -------------- produce a DEB source
|
||||
# make deb ------------------ produce a DEB
|
||||
# make docs ----------------- rebuild the manpages (results are checked in)
|
||||
# make tests ---------------- run the tests (see https://docs.ansible.com/ansible/dev_guide/testing_units.html for requirements)
|
||||
# make tests ---------------- run the tests (see test/README.md for requirements)
|
||||
# make pyflakes, make pep8 -- source code checks
|
||||
|
||||
########################################################
|
||||
@ -21,15 +22,13 @@ OS = $(shell uname -s)
|
||||
# Manpages are currently built with asciidoc -- would like to move to markdown
|
||||
# This doesn't evaluate until it's called. The -D argument is the
|
||||
# directory of the target file ($@), kinda like `dirname`.
|
||||
|
||||
MANPAGES ?= $(patsubst %.asciidoc.in,%,$(wildcard ./docs/man/man1/ansible*.1.asciidoc.in))
|
||||
MANPAGES := docs/man/man1/ansible.1 docs/man/man1/ansible-playbook.1 docs/man/man1/ansible-pull.1 docs/man/man1/ansible-doc.1 docs/man/man1/ansible-galaxy.1 docs/man/man1/ansible-vault.1
|
||||
ifneq ($(shell which a2x 2>/dev/null),)
|
||||
ASCII2MAN = a2x -L -D $(dir $@) -d manpage -f manpage $<
|
||||
ASCII2HTMLMAN = a2x -L -D docs/html/man/ -d manpage -f xhtml
|
||||
else
|
||||
ASCII2MAN = @echo "ERROR: AsciiDoc 'a2x' command is not installed but is required to build $(MANPAGES)" && exit 1
|
||||
endif
|
||||
GENERATE_CLI = docs/bin/generate_man.py
|
||||
|
||||
PYTHON=python
|
||||
SITELIB = $(shell $(PYTHON) -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")
|
||||
@ -50,10 +49,8 @@ endif
|
||||
|
||||
ifeq ($(shell echo $(OS) | egrep -c 'Darwin|FreeBSD|OpenBSD|DragonFly'),1)
|
||||
DATE := $(shell date -j -r $(shell git log -n 1 --format="%at") +%Y%m%d%H%M)
|
||||
CPUS ?= $(shell sysctl hw.ncpu|awk '{print $$2}')
|
||||
else
|
||||
DATE := $(shell date --utc --date="$(GIT_DATE)" +%Y%m%d%H%M)
|
||||
CPUS ?= $(shell nproc)
|
||||
endif
|
||||
|
||||
# DEB build parameters
|
||||
@ -104,40 +101,23 @@ RPMNVR = "$(NAME)-$(VERSION)-$(RPMRELEASE)$(RPMDIST)"
|
||||
MOCK_BIN ?= mock
|
||||
MOCK_CFG ?=
|
||||
|
||||
# ansible-test parameters
|
||||
ANSIBLE_TEST ?= test/runner/ansible-test
|
||||
TEST_FLAGS ?=
|
||||
NOSETESTS ?= nosetests
|
||||
|
||||
# ansible-test units parameters (make test / make test-py3)
|
||||
PYTHON_VERSION ?= $(shell python2 -c 'import sys; print("%s.%s" % sys.version_info[:2])')
|
||||
PYTHON3_VERSION ?= $(shell python3 -c 'import sys; print("%s.%s" % sys.version_info[:2])')
|
||||
|
||||
# ansible-test integration parameters (make integration)
|
||||
IMAGE ?= centos7
|
||||
TARGET ?=
|
||||
NOSETESTS3 ?= nosetests-3.5
|
||||
|
||||
########################################################
|
||||
|
||||
.PHONY: all
|
||||
all: clean python
|
||||
|
||||
.PHONY: tests
|
||||
tests:
|
||||
$(ANSIBLE_TEST) units -v --python $(PYTHON_VERSION) $(TEST_FLAGS)
|
||||
PYTHONPATH=./lib $(NOSETESTS) -d -w test/units -v --with-coverage --cover-package=ansible --cover-branches --cover-erase
|
||||
|
||||
.PHONY: tests-py3
|
||||
tests-py3:
|
||||
$(ANSIBLE_TEST) units -v --python $(PYTHON3_VERSION) $(TEST_FLAGS)
|
||||
PYTHONPATH=./lib $(NOSETESTS3) -d -w test/units -v --with-coverage --cover-package=ansible --cover-branches --cover-erase
|
||||
|
||||
.PHONY: tests-nonet
|
||||
tests-nonet:
|
||||
$(ANSIBLE_TEST) units -v --python $(PYTHON_VERSION) $(TEST_FLAGS) --exclude test/units/modules/network/
|
||||
|
||||
.PHONY: integration
|
||||
integration:
|
||||
$(ANSIBLE_TEST) integration -v --docker $(IMAGE) $(TARGET) $(TEST_FLAGS)
|
||||
test/utils/shippable/integration.sh
|
||||
|
||||
.PHONY: authors
|
||||
authors:
|
||||
sh hacking/authors.sh
|
||||
|
||||
@ -151,19 +131,19 @@ authors:
|
||||
%.1: %.1.asciidoc VERSION
|
||||
$(ASCII2MAN)
|
||||
|
||||
.PHONY: loc
|
||||
loc:
|
||||
sloccount lib library bin
|
||||
|
||||
.PHONY: pep8
|
||||
pep8:
|
||||
$(ANSIBLE_TEST) sanity --test pep8 --python $(PYTHON_VERSION) $(TEST_FLAGS)
|
||||
@echo "#############################################"
|
||||
@echo "# Running PEP8 Compliance Tests"
|
||||
@echo "#############################################"
|
||||
-pep8 -r --ignore=E501,E221,W291,W391,E302,E251,E203,W293,E231,E303,E201,E225,E261,E241 lib/ bin/
|
||||
# -pep8 -r --ignore=E501,E221,W291,W391,E302,E251,E203,W293,E231,E303,E201,E225,E261,E241 --filename "*" library/
|
||||
|
||||
.PHONY: pyflakes
|
||||
pyflakes:
|
||||
pyflakes lib/ansible/*.py lib/ansible/*/*.py bin/*
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
@echo "Cleaning up distutils stuff"
|
||||
rm -rf build
|
||||
@ -171,7 +151,6 @@ clean:
|
||||
rm -rf lib/ansible.egg-info/
|
||||
@echo "Cleaning up byte compiled python stuff"
|
||||
find . -type f -regex ".*\.py[co]$$" -delete
|
||||
find . -type d -name "__pycache__" -delete
|
||||
@echo "Cleaning up editor backup files"
|
||||
find . -type f -not -path ./test/units/inventory_test_data/group_vars/noparse/all.yml~ \( -name "*~" -or -name "#*" \) -delete
|
||||
find . -type f \( -name "*.swp" \) -delete
|
||||
@ -179,15 +158,8 @@ clean:
|
||||
find ./docs/man -type f -name "*.xml" -delete
|
||||
find ./docs/man -type f -name "*.asciidoc" -delete
|
||||
find ./docs/man/man3 -type f -name "*.3" -delete
|
||||
rm -f ./docs/man/man1/*
|
||||
@echo "Cleaning up output from test runs"
|
||||
rm -rf test/test_data
|
||||
rm -rf shippable/
|
||||
rm -rf logs/
|
||||
rm -rf .cache/
|
||||
rm -f test/units/.coverage*
|
||||
rm -rf test/results/*/*
|
||||
find test/ -type f -name '*.retry' -delete
|
||||
@echo "Cleaning up RPM building stuff"
|
||||
rm -rf MANIFEST rpm-build
|
||||
@echo "Cleaning up Debian building stuff"
|
||||
@ -197,33 +169,25 @@ clean:
|
||||
rm -rf docs/js
|
||||
@echo "Cleaning up authors file"
|
||||
rm -f AUTHORS.TXT
|
||||
@echo "Cleaning up docsite"
|
||||
$(MAKE) -C docs/docsite clean
|
||||
$(MAKE) -C docs/api clean
|
||||
find . -type f -name '*.pyc' -delete
|
||||
|
||||
.PHONY: python
|
||||
python:
|
||||
$(PYTHON) setup.py build
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
$(PYTHON) setup.py install
|
||||
|
||||
.PHONY: sdist
|
||||
sdist: clean docs
|
||||
$(PYTHON) setup.py sdist
|
||||
|
||||
.PHONY: sdist_upload
|
||||
sdist_upload: clean docs
|
||||
$(PYTHON) setup.py sdist upload 2>&1 |tee upload.log
|
||||
|
||||
.PHONY: rpmcommon
|
||||
rpmcommon: sdist
|
||||
rpmcommon: $(MANPAGES) sdist
|
||||
@mkdir -p rpm-build
|
||||
@cp dist/*.gz rpm-build/
|
||||
@sed -e 's#^Version:.*#Version: $(VERSION)#' -e 's#^Release:.*#Release: $(RPMRELEASE)%{?dist}$(REPOTAG)#' $(RPMSPEC) >rpm-build/$(NAME).spec
|
||||
|
||||
.PHONY: mock-srpm
|
||||
mock-srpm: /etc/mock/$(MOCK_CFG).cfg rpmcommon
|
||||
$(MOCK_BIN) -r $(MOCK_CFG) $(MOCK_ARGS) --resultdir rpm-build/ --buildsrpm --spec rpm-build/$(NAME).spec --sources rpm-build/
|
||||
@echo "#############################################"
|
||||
@ -231,7 +195,6 @@ mock-srpm: /etc/mock/$(MOCK_CFG).cfg rpmcommon
|
||||
@echo rpm-build/*.src.rpm
|
||||
@echo "#############################################"
|
||||
|
||||
.PHONY: mock-rpm
|
||||
mock-rpm: /etc/mock/$(MOCK_CFG).cfg mock-srpm
|
||||
$(MOCK_BIN) -r $(MOCK_CFG) $(MOCK_ARGS) --resultdir rpm-build/ --rebuild rpm-build/$(NAME)-*.src.rpm
|
||||
@echo "#############################################"
|
||||
@ -239,7 +202,6 @@ mock-rpm: /etc/mock/$(MOCK_CFG).cfg mock-srpm
|
||||
@echo rpm-build/*.noarch.rpm
|
||||
@echo "#############################################"
|
||||
|
||||
.PHONY: srpm
|
||||
srpm: rpmcommon
|
||||
@rpmbuild --define "_topdir %(pwd)/rpm-build" \
|
||||
--define "_builddir %{_topdir}" \
|
||||
@ -254,7 +216,6 @@ srpm: rpmcommon
|
||||
@echo " rpm-build/$(RPMNVR).src.rpm"
|
||||
@echo "#############################################"
|
||||
|
||||
.PHONY: rpm
|
||||
rpm: rpmcommon
|
||||
@rpmbuild --define "_topdir %(pwd)/rpm-build" \
|
||||
--define "_builddir %{_topdir}" \
|
||||
@ -271,7 +232,6 @@ rpm: rpmcommon
|
||||
@echo " rpm-build/$(RPMNVR).noarch.rpm"
|
||||
@echo "#############################################"
|
||||
|
||||
.PHONY: debian
|
||||
debian: sdist
|
||||
@for DIST in $(DEB_DIST) ; do \
|
||||
mkdir -p deb-build/$${DIST} ; \
|
||||
@ -280,7 +240,6 @@ debian: sdist
|
||||
sed -ie "s|%VERSION%|$(VERSION)|g;s|%RELEASE%|$(DEB_RELEASE)|;s|%DIST%|$${DIST}|g;s|%DATE%|$(DEB_DATE)|g" deb-build/$${DIST}/$(NAME)-$(VERSION)/debian/changelog ; \
|
||||
done
|
||||
|
||||
.PHONY: deb
|
||||
deb: deb-src
|
||||
@for DIST in $(DEB_DIST) ; do \
|
||||
PBUILDER_OPTS="$(PBUILDER_OPTS) --distribution $${DIST} --basetgz $(PBUILDER_CACHE_DIR)/$${DIST}-$(PBUILDER_ARCH)-base.tgz --buildresult $(CURDIR)/deb-build/$${DIST}" ; \
|
||||
@ -297,7 +256,6 @@ deb: deb-src
|
||||
|
||||
# Build package outside of pbuilder, with locally installed dependencies.
|
||||
# Install BuildRequires as noted in packaging/debian/control.
|
||||
.PHONY: local_deb
|
||||
local_deb: debian
|
||||
@for DIST in $(DEB_DIST) ; do \
|
||||
(cd deb-build/$${DIST}/$(NAME)-$(VERSION)/ && $(DEBUILD) -b) ; \
|
||||
@ -309,7 +267,6 @@ local_deb: debian
|
||||
done
|
||||
@echo "#############################################"
|
||||
|
||||
.PHONY: deb-src
|
||||
deb-src: debian
|
||||
@for DIST in $(DEB_DIST) ; do \
|
||||
(cd deb-build/$${DIST}/$(NAME)-$(VERSION)/ && $(DEBUILD) -S) ; \
|
||||
@ -321,39 +278,21 @@ deb-src: debian
|
||||
done
|
||||
@echo "#############################################"
|
||||
|
||||
.PHONY: deb-upload
|
||||
deb-upload: deb
|
||||
@for DIST in $(DEB_DIST) ; do \
|
||||
$(DPUT_BIN) $(DPUT_OPTS) $(DEB_PPA) deb-build/$${DIST}/$(NAME)_$(VERSION)-$(DEB_RELEASE)~$${DIST}_amd64.changes ; \
|
||||
done
|
||||
|
||||
.PHONY: deb-src-upload
|
||||
deb-src-upload: deb-src
|
||||
@for DIST in $(DEB_DIST) ; do \
|
||||
$(DPUT_BIN) $(DPUT_OPTS) $(DEB_PPA) deb-build/$${DIST}/$(NAME)_$(VERSION)-$(DEB_RELEASE)~$${DIST}_source.changes ; \
|
||||
done
|
||||
|
||||
.PHONY: epub
|
||||
epub:
|
||||
(cd docs/docsite/; CPUS=$(CPUS) make epub)
|
||||
|
||||
# for arch or gentoo, read instructions in the appropriate 'packaging' subdirectory directory
|
||||
.PHONY: webdocs
|
||||
|
||||
webdocs:
|
||||
(cd docs/docsite/; CPUS=$(CPUS) make docs)
|
||||
(cd docsite/; make docs)
|
||||
|
||||
.PHONY: generate_asciidoc
|
||||
generate_asciidoc: lib/ansible/cli/*.py
|
||||
mkdir -p ./docs/man/man1/ ; \
|
||||
PYTHONPATH=./lib $(GENERATE_CLI) --template-file=docs/templates/man.j2 --output-dir=docs/man/man1/ --output-format man lib/ansible/cli/*.py
|
||||
docs: $(MANPAGES)
|
||||
|
||||
|
||||
docs: generate_asciidoc
|
||||
make $(MANPAGES)
|
||||
|
||||
.PHONY: alldocs
|
||||
alldocs: docs webdocs
|
||||
|
||||
version:
|
||||
@echo $(VERSION)
|
||||
|
||||
|
||||
17
README.md
17
README.md
@ -1,5 +1,5 @@
|
||||
[](https://pypi.python.org/pypi/ansible)
|
||||
[](https://app.shippable.com/projects/573f79d02a8192902e20e34b)
|
||||
[](https://app.shippable.com/projects/573f79d02a8192902e20e34b)
|
||||
|
||||
|
||||
Ansible
|
||||
@ -9,7 +9,11 @@ Ansible is a radically simple IT automation system. It handles configuration-ma
|
||||
|
||||
Read the documentation and more at https://ansible.com/
|
||||
|
||||
You can find installation instructions [here](https://docs.ansible.com/ansible/intro_installation.html) for a variety of platforms. Most users should probably install a released version of Ansible from `pip`, a package manager or our [release repository](https://releases.ansible.com/ansible/). [Officially supported](https://www.ansible.com/ansible-engine) builds of Ansible are also available. Some power users run directly from the development branch - while significant efforts are made to ensure that `devel` is reasonably stable, you're more likely to encounter breaking changes when running Ansible this way.
|
||||
Many users run straight from the development branch (it's generally fine to do so), but you might also wish to consume a release.
|
||||
|
||||
You can find instructions [here](https://docs.ansible.com/intro_getting_started.html) for a variety of platforms. If you decide to go with the development branch, be sure to run `git submodule update --init --recursive` after doing a checkout.
|
||||
|
||||
If you want to download a tarball of a release, go to [releases.ansible.com](https://releases.ansible.com/ansible), though most users use `yum` (using the EPEL instructions linked above), `apt` (using the PPA instructions linked above), or `pip install ansible`.
|
||||
|
||||
Design Principles
|
||||
=================
|
||||
@ -23,11 +27,11 @@ Design Principles
|
||||
* Allow module development in any dynamic language, not just Python
|
||||
* Be usable as non-root
|
||||
* Be the easiest IT automation system to use, ever.
|
||||
|
||||
|
||||
Get Involved
|
||||
============
|
||||
|
||||
* Read [Community Information](https://docs.ansible.com/community.html) for all kinds of ways to contribute to and interact with the project, including mailing list information and how to submit bug reports and code to Ansible.
|
||||
* Read [Community Information](https://docs.ansible.com/community.html) for all kinds of ways to contribute to and interact with the project, including mailing list information and how to submit bug reports and code to Ansible.
|
||||
* All code submissions are done through pull requests. Take care to make sure no merge commits are in the submission, and use `git rebase` vs `git merge` for this reason. If submitting a large code change (other than modules), it's probably a good idea to join ansible-devel and talk about what you would like to do or add first and to avoid duplicate efforts. This not only helps everyone know what's going on, it also helps save time and effort if we decide some changes are needed.
|
||||
* Users list: [ansible-project](https://groups.google.com/group/ansible-project)
|
||||
* Development list: [ansible-devel](https://groups.google.com/group/ansible-devel)
|
||||
@ -39,6 +43,7 @@ Branch Info
|
||||
|
||||
* Releases are named after Led Zeppelin songs. (Releases prior to 2.0 were named after Van Halen songs.)
|
||||
* The devel branch corresponds to the release actively under development.
|
||||
* As of 1.8, modules are kept in different repos, you'll want to follow [core](https://github.com/ansible/ansible-modules-core) and [extras](https://github.com/ansible/ansible-modules-extras)
|
||||
* Various release-X.Y branches exist for previous releases.
|
||||
* We'd love to have your contributions, read [Community Information](https://docs.ansible.com/community.html) for notes on how to get started.
|
||||
|
||||
@ -49,8 +54,4 @@ Ansible was created by [Michael DeHaan](https://github.com/mpdehaan) (michael.de
|
||||
|
||||
Ansible is sponsored by [Ansible, Inc](https://ansible.com)
|
||||
|
||||
License
|
||||
=======
|
||||
GNU General Public License v3.0
|
||||
|
||||
See [COPYING](COPYING) to see the full text.
|
||||
|
||||
143
RELEASES.txt
143
RELEASES.txt
@ -5,79 +5,76 @@ VERSION RELEASE CODE NAME
|
||||
++++++++++++++++++++++++++++++
|
||||
|
||||
2.4.0 TBD "Dancing Days"
|
||||
2.3.2 2017-08-04 "Ramble On"
|
||||
2.3.1 2017-06-01 "Ramble On"
|
||||
2.3.0 2017-04-12 "Ramble On"
|
||||
2.2.3 2017-05-09 "The Battle of Evermore"
|
||||
2.2.2 2017-03-27 "The Battle of Evermore"
|
||||
2.2.1 2017-01-16 "The Battle of Evermore"
|
||||
2.2.0 2016-11-01 "The Battle of Evermore"
|
||||
2.1.6 2017-06-01 "The Song Remains the Same"
|
||||
2.1.5 2017-03-27 "The Song Remains the Same"
|
||||
2.1.4 2017-01-16 "The Song Remains the Same"
|
||||
2.1.3 2016-11-04 "The Song Remains the Same"
|
||||
2.1.2 2016-09-29 "The Song Remains the Same"
|
||||
2.1.1 2016-07-28 "The Song Remains the Same"
|
||||
2.1.0 2016-05-25 "The Song Remains the Same"
|
||||
2.0.2 2016-04-19 "Over the Hills and Far Away"
|
||||
2.0.1 2016-02-24 "Over the Hills and Far Away"
|
||||
2.0.0 2016-01-12 "Over the Hills and Far Away"
|
||||
1.9.6 2016-04-15 "Dancing In the Streets"
|
||||
1.9.5 2016-03-21 "Dancing In the Streets"
|
||||
1.9.4 2015-10-09 "Dancing In the Streets"
|
||||
1.9.3 2015-09-03 "Dancing In the Streets"
|
||||
1.9.2 2015-06-24 "Dancing In the Streets"
|
||||
1.9.1 2015-04-27 "Dancing In the Streets"
|
||||
1.9.0 2015-03-25 "Dancing In the Streets"
|
||||
1.8.4 2015-02-19 "You Really Got Me"
|
||||
1.8.3 2015-02-17 "You Really Got Me"
|
||||
1.8.2 2014-12-04 "You Really Got Me"
|
||||
1.8.1 2014-11-26 "You Really Got Me"
|
||||
1.8.0 2014-11-25 "You Really Got Me"
|
||||
1.7.2 2014-09-24 "Summer Nights"
|
||||
1.7.1 2014-08-14 "Summer Nights"
|
||||
1.7.0 2014-08-06 "Summer Nights"
|
||||
1.6.10 2014-07-25 "The Cradle Will Rock"
|
||||
1.6.9 2014-07-24 "The Cradle Will Rock"
|
||||
1.6.8 2014-07-22 "The Cradle Will Rock"
|
||||
1.6.7 2014-07-21 "The Cradle Will Rock"
|
||||
1.6.6 2014-07-01 "The Cradle Will Rock"
|
||||
1.6.5 2014-06-25 "The Cradle Will Rock"
|
||||
1.6.4 2014-06-25 "The Cradle Will Rock"
|
||||
1.6.3 2014-06-09 "The Cradle Will Rock"
|
||||
1.6.2 2014-05-23 "The Cradle Will Rock"
|
||||
1.6.1 2014-05-07 "The Cradle Will Rock"
|
||||
1.6.0 2014-05-05 "The Cradle Will Rock"
|
||||
1.5.5 2014-04-18 "Love Walks In"
|
||||
1.5.4 2014-04-01 "Love Walks In"
|
||||
1.5.3 2014-03-13 "Love Walks In"
|
||||
1.5.2 2014-03-11 "Love Walks In"
|
||||
1.5.1 2014-03-10 "Love Walks In"
|
||||
1.5.0 2014-02-28 "Love Walks In"
|
||||
1.4.5 2014-02-12 "Could This Be Magic?"
|
||||
1.4.4 2014-01-06 "Could This Be Magic?"
|
||||
1.4.3 2013-12-20 "Could This Be Magic?"
|
||||
1.4.2 2013-12-18 "Could This Be Magic?"
|
||||
1.4.1 2013-11-27 "Could This Be Magic?"
|
||||
1.4.0 2013-11-21 "Could This Be Magic?"
|
||||
1.3.4 2013-10-29 "Top of the World"
|
||||
1.3.3 2013-10-09 "Top of the World"
|
||||
1.3.2 2013-09-19 "Top of the World"
|
||||
1.3.1 2013-09-16 "Top of the World"
|
||||
1.3.0 2013-09-13 "Top of the World"
|
||||
1.2.3 2013-08-21 "Right Now"
|
||||
1.2.2 2013-07-05 "Right Now"
|
||||
1.2.1 2013-07-04 "Right Now"
|
||||
1.2.0 2013-06-10 "Right Now"
|
||||
1.1.0 2013-04-02 "Mean Street"
|
||||
1.0.0 2013-02-01 "Eruption"
|
||||
0.9.0 2012-11-30 "Dreams"
|
||||
0.8.0 2012-10-19 "Cathedral"
|
||||
0.7.0 2012-09-06 "Panama"
|
||||
0.6.0 2012-08-06 "Cabo"
|
||||
0.5.0 2012-07-04 "Amsterdam"
|
||||
0.4.0 2012-05-23 "Unchained"
|
||||
0.3.0 2012-04-23 "Baluchitherium"
|
||||
2.3.0 04-12-2017 "Ramble On"
|
||||
2.2.3 05-09-2017 "The Battle of Evermore"
|
||||
2.2.2 03-27-2017 "The Battle of Evermore"
|
||||
2.2.1 01-16-2017 "The Battle of Evermore"
|
||||
2.2.0 11-01-2016 "The Battle of Evermore"
|
||||
2.1.5 03-27-2017 "The Song Remains the Same"
|
||||
2.1.4 01-16-2017 "The Song Remains the Same"
|
||||
2.1.3 11-04-2016 "The Song Remains the Same"
|
||||
2.1.2 09-29-2016 "The Song Remains the Same"
|
||||
2.1.1 07-28-2016 "The Song Remains the Same"
|
||||
2.1.0 05-25-2016 "The Song Remains the Same"
|
||||
2.0.2 04-19-2016 "Over the Hills and Far Away"
|
||||
2.0.1 02-24-2016 "Over the Hills and Far Away"
|
||||
2.0.0 01-12-2016 "Over the Hills and Far Away"
|
||||
1.9.6 04-15-2016 "Dancing In the Streets"
|
||||
1.9.5 03-21-2016 "Dancing In the Streets"
|
||||
1.9.4 10-09-2015 "Dancing In the Streets"
|
||||
1.9.3 09-03-2015 "Dancing In the Streets"
|
||||
1.9.2 06-24-2015 "Dancing In the Streets"
|
||||
1.9.1 04-27-2015 "Dancing In the Streets"
|
||||
1.9.0 03-25-2015 "Dancing In the Streets"
|
||||
1.8.4 02-19-2015 "You Really Got Me"
|
||||
1.8.3 02-17-2015 "You Really Got Me"
|
||||
1.8.2 12-04-2014 "You Really Got Me"
|
||||
1.8.1 11-26-2014 "You Really Got Me"
|
||||
1.8.0 11-25-2014 "You Really Got Me"
|
||||
1.7.2 09-24-2014 "Summer Nights"
|
||||
1.7.1 08-14-2014 "Summer Nights"
|
||||
1.7.0 08-06-2014 "Summer Nights"
|
||||
1.6.10 07-25-2014 "The Cradle Will Rock"
|
||||
1.6.9 07-24-2014 "The Cradle Will Rock"
|
||||
1.6.8 07-22-2014 "The Cradle Will Rock"
|
||||
1.6.7 07-21-2014 "The Cradle Will Rock"
|
||||
1.6.6 07-01-2014 "The Cradle Will Rock"
|
||||
1.6.5 06-25-2014 "The Cradle Will Rock"
|
||||
1.6.4 06-25-2014 "The Cradle Will Rock"
|
||||
1.6.3 06-09-2014 "The Cradle Will Rock"
|
||||
1.6.2 05-23-2014 "The Cradle Will Rock"
|
||||
1.6.1 05-07-2014 "The Cradle Will Rock"
|
||||
1.6.0 05-05-2014 "The Cradle Will Rock"
|
||||
1.5.5 04-18-2014 "Love Walks In"
|
||||
1.5.4 04-01-2014 "Love Walks In"
|
||||
1.5.3 03-13-2014 "Love Walks In"
|
||||
1.5.2 03-11-2014 "Love Walks In"
|
||||
1.5.1 03-10-2014 "Love Walks In"
|
||||
1.5.0 02-28-2014 "Love Walks In"
|
||||
1.4.5 02-12-2014 "Could This Be Magic?"
|
||||
1.4.4 01-06-2014 "Could This Be Magic?"
|
||||
1.4.3 12-20-2013 "Could This Be Magic?"
|
||||
1.4.2 12-18-2013 "Could This Be Magic?"
|
||||
1.4.1 11-27-2013 "Could This Be Magic?"
|
||||
1.4.0 11-21-2013 "Could This Be Magic?"
|
||||
1.3.4 10-29-2013 "Top of the World"
|
||||
1.3.3 10-09-2013 "Top of the World"
|
||||
1.3.2 09-19-2013 "Top of the World"
|
||||
1.3.1 09-16-2013 "Top of the World"
|
||||
1.3.0 09-13-2013 "Top of the World"
|
||||
1.2.3 08-21-2013 "Right Now"
|
||||
1.2.2 07-05-2013 "Right Now"
|
||||
1.2.1 07-04-2013 "Right Now"
|
||||
1.2.0 06-10-2013 "Right Now"
|
||||
1.1.0 04-02-2013 "Mean Street"
|
||||
1.0.0 02-01-2013 "Eruption"
|
||||
0.9.0 11-30-2012 "Dreams"
|
||||
0.8.0 10-19-2012 "Cathedral"
|
||||
0.7.0 09-06-2012 "Panama"
|
||||
0.6.0 08-06-2012 "Cabo"
|
||||
0.5.0 07-04-2012 "Amsterdam"
|
||||
0.4.0 05-23-2012 "Unchained"
|
||||
0.3.0 04-23-2012 "Baluchitherium"
|
||||
0.2.0 ? "Untitled"
|
||||
0.1.0 ? "Untitled"
|
||||
0.0.2 ? "Untitled"
|
||||
|
||||
21
ROADMAP.rst
21
ROADMAP.rst
@ -1,28 +1,25 @@
|
||||
*******************************
|
||||
Roadmap For Ansible by Red Hat
|
||||
*******************************
|
||||
This document is now the location for published Ansible Core roadmaps.
|
||||
This document is now the location for published Ansible Core roadmaps.
|
||||
|
||||
The roadmap will be updated by version. Based on team and community feedback, an initial roadmap will be published for a major or minor version (2.0, 2.1). Subminor versions will generally not have roadmaps published.
|
||||
|
||||
2.1 was the first time Ansible has published this and asked for feedback in this manner. So feedback on the roadmap and the new process is quite welcome. The team is aiming for further transparency and better inclusion of both community desires and submissions.
|
||||
2.1 was the first time Ansible has published this and asked for feedback in this manner. So feedback on the roadmap and the new process is quite welcome. The team is aiming for further transparency and better inclusion of both community desires and submissions.
|
||||
|
||||
These roadmaps are the team's *best guess* roadmaps based on the Ansible team's experience and are also based on requests and feedback from the community. There are things that may not make it on due to time constraints, lack of community maintainers, etc. And there may be things that got missed, so each roadmap is published both as an idea of what is upcoming in Ansible, and as a medium for seeking further feedback from the community. Here are the good places for you to submit feedback:
|
||||
|
||||
- Add to the agenda of a `Core IRC Meetings <https://github.com/ansible/community/blob/master/meetings/README.md>`_ (preferred)
|
||||
- Ansible's google-group: ansible-devel
|
||||
- AnsibleFest conferences.
|
||||
- IRC freenode channel: #ansible-devel (this one may have things lost in lots of conversation, so a caution).
|
||||
- Ansible's google-group: ansible-devel
|
||||
- Ansible Fest conferences.
|
||||
- IRC freenode channel: #ansible-devel (this one may have things lost in lots of conversation, so a caution).
|
||||
|
||||
=======================================
|
||||
Current 2.4 Roadmap is open for comment
|
||||
Current 2.2 Roadmap is open for comment
|
||||
=======================================
|
||||
We are starting to build the 2.4 Roadmap. We are seeking community feedback! That document is here:
|
||||
- `2.4 Roadmap Document <docs/docsite/rst/roadmap/ROADMAP_2_4.rst>`_
|
||||
We are currently trying to finalize the 2.2 Roadmap, targeted for September delivery. We are seeking community feedback for 2.2. That document is here:
|
||||
|
||||
`2.2 Roadmap Document <docsite/rst/roadmap/ROADMAP_2_2.rst>`_
|
||||
|
||||
|
||||
**Old Roadmaps are found here:**
|
||||
- `2.3 Roadmap Document <docs/docsite/rst/roadmap/ROADMAP_2_3.rst>`_
|
||||
- `2.2 Roadmap Document <docs/docsite/rst/roadmap/ROADMAP_2_2.rst>`_
|
||||
- `2.1 Roadmap Document <docs/docsite/rst/roadmap/ROADMAP_2_1.rst>`_
|
||||
- `2.1 Roadmap Document <docsite/rst/roadmap/ROADMAP_2_1.rst>`_
|
||||
|
||||
@ -1752,6 +1752,11 @@
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.3</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>http://docs.ansible.com/ansible/fireball_module.html</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.3</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>http://docs.ansible.com/ansible/gem_module.html</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
|
||||
50
bin/ansible
50
bin/ansible
@ -37,30 +37,30 @@ import shutil
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
# for debug
|
||||
from multiprocessing import Lock
|
||||
|
||||
import ansible.constants as C
|
||||
from ansible.errors import AnsibleError, AnsibleOptionsError, AnsibleParserError
|
||||
from ansible.utils.display import Display
|
||||
from ansible.module_utils._text import to_text
|
||||
|
||||
|
||||
########################################
|
||||
### OUTPUT OF LAST RESORT ###
|
||||
class LastResort(object):
|
||||
# OUTPUT OF LAST RESORT
|
||||
def display(self, msg, log_only=None):
|
||||
def display(self, msg):
|
||||
print(msg, file=sys.stderr)
|
||||
|
||||
def error(self, msg, wrap_text=None):
|
||||
print(msg, file=sys.stderr)
|
||||
|
||||
|
||||
########################################
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
display = LastResort()
|
||||
|
||||
try: # bad ANSIBLE_CONFIG or config options can force ugly stacktrace
|
||||
import ansible.constants as C
|
||||
from ansible.utils.display import Display
|
||||
except AnsibleOptionsError as e:
|
||||
display.error(to_text(e), wrap_text=False)
|
||||
sys.exit(5)
|
||||
|
||||
cli = None
|
||||
me = os.path.basename(sys.argv[0])
|
||||
|
||||
@ -69,23 +69,17 @@ if __name__ == '__main__':
|
||||
display.debug("starting run")
|
||||
|
||||
sub = None
|
||||
target = me.split('-')
|
||||
if target[-1][0].isdigit():
|
||||
# Remove any version or python version info as downstreams
|
||||
# sometimes add that
|
||||
target = target[:-1]
|
||||
|
||||
if len(target) > 1:
|
||||
sub = target[1]
|
||||
myclass = "%sCLI" % sub.capitalize()
|
||||
elif target[0] == 'ansible':
|
||||
sub = 'adhoc'
|
||||
myclass = 'AdHocCLI'
|
||||
else:
|
||||
raise AnsibleError("Unknown Ansible alias: %s" % me)
|
||||
|
||||
try:
|
||||
mycli = getattr(__import__("ansible.cli.%s" % sub, fromlist=[myclass]), myclass)
|
||||
if me.find('-') != -1:
|
||||
target = me.split('-')
|
||||
if len(target) > 1:
|
||||
sub = target[1]
|
||||
myclass = "%sCLI" % sub.capitalize()
|
||||
mycli = getattr(__import__("ansible.cli.%s" % sub, fromlist=[myclass]), myclass)
|
||||
elif me == 'ansible':
|
||||
from ansible.cli.adhoc import AdHocCLI as mycli
|
||||
else:
|
||||
raise AnsibleError("Unknown Ansible alias: %s" % me)
|
||||
except ImportError as e:
|
||||
# ImportError members have changed in py3
|
||||
if 'msg' in dir(e):
|
||||
@ -130,12 +124,12 @@ if __name__ == '__main__':
|
||||
exit_code = 99
|
||||
except Exception as e:
|
||||
have_cli_options = cli is not None and cli.options is not None
|
||||
display.error("Unexpected Exception, this is probably a bug: %s" % to_text(e), wrap_text=False)
|
||||
display.error("Unexpected Exception: %s" % to_text(e), wrap_text=False)
|
||||
if not have_cli_options or have_cli_options and cli.options.verbosity > 2:
|
||||
log_only = False
|
||||
else:
|
||||
display.display("to see the full traceback, use -vvv")
|
||||
log_only = True
|
||||
log_only = True
|
||||
display.display(u"the full traceback was:\n\n%s" % to_text(traceback.format_exc()), log_only=log_only)
|
||||
exit_code = 250
|
||||
finally:
|
||||
|
||||
@ -1 +0,0 @@
|
||||
ansible
|
||||
@ -1,389 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# (c) 2017, Ansible, Inc. <support@ansible.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
########################################################
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
|
||||
__metaclass__ = type
|
||||
__requires__ = ['ansible']
|
||||
|
||||
try:
|
||||
import pkg_resources
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
import fcntl
|
||||
import os
|
||||
import shlex
|
||||
import signal
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
import datetime
|
||||
import errno
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.module_utils._text import to_bytes, to_native, to_text
|
||||
from ansible.module_utils.six import PY3
|
||||
from ansible.module_utils.six.moves import cPickle
|
||||
from ansible.module_utils.connection import send_data, recv_data
|
||||
from ansible.playbook.play_context import PlayContext
|
||||
from ansible.plugins.loader import connection_loader
|
||||
from ansible.utils.path import unfrackpath, makedirs_safe
|
||||
from ansible.errors import AnsibleConnectionFailure
|
||||
from ansible.utils.display import Display
|
||||
|
||||
|
||||
def do_fork():
|
||||
'''
|
||||
Does the required double fork for a daemon process. Based on
|
||||
http://code.activestate.com/recipes/66012-fork-a-daemon-process-on-unix/
|
||||
'''
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
return pid
|
||||
# This is done as a 'good practice' for daemons, but we need to keep the cwd
|
||||
# leaving it here as a note that we KNOW its good practice but are not doing it on purpose.
|
||||
# os.chdir("/")
|
||||
os.setsid()
|
||||
os.umask(0)
|
||||
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
sys.exit(0)
|
||||
|
||||
if C.DEFAULT_LOG_PATH != '':
|
||||
out_file = open(C.DEFAULT_LOG_PATH, 'ab+')
|
||||
err_file = open(C.DEFAULT_LOG_PATH, 'ab+', 0)
|
||||
else:
|
||||
out_file = open('/dev/null', 'ab+')
|
||||
err_file = open('/dev/null', 'ab+', 0)
|
||||
|
||||
os.dup2(out_file.fileno(), sys.stdout.fileno())
|
||||
os.dup2(err_file.fileno(), sys.stderr.fileno())
|
||||
os.close(sys.stdin.fileno())
|
||||
|
||||
return pid
|
||||
except OSError as e:
|
||||
sys.exit(1)
|
||||
except OSError as e:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class Server():
|
||||
|
||||
def __init__(self, socket_path, play_context):
|
||||
self.socket_path = socket_path
|
||||
self.play_context = play_context
|
||||
|
||||
display.display(
|
||||
'creating new control socket for host %s:%s as user %s' %
|
||||
(play_context.remote_addr, play_context.port, play_context.remote_user),
|
||||
log_only=True
|
||||
)
|
||||
|
||||
display.display('control socket path is %s' % socket_path, log_only=True)
|
||||
display.display('current working directory is %s' % os.getcwd(), log_only=True)
|
||||
|
||||
self._start_time = datetime.datetime.now()
|
||||
|
||||
display.display("using connection plugin %s" % self.play_context.connection, log_only=True)
|
||||
|
||||
self.connection = connection_loader.get(play_context.connection, play_context, sys.stdin)
|
||||
self.connection._connect()
|
||||
|
||||
if not self.connection.connected:
|
||||
raise AnsibleConnectionFailure('unable to connect to remote host %s' % self._play_context.remote_addr)
|
||||
|
||||
connection_time = datetime.datetime.now() - self._start_time
|
||||
display.display('connection established to %s in %s' % (play_context.remote_addr, connection_time), log_only=True)
|
||||
|
||||
self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self.socket.bind(self.socket_path)
|
||||
self.socket.listen(1)
|
||||
display.display('local socket is set to listening', log_only=True)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
while True:
|
||||
signal.signal(signal.SIGALRM, self.connect_timeout)
|
||||
signal.signal(signal.SIGTERM, self.handler)
|
||||
signal.alarm(C.PERSISTENT_CONNECT_TIMEOUT)
|
||||
|
||||
(s, addr) = self.socket.accept()
|
||||
display.display('incoming request accepted on persistent socket', log_only=True)
|
||||
signal.alarm(0)
|
||||
|
||||
while True:
|
||||
data = recv_data(s)
|
||||
if not data:
|
||||
break
|
||||
|
||||
signal.signal(signal.SIGALRM, self.command_timeout)
|
||||
signal.alarm(self.play_context.timeout)
|
||||
|
||||
op = to_text(data.split(b':')[0])
|
||||
display.display('socket operation is %s' % op, log_only=True)
|
||||
|
||||
method = getattr(self, 'do_%s' % op, None)
|
||||
|
||||
rc = 255
|
||||
stdout = stderr = ''
|
||||
|
||||
if not method:
|
||||
stderr = 'Invalid action specified'
|
||||
else:
|
||||
rc, stdout, stderr = method(data)
|
||||
|
||||
signal.alarm(0)
|
||||
|
||||
display.display('socket operation completed with rc %s' % rc, log_only=True)
|
||||
|
||||
send_data(s, to_bytes(rc))
|
||||
send_data(s, to_bytes(stdout))
|
||||
send_data(s, to_bytes(stderr))
|
||||
|
||||
s.close()
|
||||
|
||||
except Exception as e:
|
||||
# socket.accept() will raise EINTR if the socket.close() is called
|
||||
if e.errno != errno.EINTR:
|
||||
display.display(traceback.format_exc(), log_only=True)
|
||||
|
||||
finally:
|
||||
# when done, close the connection properly and cleanup
|
||||
# the socket file so it can be recreated
|
||||
self.shutdown()
|
||||
end_time = datetime.datetime.now()
|
||||
delta = end_time - self._start_time
|
||||
display.display('shutdown local socket, connection was active for %s secs' % delta, log_only=True)
|
||||
|
||||
def connect_timeout(self, signum, frame):
|
||||
display.display('persistent connection idle timeout triggered, timeout value is %s secs' % C.PERSISTENT_CONNECT_TIMEOUT, log_only=True)
|
||||
self.shutdown()
|
||||
|
||||
def command_timeout(self, signum, frame):
|
||||
display.display('command timeout triggered, timeout value is %s secs' % self.play_context.timeout, log_only=True)
|
||||
self.shutdown()
|
||||
|
||||
def handler(self, signum, frame):
|
||||
display.display('signal handler called with signal %s' % signum, log_only=True)
|
||||
self.shutdown()
|
||||
|
||||
def shutdown(self):
|
||||
display.display('shutdown persistent connection requested', log_only=True)
|
||||
|
||||
if not os.path.exists(self.socket_path):
|
||||
display.display('persistent connection is not active', log_only=True)
|
||||
return
|
||||
|
||||
try:
|
||||
if self.socket:
|
||||
display.display('closing local listener', log_only=True)
|
||||
self.socket.close()
|
||||
if self.connection:
|
||||
display.display('closing the connection', log_only=True)
|
||||
self.connection.close()
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
if os.path.exists(self.socket_path):
|
||||
display.display('removing the local control socket', log_only=True)
|
||||
os.remove(self.socket_path)
|
||||
|
||||
display.display('shutdown complete', log_only=True)
|
||||
|
||||
def do_EXEC(self, data):
|
||||
cmd = data.split(b'EXEC: ')[1]
|
||||
display.display('Command executed: %s' % cmd, log_only=True)
|
||||
return self.connection.exec_command(cmd)
|
||||
|
||||
def do_PUT(self, data):
|
||||
(op, src, dst) = shlex.split(to_native(data))
|
||||
return self.connection.fetch_file(src, dst)
|
||||
|
||||
def do_FETCH(self, data):
|
||||
(op, src, dst) = shlex.split(to_native(data))
|
||||
return self.connection.put_file(src, dst)
|
||||
|
||||
def do_CONTEXT(self, data):
|
||||
pc_data = data.split(b'CONTEXT: ', 1)[1]
|
||||
|
||||
if PY3:
|
||||
pc_data = cPickle.loads(pc_data, encoding='bytes')
|
||||
else:
|
||||
pc_data = cPickle.loads(pc_data)
|
||||
|
||||
pc = PlayContext()
|
||||
pc.deserialize(pc_data)
|
||||
|
||||
try:
|
||||
self.connection.update_play_context(pc)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return (0, 'ok', '')
|
||||
|
||||
def do_RUN(self, data):
|
||||
timeout = self.play_context.timeout
|
||||
while bool(timeout):
|
||||
if os.path.exists(self.socket_path):
|
||||
break
|
||||
time.sleep(1)
|
||||
timeout -= 1
|
||||
socket_bytes = to_bytes(self.socket_path, errors='surrogate_or_strict')
|
||||
return 0, b'\n#SOCKET_PATH#: %s\n' % socket_bytes, ''
|
||||
|
||||
|
||||
def communicate(sock, data):
|
||||
send_data(sock, data)
|
||||
rc = int(recv_data(sock), 10)
|
||||
stdout = recv_data(sock)
|
||||
stderr = recv_data(sock)
|
||||
return (rc, stdout, stderr)
|
||||
|
||||
|
||||
def main():
|
||||
# Need stdin as a byte stream
|
||||
if PY3:
|
||||
stdin = sys.stdin.buffer
|
||||
else:
|
||||
stdin = sys.stdin
|
||||
|
||||
try:
|
||||
# read the play context data via stdin, which means depickling it
|
||||
# FIXME: as noted above, we will probably need to deserialize the
|
||||
# connection loader here as well at some point, otherwise this
|
||||
# won't find role- or playbook-based connection plugins
|
||||
cur_line = stdin.readline()
|
||||
init_data = b''
|
||||
while cur_line.strip() != b'#END_INIT#':
|
||||
if cur_line == b'':
|
||||
raise Exception("EOF found before init data was complete")
|
||||
init_data += cur_line
|
||||
cur_line = stdin.readline()
|
||||
if PY3:
|
||||
pc_data = cPickle.loads(init_data, encoding='bytes')
|
||||
else:
|
||||
pc_data = cPickle.loads(init_data)
|
||||
|
||||
pc = PlayContext()
|
||||
pc.deserialize(pc_data)
|
||||
|
||||
except Exception as e:
|
||||
# FIXME: better error message/handling/logging
|
||||
sys.stderr.write(traceback.format_exc())
|
||||
sys.exit("FAIL: %s" % e)
|
||||
|
||||
ssh = connection_loader.get('ssh', class_only=True)
|
||||
cp = ssh._create_control_path(pc.remote_addr, pc.port, pc.remote_user, pc.connection)
|
||||
|
||||
# create the persistent connection dir if need be and create the paths
|
||||
# which we will be using later
|
||||
tmp_path = unfrackpath(C.PERSISTENT_CONTROL_PATH_DIR)
|
||||
makedirs_safe(tmp_path)
|
||||
lock_path = unfrackpath("%s/.ansible_pc_lock" % tmp_path)
|
||||
socket_path = unfrackpath(cp % dict(directory=tmp_path))
|
||||
|
||||
# if the socket file doesn't exist, spin up the daemon process
|
||||
lock_fd = os.open(lock_path, os.O_RDWR | os.O_CREAT, 0o600)
|
||||
fcntl.lockf(lock_fd, fcntl.LOCK_EX)
|
||||
|
||||
if not os.path.exists(socket_path):
|
||||
pid = do_fork()
|
||||
if pid == 0:
|
||||
rc = 0
|
||||
try:
|
||||
server = Server(socket_path, pc)
|
||||
except AnsibleConnectionFailure as exc:
|
||||
display.display('connecting to host %s returned an error' % pc.remote_addr, log_only=True)
|
||||
display.display(str(exc), log_only=True)
|
||||
rc = 1
|
||||
except Exception as exc:
|
||||
display.display('failed to create control socket for host %s' % pc.remote_addr, log_only=True)
|
||||
display.display(traceback.format_exc(), log_only=True)
|
||||
rc = 1
|
||||
fcntl.lockf(lock_fd, fcntl.LOCK_UN)
|
||||
os.close(lock_fd)
|
||||
if rc == 0:
|
||||
server.run()
|
||||
sys.exit(rc)
|
||||
else:
|
||||
display.display('re-using existing socket for %s@%s:%s' % (pc.remote_user, pc.remote_addr, pc.port), log_only=True)
|
||||
|
||||
fcntl.lockf(lock_fd, fcntl.LOCK_UN)
|
||||
os.close(lock_fd)
|
||||
|
||||
timeout = pc.timeout
|
||||
while bool(timeout):
|
||||
if os.path.exists(socket_path):
|
||||
display.vvvv('connected to local socket in %s' % (pc.timeout - timeout), pc.remote_addr)
|
||||
break
|
||||
time.sleep(1)
|
||||
timeout -= 1
|
||||
else:
|
||||
raise AnsibleConnectionFailure('timeout waiting for local socket', pc.remote_addr)
|
||||
|
||||
# now connect to the daemon process
|
||||
# FIXME: if the socket file existed but the daemonized process was killed,
|
||||
# the connection will timeout here. Need to make this more resilient.
|
||||
while True:
|
||||
data = stdin.readline()
|
||||
if data == b'':
|
||||
break
|
||||
if data.strip() == b'':
|
||||
continue
|
||||
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
|
||||
connect_retry_timeout = C.PERSISTENT_CONNECT_RETRY_TIMEOUT
|
||||
while bool(connect_retry_timeout):
|
||||
try:
|
||||
sock.connect(socket_path)
|
||||
break
|
||||
except socket.error:
|
||||
time.sleep(1)
|
||||
connect_retry_timeout -= 1
|
||||
else:
|
||||
display.display('connect retry timeout expired, unable to connect to control socket', pc.remote_addr, pc.remote_user, log_only=True)
|
||||
display.display('persistent_connect_retry_timeout is %s secs' % (C.PERSISTENT_CONNECT_RETRY_TIMEOUT), pc.remote_addr, pc.remote_user, log_only=True)
|
||||
sys.stderr.write('failed to connect to control socket')
|
||||
sys.exit(255)
|
||||
|
||||
# send the play_context back into the connection so the connection
|
||||
# can handle any privilege escalation activities
|
||||
pc_data = b'CONTEXT: %s' % init_data
|
||||
communicate(sock, pc_data)
|
||||
|
||||
rc, stdout, stderr = communicate(sock, data.strip())
|
||||
|
||||
sys.stdout.write(to_native(stdout))
|
||||
sys.stderr.write(to_native(stderr))
|
||||
|
||||
sock.close()
|
||||
break
|
||||
|
||||
sys.exit(rc)
|
||||
|
||||
if __name__ == '__main__':
|
||||
display = Display()
|
||||
main()
|
||||
@ -1 +0,0 @@
|
||||
ansible
|
||||
@ -1,26 +1,17 @@
|
||||
contrib
|
||||
-------
|
||||
Files here provide an extension mechanism for Ansible similar to plugins. They are not maintained by the Ansible core team or installed with Ansible.
|
||||
|
||||
|
||||
inventory
|
||||
=========
|
||||
|
||||
Before 2.4 introduced inventory plugins, inventory scripts were the only way to provide sources that were not built into Ansible. Inventory scripts allow you to store your hosts, groups, and variables in any way you like.
|
||||
Inventory scripts allow you to store your hosts, groups, and variables in any way
|
||||
you like. Examples include discovering inventory from EC2 or pulling it from
|
||||
Cobbler. These could also be used to interface with LDAP or database.
|
||||
|
||||
Starting with Ansible version 2.4, they are enabled via the 'script' inventory plugin.
|
||||
Examples of use include discovering inventory from EC2 or pulling it from Cobbler. These could also be used to interface with LDAP or the database.
|
||||
|
||||
`chmod +x` an inventory plugin and either name it `/etc/ansible/hosts` or use `ansible -i /path/to/inventory/script`. You might also need to copy a configuration file with the same name and/or set environment variables. The scripts or configuration files can provide more details.
|
||||
|
||||
vault
|
||||
=====
|
||||
|
||||
If the file passed to `--vault-password-file` has the executable bit set, Ansible will execute it and use the stdout of that execution as 'the secret'.
|
||||
Vault scripts provided here use this facility to retrieve the vault secret from a number of sources.
|
||||
chmod +x an inventory plugin and either name it /etc/ansible/hosts or use ansible
|
||||
with -i to designate the path to the script. You might also need to copy a configuration
|
||||
file with the same name and/or set environment variables, the scripts or configuration
|
||||
files have more details.
|
||||
|
||||
contributions welcome
|
||||
=====================
|
||||
|
||||
Send in pull requests to add scripts of your own. The sky is the limit!
|
||||
Send in pull requests to add plugins of your own. The sky is the limit!
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ This script generates an Ansible hosts file with these host groups:
|
||||
|
||||
ABQ_xxx: Defines a hosts itself by Abiquo VM name label
|
||||
all: Contains all hosts defined in Abiquo user's enterprise
|
||||
virtualdatecenter: Creates a host group for each virtualdatacenter containing all hosts defined on it
|
||||
virtualdatecenter: Creates a host group for each virtualdatacenter containing all hosts defined on it
|
||||
virtualappliance: Creates a host group for each virtualappliance containing all hosts defined on it
|
||||
imagetemplate: Creates a host group for each image template containing all hosts using it
|
||||
|
||||
@ -53,68 +53,64 @@ except ImportError:
|
||||
|
||||
from ansible.module_utils.urls import open_url
|
||||
|
||||
|
||||
def api_get(link, config):
|
||||
try:
|
||||
if link is None:
|
||||
url = config.get('api', 'uri') + config.get('api', 'login_path')
|
||||
headers = {"Accept": config.get('api', 'login_type')}
|
||||
if link == None:
|
||||
url = config.get('api','uri') + config.get('api','login_path')
|
||||
headers = {"Accept": config.get('api','login_type')}
|
||||
else:
|
||||
url = link['href'] + '?limit=0'
|
||||
headers = {"Accept": link['type']}
|
||||
result = open_url(url, headers=headers, url_username=config.get('auth', 'apiuser').replace('\n', ''),
|
||||
url_password=config.get('auth', 'apipass').replace('\n', ''))
|
||||
result = open_url(url, headers=headers, url_username=config.get('auth','apiuser').replace('\n', ''),
|
||||
url_password=config.get('auth','apipass').replace('\n', ''))
|
||||
return json.loads(result.read())
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def save_cache(data, config):
|
||||
''' saves item to cache '''
|
||||
dpath = config.get('cache', 'cache_dir')
|
||||
dpath = config.get('cache','cache_dir')
|
||||
try:
|
||||
cache = open('/'.join([dpath, 'inventory']), 'w')
|
||||
cache = open('/'.join([dpath,'inventory']), 'w')
|
||||
cache.write(json.dumps(data))
|
||||
cache.close()
|
||||
except IOError as e:
|
||||
pass # not really sure what to do here
|
||||
pass # not really sure what to do here
|
||||
|
||||
|
||||
def get_cache(cache_item, config):
|
||||
''' returns cached item '''
|
||||
dpath = config.get('cache', 'cache_dir')
|
||||
dpath = config.get('cache','cache_dir')
|
||||
inv = {}
|
||||
try:
|
||||
cache = open('/'.join([dpath, 'inventory']), 'r')
|
||||
cache = open('/'.join([dpath,'inventory']), 'r')
|
||||
inv = cache.read()
|
||||
cache.close()
|
||||
except IOError as e:
|
||||
pass # not really sure what to do here
|
||||
pass # not really sure what to do here
|
||||
|
||||
return inv
|
||||
|
||||
|
||||
def cache_available(config):
|
||||
''' checks if we have a 'fresh' cache available for item requested '''
|
||||
|
||||
if config.has_option('cache', 'cache_dir'):
|
||||
dpath = config.get('cache', 'cache_dir')
|
||||
if config.has_option('cache','cache_dir'):
|
||||
dpath = config.get('cache','cache_dir')
|
||||
|
||||
try:
|
||||
existing = os.stat('/'.join([dpath, 'inventory']))
|
||||
existing = os.stat( '/'.join([dpath,'inventory']))
|
||||
except:
|
||||
# cache doesn't exist or isn't accessible
|
||||
return False
|
||||
|
||||
if config.has_option('cache', 'cache_max_age'):
|
||||
maxage = config.get('cache', 'cache_max_age')
|
||||
if (int(time.time()) - int(existing.st_mtime)) <= int(maxage):
|
||||
if ((int(time.time()) - int(existing.st_mtime)) <= int(maxage)):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def generate_inv_from_api(enterprise_entity, config):
|
||||
def generate_inv_from_api(enterprise_entity,config):
|
||||
try:
|
||||
inventory['all'] = {}
|
||||
inventory['all']['children'] = []
|
||||
@ -122,58 +118,54 @@ def generate_inv_from_api(enterprise_entity, config):
|
||||
inventory['_meta'] = {}
|
||||
inventory['_meta']['hostvars'] = {}
|
||||
|
||||
enterprise = api_get(enterprise_entity, config)
|
||||
vms_entity = next(link for link in enterprise['links'] if link['rel'] == 'virtualmachines')
|
||||
vms = api_get(vms_entity, config)
|
||||
enterprise = api_get(enterprise_entity,config)
|
||||
vms_entity = next(link for link in (enterprise['links']) if (link['rel']=='virtualmachines'))
|
||||
vms = api_get(vms_entity,config)
|
||||
for vmcollection in vms['collection']:
|
||||
for link in vmcollection['links']:
|
||||
if link['rel'] == 'virtualappliance':
|
||||
vm_vapp = link['title'].replace('[', '').replace(']', '').replace(' ', '_')
|
||||
elif link['rel'] == 'virtualdatacenter':
|
||||
vm_vdc = link['title'].replace('[', '').replace(']', '').replace(' ', '_')
|
||||
elif link['rel'] == 'virtualmachinetemplate':
|
||||
vm_template = link['title'].replace('[', '').replace(']', '').replace(' ', '_')
|
||||
vm_vapp = next(link for link in (vmcollection['links']) if (link['rel']=='virtualappliance'))['title'].replace('[','').replace(']','').replace(' ','_')
|
||||
vm_vdc = next(link for link in (vmcollection['links']) if (link['rel']=='virtualdatacenter'))['title'].replace('[','').replace(']','').replace(' ','_')
|
||||
vm_template = next(link for link in (vmcollection['links']) if (link['rel']=='virtualmachinetemplate'))['title'].replace('[','').replace(']','').replace(' ','_')
|
||||
|
||||
# From abiquo.ini: Only adding to inventory VMs with public IP
|
||||
if config.getboolean('defaults', 'public_ip_only') is True:
|
||||
if (config.getboolean('defaults', 'public_ip_only')) == True:
|
||||
for link in vmcollection['links']:
|
||||
if link['type'] == 'application/vnd.abiquo.publicip+json' and link['rel'] == 'ip':
|
||||
vm_nic = link['title']
|
||||
break
|
||||
if (link['type']=='application/vnd.abiquo.publicip+json' and link['rel']=='ip'):
|
||||
vm_nic = link['title']
|
||||
break
|
||||
else:
|
||||
vm_nic = None
|
||||
vm_nic = None
|
||||
# Otherwise, assigning defined network interface IP address
|
||||
else:
|
||||
for link in vmcollection['links']:
|
||||
if link['rel'] == config.get('defaults', 'default_net_interface'):
|
||||
vm_nic = link['title']
|
||||
break
|
||||
if (link['rel']==config.get('defaults', 'default_net_interface')):
|
||||
vm_nic = link['title']
|
||||
break
|
||||
else:
|
||||
vm_nic = None
|
||||
|
||||
vm_nic = None
|
||||
|
||||
vm_state = True
|
||||
# From abiquo.ini: Only adding to inventory VMs deployed
|
||||
if config.getboolean('defaults', 'deployed_only') is True and vmcollection['state'] == 'NOT_ALLOCATED':
|
||||
if ((config.getboolean('defaults', 'deployed_only') == True) and (vmcollection['state'] == 'NOT_ALLOCATED')):
|
||||
vm_state = False
|
||||
|
||||
if vm_nic is not None and vm_state:
|
||||
if vm_vapp not in inventory:
|
||||
if not vm_nic == None and vm_state:
|
||||
if not vm_vapp in inventory.keys():
|
||||
inventory[vm_vapp] = {}
|
||||
inventory[vm_vapp]['children'] = []
|
||||
inventory[vm_vapp]['hosts'] = []
|
||||
if vm_vdc not in inventory:
|
||||
if not vm_vdc in inventory.keys():
|
||||
inventory[vm_vdc] = {}
|
||||
inventory[vm_vdc]['hosts'] = []
|
||||
inventory[vm_vdc]['children'] = []
|
||||
if vm_template not in inventory:
|
||||
if not vm_template in inventory.keys():
|
||||
inventory[vm_template] = {}
|
||||
inventory[vm_template]['children'] = []
|
||||
inventory[vm_template]['hosts'] = []
|
||||
if config.getboolean('defaults', 'get_metadata') is True:
|
||||
meta_entity = next(link for link in vmcollection['links'] if link['rel'] == 'metadata')
|
||||
if config.getboolean('defaults', 'get_metadata') == True:
|
||||
meta_entity = next(link for link in (vmcollection['links']) if (link['rel']=='metadata'))
|
||||
try:
|
||||
metadata = api_get(meta_entity, config)
|
||||
if (config.getfloat("api", "version") >= 3.0):
|
||||
metadata = api_get(meta_entity,config)
|
||||
if (config.getfloat("api","version") >= 3.0):
|
||||
vm_metadata = metadata['metadata']
|
||||
else:
|
||||
vm_metadata = metadata['metadata']['metadata']
|
||||
@ -191,8 +183,7 @@ def generate_inv_from_api(enterprise_entity, config):
|
||||
return inventory
|
||||
except Exception as e:
|
||||
# Return empty hosts output
|
||||
return {'all': {'hosts': []}, '_meta': {'hostvars': {}}}
|
||||
|
||||
return { 'all': {'hosts': []}, '_meta': { 'hostvars': {} } }
|
||||
|
||||
def get_inventory(enterprise, config):
|
||||
''' Reads the inventory from cache or Abiquo api '''
|
||||
@ -202,12 +193,11 @@ def get_inventory(enterprise, config):
|
||||
else:
|
||||
default_group = os.path.basename(sys.argv[0]).rstrip('.py')
|
||||
# MAKE ABIQUO API CALLS #
|
||||
inv = generate_inv_from_api(enterprise, config)
|
||||
inv = generate_inv_from_api(enterprise,config)
|
||||
|
||||
save_cache(inv, config)
|
||||
return json.dumps(inv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
inventory = {}
|
||||
enterprise = {}
|
||||
@ -220,8 +210,8 @@ if __name__ == '__main__':
|
||||
break
|
||||
|
||||
try:
|
||||
login = api_get(None, config)
|
||||
enterprise = next(link for link in login['links'] if link['rel'] == 'enterprise')
|
||||
login = api_get(None,config)
|
||||
enterprise = next(link for link in (login['links']) if (link['rel']=='enterprise'))
|
||||
except Exception as e:
|
||||
enterprise = None
|
||||
|
||||
|
||||
@ -82,6 +82,7 @@ class LibcloudInventory(object):
|
||||
|
||||
print(data_to_print)
|
||||
|
||||
|
||||
def is_cache_valid(self):
|
||||
''' Determines if the cache files have expired, or if it is still valid '''
|
||||
|
||||
@ -94,6 +95,7 @@ class LibcloudInventory(object):
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def read_settings(self):
|
||||
''' Reads the settings from the libcloud.ini file '''
|
||||
|
||||
@ -106,17 +108,17 @@ class LibcloudInventory(object):
|
||||
raise ValueError('libcloud.ini file must contain a [driver] section')
|
||||
|
||||
if config.has_option('driver', 'provider'):
|
||||
self.provider = config.get('driver', 'provider')
|
||||
self.provider = config.get('driver','provider')
|
||||
else:
|
||||
raise ValueError('libcloud.ini does not have a provider defined')
|
||||
|
||||
if config.has_option('driver', 'key'):
|
||||
self.key = config.get('driver', 'key')
|
||||
self.key = config.get('driver','key')
|
||||
else:
|
||||
raise ValueError('libcloud.ini does not have a key defined')
|
||||
|
||||
if config.has_option('driver', 'secret'):
|
||||
self.secret = config.get('driver', 'secret')
|
||||
self.secret = config.get('driver','secret')
|
||||
else:
|
||||
raise ValueError('libcloud.ini does not have a secret defined')
|
||||
|
||||
@ -131,7 +133,7 @@ class LibcloudInventory(object):
|
||||
if config.has_option('driver', 'path'):
|
||||
self.path = config.get('driver', 'path')
|
||||
if config.has_option('driver', 'api_version'):
|
||||
self.api_version = config.get('driver', 'api_version')
|
||||
self.api_version = config.get('driver', 'api_version')
|
||||
|
||||
Driver = get_driver(getattr(Provider, self.provider))
|
||||
|
||||
@ -143,6 +145,7 @@ class LibcloudInventory(object):
|
||||
self.cache_path_cache = cache_path + "/ansible-libcloud.cache"
|
||||
self.cache_path_index = cache_path + "/ansible-libcloud.index"
|
||||
self.cache_max_age = config.getint('cache', 'cache_max_age')
|
||||
|
||||
|
||||
def parse_cli_args(self):
|
||||
'''
|
||||
@ -151,16 +154,17 @@ class LibcloudInventory(object):
|
||||
|
||||
parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on libcloud supported providers')
|
||||
parser.add_argument('--list', action='store_true', default=True,
|
||||
help='List instances (default: True)')
|
||||
help='List instances (default: True)')
|
||||
parser.add_argument('--host', action='store',
|
||||
help='Get all the variables about a specific instance')
|
||||
help='Get all the variables about a specific instance')
|
||||
parser.add_argument('--refresh-cache', action='store_true', default=False,
|
||||
help='Force refresh of cache by making API requests to libcloud supported providers (default: False - use cache files)')
|
||||
help='Force refresh of cache by making API requests to libcloud supported providers (default: False - use cache files)')
|
||||
self.args = parser.parse_args()
|
||||
|
||||
|
||||
def do_api_calls_update_cache(self):
|
||||
'''
|
||||
Do API calls to a location, and save data in cache files
|
||||
'''
|
||||
Do API calls to a location, and save data in cache files
|
||||
'''
|
||||
|
||||
self.get_nodes()
|
||||
@ -168,6 +172,7 @@ class LibcloudInventory(object):
|
||||
self.write_to_cache(self.inventory, self.cache_path_cache)
|
||||
self.write_to_cache(self.index, self.cache_path_index)
|
||||
|
||||
|
||||
def get_nodes(self):
|
||||
'''
|
||||
Gets the list of all nodes
|
||||
@ -176,6 +181,7 @@ class LibcloudInventory(object):
|
||||
for node in self.conn.list_nodes():
|
||||
self.add_node(node)
|
||||
|
||||
|
||||
def get_node(self, node_id):
|
||||
'''
|
||||
Gets details about a specific node
|
||||
@ -183,10 +189,11 @@ class LibcloudInventory(object):
|
||||
|
||||
return [node for node in self.conn.list_nodes() if node.id == node_id][0]
|
||||
|
||||
|
||||
def add_node(self, node):
|
||||
'''
|
||||
Adds a node to the inventory and index, as long as it is
|
||||
addressable
|
||||
addressable
|
||||
'''
|
||||
|
||||
# Only want running instances
|
||||
@ -218,7 +225,7 @@ class LibcloudInventory(object):
|
||||
# Inventory: Group by key pair
|
||||
if node.extra['key_name']:
|
||||
self.push(self.inventory, self.to_safe('key_' + node.extra['key_name']), dest)
|
||||
|
||||
|
||||
# Inventory: Group by security group, quick thing to handle single sg
|
||||
if node.extra['security_group']:
|
||||
self.push(self.inventory, self.to_safe('sg_' + node.extra['security_group'][0]), dest)
|
||||
@ -237,10 +244,10 @@ class LibcloudInventory(object):
|
||||
# Need to load index from cache
|
||||
self.load_index_from_cache()
|
||||
|
||||
if self.args.host not in self.index:
|
||||
if not self.args.host in self.index:
|
||||
# try updating the cache
|
||||
self.do_api_calls_update_cache()
|
||||
if self.args.host not in self.index:
|
||||
if not self.args.host in self.index:
|
||||
# host migh not exist anymore
|
||||
return self.json_format_dict({}, True)
|
||||
|
||||
@ -248,7 +255,8 @@ class LibcloudInventory(object):
|
||||
|
||||
node = self.get_node(node_id)
|
||||
instance_vars = {}
|
||||
for key, value in vars(node).items():
|
||||
for key in vars(instance):
|
||||
value = getattr(instance, key)
|
||||
key = self.to_safe('ec2_' + key)
|
||||
|
||||
# Handle complex types
|
||||
@ -275,12 +283,13 @@ class LibcloudInventory(object):
|
||||
else:
|
||||
pass
|
||||
# TODO Product codes if someone finds them useful
|
||||
# print(key)
|
||||
# print(type(value))
|
||||
# print(value)
|
||||
#print(key)
|
||||
#print(type(value))
|
||||
#print(value)
|
||||
|
||||
return self.json_format_dict(instance_vars, True)
|
||||
|
||||
|
||||
def push(self, my_dict, key, element):
|
||||
'''
|
||||
Pushed an element onto an array that may not have been defined in
|
||||
@ -288,10 +297,11 @@ class LibcloudInventory(object):
|
||||
'''
|
||||
|
||||
if key in my_dict:
|
||||
my_dict[key].append(element)
|
||||
my_dict[key].append(element);
|
||||
else:
|
||||
my_dict[key] = [element]
|
||||
|
||||
|
||||
def get_inventory_from_cache(self):
|
||||
'''
|
||||
Reads the inventory from the cache file and returns it as a JSON
|
||||
@ -302,6 +312,7 @@ class LibcloudInventory(object):
|
||||
json_inventory = cache.read()
|
||||
return json_inventory
|
||||
|
||||
|
||||
def load_index_from_cache(self):
|
||||
'''
|
||||
Reads the index from the cache file sets self.index
|
||||
@ -311,6 +322,7 @@ class LibcloudInventory(object):
|
||||
json_index = cache.read()
|
||||
self.index = json.loads(json_index)
|
||||
|
||||
|
||||
def write_to_cache(self, data, filename):
|
||||
'''
|
||||
Writes data in JSON format to a file
|
||||
@ -321,6 +333,7 @@ class LibcloudInventory(object):
|
||||
cache.write(json_data)
|
||||
cache.close()
|
||||
|
||||
|
||||
def to_safe(self, word):
|
||||
'''
|
||||
Converts 'bad' characters in a string to underscores so they can be
|
||||
@ -329,6 +342,7 @@ class LibcloudInventory(object):
|
||||
|
||||
return re.sub("[^A-Za-z0-9\-]", "_", word)
|
||||
|
||||
|
||||
def json_format_dict(self, data, pretty=False):
|
||||
'''
|
||||
Converts a dict to a JSON object and dumps it as a formatted
|
||||
@ -340,9 +354,8 @@ class LibcloudInventory(object):
|
||||
else:
|
||||
return json.dumps(data)
|
||||
|
||||
|
||||
def main():
|
||||
LibcloudInventory()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
main()
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
# Ansible Apstra AOS external inventory script settings
|
||||
# Dynamic Inventory script parameter can be provided using this file
|
||||
# Or by using Environment Variables:
|
||||
# - AOS_SERVER, AOS_PORT, AOS_USERNAME, AOS_PASSWORD, AOS_BLUEPRINT
|
||||
#
|
||||
# This file takes precedence over the Environment Variables
|
||||
#
|
||||
|
||||
[aos]
|
||||
|
||||
# aos_server = 172.20.62.3
|
||||
# port = 8888
|
||||
# username = admin
|
||||
# password = admin
|
||||
|
||||
## Blueprint Mode
|
||||
# to use the inventory in mode Blueprint, you need to define the blueprint name you want to use
|
||||
|
||||
# blueprint = my-blueprint-l2
|
||||
# blueprint_interface = true
|
||||
@ -1,588 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# (c) 2017 Apstra Inc, <community@apstra.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
"""
|
||||
Apstra AOS external inventory script
|
||||
====================================
|
||||
|
||||
Ansible has a feature where instead of reading from /etc/ansible/hosts
|
||||
as a text file, it can query external programs to obtain the list
|
||||
of hosts, groups the hosts are in, and even variables to assign to each host.
|
||||
|
||||
To use this:
|
||||
- copy this file over /etc/ansible/hosts and chmod +x the file.
|
||||
- Copy both files (.py and .ini) in your preferred directory
|
||||
|
||||
More information about Ansible Dynamic Inventory here
|
||||
http://unix.stackexchange.com/questions/205479/in-ansible-dynamic-inventory-json-can-i-render-hostvars-based-on-the-hostname
|
||||
|
||||
2 modes are currently, supported: **device based** or **blueprint based**:
|
||||
- For **Device based**, the list of device is taken from the global device list
|
||||
the serial ID will be used as the inventory_hostname
|
||||
- For **Blueprint based**, the list of device is taken from the given blueprint
|
||||
the Node name will be used as the inventory_hostname
|
||||
|
||||
Input parameters parameter can be provided using either with the ini file or by using Environment Variables:
|
||||
The following list of Environment Variables are supported: AOS_SERVER, AOS_PORT, AOS_USERNAME, AOS_PASSWORD, AOS_BLUEPRINT
|
||||
The config file takes precedence over the Environment Variables
|
||||
|
||||
Tested with Apstra AOS 1.1
|
||||
|
||||
This script has been inspired by the cobbler.py inventory. thanks
|
||||
|
||||
Author: Damien Garros (@dgarros)
|
||||
Version: 0.2.0
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
try:
|
||||
import argparse
|
||||
HAS_ARGPARSE = True
|
||||
except ImportError:
|
||||
HAS_ARGPARSE = False
|
||||
|
||||
try:
|
||||
from apstra.aosom.session import Session
|
||||
HAS_AOS_PYEZ = True
|
||||
except ImportError:
|
||||
HAS_AOS_PYEZ = False
|
||||
|
||||
from ansible.module_utils.six.moves import configparser
|
||||
|
||||
|
||||
"""
|
||||
##
|
||||
Expected output format in Device mode
|
||||
{
|
||||
"Cumulus": {
|
||||
"hosts": [
|
||||
"52540073956E",
|
||||
"52540022211A"
|
||||
],
|
||||
"vars": {}
|
||||
},
|
||||
"EOS": {
|
||||
"hosts": [
|
||||
"5254001CAFD8",
|
||||
"525400DDDF72"
|
||||
],
|
||||
"vars": {}
|
||||
},
|
||||
"Generic Model": {
|
||||
"hosts": [
|
||||
"525400E5486D"
|
||||
],
|
||||
"vars": {}
|
||||
},
|
||||
"Ubuntu GNU/Linux": {
|
||||
"hosts": [
|
||||
"525400E5486D"
|
||||
],
|
||||
"vars": {}
|
||||
},
|
||||
"VX": {
|
||||
"hosts": [
|
||||
"52540073956E",
|
||||
"52540022211A"
|
||||
],
|
||||
"vars": {}
|
||||
},
|
||||
"_meta": {
|
||||
"hostvars": {
|
||||
"5254001CAFD8": {
|
||||
"agent_start_time": "2017-02-03T00:49:16.000000Z",
|
||||
"ansible_ssh_host": "172.20.52.6",
|
||||
"aos_hcl_model": "Arista_vEOS",
|
||||
"aos_server": "",
|
||||
"aos_version": "AOS_1.1.1_OB.5",
|
||||
"comm_state": "on",
|
||||
"device_start_time": "2017-02-03T00:47:58.454480Z",
|
||||
"domain_name": "",
|
||||
"error_message": "",
|
||||
"fqdn": "localhost",
|
||||
"hostname": "localhost",
|
||||
"hw_model": "vEOS",
|
||||
"hw_version": "",
|
||||
"is_acknowledged": false,
|
||||
"mgmt_ifname": "Management1",
|
||||
"mgmt_ipaddr": "172.20.52.6",
|
||||
"mgmt_macaddr": "52:54:00:1C:AF:D8",
|
||||
"os_arch": "x86_64",
|
||||
"os_family": "EOS",
|
||||
"os_version": "4.16.6M",
|
||||
"os_version_info": {
|
||||
"build": "6M",
|
||||
"major": "4",
|
||||
"minor": "16"
|
||||
},
|
||||
"serial_number": "5254001CAFD8",
|
||||
"state": "OOS-QUARANTINED",
|
||||
"vendor": "Arista"
|
||||
},
|
||||
"52540022211A": {
|
||||
"agent_start_time": "2017-02-03T00:45:22.000000Z",
|
||||
"ansible_ssh_host": "172.20.52.7",
|
||||
"aos_hcl_model": "Cumulus_VX",
|
||||
"aos_server": "172.20.52.3",
|
||||
"aos_version": "AOS_1.1.1_OB.5",
|
||||
"comm_state": "on",
|
||||
"device_start_time": "2017-02-03T00:45:11.019189Z",
|
||||
"domain_name": "",
|
||||
"error_message": "",
|
||||
"fqdn": "cumulus",
|
||||
"hostname": "cumulus",
|
||||
"hw_model": "VX",
|
||||
"hw_version": "",
|
||||
"is_acknowledged": false,
|
||||
"mgmt_ifname": "eth0",
|
||||
"mgmt_ipaddr": "172.20.52.7",
|
||||
"mgmt_macaddr": "52:54:00:22:21:1a",
|
||||
"os_arch": "x86_64",
|
||||
"os_family": "Cumulus",
|
||||
"os_version": "3.1.1",
|
||||
"os_version_info": {
|
||||
"build": "1",
|
||||
"major": "3",
|
||||
"minor": "1"
|
||||
},
|
||||
"serial_number": "52540022211A",
|
||||
"state": "OOS-QUARANTINED",
|
||||
"vendor": "Cumulus"
|
||||
},
|
||||
"52540073956E": {
|
||||
"agent_start_time": "2017-02-03T00:45:19.000000Z",
|
||||
"ansible_ssh_host": "172.20.52.8",
|
||||
"aos_hcl_model": "Cumulus_VX",
|
||||
"aos_server": "172.20.52.3",
|
||||
"aos_version": "AOS_1.1.1_OB.5",
|
||||
"comm_state": "on",
|
||||
"device_start_time": "2017-02-03T00:45:11.030113Z",
|
||||
"domain_name": "",
|
||||
"error_message": "",
|
||||
"fqdn": "cumulus",
|
||||
"hostname": "cumulus",
|
||||
"hw_model": "VX",
|
||||
"hw_version": "",
|
||||
"is_acknowledged": false,
|
||||
"mgmt_ifname": "eth0",
|
||||
"mgmt_ipaddr": "172.20.52.8",
|
||||
"mgmt_macaddr": "52:54:00:73:95:6e",
|
||||
"os_arch": "x86_64",
|
||||
"os_family": "Cumulus",
|
||||
"os_version": "3.1.1",
|
||||
"os_version_info": {
|
||||
"build": "1",
|
||||
"major": "3",
|
||||
"minor": "1"
|
||||
},
|
||||
"serial_number": "52540073956E",
|
||||
"state": "OOS-QUARANTINED",
|
||||
"vendor": "Cumulus"
|
||||
},
|
||||
"525400DDDF72": {
|
||||
"agent_start_time": "2017-02-03T00:49:07.000000Z",
|
||||
"ansible_ssh_host": "172.20.52.5",
|
||||
"aos_hcl_model": "Arista_vEOS",
|
||||
"aos_server": "",
|
||||
"aos_version": "AOS_1.1.1_OB.5",
|
||||
"comm_state": "on",
|
||||
"device_start_time": "2017-02-03T00:47:46.929921Z",
|
||||
"domain_name": "",
|
||||
"error_message": "",
|
||||
"fqdn": "localhost",
|
||||
"hostname": "localhost",
|
||||
"hw_model": "vEOS",
|
||||
"hw_version": "",
|
||||
"is_acknowledged": false,
|
||||
"mgmt_ifname": "Management1",
|
||||
"mgmt_ipaddr": "172.20.52.5",
|
||||
"mgmt_macaddr": "52:54:00:DD:DF:72",
|
||||
"os_arch": "x86_64",
|
||||
"os_family": "EOS",
|
||||
"os_version": "4.16.6M",
|
||||
"os_version_info": {
|
||||
"build": "6M",
|
||||
"major": "4",
|
||||
"minor": "16"
|
||||
},
|
||||
"serial_number": "525400DDDF72",
|
||||
"state": "OOS-QUARANTINED",
|
||||
"vendor": "Arista"
|
||||
},
|
||||
"525400E5486D": {
|
||||
"agent_start_time": "2017-02-02T18:44:42.000000Z",
|
||||
"ansible_ssh_host": "172.20.52.4",
|
||||
"aos_hcl_model": "Generic_Server_1RU_1x10G",
|
||||
"aos_server": "172.20.52.3",
|
||||
"aos_version": "AOS_1.1.1_OB.5",
|
||||
"comm_state": "on",
|
||||
"device_start_time": "2017-02-02T21:11:25.188734Z",
|
||||
"domain_name": "",
|
||||
"error_message": "",
|
||||
"fqdn": "localhost",
|
||||
"hostname": "localhost",
|
||||
"hw_model": "Generic Model",
|
||||
"hw_version": "pc-i440fx-trusty",
|
||||
"is_acknowledged": false,
|
||||
"mgmt_ifname": "eth0",
|
||||
"mgmt_ipaddr": "172.20.52.4",
|
||||
"mgmt_macaddr": "52:54:00:e5:48:6d",
|
||||
"os_arch": "x86_64",
|
||||
"os_family": "Ubuntu GNU/Linux",
|
||||
"os_version": "14.04 LTS",
|
||||
"os_version_info": {
|
||||
"build": "",
|
||||
"major": "14",
|
||||
"minor": "04"
|
||||
},
|
||||
"serial_number": "525400E5486D",
|
||||
"state": "OOS-QUARANTINED",
|
||||
"vendor": "Generic Manufacturer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"all": {
|
||||
"hosts": [
|
||||
"5254001CAFD8",
|
||||
"52540073956E",
|
||||
"525400DDDF72",
|
||||
"525400E5486D",
|
||||
"52540022211A"
|
||||
],
|
||||
"vars": {}
|
||||
},
|
||||
"vEOS": {
|
||||
"hosts": [
|
||||
"5254001CAFD8",
|
||||
"525400DDDF72"
|
||||
],
|
||||
"vars": {}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def fail(msg):
|
||||
sys.stderr.write("%s\n" % msg)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class AosInventory(object):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
""" Main execution path """
|
||||
|
||||
if not HAS_AOS_PYEZ:
|
||||
raise Exception('aos-pyez is not installed. Please see details here: https://github.com/Apstra/aos-pyez')
|
||||
if not HAS_ARGPARSE:
|
||||
raise Exception('argparse is not installed. Please install the argparse library or upgrade to python-2.7')
|
||||
|
||||
# Initialize inventory
|
||||
self.inventory = dict() # A list of groups and the hosts in that group
|
||||
self.inventory['_meta'] = dict()
|
||||
self.inventory['_meta']['hostvars'] = dict()
|
||||
|
||||
# Read settings and parse CLI arguments
|
||||
self.read_settings()
|
||||
self.parse_cli_args()
|
||||
|
||||
# ----------------------------------------------------
|
||||
# Open session to AOS
|
||||
# ----------------------------------------------------
|
||||
aos = Session(server=self.aos_server,
|
||||
port=self.aos_server_port,
|
||||
user=self.aos_username,
|
||||
passwd=self.aos_password)
|
||||
|
||||
aos.login()
|
||||
|
||||
# Save session information in variables of group all
|
||||
self.add_var_to_group('all', 'aos_session', aos.session)
|
||||
|
||||
# Add the AOS server itself in the inventory
|
||||
self.add_host_to_group("all", 'aos')
|
||||
self.add_var_to_host("aos", "ansible_ssh_host", self.aos_server)
|
||||
self.add_var_to_host("aos", "ansible_ssh_pass", self.aos_password)
|
||||
self.add_var_to_host("aos", "ansible_ssh_user", self.aos_username)
|
||||
|
||||
# ----------------------------------------------------
|
||||
# Build the inventory
|
||||
# 2 modes are supported: device based or blueprint based
|
||||
# - For device based, the list of device is taken from the global device list
|
||||
# the serial ID will be used as the inventory_hostname
|
||||
# - For Blueprint based, the list of device is taken from the given blueprint
|
||||
# the Node name will be used as the inventory_hostname
|
||||
# ----------------------------------------------------
|
||||
if self.aos_blueprint:
|
||||
|
||||
bp = aos.Blueprints[self.aos_blueprint]
|
||||
if bp.exists is False:
|
||||
fail("Unable to find the Blueprint: %s" % self.aos_blueprint)
|
||||
|
||||
for dev_name, dev_id in bp.params['devices'].value.items():
|
||||
|
||||
self.add_host_to_group('all', dev_name)
|
||||
device = aos.Devices.find(uid=dev_id)
|
||||
|
||||
if 'facts' in device.value.keys():
|
||||
self.add_device_facts_to_var(dev_name, device)
|
||||
|
||||
# Define admin State and Status
|
||||
if 'user_config' in device.value.keys():
|
||||
if 'admin_state' in device.value['user_config'].keys():
|
||||
self.add_var_to_host(dev_name, 'admin_state', device.value['user_config']['admin_state'])
|
||||
|
||||
self.add_device_status_to_var(dev_name, device)
|
||||
|
||||
# Go over the contents data structure
|
||||
for node in bp.contents['system']['nodes']:
|
||||
if node['display_name'] == dev_name:
|
||||
self.add_host_to_group(node['role'], dev_name)
|
||||
|
||||
# Check for additional attribute to import
|
||||
attributes_to_import = [
|
||||
'loopback_ip',
|
||||
'asn',
|
||||
'role',
|
||||
'position',
|
||||
]
|
||||
for attr in attributes_to_import:
|
||||
if attr in node.keys():
|
||||
self.add_var_to_host(dev_name, attr, node[attr])
|
||||
|
||||
# if blueprint_interface is enabled in the configuration
|
||||
# Collect links information
|
||||
if self.aos_blueprint_int:
|
||||
interfaces = dict()
|
||||
|
||||
for link in bp.contents['system']['links']:
|
||||
# each link has 2 sides [0,1], and it's unknown which one match this device
|
||||
# at first we assume, first side match(0) and peer is (1)
|
||||
peer_id = 1
|
||||
|
||||
for side in link['endpoints']:
|
||||
if side['display_name'] == dev_name:
|
||||
|
||||
# import local information first
|
||||
int_name = side['interface']
|
||||
|
||||
# init dict
|
||||
interfaces[int_name] = dict()
|
||||
if 'ip' in side.keys():
|
||||
interfaces[int_name]['ip'] = side['ip']
|
||||
|
||||
if 'interface' in side.keys():
|
||||
interfaces[int_name]['name'] = side['interface']
|
||||
|
||||
if 'display_name' in link['endpoints'][peer_id].keys():
|
||||
interfaces[int_name]['peer'] = link['endpoints'][peer_id]['display_name']
|
||||
|
||||
if 'ip' in link['endpoints'][peer_id].keys():
|
||||
interfaces[int_name]['peer_ip'] = link['endpoints'][peer_id]['ip']
|
||||
|
||||
if 'type' in link['endpoints'][peer_id].keys():
|
||||
interfaces[int_name]['peer_type'] = link['endpoints'][peer_id]['type']
|
||||
|
||||
else:
|
||||
# if we haven't match the first time, prepare the peer_id
|
||||
# for the second loop iteration
|
||||
peer_id = 0
|
||||
|
||||
self.add_var_to_host(dev_name, 'interfaces', interfaces)
|
||||
|
||||
else:
|
||||
for device in aos.Devices:
|
||||
# If not reacheable, create by key and
|
||||
# If reacheable, create by hostname
|
||||
|
||||
self.add_host_to_group('all', device.name)
|
||||
|
||||
# populate information for this host
|
||||
self.add_device_status_to_var(device.name, device)
|
||||
|
||||
if 'user_config' in device.value.keys():
|
||||
for key, value in device.value['user_config'].items():
|
||||
self.add_var_to_host(device.name, key, value)
|
||||
|
||||
# Based on device status online|offline, collect facts as well
|
||||
if device.value['status']['comm_state'] == 'on':
|
||||
|
||||
if 'facts' in device.value.keys():
|
||||
self.add_device_facts_to_var(device.name, device)
|
||||
|
||||
# Check if device is associated with a blueprint
|
||||
# if it's create a new group
|
||||
if 'blueprint_active' in device.value['status'].keys():
|
||||
if 'blueprint_id' in device.value['status'].keys():
|
||||
bp = aos.Blueprints.find(uid=device.value['status']['blueprint_id'])
|
||||
|
||||
if bp:
|
||||
self.add_host_to_group(bp.name, device.name)
|
||||
|
||||
# ----------------------------------------------------
|
||||
# Convert the inventory and return a JSON String
|
||||
# ----------------------------------------------------
|
||||
data_to_print = ""
|
||||
data_to_print += self.json_format_dict(self.inventory, True)
|
||||
|
||||
print(data_to_print)
|
||||
|
||||
def read_settings(self):
|
||||
""" Reads the settings from the apstra_aos.ini file """
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read(os.path.dirname(os.path.realpath(__file__)) + '/apstra_aos.ini')
|
||||
|
||||
# Default Values
|
||||
self.aos_blueprint = False
|
||||
self.aos_blueprint_int = True
|
||||
self.aos_username = 'admin'
|
||||
self.aos_password = 'admin'
|
||||
self.aos_server_port = 8888
|
||||
|
||||
# Try to reach all parameters from File, if not available try from ENV
|
||||
try:
|
||||
self.aos_server = config.get('aos', 'aos_server')
|
||||
except:
|
||||
if 'AOS_SERVER' in os.environ.keys():
|
||||
self.aos_server = os.environ['AOS_SERVER']
|
||||
|
||||
try:
|
||||
self.aos_server_port = config.get('aos', 'port')
|
||||
except:
|
||||
if 'AOS_PORT' in os.environ.keys():
|
||||
self.aos_server_port = os.environ['AOS_PORT']
|
||||
|
||||
try:
|
||||
self.aos_username = config.get('aos', 'username')
|
||||
except:
|
||||
if 'AOS_USERNAME' in os.environ.keys():
|
||||
self.aos_username = os.environ['AOS_USERNAME']
|
||||
|
||||
try:
|
||||
self.aos_password = config.get('aos', 'password')
|
||||
except:
|
||||
if 'AOS_PASSWORD' in os.environ.keys():
|
||||
self.aos_password = os.environ['AOS_PASSWORD']
|
||||
|
||||
try:
|
||||
self.aos_blueprint = config.get('aos', 'blueprint')
|
||||
except:
|
||||
if 'AOS_BLUEPRINT' in os.environ.keys():
|
||||
self.aos_blueprint = os.environ['AOS_BLUEPRINT']
|
||||
|
||||
try:
|
||||
if config.get('aos', 'blueprint_interface') in ['false', 'no']:
|
||||
self.aos_blueprint_int = False
|
||||
except:
|
||||
pass
|
||||
|
||||
def parse_cli_args(self):
|
||||
""" Command line argument processing """
|
||||
|
||||
parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on Apstra AOS')
|
||||
parser.add_argument('--list', action='store_true', default=True, help='List instances (default: True)')
|
||||
parser.add_argument('--host', action='store', help='Get all the variables about a specific instance')
|
||||
self.args = parser.parse_args()
|
||||
|
||||
def json_format_dict(self, data, pretty=False):
|
||||
""" Converts a dict to a JSON object and dumps it as a formatted string """
|
||||
|
||||
if pretty:
|
||||
return json.dumps(data, sort_keys=True, indent=2)
|
||||
else:
|
||||
return json.dumps(data)
|
||||
|
||||
def add_host_to_group(self, group, host):
|
||||
|
||||
# Cleanup group name first
|
||||
clean_group = self.cleanup_group_name(group)
|
||||
|
||||
# Check if the group exist, if not initialize it
|
||||
if clean_group not in self.inventory.keys():
|
||||
self.inventory[clean_group] = {}
|
||||
self.inventory[clean_group]['hosts'] = []
|
||||
self.inventory[clean_group]['vars'] = {}
|
||||
|
||||
self.inventory[clean_group]['hosts'].append(host)
|
||||
|
||||
def add_var_to_host(self, host, var, value):
|
||||
|
||||
# Check if the host exist, if not initialize it
|
||||
if host not in self.inventory['_meta']['hostvars'].keys():
|
||||
self.inventory['_meta']['hostvars'][host] = {}
|
||||
|
||||
self.inventory['_meta']['hostvars'][host][var] = value
|
||||
|
||||
def add_var_to_group(self, group, var, value):
|
||||
|
||||
# Cleanup group name first
|
||||
clean_group = self.cleanup_group_name(group)
|
||||
|
||||
# Check if the group exist, if not initialize it
|
||||
if clean_group not in self.inventory.keys():
|
||||
self.inventory[clean_group] = {}
|
||||
self.inventory[clean_group]['hosts'] = []
|
||||
self.inventory[clean_group]['vars'] = {}
|
||||
|
||||
self.inventory[clean_group]['vars'][var] = value
|
||||
|
||||
def add_device_facts_to_var(self, device_name, device):
|
||||
|
||||
# Populate variables for this host
|
||||
self.add_var_to_host(device_name,
|
||||
'ansible_ssh_host',
|
||||
device.value['facts']['mgmt_ipaddr'])
|
||||
|
||||
self.add_var_to_host(device_name, 'id', device.id)
|
||||
|
||||
# self.add_host_to_group('all', device.name)
|
||||
for key, value in device.value['facts'].items():
|
||||
self.add_var_to_host(device_name, key, value)
|
||||
|
||||
if key == 'os_family':
|
||||
self.add_host_to_group(value, device_name)
|
||||
elif key == 'hw_model':
|
||||
self.add_host_to_group(value, device_name)
|
||||
|
||||
def cleanup_group_name(self, group_name):
|
||||
"""
|
||||
Clean up group name by :
|
||||
- Replacing all non-alphanumeric caracter by underscore
|
||||
- Converting to lowercase
|
||||
"""
|
||||
|
||||
rx = re.compile('\W+')
|
||||
clean_group = rx.sub('_', group_name).lower()
|
||||
|
||||
return clean_group
|
||||
|
||||
def add_device_status_to_var(self, device_name, device):
|
||||
|
||||
if 'status' in device.value.keys():
|
||||
for key, value in device.value['status'].items():
|
||||
self.add_var_to_host(device.name, key, value)
|
||||
|
||||
# Run the script
|
||||
if __name__ == '__main__':
|
||||
AosInventory()
|
||||
@ -49,7 +49,6 @@ Command line arguments:
|
||||
- tenant
|
||||
- ad_user
|
||||
- password
|
||||
- cloud_environment
|
||||
|
||||
Environment variables:
|
||||
- AZURE_PROFILE
|
||||
@ -59,7 +58,6 @@ Environment variables:
|
||||
- AZURE_TENANT
|
||||
- AZURE_AD_USER
|
||||
- AZURE_PASSWORD
|
||||
- AZURE_CLOUD_ENVIRONMENT
|
||||
|
||||
Run for Specific Host
|
||||
-----------------------
|
||||
@ -192,27 +190,22 @@ import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import inspect
|
||||
import traceback
|
||||
|
||||
|
||||
from packaging.version import Version
|
||||
|
||||
from os.path import expanduser
|
||||
import ansible.module_utils.six.moves.urllib.parse as urlparse
|
||||
|
||||
HAS_AZURE = True
|
||||
HAS_AZURE_EXC = None
|
||||
|
||||
try:
|
||||
from msrestazure.azure_exceptions import CloudError
|
||||
from msrestazure import azure_cloud
|
||||
from azure.mgmt.compute import __version__ as azure_compute_version
|
||||
from azure.common import AzureMissingResourceHttpError, AzureHttpError
|
||||
from azure.common.credentials import ServicePrincipalCredentials, UserPassCredentials
|
||||
from azure.mgmt.network import NetworkManagementClient
|
||||
from azure.mgmt.resource.resources import ResourceManagementClient
|
||||
from azure.mgmt.compute import ComputeManagementClient
|
||||
from azure.mgmt.network.network_management_client import NetworkManagementClient
|
||||
from azure.mgmt.resource.resources.resource_management_client import ResourceManagementClient
|
||||
from azure.mgmt.compute.compute_management_client import ComputeManagementClient
|
||||
except ImportError as exc:
|
||||
HAS_AZURE_EXC = exc
|
||||
HAS_AZURE = False
|
||||
@ -225,8 +218,7 @@ AZURE_CREDENTIAL_ENV_MAPPING = dict(
|
||||
secret='AZURE_SECRET',
|
||||
tenant='AZURE_TENANT',
|
||||
ad_user='AZURE_AD_USER',
|
||||
password='AZURE_PASSWORD',
|
||||
cloud_environment='AZURE_CLOUD_ENVIRONMENT',
|
||||
password='AZURE_PASSWORD'
|
||||
)
|
||||
|
||||
AZURE_CONFIG_SETTINGS = dict(
|
||||
@ -240,7 +232,7 @@ AZURE_CONFIG_SETTINGS = dict(
|
||||
group_by_tag='AZURE_GROUP_BY_TAG'
|
||||
)
|
||||
|
||||
AZURE_MIN_VERSION = "2.0.0"
|
||||
AZURE_MIN_VERSION = "0.30.0rc5"
|
||||
|
||||
|
||||
def azure_id_to_dict(id):
|
||||
@ -257,7 +249,6 @@ class AzureRM(object):
|
||||
|
||||
def __init__(self, args):
|
||||
self._args = args
|
||||
self._cloud_environment = None
|
||||
self._compute_client = None
|
||||
self._resource_client = None
|
||||
self._network_client = None
|
||||
@ -271,26 +262,6 @@ class AzureRM(object):
|
||||
self.fail("Failed to get credentials. Either pass as parameters, set environment variables, "
|
||||
"or define a profile in ~/.azure/credentials.")
|
||||
|
||||
# if cloud_environment specified, look up/build Cloud object
|
||||
raw_cloud_env = self.credentials.get('cloud_environment')
|
||||
if not raw_cloud_env:
|
||||
self._cloud_environment = azure_cloud.AZURE_PUBLIC_CLOUD # SDK default
|
||||
else:
|
||||
# try to look up "well-known" values via the name attribute on azure_cloud members
|
||||
all_clouds = [x[1] for x in inspect.getmembers(azure_cloud) if isinstance(x[1], azure_cloud.Cloud)]
|
||||
matched_clouds = [x for x in all_clouds if x.name == raw_cloud_env]
|
||||
if len(matched_clouds) == 1:
|
||||
self._cloud_environment = matched_clouds[0]
|
||||
elif len(matched_clouds) > 1:
|
||||
self.fail("Azure SDK failure: more than one cloud matched for cloud_environment name '{0}'".format(raw_cloud_env))
|
||||
else:
|
||||
if not urlparse.urlparse(raw_cloud_env).scheme:
|
||||
self.fail("cloud_environment must be an endpoint discovery URL or one of {0}".format([x.name for x in all_clouds]))
|
||||
try:
|
||||
self._cloud_environment = azure_cloud.get_cloud_from_metadata_endpoint(raw_cloud_env)
|
||||
except Exception as e:
|
||||
self.fail("cloud_environment {0} could not be resolved: {1}".format(raw_cloud_env, e.message))
|
||||
|
||||
if self.credentials.get('subscription_id', None) is None:
|
||||
self.fail("Credentials did not include a subscription_id value.")
|
||||
self.log("setting subscription_id")
|
||||
@ -301,23 +272,16 @@ class AzureRM(object):
|
||||
self.credentials.get('tenant') is not None:
|
||||
self.azure_credentials = ServicePrincipalCredentials(client_id=self.credentials['client_id'],
|
||||
secret=self.credentials['secret'],
|
||||
tenant=self.credentials['tenant'],
|
||||
cloud_environment=self._cloud_environment)
|
||||
tenant=self.credentials['tenant'])
|
||||
elif self.credentials.get('ad_user') is not None and self.credentials.get('password') is not None:
|
||||
tenant = self.credentials.get('tenant')
|
||||
if not tenant:
|
||||
tenant = 'common'
|
||||
self.azure_credentials = UserPassCredentials(self.credentials['ad_user'],
|
||||
self.credentials['password'],
|
||||
tenant=tenant,
|
||||
cloud_environment=self._cloud_environment)
|
||||
self.azure_credentials = UserPassCredentials(self.credentials['ad_user'], self.credentials['password'])
|
||||
else:
|
||||
self.fail("Failed to authenticate with provided credentials. Some attributes were missing. "
|
||||
"Credentials must include client_id, secret and tenant or ad_user and password.")
|
||||
|
||||
def log(self, msg):
|
||||
if self.debug:
|
||||
print(msg + u'\n')
|
||||
print (msg + u'\n')
|
||||
|
||||
def fail(self, msg):
|
||||
raise Exception(msg)
|
||||
@ -377,10 +341,6 @@ class AzureRM(object):
|
||||
self.log('Received credentials from parameters.')
|
||||
return arg_credentials
|
||||
|
||||
if arg_credentials['ad_user'] is not None:
|
||||
self.log('Received credentials from parameters.')
|
||||
return arg_credentials
|
||||
|
||||
# try environment
|
||||
env_credentials = self._get_env_credentials()
|
||||
if env_credentials:
|
||||
@ -412,12 +372,7 @@ class AzureRM(object):
|
||||
def network_client(self):
|
||||
self.log('Getting network client')
|
||||
if not self._network_client:
|
||||
self._network_client = NetworkManagementClient(
|
||||
self.azure_credentials,
|
||||
self.subscription_id,
|
||||
base_url=self._cloud_environment.endpoints.resource_manager,
|
||||
api_version='2017-06-01'
|
||||
)
|
||||
self._network_client = NetworkManagementClient(self.azure_credentials, self.subscription_id)
|
||||
self._register('Microsoft.Network')
|
||||
return self._network_client
|
||||
|
||||
@ -425,24 +380,14 @@ class AzureRM(object):
|
||||
def rm_client(self):
|
||||
self.log('Getting resource manager client')
|
||||
if not self._resource_client:
|
||||
self._resource_client = ResourceManagementClient(
|
||||
self.azure_credentials,
|
||||
self.subscription_id,
|
||||
base_url=self._cloud_environment.endpoints.resource_manager,
|
||||
api_version='2017-05-10'
|
||||
)
|
||||
self._resource_client = ResourceManagementClient(self.azure_credentials, self.subscription_id)
|
||||
return self._resource_client
|
||||
|
||||
@property
|
||||
def compute_client(self):
|
||||
self.log('Getting compute client')
|
||||
if not self._compute_client:
|
||||
self._compute_client = ComputeManagementClient(
|
||||
self.azure_credentials,
|
||||
self.subscription_id,
|
||||
base_url=self._cloud_environment.endpoints.resource_manager,
|
||||
api_version='2017-03-30'
|
||||
)
|
||||
self._compute_client = ComputeManagementClient(self.azure_credentials, self.subscription_id)
|
||||
self._register('Microsoft.Compute')
|
||||
return self._compute_client
|
||||
|
||||
@ -495,7 +440,7 @@ class AzureInventory(object):
|
||||
self.include_powerstate = False
|
||||
|
||||
self.get_inventory()
|
||||
print(self._json_format_dict(pretty=self._args.pretty))
|
||||
print (self._json_format_dict(pretty=self._args.pretty))
|
||||
sys.exit(0)
|
||||
|
||||
def _parse_cli_args(self):
|
||||
@ -503,13 +448,13 @@ class AzureInventory(object):
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Produce an Ansible Inventory file for an Azure subscription')
|
||||
parser.add_argument('--list', action='store_true', default=True,
|
||||
help='List instances (default: True)')
|
||||
help='List instances (default: True)')
|
||||
parser.add_argument('--debug', action='store_true', default=False,
|
||||
help='Send debug messages to STDOUT')
|
||||
help='Send debug messages to STDOUT')
|
||||
parser.add_argument('--host', action='store',
|
||||
help='Get all information about an instance')
|
||||
help='Get all information about an instance')
|
||||
parser.add_argument('--pretty', action='store_true', default=False,
|
||||
help='Pretty print JSON output(default: False)')
|
||||
help='Pretty print JSON output(default: False)')
|
||||
parser.add_argument('--profile', action='store',
|
||||
help='Azure profile contained in ~/.azure/credentials')
|
||||
parser.add_argument('--subscription_id', action='store',
|
||||
@ -520,12 +465,10 @@ class AzureInventory(object):
|
||||
help='Azure Client Secret')
|
||||
parser.add_argument('--tenant', action='store',
|
||||
help='Azure Tenant Id')
|
||||
parser.add_argument('--ad_user', action='store',
|
||||
parser.add_argument('--ad-user', action='store',
|
||||
help='Active Directory User')
|
||||
parser.add_argument('--password', action='store',
|
||||
help='password')
|
||||
parser.add_argument('--cloud_environment', action='store',
|
||||
help='Azure Cloud Environment name or metadata discovery URL')
|
||||
parser.add_argument('--resource-groups', action='store',
|
||||
help='Return inventory for comma separated list of resource group names')
|
||||
parser.add_argument('--tags', action='store',
|
||||
@ -543,7 +486,8 @@ class AzureInventory(object):
|
||||
try:
|
||||
virtual_machines = self._compute_client.virtual_machines.list(resource_group)
|
||||
except Exception as exc:
|
||||
sys.exit("Error: fetching virtual machines for resource group {0} - {1}".format(resource_group, str(exc)))
|
||||
sys.exit("Error: fetching virtual machines for resource group {0} - {1}".format(resource_group,
|
||||
str(exc)))
|
||||
if self._args.host or self.tags:
|
||||
selected_machines = self._selected_machines(virtual_machines)
|
||||
self._load_machines(selected_machines)
|
||||
@ -566,7 +510,7 @@ class AzureInventory(object):
|
||||
for machine in machines:
|
||||
id_dict = azure_id_to_dict(machine.id)
|
||||
|
||||
# TODO - The API is returning an ID value containing resource group name in ALL CAPS. If/when it gets
|
||||
#TODO - The API is returning an ID value containing resource group name in ALL CAPS. If/when it gets
|
||||
# fixed, we should remove the .lower(). Opened Issue
|
||||
# #574: https://github.com/Azure/azure-sdk-for-python/issues/574
|
||||
resource_group = id_dict['resourceGroups'].lower()
|
||||
@ -594,7 +538,7 @@ class AzureInventory(object):
|
||||
mac_address=None,
|
||||
plan=(machine.plan.name if machine.plan else None),
|
||||
virtual_machine_size=machine.hardware_profile.vm_size,
|
||||
computer_name=(machine.os_profile.computer_name if machine.os_profile else None),
|
||||
computer_name=machine.os_profile.computer_name,
|
||||
provisioning_state=machine.provisioning_state,
|
||||
)
|
||||
|
||||
@ -615,7 +559,7 @@ class AzureInventory(object):
|
||||
)
|
||||
|
||||
# Add windows details
|
||||
if machine.os_profile is not None and machine.os_profile.windows_configuration is not None:
|
||||
if machine.os_profile.windows_configuration is not None:
|
||||
host_vars['windows_auto_updates_enabled'] = \
|
||||
machine.os_profile.windows_configuration.enable_automatic_updates
|
||||
host_vars['windows_timezone'] = machine.os_profile.windows_configuration.time_zone
|
||||
@ -846,10 +790,13 @@ class AzureInventory(object):
|
||||
|
||||
def main():
|
||||
if not HAS_AZURE:
|
||||
sys.exit("The Azure python sdk is not installed (try `pip install 'azure>={0}' --upgrade`) - {1}".format(AZURE_MIN_VERSION, HAS_AZURE_EXC))
|
||||
sys.exit("The Azure python sdk is not installed (try `pip install 'azure>=2.0.0rc5' --upgrade`) - {0}".format(HAS_AZURE_EXC))
|
||||
|
||||
if Version(azure_compute_version) < Version(AZURE_MIN_VERSION):
|
||||
sys.exit("Expecting azure.mgmt.compute.__version__ to be {0}. Found version {1} "
|
||||
"Do you have Azure >= 2.0.0rc5 installed? (try `pip install 'azure>=2.0.0rc5' --upgrade`)".format(AZURE_MIN_VERSION, azure_compute_version))
|
||||
|
||||
AzureInventory()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
#!/usr/bin/python
|
||||
# Copyright 2016 Doalitic.
|
||||
#
|
||||
# This file is part of Ansible
|
||||
|
||||
@ -32,13 +32,11 @@ included, provided the requesting user belongs to it.
|
||||
|
||||
The following variables are established for every host. They can be retrieved from the hostvars
|
||||
dictionary.
|
||||
- brook_pid: str
|
||||
- brook_name: str
|
||||
- brook_description: str
|
||||
- brook_project: str
|
||||
- brook_template: str
|
||||
- brook_region: str
|
||||
- brook_zone: str
|
||||
- brook_status: str
|
||||
- brook_tags: list(str)
|
||||
- brook_internal_ips: list(str)
|
||||
@ -72,7 +70,7 @@ Support:
|
||||
This script is tested on Python 2.7 and 3.4. It may work on other versions though.
|
||||
|
||||
Author: Francisco Ros <fjros@doalitic.com>
|
||||
Version: 0.2
|
||||
Version: 0.1
|
||||
"""
|
||||
|
||||
|
||||
@ -124,8 +122,9 @@ class BrookInventory:
|
||||
self.project_id = config.get('brook', 'project_id')
|
||||
|
||||
if not self.api_token:
|
||||
sys.exit('You must provide (at least) your Brook.io API token to generate the dynamic '
|
||||
'inventory.')
|
||||
print('You must provide (at least) your Brook.io API token to generate the dynamic '
|
||||
'inventory.')
|
||||
sys.exit(1)
|
||||
|
||||
def get_api_client(self):
|
||||
"""Authenticate user via the provided credentials and return the corresponding API client.
|
||||
@ -169,8 +168,8 @@ class BrookInventory:
|
||||
for project_id in projects:
|
||||
project = projects_api.show_project(project_id=project_id)
|
||||
for instance in instances_api.index_instances(project_id=project_id):
|
||||
# Get template used for this instance if known
|
||||
template = templates_api.show_template(template_id=instance.template) if instance.template else None
|
||||
# Get template used for this instance
|
||||
template = templates_api.show_template(template_id=instance.template)
|
||||
|
||||
# Update hostvars
|
||||
try:
|
||||
@ -181,14 +180,14 @@ class BrookInventory:
|
||||
|
||||
# Group by project
|
||||
project_group = 'project_%s' % project.name
|
||||
if project_group in groups:
|
||||
if project_group in groups.keys():
|
||||
groups[project_group].append(instance.name)
|
||||
else:
|
||||
groups[project_group] = [instance.name]
|
||||
|
||||
# Group by status
|
||||
status_group = 'status_%s' % meta['hostvars'][instance.name]['brook_status']
|
||||
if status_group in groups:
|
||||
if status_group in groups.keys():
|
||||
groups[status_group].append(instance.name)
|
||||
else:
|
||||
groups[status_group] = [instance.name]
|
||||
@ -197,7 +196,7 @@ class BrookInventory:
|
||||
tags = meta['hostvars'][instance.name]['brook_tags']
|
||||
for tag in tags:
|
||||
tag_group = 'tag_%s' % tag
|
||||
if tag_group in groups:
|
||||
if tag_group in groups.keys():
|
||||
groups[tag_group].append(instance.name)
|
||||
else:
|
||||
groups[tag_group] = [instance.name]
|
||||
@ -213,13 +212,11 @@ class BrookInventory:
|
||||
"""
|
||||
|
||||
hostvars = instance.to_dict()
|
||||
hostvars['brook_pid'] = hostvars.pop('pid')
|
||||
hostvars['brook_name'] = hostvars.pop('name')
|
||||
hostvars['brook_description'] = hostvars.pop('description')
|
||||
hostvars['brook_project'] = hostvars.pop('project')
|
||||
hostvars['brook_template'] = hostvars.pop('template')
|
||||
hostvars['brook_region'] = hostvars.pop('region')
|
||||
hostvars['brook_zone'] = hostvars.pop('zone')
|
||||
hostvars['brook_created_at'] = hostvars.pop('created_at')
|
||||
hostvars['brook_updated_at'] = hostvars.pop('updated_at')
|
||||
del hostvars['id']
|
||||
@ -230,7 +227,7 @@ class BrookInventory:
|
||||
# Substitute identifiers for names
|
||||
#
|
||||
hostvars['brook_project'] = project.name
|
||||
hostvars['brook_template'] = template.name if template else None
|
||||
hostvars['brook_template'] = template.name
|
||||
|
||||
# Retrieve instance state
|
||||
#
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/python
|
||||
# vim: set fileencoding=utf-8 :
|
||||
#
|
||||
# Copyright (C) 2016 Guido Günther <agx@sigxcpu.org>
|
||||
|
||||
@ -67,11 +67,12 @@ based on the data obtained from CloudStack API:
|
||||
}
|
||||
|
||||
|
||||
usage: cloudstack.py [--list] [--host HOST] [--project PROJECT] [--domain DOMAIN]
|
||||
usage: cloudstack.py [--list] [--host HOST] [--project PROJECT]
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
@ -96,44 +97,32 @@ class CloudStackInventory(object):
|
||||
parser.add_argument('--host')
|
||||
parser.add_argument('--list', action='store_true')
|
||||
parser.add_argument('--project')
|
||||
parser.add_argument('--domain')
|
||||
|
||||
options = parser.parse_args()
|
||||
try:
|
||||
self.cs = CloudStack(**read_config())
|
||||
except CloudStackException:
|
||||
except CloudStackException as e:
|
||||
print("Error: Could not connect to CloudStack API", file=sys.stderr)
|
||||
|
||||
domain_id = None
|
||||
if options.domain:
|
||||
domain_id = self.get_domain_id(options.domain)
|
||||
|
||||
project_id = None
|
||||
project_id = ''
|
||||
if options.project:
|
||||
project_id = self.get_project_id(options.project, domain_id)
|
||||
project_id = self.get_project_id(options.project)
|
||||
|
||||
if options.host:
|
||||
data = self.get_host(options.host, project_id, domain_id)
|
||||
data = self.get_host(options.host, project_id)
|
||||
print(json.dumps(data, indent=2))
|
||||
|
||||
elif options.list:
|
||||
data = self.get_list(project_id, domain_id)
|
||||
data = self.get_list(project_id)
|
||||
print(json.dumps(data, indent=2))
|
||||
else:
|
||||
print("usage: --list | --host <hostname> [--project <project>] [--domain <domain_path>]", file=sys.stderr)
|
||||
print("usage: --list | --host <hostname> [--project <project>]",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def get_domain_id(self, domain):
|
||||
domains = self.cs.listDomains(listall=True)
|
||||
if domains:
|
||||
for d in domains['domain']:
|
||||
if d['path'].lower() == domain.lower():
|
||||
return d['id']
|
||||
print("Error: Domain %s not found." % domain, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def get_project_id(self, project, domain_id=None):
|
||||
projects = self.cs.listProjects(domainid=domain_id)
|
||||
def get_project_id(self, project):
|
||||
projects = self.cs.listProjects()
|
||||
if projects:
|
||||
for p in projects['project']:
|
||||
if p['name'] == project or p['id'] == project:
|
||||
@ -141,8 +130,9 @@ class CloudStackInventory(object):
|
||||
print("Error: Project %s not found." % project, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def get_host(self, name, project_id=None, domain_id=None):
|
||||
hosts = self.cs.listVirtualMachines(projectid=project_id, domainid=domain_id)
|
||||
|
||||
def get_host(self, name, project_id=''):
|
||||
hosts = self.cs.listVirtualMachines(projectid=project_id)
|
||||
data = {}
|
||||
if not hosts:
|
||||
return data
|
||||
@ -175,45 +165,37 @@ class CloudStackInventory(object):
|
||||
})
|
||||
if nic['isdefault']:
|
||||
data['default_ip'] = nic['ipaddress']
|
||||
break
|
||||
break;
|
||||
return data
|
||||
|
||||
def get_list(self, project_id=None, domain_id=None):
|
||||
|
||||
def get_list(self, project_id=''):
|
||||
data = {
|
||||
'all': {
|
||||
'hosts': [],
|
||||
},
|
||||
},
|
||||
'_meta': {
|
||||
'hostvars': {},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
groups = self.cs.listInstanceGroups(projectid=project_id, domainid=domain_id)
|
||||
groups = self.cs.listInstanceGroups(projectid=project_id)
|
||||
if groups:
|
||||
for group in groups['instancegroup']:
|
||||
group_name = group['name']
|
||||
if group_name and group_name not in data:
|
||||
if group_name and not group_name in data:
|
||||
data[group_name] = {
|
||||
'hosts': []
|
||||
}
|
||||
'hosts': []
|
||||
}
|
||||
|
||||
hosts = self.cs.listVirtualMachines(projectid=project_id, domainid=domain_id)
|
||||
hosts = self.cs.listVirtualMachines(projectid=project_id)
|
||||
if not hosts:
|
||||
return data
|
||||
for host in hosts['virtualmachine']:
|
||||
host_name = host['displayname']
|
||||
data['all']['hosts'].append(host_name)
|
||||
data['_meta']['hostvars'][host_name] = {}
|
||||
|
||||
# Make a group per zone
|
||||
data['_meta']['hostvars'][host_name]['zone'] = host['zonename']
|
||||
group_name = host['zonename']
|
||||
if group_name not in data:
|
||||
data[group_name] = {
|
||||
'hosts': []
|
||||
}
|
||||
data[group_name]['hosts'].append(host_name)
|
||||
|
||||
if 'group' in host:
|
||||
data['_meta']['hostvars'][host_name]['group'] = host['group']
|
||||
data['_meta']['hostvars'][host_name]['state'] = host['state']
|
||||
@ -237,7 +219,7 @@ class CloudStackInventory(object):
|
||||
'netmask': nic['netmask'],
|
||||
'gateway': nic['gateway'],
|
||||
'type': nic['type'],
|
||||
})
|
||||
})
|
||||
if nic['isdefault']:
|
||||
data['_meta']['hostvars'][host_name]['default_ip'] = nic['ipaddress']
|
||||
|
||||
|
||||
@ -110,9 +110,9 @@ class CobblerInventory(object):
|
||||
if self.args.host:
|
||||
data_to_print += self.get_host_info()
|
||||
else:
|
||||
self.inventory['_meta'] = {'hostvars': {}}
|
||||
self.inventory['_meta'] = { 'hostvars': {} }
|
||||
for hostname in self.cache:
|
||||
self.inventory['_meta']['hostvars'][hostname] = {'cobbler': self.cache[hostname]}
|
||||
self.inventory['_meta']['hostvars'][hostname] = {'cobbler': self.cache[hostname] }
|
||||
data_to_print += self.json_format_dict(self.inventory, True)
|
||||
|
||||
print(data_to_print)
|
||||
@ -179,7 +179,7 @@ class CobblerInventory(object):
|
||||
|
||||
for host in data:
|
||||
# Get the FQDN for the host and add it to the right groups
|
||||
dns_name = host['hostname'] # None
|
||||
dns_name = host['hostname'] #None
|
||||
ksmeta = None
|
||||
interfaces = host['interfaces']
|
||||
# hostname is often empty for non-static IP hosts
|
||||
@ -190,7 +190,7 @@ class CobblerInventory(object):
|
||||
if this_dns_name is not None and this_dns_name is not "":
|
||||
dns_name = this_dns_name
|
||||
|
||||
if dns_name == '' or dns_name is None:
|
||||
if dns_name == '':
|
||||
continue
|
||||
|
||||
status = host['status']
|
||||
@ -229,11 +229,11 @@ class CobblerInventory(object):
|
||||
# Need to load index from cache
|
||||
self.load_cache_from_cache()
|
||||
|
||||
if self.args.host not in self.cache:
|
||||
if not self.args.host in self.cache:
|
||||
# try updating the cache
|
||||
self.update_cache()
|
||||
|
||||
if self.args.host not in self.cache:
|
||||
if not self.args.host in self.cache:
|
||||
# host might not exist anymore
|
||||
return self.json_format_dict({}, True)
|
||||
|
||||
|
||||
@ -74,6 +74,7 @@ import re
|
||||
import sys
|
||||
from time import time
|
||||
import traceback
|
||||
import urllib
|
||||
|
||||
try:
|
||||
import json
|
||||
@ -81,11 +82,9 @@ except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
from six import iteritems
|
||||
from six.moves.urllib.parse import urlencode
|
||||
|
||||
from ansible.module_utils.urls import open_url
|
||||
|
||||
|
||||
class CollinsDefaults(object):
|
||||
ASSETS_API_ENDPOINT = '%s/api/assets'
|
||||
SPECIAL_ATTRIBUTES = set([
|
||||
@ -118,7 +117,7 @@ class CollinsInventory(object):
|
||||
self.parse_cli_args()
|
||||
|
||||
logging.basicConfig(format=CollinsDefaults.LOG_FORMAT,
|
||||
filename=self.log_location)
|
||||
filename=self.log_location)
|
||||
self.log = logging.getLogger('CollinsInventory')
|
||||
|
||||
def _asset_get_attribute(self, asset, attrib):
|
||||
@ -169,14 +168,14 @@ class CollinsInventory(object):
|
||||
print(data_to_print)
|
||||
return successful
|
||||
|
||||
def find_assets(self, attributes=None, operation='AND'):
|
||||
def find_assets(self, attributes = {}, operation = 'AND'):
|
||||
""" Obtains Collins assets matching the provided attributes. """
|
||||
attributes = {} if attributes is None else attributes
|
||||
|
||||
# Formats asset search query to locate assets matching attributes, using
|
||||
# the CQL search feature as described here:
|
||||
# http://tumblr.github.io/collins/recipes.html
|
||||
attributes_query = ['='.join(attr_pair) for attr_pair in iteritems(attributes)]
|
||||
attributes_query = [ '='.join(attr_pair)
|
||||
for attr_pair in iteritems(attributes) ]
|
||||
query_parameters = {
|
||||
'details': ['True'],
|
||||
'operation': [operation],
|
||||
@ -191,18 +190,19 @@ class CollinsInventory(object):
|
||||
# Locates all assets matching the provided query, exhausting pagination.
|
||||
while True:
|
||||
if num_retries == self.collins_max_retries:
|
||||
raise MaxRetriesError("Maximum of %s retries reached; giving up" % self.collins_max_retries)
|
||||
raise MaxRetriesError("Maximum of %s retries reached; giving up" % \
|
||||
self.collins_max_retries)
|
||||
query_parameters['page'] = cur_page
|
||||
query_url = "%s?%s" % (
|
||||
(CollinsDefaults.ASSETS_API_ENDPOINT % self.collins_host),
|
||||
urlencode(query_parameters, doseq=True)
|
||||
urllib.urlencode(query_parameters, doseq=True)
|
||||
)
|
||||
try:
|
||||
response = open_url(query_url,
|
||||
timeout=self.collins_timeout_secs,
|
||||
url_username=self.collins_username,
|
||||
url_password=self.collins_password,
|
||||
force_basic_auth=True)
|
||||
timeout=self.collins_timeout_secs,
|
||||
url_username=self.collins_username,
|
||||
url_password=self.collins_password,
|
||||
force_basic_auth=True)
|
||||
json_response = json.loads(response.read())
|
||||
# Adds any assets found to the array of assets.
|
||||
assets += json_response['data']['Data']
|
||||
@ -212,7 +212,8 @@ class CollinsInventory(object):
|
||||
cur_page += 1
|
||||
num_retries = 0
|
||||
except:
|
||||
self.log.error("Error while communicating with Collins, retrying:\n%s", traceback.format_exc())
|
||||
self.log.error("Error while communicating with Collins, retrying:\n%s",
|
||||
traceback.format_exc())
|
||||
num_retries += 1
|
||||
return assets
|
||||
|
||||
@ -231,15 +232,19 @@ class CollinsInventory(object):
|
||||
def read_settings(self):
|
||||
""" Reads the settings from the collins.ini file """
|
||||
|
||||
config_loc = os.getenv('COLLINS_CONFIG', os.path.dirname(os.path.realpath(__file__)) + '/collins.ini')
|
||||
config_loc = os.getenv('COLLINS_CONFIG',
|
||||
os.path.dirname(os.path.realpath(__file__)) + '/collins.ini')
|
||||
|
||||
config = ConfigParser.SafeConfigParser()
|
||||
config.read(os.path.dirname(os.path.realpath(__file__)) + '/collins.ini')
|
||||
|
||||
self.collins_host = config.get('collins', 'host')
|
||||
self.collins_username = os.getenv('COLLINS_USERNAME', config.get('collins', 'username'))
|
||||
self.collins_password = os.getenv('COLLINS_PASSWORD', config.get('collins', 'password'))
|
||||
self.collins_asset_type = os.getenv('COLLINS_ASSET_TYPE', config.get('collins', 'asset_type'))
|
||||
self.collins_username = os.getenv('COLLINS_USERNAME',
|
||||
config.get('collins', 'username'))
|
||||
self.collins_password = os.getenv('COLLINS_PASSWORD',
|
||||
config.get('collins', 'password'))
|
||||
self.collins_asset_type = os.getenv('COLLINS_ASSET_TYPE',
|
||||
config.get('collins', 'asset_type'))
|
||||
self.collins_timeout_secs = config.getint('collins', 'timeout_secs')
|
||||
self.collins_max_retries = config.getint('collins', 'max_retries')
|
||||
|
||||
@ -263,12 +268,16 @@ class CollinsInventory(object):
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Produces an Ansible Inventory file based on Collins')
|
||||
parser.add_argument('--list', action='store_true', default=True, help='List instances (default: True)')
|
||||
parser.add_argument('--host', action='store', help='Get all the variables about a specific instance')
|
||||
parser.add_argument('--refresh-cache', action='store_true', default=False,
|
||||
help='Force refresh of cache by making API requests to Collins '
|
||||
'(default: False - use cache files)')
|
||||
parser.add_argument('--pretty', action='store_true', default=False, help='Pretty print all JSON output')
|
||||
parser.add_argument('--list',
|
||||
action='store_true', default=True, help='List instances (default: True)')
|
||||
parser.add_argument('--host',
|
||||
action='store', help='Get all the variables about a specific instance')
|
||||
parser.add_argument('--refresh-cache',
|
||||
action='store_true', default=False,
|
||||
help='Force refresh of cache by making API requests to Collins ' \
|
||||
'(default: False - use cache files)')
|
||||
parser.add_argument('--pretty',
|
||||
action='store_true', default=False, help='Pretty print all JSON output')
|
||||
self.args = parser.parse_args()
|
||||
|
||||
def update_cache(self):
|
||||
@ -281,7 +290,8 @@ class CollinsInventory(object):
|
||||
try:
|
||||
server_assets = self.find_assets()
|
||||
except:
|
||||
self.log.error("Error while locating assets from Collins:\n%s", traceback.format_exc())
|
||||
self.log.error("Error while locating assets from Collins:\n%s",
|
||||
traceback.format_exc())
|
||||
return False
|
||||
|
||||
for asset in server_assets:
|
||||
@ -305,7 +315,8 @@ class CollinsInventory(object):
|
||||
if self.prefer_hostnames and self._asset_has_attribute(asset, 'HOSTNAME'):
|
||||
asset_identifier = self._asset_get_attribute(asset, 'HOSTNAME')
|
||||
elif 'ADDRESSES' not in asset:
|
||||
self.log.warning("No IP addresses found for asset '%s', skipping", asset)
|
||||
self.log.warning("No IP addresses found for asset '%s', skipping",
|
||||
asset)
|
||||
continue
|
||||
elif len(asset['ADDRESSES']) < ip_index + 1:
|
||||
self.log.warning(
|
||||
@ -373,11 +384,11 @@ class CollinsInventory(object):
|
||||
# Need to load index from cache
|
||||
self.load_cache_from_cache()
|
||||
|
||||
if self.args.host not in self.cache:
|
||||
if not self.args.host in self.cache:
|
||||
# try updating the cache
|
||||
self.update_cache()
|
||||
|
||||
if self.args.host not in self.cache:
|
||||
if not self.args.host in self.cache:
|
||||
# host might not exist anymore
|
||||
return self.json_format_dict({}, self.args.pretty)
|
||||
|
||||
@ -393,7 +404,7 @@ class CollinsInventory(object):
|
||||
return True
|
||||
except:
|
||||
self.log.error("Error while loading inventory:\n%s",
|
||||
traceback.format_exc())
|
||||
traceback.format_exc())
|
||||
self.inventory = {}
|
||||
return False
|
||||
|
||||
@ -407,7 +418,7 @@ class CollinsInventory(object):
|
||||
return True
|
||||
except:
|
||||
self.log.error("Error while loading host cache:\n%s",
|
||||
traceback.format_exc())
|
||||
traceback.format_exc())
|
||||
self.cache = {}
|
||||
return False
|
||||
|
||||
|
||||
37
contrib/inventory/consul.ini
Normal file
37
contrib/inventory/consul.ini
Normal file
@ -0,0 +1,37 @@
|
||||
# Ansible Consul external inventory script settings.
|
||||
|
||||
[consul]
|
||||
|
||||
# restrict included nodes to those from this datacenter
|
||||
#datacenter = nyc1
|
||||
|
||||
# url of the the consul cluster to query
|
||||
#url = http://demo.consul.io
|
||||
url = http://localhost:8500
|
||||
|
||||
# suffix added to each service to create a group name e.g Service of 'redis' and
|
||||
# a suffix of '_servers' will add each address to the group name 'redis_servers'
|
||||
servers_suffix = _servers
|
||||
|
||||
# if specified then the inventory will generate domain names that will resolve
|
||||
# via Consul's inbuilt DNS.
|
||||
#domain=consul
|
||||
|
||||
# make groups from service tags. the name of the group is derived from the
|
||||
# service name and the tag name e.g. a service named nginx with tags ['master', 'v1']
|
||||
# will create groups nginx_master and nginx_v1
|
||||
tags = true
|
||||
|
||||
# looks up the node name at the given path for a list of groups to which the
|
||||
# node should be added.
|
||||
kv_groups=ansible/groups
|
||||
|
||||
# looks up the node name at the given path for a json dictionary of metadata that
|
||||
# should be attached as metadata for the node
|
||||
kv_metadata=ansible/metadata
|
||||
|
||||
# looks up the health of each service and adds the node to 'up' and 'down' groups
|
||||
# based on the service availibility
|
||||
availability = true
|
||||
available_suffix = _up
|
||||
unavailable_suffix = _down
|
||||
@ -1,37 +0,0 @@
|
||||
# Ansible Consul external inventory script settings.
|
||||
|
||||
[consul]
|
||||
|
||||
# restrict included nodes to those from this datacenter
|
||||
#datacenter = nyc1
|
||||
|
||||
# url of the consul cluster to query
|
||||
#url = http://demo.consul.io
|
||||
url = http://localhost:8500
|
||||
|
||||
# suffix added to each service to create a group name e.g Service of 'redis' and
|
||||
# a suffix of '_servers' will add each address to the group name 'redis_servers'
|
||||
servers_suffix = _servers
|
||||
|
||||
# if specified then the inventory will generate domain names that will resolve
|
||||
# via Consul's inbuilt DNS.
|
||||
#domain=consul
|
||||
|
||||
# make groups from service tags. the name of the group is derived from the
|
||||
# service name and the tag name e.g. a service named nginx with tags ['master', 'v1']
|
||||
# will create groups nginx_master and nginx_v1
|
||||
tags = true
|
||||
|
||||
# looks up the node name at the given path for a list of groups to which the
|
||||
# node should be added.
|
||||
kv_groups=ansible/groups
|
||||
|
||||
# looks up the node name at the given path for a json dictionary of metadata that
|
||||
# should be attached as metadata for the node
|
||||
kv_metadata=ansible/metadata
|
||||
|
||||
# looks up the health of each service and adds the node to 'up' and 'down' groups
|
||||
# based on the service availibility
|
||||
availability = true
|
||||
available_suffix = _up
|
||||
unavailable_suffix = _down
|
||||
@ -37,7 +37,7 @@ This script can be run with the switches
|
||||
--datacenter, to restrict the nodes to a single datacenter
|
||||
--host to restrict the inventory to a single named node. (requires datacenter config)
|
||||
|
||||
The configuration for this plugin is read from a consul_io.ini file located in the
|
||||
The configuration for this plugin is read from a consul.ini file located in the
|
||||
same directory as this inventory script. All config options in the config file
|
||||
are optional except the host and port, which must point to a valid agent or
|
||||
server running the http api. For more information on enabling the endpoint see.
|
||||
@ -50,12 +50,6 @@ Other options include:
|
||||
|
||||
which restricts the included nodes to those from the given datacenter
|
||||
|
||||
'url':
|
||||
|
||||
the URL of the Consul cluster. host, port and scheme are derived from the
|
||||
URL. If not specified, connection configuration defaults to http requests
|
||||
to localhost on port 8500.
|
||||
|
||||
'domain':
|
||||
|
||||
if specified then the inventory will generate domain names that will resolve
|
||||
@ -101,8 +95,8 @@ kv_metadata is used to lookup metadata for each discovered node. Like kv_groups
|
||||
above it is used to build a path to lookup in the kv store where it expects to
|
||||
find a json dictionary of metadata entries. If found, each key/value pair in the
|
||||
dictionary is added to the metadata for the node. eg node 'nyc-web-1' in datacenter
|
||||
'nyc-dc1' and kv_metadata = 'ansible/metadata', then the key
|
||||
'ansible/metadata/nyc-dc1/nyc-web-1' should contain '{"databse": "postgres"}'
|
||||
'nyc-dc1' and kv_metadata = 'ansible/metadata', then the key
|
||||
'ansible/groups/nyc-dc1/nyc-web-1' should contain '{"databse": "postgres"}'
|
||||
|
||||
'availability':
|
||||
|
||||
@ -133,11 +127,10 @@ import os
|
||||
import re
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
try:
|
||||
import configparser
|
||||
except ImportError:
|
||||
import ConfigParser as configparser
|
||||
from time import time
|
||||
import sys
|
||||
import ConfigParser
|
||||
import urllib, urllib2, base64
|
||||
|
||||
|
||||
def get_log_filename():
|
||||
@ -192,14 +185,14 @@ if os.getenv('ANSIBLE_INVENTORY_CONSUL_IO_LOG_ENABLED'):
|
||||
|
||||
|
||||
try:
|
||||
import json
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
import simplejson as json
|
||||
|
||||
try:
|
||||
import consul
|
||||
import consul
|
||||
except ImportError as e:
|
||||
sys.exit("""failed=True msg='python-consul required for this module.
|
||||
sys.exit("""failed=True msg='python-consul required for this module.
|
||||
See http://python-consul.readthedocs.org/en/latest/#installation'""")
|
||||
|
||||
from six import iteritems
|
||||
@ -207,288 +200,284 @@ from six import iteritems
|
||||
|
||||
class ConsulInventory(object):
|
||||
|
||||
def __init__(self):
|
||||
''' Create an inventory based on the catalog of nodes and services
|
||||
registered in a consul cluster'''
|
||||
self.node_metadata = {}
|
||||
self.nodes = {}
|
||||
self.nodes_by_service = {}
|
||||
self.nodes_by_tag = {}
|
||||
self.nodes_by_datacenter = {}
|
||||
self.nodes_by_kv = {}
|
||||
self.nodes_by_availability = {}
|
||||
self.current_dc = None
|
||||
def __init__(self):
|
||||
''' Create an inventory based on the catalog of nodes and services
|
||||
registered in a consul cluster'''
|
||||
self.node_metadata = {}
|
||||
self.nodes = {}
|
||||
self.nodes_by_service = {}
|
||||
self.nodes_by_tag = {}
|
||||
self.nodes_by_datacenter = {}
|
||||
self.nodes_by_kv = {}
|
||||
self.nodes_by_availability = {}
|
||||
self.current_dc = None
|
||||
|
||||
config = ConsulConfig()
|
||||
self.config = config
|
||||
config = ConsulConfig()
|
||||
self.config = config
|
||||
|
||||
self.consul_api = config.get_consul_api()
|
||||
self.consul_api = config.get_consul_api()
|
||||
|
||||
if config.has_config('datacenter'):
|
||||
if config.has_config('host'):
|
||||
self.load_data_for_node(config.host, config.datacenter)
|
||||
else:
|
||||
self.load_data_for_datacenter(config.datacenter)
|
||||
else:
|
||||
self.load_all_data_consul()
|
||||
if config.has_config('datacenter'):
|
||||
if config.has_config('host'):
|
||||
self.load_data_for_node(config.host, config.datacenter)
|
||||
else:
|
||||
self.load_data_for_datacenter(config.datacenter)
|
||||
else:
|
||||
self.load_all_data_consul()
|
||||
|
||||
self.combine_all_results()
|
||||
print(json.dumps(self.inventory, sort_keys=True, indent=2))
|
||||
self.combine_all_results()
|
||||
print(json.dumps(self.inventory, sort_keys=True, indent=2))
|
||||
|
||||
def load_all_data_consul(self):
|
||||
''' cycle through each of the datacenters in the consul catalog and process
|
||||
the nodes in each '''
|
||||
self.datacenters = self.consul_api.catalog.datacenters()
|
||||
for datacenter in self.datacenters:
|
||||
self.current_dc = datacenter
|
||||
self.load_data_for_datacenter(datacenter)
|
||||
def load_all_data_consul(self):
|
||||
''' cycle through each of the datacenters in the consul catalog and process
|
||||
the nodes in each '''
|
||||
self.datacenters = self.consul_api.catalog.datacenters()
|
||||
for datacenter in self.datacenters:
|
||||
self.current_dc = datacenter
|
||||
self.load_data_for_datacenter(datacenter)
|
||||
|
||||
def load_availability_groups(self, node, datacenter):
|
||||
'''check the health of each service on a node and add add the node to either
|
||||
an 'available' or 'unavailable' grouping. The suffix for each group can be
|
||||
controlled from the config'''
|
||||
if self.config.has_config('availability'):
|
||||
for service_name, service in iteritems(node['Services']):
|
||||
for node in self.consul_api.health.service(service_name)[1]:
|
||||
for check in node['Checks']:
|
||||
if check['ServiceName'] == service_name:
|
||||
ok = 'passing' == check['Status']
|
||||
if ok:
|
||||
suffix = self.config.get_availability_suffix(
|
||||
|
||||
def load_availability_groups(self, node, datacenter):
|
||||
'''check the health of each service on a node and add add the node to either
|
||||
an 'available' or 'unavailable' grouping. The suffix for each group can be
|
||||
controlled from the config'''
|
||||
if self.config.has_config('availability'):
|
||||
for service_name, service in iteritems(node['Services']):
|
||||
for node in self.consul_api.health.service(service_name)[1]:
|
||||
for check in node['Checks']:
|
||||
if check['ServiceName'] == service_name:
|
||||
ok = 'passing' == check['Status']
|
||||
if ok:
|
||||
suffix = self.config.get_availability_suffix(
|
||||
'available_suffix', '_available')
|
||||
else:
|
||||
suffix = self.config.get_availability_suffix(
|
||||
else:
|
||||
suffix = self.config.get_availability_suffix(
|
||||
'unavailable_suffix', '_unavailable')
|
||||
self.add_node_to_map(self.nodes_by_availability,
|
||||
service_name + suffix, node['Node'])
|
||||
self.add_node_to_map(self.nodes_by_availability,
|
||||
service_name + suffix, node['Node'])
|
||||
|
||||
def load_data_for_datacenter(self, datacenter):
|
||||
'''processes all the nodes in a particular datacenter'''
|
||||
index, nodes = self.consul_api.catalog.nodes(dc=datacenter)
|
||||
for node in nodes:
|
||||
self.add_node_to_map(self.nodes_by_datacenter, datacenter, node)
|
||||
self.load_data_for_node(node['Node'], datacenter)
|
||||
|
||||
def load_data_for_node(self, node, datacenter):
|
||||
'''loads the data for a sinle node adding it to various groups based on
|
||||
metadata retrieved from the kv store and service availability'''
|
||||
def load_data_for_datacenter(self, datacenter):
|
||||
'''processes all the nodes in a particular datacenter'''
|
||||
index, nodes = self.consul_api.catalog.nodes(dc=datacenter)
|
||||
for node in nodes:
|
||||
self.add_node_to_map(self.nodes_by_datacenter, datacenter, node)
|
||||
self.load_data_for_node(node['Node'], datacenter)
|
||||
|
||||
index, node_data = self.consul_api.catalog.node(node, dc=datacenter)
|
||||
node = node_data['Node']
|
||||
self.add_node_to_map(self.nodes, 'all', node)
|
||||
self.add_metadata(node_data, "consul_datacenter", datacenter)
|
||||
self.add_metadata(node_data, "consul_nodename", node['Node'])
|
||||
def load_data_for_node(self, node, datacenter):
|
||||
'''loads the data for a sinle node adding it to various groups based on
|
||||
metadata retrieved from the kv store and service availability'''
|
||||
|
||||
self.load_groups_from_kv(node_data)
|
||||
self.load_node_metadata_from_kv(node_data)
|
||||
self.load_availability_groups(node_data, datacenter)
|
||||
index, node_data = self.consul_api.catalog.node(node, dc=datacenter)
|
||||
node = node_data['Node']
|
||||
self.add_node_to_map(self.nodes, 'all', node)
|
||||
self.add_metadata(node_data, "consul_datacenter", datacenter)
|
||||
self.add_metadata(node_data, "consul_nodename", node['Node'])
|
||||
|
||||
for name, service in node_data['Services'].items():
|
||||
self.load_data_from_service(name, service, node_data)
|
||||
self.load_groups_from_kv(node_data)
|
||||
self.load_node_metadata_from_kv(node_data)
|
||||
self.load_availability_groups(node_data, datacenter)
|
||||
|
||||
def load_node_metadata_from_kv(self, node_data):
|
||||
''' load the json dict at the metadata path defined by the kv_metadata value
|
||||
and the node name add each entry in the dictionary to the node's
|
||||
metadata '''
|
||||
node = node_data['Node']
|
||||
if self.config.has_config('kv_metadata'):
|
||||
key = "%s/%s/%s" % (self.config.kv_metadata, self.current_dc, node['Node'])
|
||||
index, metadata = self.consul_api.kv.get(key)
|
||||
if metadata and metadata['Value']:
|
||||
try:
|
||||
metadata = json.loads(metadata['Value'])
|
||||
for k, v in metadata.items():
|
||||
self.add_metadata(node_data, k, v)
|
||||
except:
|
||||
pass
|
||||
for name, service in node_data['Services'].items():
|
||||
self.load_data_from_service(name, service, node_data)
|
||||
|
||||
def load_groups_from_kv(self, node_data):
|
||||
''' load the comma separated list of groups at the path defined by the
|
||||
kv_groups config value and the node name add the node address to each
|
||||
group found '''
|
||||
node = node_data['Node']
|
||||
if self.config.has_config('kv_groups'):
|
||||
key = "%s/%s/%s" % (self.config.kv_groups, self.current_dc, node['Node'])
|
||||
index, groups = self.consul_api.kv.get(key)
|
||||
if groups and groups['Value']:
|
||||
for group in groups['Value'].split(','):
|
||||
self.add_node_to_map(self.nodes_by_kv, group.strip(), node)
|
||||
def load_node_metadata_from_kv(self, node_data):
|
||||
''' load the json dict at the metadata path defined by the kv_metadata value
|
||||
and the node name add each entry in the dictionary to the the node's
|
||||
metadata '''
|
||||
node = node_data['Node']
|
||||
if self.config.has_config('kv_metadata'):
|
||||
key = "%s/%s/%s" % (self.config.kv_metadata, self.current_dc, node['Node'])
|
||||
index, metadata = self.consul_api.kv.get(key)
|
||||
if metadata and metadata['Value']:
|
||||
try:
|
||||
metadata = json.loads(metadata['Value'])
|
||||
for k,v in metadata.items():
|
||||
self.add_metadata(node_data, k, v)
|
||||
except:
|
||||
pass
|
||||
|
||||
def load_data_from_service(self, service_name, service, node_data):
|
||||
'''process a service registered on a node, adding the node to a group with
|
||||
the service name. Each service tag is extracted and the node is added to a
|
||||
tag grouping also'''
|
||||
self.add_metadata(node_data, "consul_services", service_name, True)
|
||||
def load_groups_from_kv(self, node_data):
|
||||
''' load the comma separated list of groups at the path defined by the
|
||||
kv_groups config value and the node name add the node address to each
|
||||
group found '''
|
||||
node = node_data['Node']
|
||||
if self.config.has_config('kv_groups'):
|
||||
key = "%s/%s/%s" % (self.config.kv_groups, self.current_dc, node['Node'])
|
||||
index, groups = self.consul_api.kv.get(key)
|
||||
if groups and groups['Value']:
|
||||
for group in groups['Value'].split(','):
|
||||
self.add_node_to_map(self.nodes_by_kv, group.strip(), node)
|
||||
|
||||
if self.is_service("ssh", service_name):
|
||||
self.add_metadata(node_data, "ansible_ssh_port", service['Port'])
|
||||
def load_data_from_service(self, service_name, service, node_data):
|
||||
'''process a service registered on a node, adding the node to a group with
|
||||
the service name. Each service tag is extracted and the node is added to a
|
||||
tag grouping also'''
|
||||
self.add_metadata(node_data, "consul_services", service_name, True)
|
||||
|
||||
if self.config.has_config('servers_suffix'):
|
||||
service_name = service_name + self.config.servers_suffix
|
||||
if self.is_service("ssh", service_name):
|
||||
self.add_metadata(node_data, "ansible_ssh_port", service['Port'])
|
||||
|
||||
self.add_node_to_map(self.nodes_by_service, service_name, node_data['Node'])
|
||||
self.extract_groups_from_tags(service_name, service, node_data)
|
||||
if self.config.has_config('servers_suffix'):
|
||||
service_name = service_name + self.config.servers_suffix
|
||||
|
||||
def is_service(self, target, name):
|
||||
return name and (name.lower() == target.lower())
|
||||
self.add_node_to_map(self.nodes_by_service, service_name, node_data['Node'])
|
||||
self.extract_groups_from_tags(service_name, service, node_data)
|
||||
|
||||
def extract_groups_from_tags(self, service_name, service, node_data):
|
||||
'''iterates each service tag and adds the node to groups derived from the
|
||||
service and tag names e.g. nginx_master'''
|
||||
if self.config.has_config('tags') and service['Tags']:
|
||||
tags = service['Tags']
|
||||
self.add_metadata(node_data, "consul_%s_tags" % service_name, tags)
|
||||
for tag in service['Tags']:
|
||||
tagname = service_name + '_' + tag
|
||||
self.add_node_to_map(self.nodes_by_tag, tagname, node_data['Node'])
|
||||
def is_service(self, target, name):
|
||||
return name and (name.lower() == target.lower())
|
||||
|
||||
def combine_all_results(self):
|
||||
'''prunes and sorts all groupings for combination into the final map'''
|
||||
self.inventory = {"_meta": {"hostvars": self.node_metadata}}
|
||||
groupings = [self.nodes, self.nodes_by_datacenter, self.nodes_by_service,
|
||||
self.nodes_by_tag, self.nodes_by_kv, self.nodes_by_availability]
|
||||
for grouping in groupings:
|
||||
for name, addresses in grouping.items():
|
||||
self.inventory[name] = sorted(list(set(addresses)))
|
||||
def extract_groups_from_tags(self, service_name, service, node_data):
|
||||
'''iterates each service tag and adds the node to groups derived from the
|
||||
service and tag names e.g. nginx_master'''
|
||||
if self.config.has_config('tags') and service['Tags']:
|
||||
tags = service['Tags']
|
||||
self.add_metadata(node_data, "consul_%s_tags" % service_name, tags)
|
||||
for tag in service['Tags']:
|
||||
tagname = service_name +'_'+tag
|
||||
self.add_node_to_map(self.nodes_by_tag, tagname, node_data['Node'])
|
||||
|
||||
def add_metadata(self, node_data, key, value, is_list=False):
|
||||
''' Pushed an element onto a metadata dict for the node, creating
|
||||
the dict if it doesn't exist '''
|
||||
key = self.to_safe(key)
|
||||
node = self.get_inventory_name(node_data['Node'])
|
||||
def combine_all_results(self):
|
||||
'''prunes and sorts all groupings for combination into the final map'''
|
||||
self.inventory = {"_meta": { "hostvars" : self.node_metadata}}
|
||||
groupings = [self.nodes, self.nodes_by_datacenter, self.nodes_by_service,
|
||||
self.nodes_by_tag, self.nodes_by_kv, self.nodes_by_availability]
|
||||
for grouping in groupings:
|
||||
for name, addresses in grouping.items():
|
||||
self.inventory[name] = sorted(list(set(addresses)))
|
||||
|
||||
if node in self.node_metadata:
|
||||
metadata = self.node_metadata[node]
|
||||
else:
|
||||
metadata = {}
|
||||
self.node_metadata[node] = metadata
|
||||
if is_list:
|
||||
self.push(metadata, key, value)
|
||||
else:
|
||||
metadata[key] = value
|
||||
def add_metadata(self, node_data, key, value, is_list = False):
|
||||
''' Pushed an element onto a metadata dict for the node, creating
|
||||
the dict if it doesn't exist '''
|
||||
key = self.to_safe(key)
|
||||
node = self.get_inventory_name(node_data['Node'])
|
||||
|
||||
def get_inventory_name(self, node_data):
|
||||
'''return the ip or a node name that can be looked up in consul's dns'''
|
||||
domain = self.config.domain
|
||||
if domain:
|
||||
node_name = node_data['Node']
|
||||
if self.current_dc:
|
||||
return '%s.node.%s.%s' % (node_name, self.current_dc, domain)
|
||||
else:
|
||||
return '%s.node.%s' % (node_name, domain)
|
||||
else:
|
||||
return node_data['Address']
|
||||
if node in self.node_metadata:
|
||||
metadata = self.node_metadata[node]
|
||||
else:
|
||||
metadata = {}
|
||||
self.node_metadata[node] = metadata
|
||||
if is_list:
|
||||
self.push(metadata, key, value)
|
||||
else:
|
||||
metadata[key] = value
|
||||
|
||||
def add_node_to_map(self, map, name, node):
|
||||
self.push(map, name, self.get_inventory_name(node))
|
||||
def get_inventory_name(self, node_data):
|
||||
'''return the ip or a node name that can be looked up in consul's dns'''
|
||||
domain = self.config.domain
|
||||
if domain:
|
||||
node_name = node_data['Node']
|
||||
if self.current_dc:
|
||||
return '%s.node.%s.%s' % ( node_name, self.current_dc, domain)
|
||||
else:
|
||||
return '%s.node.%s' % ( node_name, domain)
|
||||
else:
|
||||
return node_data['Address']
|
||||
|
||||
def push(self, my_dict, key, element):
|
||||
''' Pushed an element onto an array that may not have been defined in the
|
||||
dict '''
|
||||
key = self.to_safe(key)
|
||||
if key in my_dict:
|
||||
my_dict[key].append(element)
|
||||
else:
|
||||
my_dict[key] = [element]
|
||||
def add_node_to_map(self, map, name, node):
|
||||
self.push(map, name, self.get_inventory_name(node))
|
||||
|
||||
def to_safe(self, word):
|
||||
''' Converts 'bad' characters in a string to underscores so they can be used
|
||||
as Ansible groups '''
|
||||
return re.sub('[^A-Za-z0-9\-\.]', '_', word)
|
||||
|
||||
def sanitize_dict(self, d):
|
||||
def push(self, my_dict, key, element):
|
||||
''' Pushed an element onto an array that may not have been defined in the
|
||||
dict '''
|
||||
key = self.to_safe(key)
|
||||
if key in my_dict:
|
||||
my_dict[key].append(element)
|
||||
else:
|
||||
my_dict[key] = [element]
|
||||
|
||||
new_dict = {}
|
||||
for k, v in d.items():
|
||||
if v is not None:
|
||||
new_dict[self.to_safe(str(k))] = self.to_safe(str(v))
|
||||
return new_dict
|
||||
def to_safe(self, word):
|
||||
''' Converts 'bad' characters in a string to underscores so they can be used
|
||||
as Ansible groups '''
|
||||
return re.sub('[^A-Za-z0-9\-\.]', '_', word)
|
||||
|
||||
def sanitize_list(self, seq):
|
||||
new_seq = []
|
||||
for d in seq:
|
||||
new_seq.append(self.sanitize_dict(d))
|
||||
return new_seq
|
||||
def sanitize_dict(self, d):
|
||||
|
||||
new_dict = {}
|
||||
for k, v in d.items():
|
||||
if v != None:
|
||||
new_dict[self.to_safe(str(k))] = self.to_safe(str(v))
|
||||
return new_dict
|
||||
|
||||
def sanitize_list(self, seq):
|
||||
new_seq = []
|
||||
for d in seq:
|
||||
new_seq.append(self.sanitize_dict(d))
|
||||
return new_seq
|
||||
|
||||
|
||||
class ConsulConfig(dict):
|
||||
|
||||
def __init__(self):
|
||||
self.read_settings()
|
||||
self.read_cli_args()
|
||||
def __init__(self):
|
||||
self.read_settings()
|
||||
self.read_cli_args()
|
||||
|
||||
def has_config(self, name):
|
||||
if hasattr(self, name):
|
||||
return getattr(self, name)
|
||||
else:
|
||||
return False
|
||||
def has_config(self, name):
|
||||
if hasattr(self, name):
|
||||
return getattr(self, name)
|
||||
else:
|
||||
return False
|
||||
|
||||
def read_settings(self):
|
||||
''' Reads the settings from the consul_io.ini file (or consul.ini for backwards compatibility)'''
|
||||
config = configparser.SafeConfigParser()
|
||||
if os.path.isfile(os.path.dirname(os.path.realpath(__file__)) + '/consul_io.ini'):
|
||||
config.read(os.path.dirname(os.path.realpath(__file__)) + '/consul_io.ini')
|
||||
else:
|
||||
config.read(os.path.dirname(os.path.realpath(__file__)) + '/consul.ini')
|
||||
def read_settings(self):
|
||||
''' Reads the settings from the consul.ini file '''
|
||||
config = ConfigParser.SafeConfigParser()
|
||||
config.read(os.path.dirname(os.path.realpath(__file__)) + '/consul.ini')
|
||||
|
||||
config_options = ['host', 'token', 'datacenter', 'servers_suffix',
|
||||
'tags', 'kv_metadata', 'kv_groups', 'availability',
|
||||
'unavailable_suffix', 'available_suffix', 'url',
|
||||
'domain']
|
||||
for option in config_options:
|
||||
value = None
|
||||
if config.has_option('consul', option):
|
||||
value = config.get('consul', option)
|
||||
setattr(self, option, value)
|
||||
config_options = ['host', 'token', 'datacenter', 'servers_suffix',
|
||||
'tags', 'kv_metadata', 'kv_groups', 'availability',
|
||||
'unavailable_suffix', 'available_suffix', 'url',
|
||||
'domain']
|
||||
for option in config_options:
|
||||
value = None
|
||||
if config.has_option('consul', option):
|
||||
value = config.get('consul', option)
|
||||
setattr(self, option, value)
|
||||
|
||||
def read_cli_args(self):
|
||||
''' Command line argument processing '''
|
||||
parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based nodes in a Consul cluster')
|
||||
def read_cli_args(self):
|
||||
''' Command line argument processing '''
|
||||
parser = argparse.ArgumentParser(description=
|
||||
'Produce an Ansible Inventory file based nodes in a Consul cluster')
|
||||
|
||||
parser.add_argument('--list', action='store_true',
|
||||
help='Get all inventory variables from all nodes in the consul cluster')
|
||||
parser.add_argument('--host', action='store',
|
||||
help='Get all inventory variables about a specific consul node,'
|
||||
'requires datacenter set in consul.ini.')
|
||||
parser.add_argument('--datacenter', action='store',
|
||||
help='Get all inventory about a specific consul datacenter')
|
||||
parser.add_argument('--list', action='store_true',
|
||||
help='Get all inventory variables from all nodes in the consul cluster')
|
||||
parser.add_argument('--host', action='store',
|
||||
help='Get all inventory variables about a specific consul node, \
|
||||
requires datacenter set in consul.ini.')
|
||||
parser.add_argument('--datacenter', action='store',
|
||||
help='Get all inventory about a specific consul datacenter')
|
||||
|
||||
args = parser.parse_args()
|
||||
arg_names = ['host', 'datacenter']
|
||||
args = parser.parse_args()
|
||||
arg_names = ['host', 'datacenter']
|
||||
|
||||
for arg in arg_names:
|
||||
if getattr(args, arg):
|
||||
setattr(self, arg, getattr(args, arg))
|
||||
for arg in arg_names:
|
||||
if getattr(args, arg):
|
||||
setattr(self, arg, getattr(args, arg))
|
||||
|
||||
def get_availability_suffix(self, suffix, default):
|
||||
if self.has_config(suffix):
|
||||
return self.has_config(suffix)
|
||||
return default
|
||||
def get_availability_suffix(self, suffix, default):
|
||||
if self.has_config(suffix):
|
||||
return self.has_config(suffix)
|
||||
return default
|
||||
|
||||
def get_consul_api(self):
|
||||
'''get an instance of the api based on the supplied configuration'''
|
||||
host = 'localhost'
|
||||
port = 8500
|
||||
token = None
|
||||
scheme = 'http'
|
||||
|
||||
if hasattr(self, 'url'):
|
||||
try:
|
||||
from urlparse import urlparse
|
||||
except ImportError:
|
||||
from urllib.parse import urlparse
|
||||
o = urlparse(self.url)
|
||||
if o.hostname:
|
||||
host = o.hostname
|
||||
if o.port:
|
||||
port = o.port
|
||||
if o.scheme:
|
||||
scheme = o.scheme
|
||||
def get_consul_api(self):
|
||||
'''get an instance of the api based on the supplied configuration'''
|
||||
host = 'localhost'
|
||||
port = 8500
|
||||
token = None
|
||||
|
||||
if hasattr(self, 'token'):
|
||||
token = self.token
|
||||
if not token:
|
||||
token = 'anonymous'
|
||||
return consul.Consul(host=host, port=port, token=token, scheme=scheme)
|
||||
if hasattr(self, 'url'):
|
||||
from urlparse import urlparse
|
||||
o = urlparse(self.url)
|
||||
if o.hostname:
|
||||
host = o.hostname
|
||||
if o.port:
|
||||
port = o.port
|
||||
|
||||
if hasattr(self, 'token'):
|
||||
token = self.token
|
||||
if not token:
|
||||
token = 'anonymous'
|
||||
return consul.Consul(host=host, port=port, token=token)
|
||||
|
||||
ConsulInventory()
|
||||
|
||||
@ -22,7 +22,7 @@ found. You can force this script to use the cache with --force-cache.
|
||||
|
||||
----
|
||||
Configuration is read from `digital_ocean.ini`, then from environment variables,
|
||||
and then from command-line arguments.
|
||||
then and command-line arguments.
|
||||
|
||||
Most notably, the DigitalOcean API Token must be specified. It can be specified
|
||||
in the INI file or with the following environment variables:
|
||||
@ -47,7 +47,7 @@ The following groups are generated from --list:
|
||||
- size_NAME
|
||||
- status_STATUS
|
||||
|
||||
For each host, the following variables are registered:
|
||||
When run against a specific host, this script returns the following variables:
|
||||
- do_backup_ids
|
||||
- do_created_at
|
||||
- do_disk
|
||||
@ -58,7 +58,7 @@ For each host, the following variables are registered:
|
||||
- do_private_ip_address
|
||||
- do_kernel - object
|
||||
- do_locked
|
||||
- do_memory
|
||||
- de_memory
|
||||
- do_name
|
||||
- do_networks - object
|
||||
- do_next_backup_window
|
||||
@ -67,9 +67,7 @@ For each host, the following variables are registered:
|
||||
- do_size_slug
|
||||
- do_snapshot_ids - list
|
||||
- do_status
|
||||
- do_tags
|
||||
- do_vcpus
|
||||
- do_volume_ids
|
||||
|
||||
-----
|
||||
```
|
||||
@ -138,13 +136,9 @@ import sys
|
||||
import re
|
||||
import argparse
|
||||
from time import time
|
||||
import ConfigParser
|
||||
import ast
|
||||
|
||||
try:
|
||||
import ConfigParser
|
||||
except ImportError:
|
||||
import configparser as ConfigParser
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
@ -153,7 +147,8 @@ except ImportError:
|
||||
try:
|
||||
from dopy.manager import DoManager
|
||||
except ImportError as e:
|
||||
sys.exit("failed=True msg={}".format(e.message))
|
||||
sys.exit("failed=True msg='`dopy` library required for this script'")
|
||||
|
||||
|
||||
|
||||
class DigitalOceanInventory(object):
|
||||
@ -166,8 +161,8 @@ class DigitalOceanInventory(object):
|
||||
''' Main execution path '''
|
||||
|
||||
# DigitalOceanInventory data
|
||||
self.data = {} # All DigitalOcean data
|
||||
self.inventory = {} # Ansible Inventory
|
||||
self.data = {} # All DigitalOcean data
|
||||
self.inventory = {} # Ansible Inventory
|
||||
|
||||
# Define defaults
|
||||
self.cache_path = '.'
|
||||
@ -196,7 +191,7 @@ or environment variables (DO_API_TOKEN)\n''')
|
||||
self.cache_filename = self.cache_path + "/ansible-digital_ocean.cache"
|
||||
self.cache_refreshed = False
|
||||
|
||||
if self.is_cache_valid():
|
||||
if self.is_cache_valid:
|
||||
self.load_from_cache()
|
||||
if len(self.data) == 0:
|
||||
if self.args.force_cache:
|
||||
@ -243,6 +238,7 @@ or environment variables (DO_API_TOKEN)\n''')
|
||||
print(json.dumps(json_data))
|
||||
# That's all she wrote...
|
||||
|
||||
|
||||
###########################################################################
|
||||
# Script configuration
|
||||
###########################################################################
|
||||
@ -278,6 +274,7 @@ or environment variables (DO_API_TOKEN)\n''')
|
||||
if os.getenv("DO_API_KEY"):
|
||||
self.api_token = os.getenv("DO_API_KEY")
|
||||
|
||||
|
||||
def read_cli_args(self):
|
||||
''' Command line argument processing '''
|
||||
parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on DigitalOcean credentials')
|
||||
@ -286,23 +283,23 @@ or environment variables (DO_API_TOKEN)\n''')
|
||||
parser.add_argument('--host', action='store', help='Get all Ansible inventory variables about a specific Droplet')
|
||||
|
||||
parser.add_argument('--all', action='store_true', help='List all DigitalOcean information as JSON')
|
||||
parser.add_argument('--droplets', '-d', action='store_true', help='List Droplets as JSON')
|
||||
parser.add_argument('--droplets','-d', action='store_true', help='List Droplets as JSON')
|
||||
parser.add_argument('--regions', action='store_true', help='List Regions as JSON')
|
||||
parser.add_argument('--images', action='store_true', help='List Images as JSON')
|
||||
parser.add_argument('--sizes', action='store_true', help='List Sizes as JSON')
|
||||
parser.add_argument('--ssh-keys', action='store_true', help='List SSH keys as JSON')
|
||||
parser.add_argument('--domains', action='store_true', help='List Domains as JSON')
|
||||
parser.add_argument('--domains', action='store_true',help='List Domains as JSON')
|
||||
|
||||
parser.add_argument('--pretty', '-p', action='store_true', help='Pretty-print results')
|
||||
parser.add_argument('--pretty','-p', action='store_true', help='Pretty-print results')
|
||||
|
||||
parser.add_argument('--cache-path', action='store', help='Path to the cache files (default: .)')
|
||||
parser.add_argument('--cache-max_age', action='store', help='Maximum age of the cached items (default: 0)')
|
||||
parser.add_argument('--force-cache', action='store_true', default=False, help='Only use data from the cache')
|
||||
parser.add_argument('--refresh-cache', '-r', action='store_true', default=False,
|
||||
parser.add_argument('--refresh-cache','-r', action='store_true', default=False,
|
||||
help='Force refresh of cache by making API requests to DigitalOcean (default: False - use cache files)')
|
||||
|
||||
parser.add_argument('--env', '-e', action='store_true', help='Display DO_API_TOKEN')
|
||||
parser.add_argument('--api-token', '-a', action='store', help='DigitalOcean API Token')
|
||||
parser.add_argument('--env','-e', action='store_true', help='Display DO_API_TOKEN')
|
||||
parser.add_argument('--api-token','-a', action='store', help='DigitalOcean API Token')
|
||||
|
||||
self.args = parser.parse_args()
|
||||
|
||||
@ -316,19 +313,20 @@ or environment variables (DO_API_TOKEN)\n''')
|
||||
not self.args.all and not self.args.host):
|
||||
self.args.list = True
|
||||
|
||||
|
||||
###########################################################################
|
||||
# Data Management
|
||||
###########################################################################
|
||||
|
||||
def load_from_digital_ocean(self, resource=None):
|
||||
'''Get JSON from DigitalOcean API'''
|
||||
if self.args.force_cache and os.path.isfile(self.cache_filename):
|
||||
if self.args.force_cache:
|
||||
return
|
||||
# We always get fresh droplets
|
||||
if self.is_cache_valid() and not (resource == 'droplets' or resource is None):
|
||||
if self.is_cache_valid() and not (resource=='droplets' or resource is None):
|
||||
return
|
||||
if self.args.refresh_cache:
|
||||
resource = None
|
||||
resource=None
|
||||
|
||||
if resource == 'droplets' or resource is None:
|
||||
self.data['droplets'] = self.manager.all_active_droplets()
|
||||
@ -349,23 +347,24 @@ or environment variables (DO_API_TOKEN)\n''')
|
||||
self.data['domains'] = self.manager.all_domains()
|
||||
self.cache_refreshed = True
|
||||
|
||||
|
||||
def build_inventory(self):
|
||||
'''Build Ansible inventory of droplets'''
|
||||
self.inventory = {
|
||||
'all': {
|
||||
'hosts': [],
|
||||
'vars': self.group_variables
|
||||
},
|
||||
'_meta': {'hostvars': {}}
|
||||
}
|
||||
'all': {
|
||||
'hosts': [],
|
||||
'vars': self.group_variables
|
||||
},
|
||||
'_meta': {'hostvars': {}}
|
||||
}
|
||||
|
||||
# add all droplets by id and name
|
||||
for droplet in self.data['droplets']:
|
||||
# when using private_networking, the API reports the private one in "ip_address".
|
||||
#when using private_networking, the API reports the private one in "ip_address".
|
||||
if 'private_networking' in droplet['features'] and not self.use_private_network:
|
||||
for net in droplet['networks']['v4']:
|
||||
if net['type'] == 'public':
|
||||
dest = net['ip_address']
|
||||
if net['type']=='public':
|
||||
dest=net['ip_address']
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
@ -377,41 +376,46 @@ or environment variables (DO_API_TOKEN)\n''')
|
||||
self.inventory[droplet['name']] = [dest]
|
||||
|
||||
# groups that are always present
|
||||
for group in ('region_' + droplet['region']['slug'],
|
||||
'image_' + str(droplet['image']['id']),
|
||||
'size_' + droplet['size']['slug'],
|
||||
'distro_' + self.to_safe(droplet['image']['distribution']),
|
||||
'status_' + droplet['status']):
|
||||
for group in [
|
||||
'region_' + droplet['region']['slug'],
|
||||
'image_' + str(droplet['image']['id']),
|
||||
'size_' + droplet['size']['slug'],
|
||||
'distro_' + self.to_safe(droplet['image']['distribution']),
|
||||
'status_' + droplet['status'],
|
||||
|
||||
]:
|
||||
if group not in self.inventory:
|
||||
self.inventory[group] = {'hosts': [], 'vars': {}}
|
||||
self.inventory[group] = { 'hosts': [ ], 'vars': {} }
|
||||
self.inventory[group]['hosts'].append(dest)
|
||||
|
||||
# groups that are not always present
|
||||
for group in (droplet['image']['slug'],
|
||||
droplet['image']['name']):
|
||||
for group in [
|
||||
droplet['image']['slug'],
|
||||
droplet['image']['name']
|
||||
]:
|
||||
if group:
|
||||
image = 'image_' + self.to_safe(group)
|
||||
if image not in self.inventory:
|
||||
self.inventory[image] = {'hosts': [], 'vars': {}}
|
||||
self.inventory[image] = { 'hosts': [ ], 'vars': {} }
|
||||
self.inventory[image]['hosts'].append(dest)
|
||||
|
||||
if droplet['tags']:
|
||||
for tag in droplet['tags']:
|
||||
if tag not in self.inventory:
|
||||
self.inventory[tag] = {'hosts': [], 'vars': {}}
|
||||
self.inventory[tag]['hosts'].append(dest)
|
||||
|
||||
# hostvars
|
||||
info = self.do_namespace(droplet)
|
||||
self.inventory['_meta']['hostvars'][dest] = info
|
||||
|
||||
def load_droplet_variables_for_host(self):
|
||||
'''Generate a JSON response to a --host call'''
|
||||
host = int(self.args.host)
|
||||
|
||||
droplet = self.manager.show_droplet(host)
|
||||
info = self.do_namespace(droplet)
|
||||
|
||||
# Put all the information in a 'do_' namespace
|
||||
info = {}
|
||||
for k, v in droplet.items():
|
||||
info['do_'+k] = v
|
||||
|
||||
return {'droplet': info}
|
||||
|
||||
|
||||
|
||||
###########################################################################
|
||||
# Cache Management
|
||||
###########################################################################
|
||||
@ -425,6 +429,7 @@ or environment variables (DO_API_TOKEN)\n''')
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def load_from_cache(self):
|
||||
''' Reads the data from the cache file and assigns it to member variables as Python Objects'''
|
||||
try:
|
||||
@ -438,15 +443,17 @@ or environment variables (DO_API_TOKEN)\n''')
|
||||
self.data = data['data']
|
||||
self.inventory = data['inventory']
|
||||
|
||||
|
||||
def write_to_cache(self):
|
||||
''' Writes data in JSON format to a file '''
|
||||
data = {'data': self.data, 'inventory': self.inventory}
|
||||
data = { 'data': self.data, 'inventory': self.inventory }
|
||||
json_data = json.dumps(data, sort_keys=True, indent=2)
|
||||
|
||||
cache = open(self.cache_filename, 'w')
|
||||
cache.write(json_data)
|
||||
cache.close()
|
||||
|
||||
|
||||
###########################################################################
|
||||
# Utilities
|
||||
###########################################################################
|
||||
@ -458,16 +465,11 @@ or environment variables (DO_API_TOKEN)\n''')
|
||||
else:
|
||||
my_dict[key] = [element]
|
||||
|
||||
|
||||
def to_safe(self, word):
|
||||
''' Converts 'bad' characters in a string to underscores so they can be used as Ansible groups '''
|
||||
return re.sub("[^A-Za-z0-9\-\.]", "_", word)
|
||||
|
||||
def do_namespace(self, data):
|
||||
''' Returns a copy of the dictionary with all the keys put in a 'do_' namespace '''
|
||||
info = {}
|
||||
for k, v in data.items():
|
||||
info['do_' + k] = v
|
||||
return info
|
||||
|
||||
|
||||
###########################################################################
|
||||
|
||||
@ -133,7 +133,7 @@ When run for a specific container using the --host option this script returns th
|
||||
"docker_hostspath": "/mnt/sda1/var/lib/docker/containers/9f2f80b0a702361d1ac432e6af816c19bda46da15c21264fb418c873de635a14/hosts",
|
||||
"docker_id": "9f2f80b0a702361d1ac432e6af816c19bda46da15c21264fb418c873de635a14",
|
||||
"docker_image": "0a6ba66e537a53a5ea94f7c6a99c534c6adb12e3ed09326d4bf3b38f7c3ba4e7",
|
||||
"docker_logpath": "/mnt/sda1/var/lib/docker/containers/9f2f80b0a702361d1ac432e6af816c19bda46da15c21264fb418c873de635a14/9f2f80b0a702361d1ac432e6a-json.log",
|
||||
"docker_logpath": "/mnt/sda1/var/lib/docker/containers/9f2f80b0a702361d1ac432e6af816c19bda46da15c21264fb418c873de635a14/9f2f80b0a702361d1ac432e6af816c19bda46da15c21264fb418c873de635a14-json.log",
|
||||
"docker_mountlabel": "",
|
||||
"docker_mounts": [],
|
||||
"docker_name": "/hello-world",
|
||||
@ -371,6 +371,7 @@ HAS_DOCKER_PY = True
|
||||
HAS_DOCKER_ERROR = False
|
||||
|
||||
try:
|
||||
from docker import Client
|
||||
from docker.errors import APIError, TLSParameterError
|
||||
from docker.tls import TLSConfig
|
||||
from docker.constants import DEFAULT_TIMEOUT_SECONDS, DEFAULT_DOCKER_API_VERSION
|
||||
@ -378,19 +379,6 @@ except ImportError as exc:
|
||||
HAS_DOCKER_ERROR = str(exc)
|
||||
HAS_DOCKER_PY = False
|
||||
|
||||
# Client has recently been split into DockerClient and APIClient
|
||||
try:
|
||||
from docker import Client
|
||||
except ImportError as exc:
|
||||
try:
|
||||
from docker import APIClient as Client
|
||||
except ImportError as exc:
|
||||
HAS_DOCKER_ERROR = str(exc)
|
||||
HAS_DOCKER_PY = False
|
||||
|
||||
class Client:
|
||||
pass
|
||||
|
||||
DEFAULT_DOCKER_HOST = 'unix://var/run/docker.sock'
|
||||
DEFAULT_TLS = False
|
||||
DEFAULT_TLS_VERIFY = False
|
||||
@ -456,7 +444,7 @@ class AnsibleDockerClient(Client):
|
||||
tls_config = TLSConfig(**kwargs)
|
||||
return tls_config
|
||||
except TLSParameterError as exc:
|
||||
self.fail("TLS config error: %s" % exc)
|
||||
self.fail("TLS config error: %s" % exc)
|
||||
|
||||
def _get_connect_params(self):
|
||||
auth = self.auth_params
|
||||
@ -537,7 +525,7 @@ class AnsibleDockerClient(Client):
|
||||
msg = "You asked for verification that Docker host name matches %s. The actual hostname is %s. " \
|
||||
"Most likely you need to set DOCKER_TLS_HOSTNAME or pass tls_hostname with a value of %s. " \
|
||||
"You may also use TLS without verification by setting the tls parameter to true." \
|
||||
% (self.auth_params['tls_hostname'], match.group(1), match.group(1))
|
||||
% (self.auth_params['tls_hostname'], match.group(1))
|
||||
self.fail(msg)
|
||||
self.fail("SSL Exception: %s" % (error))
|
||||
|
||||
@ -621,7 +609,7 @@ class DockerInventory(object):
|
||||
|
||||
self.groups[id].append(name)
|
||||
self.groups[name].append(name)
|
||||
if short_id not in self.groups:
|
||||
if short_id not in self.groups.keys():
|
||||
self.groups[short_id].append(name)
|
||||
self.groups[hostname].append(name)
|
||||
|
||||
@ -688,7 +676,7 @@ class DockerInventory(object):
|
||||
# use hosts from config file
|
||||
for host in hosts_list:
|
||||
docker_host = host.get('host') or def_host or self._args.docker_host or \
|
||||
self._env_args.docker_host or DEFAULT_DOCKER_HOST
|
||||
self._env_args.docker_host or DEFAULT_DOCKER_HOST
|
||||
api_version = host.get('version') or def_version or self._args.api_version or \
|
||||
self._env_args.api_version or DEFAULT_DOCKER_API_VERSION
|
||||
tls_hostname = host.get('tls_hostname') or def_tls_hostname or self._args.tls_hostname or \
|
||||
@ -747,7 +735,7 @@ class DockerInventory(object):
|
||||
|
||||
cert_path = def_cert_path or self._args.cert_path or self._env_args.cert_path
|
||||
if cert_path and cert_path == self._env_args.cert_path:
|
||||
cert_path = os.path.join(cert_path, 'cert.pem')
|
||||
cert_path = os.path.join(cert_path, 'cert.pem')
|
||||
|
||||
cacert_path = def_cacert_path or self._args.cacert_path or self._env_args.cert_path
|
||||
if cacert_path and cacert_path == self._env_args.cert_path:
|
||||
@ -791,10 +779,6 @@ class DockerInventory(object):
|
||||
if config_path:
|
||||
try:
|
||||
config_file = os.path.abspath(config_path)
|
||||
# default config path is docker.yml in same directory as this script
|
||||
# old behaviour is docker.yml in current directory. Handle both.
|
||||
if not os.path.exists(config_file):
|
||||
config_file = os.path.abspath(os.path.basename(config_path))
|
||||
except:
|
||||
config_file = None
|
||||
|
||||
@ -829,30 +813,30 @@ class DockerInventory(object):
|
||||
# Parse command line arguments
|
||||
|
||||
basename = os.path.splitext(os.path.basename(__file__))[0]
|
||||
default_config = os.path.join(os.path.dirname(__file__), basename + '.yml')
|
||||
default_config = basename + '.yml'
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Return Ansible inventory for one or more Docker hosts.')
|
||||
description='Return Ansible inventory for one or more Docker hosts.')
|
||||
parser.add_argument('--list', action='store_true', default=True,
|
||||
help='List all containers (default: True)')
|
||||
help='List all containers (default: True)')
|
||||
parser.add_argument('--debug', action='store_true', default=False,
|
||||
help='Send debug messages to STDOUT')
|
||||
help='Send debug messages to STDOUT')
|
||||
parser.add_argument('--host', action='store',
|
||||
help='Only get information for a specific container.')
|
||||
parser.add_argument('--pretty', action='store_true', default=False,
|
||||
help='Pretty print JSON output(default: False)')
|
||||
help='Pretty print JSON output(default: False)')
|
||||
parser.add_argument('--config-file', action='store', default=default_config,
|
||||
help="Name of the config file to use. Default is %s" % (default_config))
|
||||
parser.add_argument('--docker-host', action='store', default=None,
|
||||
help="The base url or Unix sock path to connect to the docker daemon. Defaults to %s"
|
||||
% (DEFAULT_DOCKER_HOST))
|
||||
% (DEFAULT_DOCKER_HOST))
|
||||
parser.add_argument('--tls-hostname', action='store', default='localhost',
|
||||
help="Host name to expect in TLS certs. Defaults to 'localhost'")
|
||||
parser.add_argument('--api-version', action='store', default=None,
|
||||
help="Docker daemon API version. Defaults to %s" % (DEFAULT_DOCKER_API_VERSION))
|
||||
parser.add_argument('--timeout', action='store', default=None,
|
||||
help="Docker connection timeout in seconds. Defaults to %s"
|
||||
% (DEFAULT_TIMEOUT_SECONDS))
|
||||
% (DEFAULT_TIMEOUT_SECONDS))
|
||||
parser.add_argument('--cacert-path', action='store', default=None,
|
||||
help="Path to the TLS certificate authority pem file.")
|
||||
parser.add_argument('--cert-path', action='store', default=None,
|
||||
|
||||
@ -10,11 +10,9 @@
|
||||
|
||||
# AWS regions to make calls to. Set this to 'all' to make request to all regions
|
||||
# in AWS and merge the results together. Alternatively, set this to a comma
|
||||
# separated list of regions. E.g. 'us-east-1,us-west-1,us-west-2' and do not
|
||||
# provide the 'regions_exclude' option. If this is set to 'auto', AWS_REGION or
|
||||
# AWS_DEFAULT_REGION environment variable will be read to determine the region.
|
||||
# separated list of regions. E.g. 'us-east-1,us-west-1,us-west-2'
|
||||
regions = all
|
||||
regions_exclude = us-gov-west-1, cn-north-1
|
||||
regions_exclude = us-gov-west-1,cn-north-1
|
||||
|
||||
# When generating inventory, Ansible needs to know how to address a server.
|
||||
# Each EC2 instance has a lot of variables associated with it. Here is the list:
|
||||
@ -58,14 +56,9 @@ vpc_destination_variable = ip_address
|
||||
#destination_format_tags = Name,environment
|
||||
|
||||
# To tag instances on EC2 with the resource records that point to them from
|
||||
# Route53, set 'route53' to True.
|
||||
# Route53, uncomment and set 'route53' to True.
|
||||
route53 = False
|
||||
|
||||
# To use Route53 records as the inventory hostnames, uncomment and set
|
||||
# to equal the domain name you wish to use. You must also have 'route53' (above)
|
||||
# set to True.
|
||||
# route53_hostnames = .example.com
|
||||
|
||||
# To exclude RDS instances from the inventory, uncomment and set to False.
|
||||
#rds = False
|
||||
|
||||
@ -82,7 +75,7 @@ all_instances = False
|
||||
|
||||
# By default, only EC2 instances in the 'running' state are returned. Specify
|
||||
# EC2 instance states to return as a comma-separated list. This
|
||||
# option is overridden when 'all_instances' is True.
|
||||
# option is overriden when 'all_instances' is True.
|
||||
# instance_states = pending, running, shutting-down, terminated, stopping, stopped
|
||||
|
||||
# By default, only RDS instances in the 'available' state are returned. Set
|
||||
@ -131,11 +124,8 @@ expand_csv_tags = False
|
||||
group_by_instance_id = True
|
||||
group_by_region = True
|
||||
group_by_availability_zone = True
|
||||
group_by_aws_account = False
|
||||
group_by_ami_id = True
|
||||
group_by_instance_type = True
|
||||
group_by_instance_state = False
|
||||
group_by_platform = True
|
||||
group_by_key_pair = True
|
||||
group_by_vpc_id = True
|
||||
group_by_security_group = True
|
||||
@ -159,15 +149,7 @@ group_by_elasticache_replication_group = True
|
||||
# inventory. For the full list of possible filters, please read the EC2 API
|
||||
# docs: http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeInstances.html#query-DescribeInstances-filters
|
||||
# Filters are key/value pairs separated by '=', to list multiple filters use
|
||||
# a list separated by commas. To "AND" criteria together, use "&". Note that
|
||||
# the "AND" is not useful along with stack_filters and so such usage is not allowed.
|
||||
# See examples below.
|
||||
|
||||
# If you want to apply multiple filters simultaneously, set stack_filters to
|
||||
# True. Default behaviour is to combine the results of all filters. Stacking
|
||||
# allows the use of multiple conditions to filter down, for example by
|
||||
# environment and type of host.
|
||||
stack_filters = False
|
||||
# a list separated by commas. See examples below.
|
||||
|
||||
# Retrieve only instances with (key=value) env=staging tag
|
||||
# instance_filters = tag:env=staging
|
||||
@ -183,18 +165,6 @@ stack_filters = False
|
||||
# (ex. webservers15, webservers1a, webservers123 etc)
|
||||
# instance_filters = tag:Name=webservers1*
|
||||
|
||||
# Retrieve only instances of type t1.micro that also have tag env=stage
|
||||
# instance_filters = instance-type=t1.micro&tag:env=stage
|
||||
|
||||
# Retrieve instances of type t1.micro AND tag env=stage, as well as any instance
|
||||
# that are of type m3.large, regardless of env tag
|
||||
# instance_filters = instance-type=t1.micro&tag:env=stage,instance-type=m3.large
|
||||
|
||||
# An IAM role can be assumed, so all requests are run as that role.
|
||||
# This can be useful for connecting across different accounts, or to limit user
|
||||
# access
|
||||
# iam_role = role-arn
|
||||
|
||||
# A boto configuration profile may be used to separate out credentials
|
||||
# see http://boto.readthedocs.org/en/latest/boto_config_tut.html
|
||||
# boto_profile = some-boto-profile-name
|
||||
|
||||
@ -12,8 +12,6 @@ variables needed for Boto have already been set:
|
||||
export AWS_ACCESS_KEY_ID='AK123'
|
||||
export AWS_SECRET_ACCESS_KEY='abc123'
|
||||
|
||||
optional region environment variable if region is 'auto'
|
||||
|
||||
This script also assumes there is an ec2.ini file alongside it. To specify a
|
||||
different path to ec2.ini, define the EC2_INI_PATH environment variable:
|
||||
|
||||
@ -132,14 +130,13 @@ from boto import ec2
|
||||
from boto import rds
|
||||
from boto import elasticache
|
||||
from boto import route53
|
||||
from boto import sts
|
||||
import six
|
||||
|
||||
from ansible.module_utils import ec2 as ec2_utils
|
||||
|
||||
HAS_BOTO3 = False
|
||||
try:
|
||||
import boto3 # noqa
|
||||
import boto3
|
||||
HAS_BOTO3 = True
|
||||
except ImportError:
|
||||
pass
|
||||
@ -152,65 +149,11 @@ try:
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
DEFAULTS = {
|
||||
'all_elasticache_clusters': 'False',
|
||||
'all_elasticache_nodes': 'False',
|
||||
'all_elasticache_replication_groups': 'False',
|
||||
'all_instances': 'False',
|
||||
'all_rds_instances': 'False',
|
||||
'aws_access_key_id': None,
|
||||
'aws_secret_access_key': None,
|
||||
'aws_security_token': None,
|
||||
'boto_profile': None,
|
||||
'cache_max_age': '300',
|
||||
'cache_path': '~/.ansible/tmp',
|
||||
'destination_variable': 'public_dns_name',
|
||||
'elasticache': 'True',
|
||||
'eucalyptus': 'False',
|
||||
'eucalyptus_host': None,
|
||||
'expand_csv_tags': 'False',
|
||||
'group_by_ami_id': 'True',
|
||||
'group_by_availability_zone': 'True',
|
||||
'group_by_aws_account': 'False',
|
||||
'group_by_elasticache_cluster': 'True',
|
||||
'group_by_elasticache_engine': 'True',
|
||||
'group_by_elasticache_parameter_group': 'True',
|
||||
'group_by_elasticache_replication_group': 'True',
|
||||
'group_by_instance_id': 'True',
|
||||
'group_by_instance_state': 'False',
|
||||
'group_by_instance_type': 'True',
|
||||
'group_by_key_pair': 'True',
|
||||
'group_by_platform': 'True',
|
||||
'group_by_rds_engine': 'True',
|
||||
'group_by_rds_parameter_group': 'True',
|
||||
'group_by_region': 'True',
|
||||
'group_by_route53_names': 'True',
|
||||
'group_by_security_group': 'True',
|
||||
'group_by_tag_keys': 'True',
|
||||
'group_by_tag_none': 'True',
|
||||
'group_by_vpc_id': 'True',
|
||||
'hostname_variable': None,
|
||||
'iam_role': None,
|
||||
'include_rds_clusters': 'False',
|
||||
'nested_groups': 'False',
|
||||
'pattern_exclude': None,
|
||||
'pattern_include': None,
|
||||
'rds': 'False',
|
||||
'regions': 'all',
|
||||
'regions_exclude': 'us-gov-west-1, cn-north-1',
|
||||
'replace_dash_in_groups': 'True',
|
||||
'route53': 'False',
|
||||
'route53_excluded_zones': '',
|
||||
'route53_hostnames': None,
|
||||
'stack_filters': 'False',
|
||||
'vpc_destination_variable': 'ip_address'
|
||||
}
|
||||
|
||||
|
||||
class Ec2Inventory(object):
|
||||
|
||||
def _empty_inventory(self):
|
||||
return {"_meta": {"hostvars": {}}}
|
||||
return {"_meta" : {"hostvars" : {}}}
|
||||
|
||||
def __init__(self):
|
||||
''' Main execution path '''
|
||||
@ -219,8 +162,6 @@ class Ec2Inventory(object):
|
||||
# and availability zones
|
||||
self.inventory = self._empty_inventory()
|
||||
|
||||
self.aws_account_id = None
|
||||
|
||||
# Index of hostname (address) to instance ID
|
||||
self.index = {}
|
||||
|
||||
@ -259,6 +200,7 @@ class Ec2Inventory(object):
|
||||
|
||||
print(data_to_print)
|
||||
|
||||
|
||||
def is_cache_valid(self):
|
||||
''' Determines if the cache files have expired, or if it is still valid '''
|
||||
|
||||
@ -271,72 +213,47 @@ class Ec2Inventory(object):
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def read_settings(self):
|
||||
''' Reads the settings from the ec2.ini file '''
|
||||
|
||||
scriptbasename = __file__
|
||||
scriptbasename = os.path.basename(scriptbasename)
|
||||
scriptbasename = scriptbasename.replace('.py', '')
|
||||
|
||||
defaults = {
|
||||
'ec2': {
|
||||
'ini_fallback': os.path.join(os.path.dirname(__file__), 'ec2.ini'),
|
||||
'ini_path': os.path.join(os.path.dirname(__file__), '%s.ini' % scriptbasename)
|
||||
}
|
||||
}
|
||||
|
||||
if six.PY3:
|
||||
config = configparser.ConfigParser(DEFAULTS)
|
||||
config = configparser.ConfigParser()
|
||||
else:
|
||||
config = configparser.SafeConfigParser(DEFAULTS)
|
||||
ec2_ini_path = os.environ.get('EC2_INI_PATH', defaults['ec2']['ini_path'])
|
||||
ec2_ini_path = os.path.expanduser(os.path.expandvars(ec2_ini_path))
|
||||
|
||||
if not os.path.isfile(ec2_ini_path):
|
||||
ec2_ini_path = os.path.expanduser(defaults['ec2']['ini_fallback'])
|
||||
|
||||
if os.path.isfile(ec2_ini_path):
|
||||
config.read(ec2_ini_path)
|
||||
|
||||
# Add empty sections if they don't exist
|
||||
try:
|
||||
config.add_section('ec2')
|
||||
except configparser.DuplicateSectionError:
|
||||
pass
|
||||
|
||||
try:
|
||||
config.add_section('credentials')
|
||||
except configparser.DuplicateSectionError:
|
||||
pass
|
||||
config = configparser.SafeConfigParser()
|
||||
ec2_default_ini_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'ec2.ini')
|
||||
ec2_ini_path = os.path.expanduser(os.path.expandvars(os.environ.get('EC2_INI_PATH', ec2_default_ini_path)))
|
||||
config.read(ec2_ini_path)
|
||||
|
||||
# is eucalyptus?
|
||||
self.eucalyptus = config.getboolean('ec2', 'eucalyptus')
|
||||
self.eucalyptus_host = config.get('ec2', 'eucalyptus_host')
|
||||
self.eucalyptus_host = None
|
||||
self.eucalyptus = False
|
||||
if config.has_option('ec2', 'eucalyptus'):
|
||||
self.eucalyptus = config.getboolean('ec2', 'eucalyptus')
|
||||
if self.eucalyptus and config.has_option('ec2', 'eucalyptus_host'):
|
||||
self.eucalyptus_host = config.get('ec2', 'eucalyptus_host')
|
||||
|
||||
# Regions
|
||||
self.regions = []
|
||||
configRegions = config.get('ec2', 'regions')
|
||||
configRegions_exclude = config.get('ec2', 'regions_exclude')
|
||||
if (configRegions == 'all'):
|
||||
if self.eucalyptus_host:
|
||||
self.regions.append(boto.connect_euca(host=self.eucalyptus_host).region.name, **self.credentials)
|
||||
else:
|
||||
configRegions_exclude = config.get('ec2', 'regions_exclude')
|
||||
|
||||
for regionInfo in ec2.regions():
|
||||
if regionInfo.name not in configRegions_exclude:
|
||||
self.regions.append(regionInfo.name)
|
||||
else:
|
||||
self.regions = configRegions.split(",")
|
||||
if 'auto' in self.regions:
|
||||
env_region = os.environ.get('AWS_REGION')
|
||||
if env_region is None:
|
||||
env_region = os.environ.get('AWS_DEFAULT_REGION')
|
||||
self.regions = [env_region]
|
||||
|
||||
# Destination addresses
|
||||
self.destination_variable = config.get('ec2', 'destination_variable')
|
||||
self.vpc_destination_variable = config.get('ec2', 'vpc_destination_variable')
|
||||
self.hostname_variable = config.get('ec2', 'hostname_variable')
|
||||
|
||||
if config.has_option('ec2', 'hostname_variable'):
|
||||
self.hostname_variable = config.get('ec2', 'hostname_variable')
|
||||
else:
|
||||
self.hostname_variable = None
|
||||
|
||||
if config.has_option('ec2', 'destination_format') and \
|
||||
config.has_option('ec2', 'destination_format_tags'):
|
||||
@ -348,22 +265,32 @@ class Ec2Inventory(object):
|
||||
|
||||
# Route53
|
||||
self.route53_enabled = config.getboolean('ec2', 'route53')
|
||||
self.route53_hostnames = config.get('ec2', 'route53_hostnames')
|
||||
|
||||
self.route53_excluded_zones = []
|
||||
self.route53_excluded_zones = [a for a in config.get('ec2', 'route53_excluded_zones').split(',') if a]
|
||||
if config.has_option('ec2', 'route53_excluded_zones'):
|
||||
self.route53_excluded_zones.extend(
|
||||
config.get('ec2', 'route53_excluded_zones', '').split(','))
|
||||
|
||||
# Include RDS instances?
|
||||
self.rds_enabled = config.get('ec2', 'rds')
|
||||
self.rds_enabled = True
|
||||
if config.has_option('ec2', 'rds'):
|
||||
self.rds_enabled = config.getboolean('ec2', 'rds')
|
||||
|
||||
# Include RDS cluster instances?
|
||||
self.include_rds_clusters = config.getboolean('ec2', 'include_rds_clusters')
|
||||
if config.has_option('ec2', 'include_rds_clusters'):
|
||||
self.include_rds_clusters = config.getboolean('ec2', 'include_rds_clusters')
|
||||
else:
|
||||
self.include_rds_clusters = False
|
||||
|
||||
# Include ElastiCache instances?
|
||||
self.elasticache_enabled = config.getboolean('ec2', 'elasticache')
|
||||
self.elasticache_enabled = True
|
||||
if config.has_option('ec2', 'elasticache'):
|
||||
self.elasticache_enabled = config.getboolean('ec2', 'elasticache')
|
||||
|
||||
# Return all EC2 instances?
|
||||
self.all_instances = config.getboolean('ec2', 'all_instances')
|
||||
if config.has_option('ec2', 'all_instances'):
|
||||
self.all_instances = config.getboolean('ec2', 'all_instances')
|
||||
else:
|
||||
self.all_instances = False
|
||||
|
||||
# Instance states to be gathered in inventory. Default is 'running'.
|
||||
# Setting 'all_instances' to 'yes' overrides this option.
|
||||
@ -379,39 +306,58 @@ class Ec2Inventory(object):
|
||||
if self.all_instances:
|
||||
self.ec2_instance_states = ec2_valid_instance_states
|
||||
elif config.has_option('ec2', 'instance_states'):
|
||||
for instance_state in config.get('ec2', 'instance_states').split(','):
|
||||
instance_state = instance_state.strip()
|
||||
if instance_state not in ec2_valid_instance_states:
|
||||
continue
|
||||
self.ec2_instance_states.append(instance_state)
|
||||
for instance_state in config.get('ec2', 'instance_states').split(','):
|
||||
instance_state = instance_state.strip()
|
||||
if instance_state not in ec2_valid_instance_states:
|
||||
continue
|
||||
self.ec2_instance_states.append(instance_state)
|
||||
else:
|
||||
self.ec2_instance_states = ['running']
|
||||
self.ec2_instance_states = ['running']
|
||||
|
||||
# Return all RDS instances? (if RDS is enabled)
|
||||
self.all_rds_instances = config.getboolean('ec2', 'all_rds_instances')
|
||||
if config.has_option('ec2', 'all_rds_instances') and self.rds_enabled:
|
||||
self.all_rds_instances = config.getboolean('ec2', 'all_rds_instances')
|
||||
else:
|
||||
self.all_rds_instances = False
|
||||
|
||||
# Return all ElastiCache replication groups? (if ElastiCache is enabled)
|
||||
self.all_elasticache_replication_groups = config.getboolean('ec2', 'all_elasticache_replication_groups')
|
||||
if config.has_option('ec2', 'all_elasticache_replication_groups') and self.elasticache_enabled:
|
||||
self.all_elasticache_replication_groups = config.getboolean('ec2', 'all_elasticache_replication_groups')
|
||||
else:
|
||||
self.all_elasticache_replication_groups = False
|
||||
|
||||
# Return all ElastiCache clusters? (if ElastiCache is enabled)
|
||||
self.all_elasticache_clusters = config.getboolean('ec2', 'all_elasticache_clusters')
|
||||
if config.has_option('ec2', 'all_elasticache_clusters') and self.elasticache_enabled:
|
||||
self.all_elasticache_clusters = config.getboolean('ec2', 'all_elasticache_clusters')
|
||||
else:
|
||||
self.all_elasticache_clusters = False
|
||||
|
||||
# Return all ElastiCache nodes? (if ElastiCache is enabled)
|
||||
self.all_elasticache_nodes = config.getboolean('ec2', 'all_elasticache_nodes')
|
||||
if config.has_option('ec2', 'all_elasticache_nodes') and self.elasticache_enabled:
|
||||
self.all_elasticache_nodes = config.getboolean('ec2', 'all_elasticache_nodes')
|
||||
else:
|
||||
self.all_elasticache_nodes = False
|
||||
|
||||
# boto configuration profile (prefer CLI argument then environment variables then config file)
|
||||
self.boto_profile = self.args.boto_profile or \
|
||||
os.environ.get('AWS_PROFILE') or \
|
||||
config.get('ec2', 'boto_profile')
|
||||
# boto configuration profile (prefer CLI argument)
|
||||
self.boto_profile = self.args.boto_profile
|
||||
if config.has_option('ec2', 'boto_profile') and not self.boto_profile:
|
||||
self.boto_profile = config.get('ec2', 'boto_profile')
|
||||
|
||||
# AWS credentials (prefer environment variables)
|
||||
if not (self.boto_profile or os.environ.get('AWS_ACCESS_KEY_ID') or
|
||||
os.environ.get('AWS_PROFILE')):
|
||||
|
||||
aws_access_key_id = config.get('credentials', 'aws_access_key_id')
|
||||
aws_secret_access_key = config.get('credentials', 'aws_secret_access_key')
|
||||
aws_security_token = config.get('credentials', 'aws_security_token')
|
||||
|
||||
if config.has_option('credentials', 'aws_access_key_id'):
|
||||
aws_access_key_id = config.get('credentials', 'aws_access_key_id')
|
||||
else:
|
||||
aws_access_key_id = None
|
||||
if config.has_option('credentials', 'aws_secret_access_key'):
|
||||
aws_secret_access_key = config.get('credentials', 'aws_secret_access_key')
|
||||
else:
|
||||
aws_secret_access_key = None
|
||||
if config.has_option('credentials', 'aws_security_token'):
|
||||
aws_security_token = config.get('credentials', 'aws_security_token')
|
||||
else:
|
||||
aws_security_token = None
|
||||
if aws_access_key_id:
|
||||
self.credentials = {
|
||||
'aws_access_key_id': aws_access_key_id,
|
||||
@ -428,81 +374,109 @@ class Ec2Inventory(object):
|
||||
os.makedirs(cache_dir)
|
||||
|
||||
cache_name = 'ansible-ec2'
|
||||
cache_id = self.boto_profile or os.environ.get('AWS_ACCESS_KEY_ID', self.credentials.get('aws_access_key_id'))
|
||||
if cache_id:
|
||||
cache_name = '%s-%s' % (cache_name, cache_id)
|
||||
self.cache_path_cache = os.path.join(cache_dir, "%s.cache" % cache_name)
|
||||
self.cache_path_index = os.path.join(cache_dir, "%s.index" % cache_name)
|
||||
aws_profile = lambda: (self.boto_profile or
|
||||
os.environ.get('AWS_PROFILE') or
|
||||
os.environ.get('AWS_ACCESS_KEY_ID') or
|
||||
self.credentials.get('aws_access_key_id', None))
|
||||
if aws_profile():
|
||||
cache_name = '%s-%s' % (cache_name, aws_profile())
|
||||
self.cache_path_cache = cache_dir + "/%s.cache" % cache_name
|
||||
self.cache_path_index = cache_dir + "/%s.index" % cache_name
|
||||
self.cache_max_age = config.getint('ec2', 'cache_max_age')
|
||||
|
||||
self.expand_csv_tags = config.getboolean('ec2', 'expand_csv_tags')
|
||||
if config.has_option('ec2', 'expand_csv_tags'):
|
||||
self.expand_csv_tags = config.getboolean('ec2', 'expand_csv_tags')
|
||||
else:
|
||||
self.expand_csv_tags = False
|
||||
|
||||
# Configure nested groups instead of flat namespace.
|
||||
self.nested_groups = config.getboolean('ec2', 'nested_groups')
|
||||
if config.has_option('ec2', 'nested_groups'):
|
||||
self.nested_groups = config.getboolean('ec2', 'nested_groups')
|
||||
else:
|
||||
self.nested_groups = False
|
||||
|
||||
# Replace dash or not in group names
|
||||
self.replace_dash_in_groups = config.getboolean('ec2', 'replace_dash_in_groups')
|
||||
|
||||
# IAM role to assume for connection
|
||||
self.iam_role = config.get('ec2', 'iam_role')
|
||||
if config.has_option('ec2', 'replace_dash_in_groups'):
|
||||
self.replace_dash_in_groups = config.getboolean('ec2', 'replace_dash_in_groups')
|
||||
else:
|
||||
self.replace_dash_in_groups = True
|
||||
|
||||
# Configure which groups should be created.
|
||||
|
||||
group_by_options = [a for a in DEFAULTS if a.startswith('group_by')]
|
||||
group_by_options = [
|
||||
'group_by_instance_id',
|
||||
'group_by_region',
|
||||
'group_by_availability_zone',
|
||||
'group_by_ami_id',
|
||||
'group_by_instance_type',
|
||||
'group_by_key_pair',
|
||||
'group_by_vpc_id',
|
||||
'group_by_security_group',
|
||||
'group_by_tag_keys',
|
||||
'group_by_tag_none',
|
||||
'group_by_route53_names',
|
||||
'group_by_rds_engine',
|
||||
'group_by_rds_parameter_group',
|
||||
'group_by_elasticache_engine',
|
||||
'group_by_elasticache_cluster',
|
||||
'group_by_elasticache_parameter_group',
|
||||
'group_by_elasticache_replication_group',
|
||||
]
|
||||
for option in group_by_options:
|
||||
setattr(self, option, config.getboolean('ec2', option))
|
||||
if config.has_option('ec2', option):
|
||||
setattr(self, option, config.getboolean('ec2', option))
|
||||
else:
|
||||
setattr(self, option, True)
|
||||
|
||||
# Do we need to just include hosts that match a pattern?
|
||||
self.pattern_include = config.get('ec2', 'pattern_include')
|
||||
if self.pattern_include:
|
||||
self.pattern_include = re.compile(self.pattern_include)
|
||||
try:
|
||||
pattern_include = config.get('ec2', 'pattern_include')
|
||||
if pattern_include and len(pattern_include) > 0:
|
||||
self.pattern_include = re.compile(pattern_include)
|
||||
else:
|
||||
self.pattern_include = None
|
||||
except configparser.NoOptionError:
|
||||
self.pattern_include = None
|
||||
|
||||
# Do we need to exclude hosts that match a pattern?
|
||||
self.pattern_exclude = config.get('ec2', 'pattern_exclude')
|
||||
if self.pattern_exclude:
|
||||
self.pattern_exclude = re.compile(self.pattern_exclude)
|
||||
|
||||
# Do we want to stack multiple filters?
|
||||
self.stack_filters = config.getboolean('ec2', 'stack_filters')
|
||||
try:
|
||||
pattern_exclude = config.get('ec2', 'pattern_exclude');
|
||||
if pattern_exclude and len(pattern_exclude) > 0:
|
||||
self.pattern_exclude = re.compile(pattern_exclude)
|
||||
else:
|
||||
self.pattern_exclude = None
|
||||
except configparser.NoOptionError:
|
||||
self.pattern_exclude = None
|
||||
|
||||
# Instance filters (see boto and EC2 API docs). Ignore invalid filters.
|
||||
self.ec2_instance_filters = []
|
||||
|
||||
self.ec2_instance_filters = defaultdict(list)
|
||||
if config.has_option('ec2', 'instance_filters'):
|
||||
filters = config.get('ec2', 'instance_filters')
|
||||
|
||||
if self.stack_filters and '&' in filters:
|
||||
self.fail_with_error("AND filters along with stack_filter enabled is not supported.\n")
|
||||
filters = [f for f in config.get('ec2', 'instance_filters').split(',') if f]
|
||||
|
||||
filter_sets = [f for f in filters.split(',') if f]
|
||||
|
||||
for filter_set in filter_sets:
|
||||
filters = {}
|
||||
filter_set = filter_set.strip()
|
||||
for instance_filter in filter_set.split("&"):
|
||||
instance_filter = instance_filter.strip()
|
||||
if not instance_filter or '=' not in instance_filter:
|
||||
continue
|
||||
filter_key, filter_value = [x.strip() for x in instance_filter.split('=', 1)]
|
||||
if not filter_key:
|
||||
continue
|
||||
filters[filter_key] = filter_value
|
||||
self.ec2_instance_filters.append(filters.copy())
|
||||
for instance_filter in filters:
|
||||
instance_filter = instance_filter.strip()
|
||||
if not instance_filter or '=' not in instance_filter:
|
||||
continue
|
||||
filter_key, filter_value = [x.strip() for x in instance_filter.split('=', 1)]
|
||||
if not filter_key:
|
||||
continue
|
||||
self.ec2_instance_filters[filter_key].append(filter_value)
|
||||
|
||||
def parse_cli_args(self):
|
||||
''' Command line argument processing '''
|
||||
|
||||
parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on EC2')
|
||||
parser.add_argument('--list', action='store_true', default=True,
|
||||
help='List instances (default: True)')
|
||||
help='List instances (default: True)')
|
||||
parser.add_argument('--host', action='store',
|
||||
help='Get all the variables about a specific instance')
|
||||
help='Get all the variables about a specific instance')
|
||||
parser.add_argument('--refresh-cache', action='store_true', default=False,
|
||||
help='Force refresh of cache by making API requests to EC2 (default: False - use cache files)')
|
||||
help='Force refresh of cache by making API requests to EC2 (default: False - use cache files)')
|
||||
parser.add_argument('--profile', '--boto-profile', action='store', dest='boto_profile',
|
||||
help='Use boto profile for connections to EC2')
|
||||
help='Use boto profile for connections to EC2')
|
||||
self.args = parser.parse_args()
|
||||
|
||||
|
||||
def do_api_calls_update_cache(self):
|
||||
''' Do API calls to each region, and save data in cache files '''
|
||||
|
||||
@ -546,13 +520,6 @@ class Ec2Inventory(object):
|
||||
connect_args['profile_name'] = self.boto_profile
|
||||
self.boto_fix_security_token_in_profile(connect_args)
|
||||
|
||||
if self.iam_role:
|
||||
sts_conn = sts.connect_to_region(region, **connect_args)
|
||||
role = sts_conn.assume_role(self.iam_role, 'ansible_dynamic_inventory')
|
||||
connect_args['aws_access_key_id'] = role.credentials.access_key
|
||||
connect_args['aws_secret_access_key'] = role.credentials.secret_key
|
||||
connect_args['security_token'] = role.credentials.session_token
|
||||
|
||||
conn = module.connect_to_region(region, **connect_args)
|
||||
# connect_to_region will fail "silently" by returning None if the region name is wrong or not supported
|
||||
if conn is None:
|
||||
@ -567,14 +534,8 @@ class Ec2Inventory(object):
|
||||
conn = self.connect(region)
|
||||
reservations = []
|
||||
if self.ec2_instance_filters:
|
||||
if self.stack_filters:
|
||||
filters_dict = {}
|
||||
for filters in self.ec2_instance_filters:
|
||||
filters_dict.update(filters)
|
||||
reservations.extend(conn.get_all_instances(filters=filters_dict))
|
||||
else:
|
||||
for filters in self.ec2_instance_filters:
|
||||
reservations.extend(conn.get_all_instances(filters=filters))
|
||||
for filter_key, filter_values in self.ec2_instance_filters.items():
|
||||
reservations.extend(conn.get_all_instances(filters = { filter_key : filter_values }))
|
||||
else:
|
||||
reservations = conn.get_all_instances()
|
||||
|
||||
@ -588,15 +549,12 @@ class Ec2Inventory(object):
|
||||
max_filter_value = 199
|
||||
tags = []
|
||||
for i in range(0, len(instance_ids), max_filter_value):
|
||||
tags.extend(conn.get_all_tags(filters={'resource-type': 'instance', 'resource-id': instance_ids[i:i + max_filter_value]}))
|
||||
tags.extend(conn.get_all_tags(filters={'resource-type': 'instance', 'resource-id': instance_ids[i:i+max_filter_value]}))
|
||||
|
||||
tags_by_instance_id = defaultdict(dict)
|
||||
for tag in tags:
|
||||
tags_by_instance_id[tag.res_id][tag.name] = tag.value
|
||||
|
||||
if (not self.aws_account_id) and reservations:
|
||||
self.aws_account_id = reservations[0].owner_id
|
||||
|
||||
for reservation in reservations:
|
||||
for instance in reservation.instances:
|
||||
instance.tags = tags_by_instance_id[instance.id]
|
||||
@ -610,44 +568,10 @@ class Ec2Inventory(object):
|
||||
error = "Error connecting to %s backend.\n%s" % (backend, e.message)
|
||||
self.fail_with_error(error, 'getting EC2 instances')
|
||||
|
||||
def tags_match_filters(self, tags):
|
||||
''' return True if given tags match configured filters '''
|
||||
if not self.ec2_instance_filters:
|
||||
return True
|
||||
|
||||
for filters in self.ec2_instance_filters:
|
||||
for filter_name, filter_value in filters.items():
|
||||
if filter_name[:4] != 'tag:':
|
||||
continue
|
||||
filter_name = filter_name[4:]
|
||||
if filter_name not in tags:
|
||||
if self.stack_filters:
|
||||
return False
|
||||
continue
|
||||
if isinstance(filter_value, list):
|
||||
if self.stack_filters and tags[filter_name] not in filter_value:
|
||||
return False
|
||||
if not self.stack_filters and tags[filter_name] in filter_value:
|
||||
return True
|
||||
if isinstance(filter_value, six.string_types):
|
||||
if self.stack_filters and tags[filter_name] != filter_value:
|
||||
return False
|
||||
if not self.stack_filters and tags[filter_name] == filter_value:
|
||||
return True
|
||||
|
||||
return self.stack_filters
|
||||
|
||||
def get_rds_instances_by_region(self, region):
|
||||
''' Makes an AWS API call to the list of RDS instances in a particular
|
||||
region '''
|
||||
|
||||
if not HAS_BOTO3:
|
||||
self.fail_with_error("Working with RDS instances requires boto3 - please install boto3 and try again",
|
||||
"getting RDS instances")
|
||||
|
||||
client = ec2_utils.boto3_inventory_conn('client', 'rds', region, **self.credentials)
|
||||
db_instances = client.describe_db_instances()
|
||||
|
||||
try:
|
||||
conn = self.connect_to_aws(rds, region)
|
||||
if conn:
|
||||
@ -655,15 +579,8 @@ class Ec2Inventory(object):
|
||||
while True:
|
||||
instances = conn.get_all_dbinstances(marker=marker)
|
||||
marker = instances.marker
|
||||
for index, instance in enumerate(instances):
|
||||
# Add tags to instances.
|
||||
instance.arn = db_instances['DBInstances'][index]['DBInstanceArn']
|
||||
tags = client.list_tags_for_resource(ResourceName=instance.arn)['TagList']
|
||||
instance.tags = {}
|
||||
for tag in tags:
|
||||
instance.tags[tag['Key']] = tag['Value']
|
||||
if self.tags_match_filters(instance.tags):
|
||||
self.add_rds_instance(instance, region)
|
||||
for instance in instances:
|
||||
self.add_rds_instance(instance, region)
|
||||
if not marker:
|
||||
break
|
||||
except boto.exception.BotoServerError as e:
|
||||
@ -671,11 +588,7 @@ class Ec2Inventory(object):
|
||||
|
||||
if e.error_code == 'AuthFailure':
|
||||
error = self.get_auth_error_message()
|
||||
elif e.error_code == "OptInRequired":
|
||||
error = "RDS hasn't been enabled for this account yet. " \
|
||||
"You must either log in to the RDS service through the AWS console to enable it, " \
|
||||
"or set 'rds = False' in ec2.ini"
|
||||
elif not e.reason == "Forbidden":
|
||||
if not e.reason == "Forbidden":
|
||||
error = "Looks like AWS RDS is down:\n%s" % e.message
|
||||
self.fail_with_error(error, 'getting RDS instances')
|
||||
|
||||
@ -702,7 +615,7 @@ class Ec2Inventory(object):
|
||||
if 'LatestRestorableTime' in c:
|
||||
del c['LatestRestorableTime']
|
||||
|
||||
if not self.ec2_instance_filters:
|
||||
if self.ec2_instance_filters == {}:
|
||||
matches_filter = True
|
||||
else:
|
||||
matches_filter = False
|
||||
@ -714,18 +627,14 @@ class Ec2Inventory(object):
|
||||
c['Tags'] = tags['TagList']
|
||||
|
||||
if self.ec2_instance_filters:
|
||||
for filters in self.ec2_instance_filters:
|
||||
for filter_key, filter_values in filters.items():
|
||||
# get AWS tag key e.g. tag:env will be 'env'
|
||||
tag_name = filter_key.split(":", 1)[1]
|
||||
# Filter values is a list (if you put multiple values for the same tag name)
|
||||
matches_filter = any(d['Key'] == tag_name and d['Value'] in filter_values for d in c['Tags'])
|
||||
|
||||
if matches_filter:
|
||||
# it matches a filter, so stop looking for further matches
|
||||
break
|
||||
for filter_key, filter_values in self.ec2_instance_filters.items():
|
||||
# get AWS tag key e.g. tag:env will be 'env'
|
||||
tag_name = filter_key.split(":", 1)[1]
|
||||
# Filter values is a list (if you put multiple values for the same tag name)
|
||||
matches_filter = any(d['Key'] == tag_name and d['Value'] in filter_values for d in c['Tags'])
|
||||
|
||||
if matches_filter:
|
||||
# it matches a filter, so stop looking for further matches
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
@ -746,7 +655,7 @@ class Ec2Inventory(object):
|
||||
''' Makes an AWS API call to the list of ElastiCache clusters (with
|
||||
nodes' info) in a particular region.'''
|
||||
|
||||
# ElastiCache boto module doesn't provide a get_all_instances method,
|
||||
# ElastiCache boto module doesn't provide a get_all_intances method,
|
||||
# that's why we need to call describe directly (it would be called by
|
||||
# the shorthand method anyway...)
|
||||
try:
|
||||
@ -761,17 +670,13 @@ class Ec2Inventory(object):
|
||||
|
||||
if e.error_code == 'AuthFailure':
|
||||
error = self.get_auth_error_message()
|
||||
elif e.error_code == "OptInRequired":
|
||||
error = "ElastiCache hasn't been enabled for this account yet. " \
|
||||
"You must either log in to the ElastiCache service through the AWS console to enable it, " \
|
||||
"or set 'elasticache = False' in ec2.ini"
|
||||
elif not e.reason == "Forbidden":
|
||||
if not e.reason == "Forbidden":
|
||||
error = "Looks like AWS ElastiCache is down:\n%s" % e.message
|
||||
self.fail_with_error(error, 'getting ElastiCache clusters')
|
||||
|
||||
try:
|
||||
# Boto also doesn't provide wrapper classes to CacheClusters or
|
||||
# CacheNodes. Because of that we can't make use of the get_list
|
||||
# CacheNodes. Because of that wo can't make use of the get_list
|
||||
# method in the AWSQueryConnection. Let's do the work manually
|
||||
clusters = response['DescribeCacheClustersResponse']['DescribeCacheClustersResult']['CacheClusters']
|
||||
|
||||
@ -786,7 +691,7 @@ class Ec2Inventory(object):
|
||||
''' Makes an AWS API call to the list of ElastiCache replication groups
|
||||
in a particular region.'''
|
||||
|
||||
# ElastiCache boto module doesn't provide a get_all_instances method,
|
||||
# ElastiCache boto module doesn't provide a get_all_intances method,
|
||||
# that's why we need to call describe directly (it would be called by
|
||||
# the shorthand method anyway...)
|
||||
try:
|
||||
@ -805,7 +710,7 @@ class Ec2Inventory(object):
|
||||
|
||||
try:
|
||||
# Boto also doesn't provide wrapper classes to ReplicationGroups
|
||||
# Because of that we can't make use of the get_list method in the
|
||||
# Because of that wo can't make use of the get_list method in the
|
||||
# AWSQueryConnection. Let's do the work manually
|
||||
replication_groups = response['DescribeReplicationGroupsResponse']['DescribeReplicationGroupsResult']['ReplicationGroups']
|
||||
|
||||
@ -825,7 +730,7 @@ class Ec2Inventory(object):
|
||||
errors.append(' - AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment vars found but may not be correct')
|
||||
|
||||
boto_paths = ['/etc/boto.cfg', '~/.boto', '~/.aws/credentials']
|
||||
boto_config_found = [p for p in boto_paths if os.path.isfile(os.path.expanduser(p))]
|
||||
boto_config_found = list(p for p in boto_paths if os.path.isfile(os.path.expanduser(p)))
|
||||
if len(boto_config_found) > 0:
|
||||
errors.append(" - Boto configs found at '%s', but the credentials contained may not be correct" % ', '.join(boto_config_found))
|
||||
else:
|
||||
@ -859,7 +764,7 @@ class Ec2Inventory(object):
|
||||
|
||||
# Select the best destination address
|
||||
if self.destination_format and self.destination_format_tags:
|
||||
dest = self.destination_format.format(*[getattr(instance, 'tags').get(tag, '') for tag in self.destination_format_tags])
|
||||
dest = self.destination_format.format(*[ getattr(instance, 'tags').get(tag, '') for tag in self.destination_format_tags ])
|
||||
elif instance.subnet_id:
|
||||
dest = getattr(instance, self.vpc_destination_variable, None)
|
||||
if dest is None:
|
||||
@ -881,19 +786,9 @@ class Ec2Inventory(object):
|
||||
else:
|
||||
hostname = getattr(instance, self.hostname_variable)
|
||||
|
||||
# set the hostname from route53
|
||||
if self.route53_enabled and self.route53_hostnames:
|
||||
route53_names = self.get_instance_route53_names(instance)
|
||||
for name in route53_names:
|
||||
if name.endswith(self.route53_hostnames):
|
||||
hostname = name
|
||||
|
||||
# If we can't get a nice hostname, use the destination address
|
||||
if not hostname:
|
||||
hostname = dest
|
||||
# to_safe strips hostname characters like dots, so don't strip route53 hostnames
|
||||
elif self.route53_enabled and self.route53_hostnames and hostname.endswith(self.route53_hostnames):
|
||||
hostname = hostname.lower()
|
||||
else:
|
||||
hostname = self.to_safe(hostname).lower()
|
||||
|
||||
@ -942,23 +837,6 @@ class Ec2Inventory(object):
|
||||
if self.nested_groups:
|
||||
self.push_group(self.inventory, 'types', type_name)
|
||||
|
||||
# Inventory: Group by instance state
|
||||
if self.group_by_instance_state:
|
||||
state_name = self.to_safe('instance_state_' + instance.state)
|
||||
self.push(self.inventory, state_name, hostname)
|
||||
if self.nested_groups:
|
||||
self.push_group(self.inventory, 'instance_states', state_name)
|
||||
|
||||
# Inventory: Group by platform
|
||||
if self.group_by_platform:
|
||||
if instance.platform:
|
||||
platform = self.to_safe('platform_' + instance.platform)
|
||||
else:
|
||||
platform = self.to_safe('platform_undefined')
|
||||
self.push(self.inventory, platform, hostname)
|
||||
if self.nested_groups:
|
||||
self.push_group(self.inventory, 'platforms', platform)
|
||||
|
||||
# Inventory: Group by key pair
|
||||
if self.group_by_key_pair and instance.key_name:
|
||||
key_name = self.to_safe('key_' + instance.key_name)
|
||||
@ -983,13 +861,7 @@ class Ec2Inventory(object):
|
||||
self.push_group(self.inventory, 'security_groups', key)
|
||||
except AttributeError:
|
||||
self.fail_with_error('\n'.join(['Package boto seems a bit older.',
|
||||
'Please upgrade boto >= 2.3.0.']))
|
||||
|
||||
# Inventory: Group by AWS account ID
|
||||
if self.group_by_aws_account:
|
||||
self.push(self.inventory, self.aws_account_id, dest)
|
||||
if self.nested_groups:
|
||||
self.push_group(self.inventory, 'accounts', self.aws_account_id)
|
||||
'Please upgrade boto >= 2.3.0.']))
|
||||
|
||||
# Inventory: Group by tag keys
|
||||
if self.group_by_tag_keys:
|
||||
@ -1028,7 +900,8 @@ class Ec2Inventory(object):
|
||||
self.push(self.inventory, 'ec2', hostname)
|
||||
|
||||
self.inventory["_meta"]["hostvars"][hostname] = self.get_host_info_dict_from_instance(instance)
|
||||
self.inventory["_meta"]["hostvars"][hostname]['ansible_host'] = dest
|
||||
self.inventory["_meta"]["hostvars"][hostname]['ansible_ssh_host'] = dest
|
||||
|
||||
|
||||
def add_rds_instance(self, instance, region):
|
||||
''' Adds an RDS instance to the inventory and index, as long as it is
|
||||
@ -1107,25 +980,8 @@ class Ec2Inventory(object):
|
||||
|
||||
except AttributeError:
|
||||
self.fail_with_error('\n'.join(['Package boto seems a bit older.',
|
||||
'Please upgrade boto >= 2.3.0.']))
|
||||
# Inventory: Group by tag keys
|
||||
if self.group_by_tag_keys:
|
||||
for k, v in instance.tags.items():
|
||||
if self.expand_csv_tags and v and ',' in v:
|
||||
values = map(lambda x: x.strip(), v.split(','))
|
||||
else:
|
||||
values = [v]
|
||||
'Please upgrade boto >= 2.3.0.']))
|
||||
|
||||
for v in values:
|
||||
if v:
|
||||
key = self.to_safe("tag_" + k + "=" + v)
|
||||
else:
|
||||
key = self.to_safe("tag_" + k)
|
||||
self.push(self.inventory, key, hostname)
|
||||
if self.nested_groups:
|
||||
self.push_group(self.inventory, 'tags', self.to_safe("tag_" + k))
|
||||
if v:
|
||||
self.push_group(self.inventory, self.to_safe("tag_" + k), key)
|
||||
|
||||
# Inventory: Group by engine
|
||||
if self.group_by_rds_engine:
|
||||
@ -1139,17 +995,11 @@ class Ec2Inventory(object):
|
||||
if self.nested_groups:
|
||||
self.push_group(self.inventory, 'rds_parameter_groups', self.to_safe("rds_parameter_group_" + instance.parameter_group.name))
|
||||
|
||||
# Global Tag: instances without tags
|
||||
if self.group_by_tag_none and len(instance.tags) == 0:
|
||||
self.push(self.inventory, 'tag_none', hostname)
|
||||
if self.nested_groups:
|
||||
self.push_group(self.inventory, 'tags', 'tag_none')
|
||||
|
||||
# Global Tag: all RDS instances
|
||||
self.push(self.inventory, 'rds', hostname)
|
||||
|
||||
self.inventory["_meta"]["hostvars"][hostname] = self.get_host_info_dict_from_instance(instance)
|
||||
self.inventory["_meta"]["hostvars"][hostname]['ansible_host'] = dest
|
||||
self.inventory["_meta"]["hostvars"][hostname]['ansible_ssh_host'] = dest
|
||||
|
||||
def add_elasticache_cluster(self, cluster, region):
|
||||
''' Adds an ElastiCache cluster to the inventory and index, as long as
|
||||
@ -1344,14 +1194,13 @@ class Ec2Inventory(object):
|
||||
if not self.all_elasticache_replication_groups and replication_group['Status'] != 'available':
|
||||
return
|
||||
|
||||
# Skip clusters we cannot address (e.g. private VPC subnet or clustered redis)
|
||||
if replication_group['NodeGroups'][0]['PrimaryEndpoint'] is None or \
|
||||
replication_group['NodeGroups'][0]['PrimaryEndpoint']['Address'] is None:
|
||||
return
|
||||
|
||||
# Select the best destination address (PrimaryEndpoint)
|
||||
dest = replication_group['NodeGroups'][0]['PrimaryEndpoint']['Address']
|
||||
|
||||
if not dest:
|
||||
# Skip clusters we cannot address (e.g. private VPC subnet)
|
||||
return
|
||||
|
||||
# Add to index
|
||||
self.index[dest] = [region, replication_group['ReplicationGroupId']]
|
||||
|
||||
@ -1394,13 +1243,11 @@ class Ec2Inventory(object):
|
||||
''' Get and store the map of resource records to domain names that
|
||||
point to them. '''
|
||||
|
||||
if self.boto_profile:
|
||||
r53_conn = route53.Route53Connection(profile_name=self.boto_profile)
|
||||
else:
|
||||
r53_conn = route53.Route53Connection()
|
||||
r53_conn = route53.Route53Connection()
|
||||
all_zones = r53_conn.get_zones()
|
||||
|
||||
route53_zones = [zone for zone in all_zones if zone.name[:-1] not in self.route53_excluded_zones]
|
||||
route53_zones = [ zone for zone in all_zones if zone.name[:-1]
|
||||
not in self.route53_excluded_zones ]
|
||||
|
||||
self.route53_records = {}
|
||||
|
||||
@ -1417,13 +1264,14 @@ class Ec2Inventory(object):
|
||||
self.route53_records.setdefault(resource, set())
|
||||
self.route53_records[resource].add(record_name)
|
||||
|
||||
|
||||
def get_instance_route53_names(self, instance):
|
||||
''' Check if an instance is referenced in the records we have from
|
||||
Route53. If it is, return the list of domain names pointing to said
|
||||
instance. If nothing points to it, return an empty list. '''
|
||||
|
||||
instance_attributes = ['public_dns_name', 'private_dns_name',
|
||||
'ip_address', 'private_ip_address']
|
||||
instance_attributes = [ 'public_dns_name', 'private_dns_name',
|
||||
'ip_address', 'private_ip_address' ]
|
||||
|
||||
name_list = set()
|
||||
|
||||
@ -1452,11 +1300,11 @@ class Ec2Inventory(object):
|
||||
elif key == 'ec2__previous_state':
|
||||
instance_vars['ec2_previous_state'] = instance.previous_state or ''
|
||||
instance_vars['ec2_previous_state_code'] = instance.previous_state_code
|
||||
elif isinstance(value, (int, bool)):
|
||||
elif type(value) in [int, bool]:
|
||||
instance_vars[key] = value
|
||||
elif isinstance(value, six.string_types):
|
||||
instance_vars[key] = value.strip()
|
||||
elif value is None:
|
||||
elif type(value) == type(None):
|
||||
instance_vars[key] = ''
|
||||
elif key == 'ec2_region':
|
||||
instance_vars[key] = value.name
|
||||
@ -1479,15 +1327,13 @@ class Ec2Inventory(object):
|
||||
elif key == 'ec2_block_device_mapping':
|
||||
instance_vars["ec2_block_devices"] = {}
|
||||
for k, v in value.items():
|
||||
instance_vars["ec2_block_devices"][os.path.basename(k)] = v.volume_id
|
||||
instance_vars["ec2_block_devices"][ os.path.basename(k) ] = v.volume_id
|
||||
else:
|
||||
pass
|
||||
# TODO Product codes if someone finds them useful
|
||||
# print key
|
||||
# print type(value)
|
||||
# print value
|
||||
|
||||
instance_vars[self.to_safe('ec2_account_id')] = self.aws_account_id
|
||||
#print key
|
||||
#print type(value)
|
||||
#print value
|
||||
|
||||
return instance_vars
|
||||
|
||||
@ -1529,9 +1375,9 @@ class Ec2Inventory(object):
|
||||
host_info['ec2_primary_cluster_port'] = node['ReadEndpoint']['Port']
|
||||
host_info['ec2_primary_cluster_id'] = node['CacheClusterId']
|
||||
elif node['CurrentRole'] == 'replica':
|
||||
host_info['ec2_replica_cluster_address_' + str(replica_count)] = node['ReadEndpoint']['Address']
|
||||
host_info['ec2_replica_cluster_port_' + str(replica_count)] = node['ReadEndpoint']['Port']
|
||||
host_info['ec2_replica_cluster_id_' + str(replica_count)] = node['CacheClusterId']
|
||||
host_info['ec2_replica_cluster_address_'+ str(replica_count)] = node['ReadEndpoint']['Address']
|
||||
host_info['ec2_replica_cluster_port_'+ str(replica_count)] = node['ReadEndpoint']['Port']
|
||||
host_info['ec2_replica_cluster_id_'+ str(replica_count)] = node['CacheClusterId']
|
||||
replica_count += 1
|
||||
|
||||
# Target: Redis Replication Groups
|
||||
@ -1557,7 +1403,7 @@ class Ec2Inventory(object):
|
||||
|
||||
# Target: Everything
|
||||
# Preserve booleans and integers
|
||||
elif isinstance(value, (int, bool)):
|
||||
elif type(value) in [int, bool]:
|
||||
host_info[key] = value
|
||||
|
||||
# Target: Everything
|
||||
@ -1567,7 +1413,7 @@ class Ec2Inventory(object):
|
||||
|
||||
# Target: Everything
|
||||
# Replace None by an empty string
|
||||
elif value is None:
|
||||
elif type(value) == type(None):
|
||||
host_info[key] = ''
|
||||
|
||||
else:
|
||||
@ -1583,10 +1429,10 @@ class Ec2Inventory(object):
|
||||
# Need to load index from cache
|
||||
self.load_index_from_cache()
|
||||
|
||||
if self.args.host not in self.index:
|
||||
if not self.args.host in self.index:
|
||||
# try updating the cache
|
||||
self.do_api_calls_update_cache()
|
||||
if self.args.host not in self.index:
|
||||
if not self.args.host in self.index:
|
||||
# host might not exist anymore
|
||||
return self.json_format_dict({}, True)
|
||||
|
||||
@ -1618,22 +1464,26 @@ class Ec2Inventory(object):
|
||||
''' Reads the inventory from the cache file and returns it as a JSON
|
||||
object '''
|
||||
|
||||
with open(self.cache_path_cache, 'r') as f:
|
||||
json_inventory = f.read()
|
||||
return json_inventory
|
||||
cache = open(self.cache_path_cache, 'r')
|
||||
json_inventory = cache.read()
|
||||
return json_inventory
|
||||
|
||||
|
||||
def load_index_from_cache(self):
|
||||
''' Reads the index from the cache file sets self.index '''
|
||||
|
||||
with open(self.cache_path_index, 'rb') as f:
|
||||
self.index = json.load(f)
|
||||
cache = open(self.cache_path_index, 'r')
|
||||
json_index = cache.read()
|
||||
self.index = json.loads(json_index)
|
||||
|
||||
|
||||
def write_to_cache(self, data, filename):
|
||||
''' Writes data in JSON format to a file '''
|
||||
|
||||
json_data = self.json_format_dict(data, True)
|
||||
with open(filename, 'w') as f:
|
||||
f.write(json_data)
|
||||
cache = open(filename, 'w')
|
||||
cache.write(json_data)
|
||||
cache.close()
|
||||
|
||||
def uncammelize(self, key):
|
||||
temp = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', key)
|
||||
@ -1656,6 +1506,5 @@ class Ec2Inventory(object):
|
||||
return json.dumps(data)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Run the script
|
||||
Ec2Inventory()
|
||||
# Run the script
|
||||
Ec2Inventory()
|
||||
|
||||
@ -35,7 +35,7 @@ except:
|
||||
import simplejson as json
|
||||
|
||||
# Options
|
||||
# ------------------------------
|
||||
#------------------------------
|
||||
|
||||
parser = OptionParser(usage="%prog [options] --list | --host <machine>")
|
||||
parser.add_option('--list', default=False, dest="list", action="store_true",
|
||||
@ -48,7 +48,6 @@ parser.add_option('--host', default=None, dest="host",
|
||||
# helper functions
|
||||
#
|
||||
|
||||
|
||||
def get_ssh_config():
|
||||
configs = []
|
||||
for box in list_running_boxes():
|
||||
@ -56,8 +55,7 @@ def get_ssh_config():
|
||||
configs.append(config)
|
||||
return configs
|
||||
|
||||
|
||||
# list all the running instances in the fleet
|
||||
#list all the running instances in the fleet
|
||||
def list_running_boxes():
|
||||
boxes = []
|
||||
for line in subprocess.check_output(["fleetctl", "list-machines"]).split('\n'):
|
||||
@ -67,7 +65,6 @@ def list_running_boxes():
|
||||
|
||||
return boxes
|
||||
|
||||
|
||||
def get_a_ssh_config(box_name):
|
||||
config = {}
|
||||
config['Host'] = box_name
|
||||
@ -75,13 +72,12 @@ def get_a_ssh_config(box_name):
|
||||
config['ansible_python_interpreter'] = '/opt/bin/python'
|
||||
return config
|
||||
|
||||
|
||||
# List out servers that vagrant has running
|
||||
# ------------------------------
|
||||
#------------------------------
|
||||
if options.list:
|
||||
ssh_config = get_ssh_config()
|
||||
hosts = {'coreos': []}
|
||||
|
||||
hosts = { 'coreos': []}
|
||||
|
||||
for data in ssh_config:
|
||||
hosts['coreos'].append(data['Host'])
|
||||
|
||||
@ -89,22 +85,23 @@ if options.list:
|
||||
sys.exit(1)
|
||||
|
||||
# Get out the host details
|
||||
# ------------------------------
|
||||
#------------------------------
|
||||
elif options.host:
|
||||
result = {}
|
||||
ssh_config = get_ssh_config()
|
||||
|
||||
details = filter(lambda x: (x['Host'] == options.host), ssh_config)
|
||||
if len(details) > 0:
|
||||
# pass through the port, in case it's non standard.
|
||||
#pass through the port, in case it's non standard.
|
||||
result = details[0]
|
||||
result
|
||||
|
||||
print(json.dumps(result))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Print out help
|
||||
# ------------------------------
|
||||
#------------------------------
|
||||
else:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
@ -1,157 +0,0 @@
|
||||
# Foreman inventory (https://github.com/theforeman/foreman_ansible_inventory)
|
||||
#
|
||||
# This script can be used as an Ansible dynamic inventory.
|
||||
# The connection parameters are set up via *foreman.ini*
|
||||
# This is how the script founds the configuration file in
|
||||
# order of discovery.
|
||||
#
|
||||
# * `/etc/ansible/foreman.ini`
|
||||
# * Current directory of your inventory script.
|
||||
# * `FOREMAN_INI_PATH` environment variable.
|
||||
#
|
||||
# ## Variables and Parameters
|
||||
#
|
||||
# The data returned from Foreman for each host is stored in a foreman
|
||||
# hash so they're available as *host_vars* along with the parameters
|
||||
# of the host and it's hostgroups:
|
||||
#
|
||||
# "foo.example.com": {
|
||||
# "foreman": {
|
||||
# "architecture_id": 1,
|
||||
# "architecture_name": "x86_64",
|
||||
# "build": false,
|
||||
# "build_status": 0,
|
||||
# "build_status_label": "Installed",
|
||||
# "capabilities": [
|
||||
# "build",
|
||||
# "image"
|
||||
# ],
|
||||
# "compute_profile_id": 4,
|
||||
# "hostgroup_name": "webtier/myapp",
|
||||
# "id": 70,
|
||||
# "image_name": "debian8.1",
|
||||
# ...
|
||||
# "uuid": "50197c10-5ebb-b5cf-b384-a1e203e19e77"
|
||||
# },
|
||||
# "foreman_params": {
|
||||
# "testparam1": "foobar",
|
||||
# "testparam2": "small",
|
||||
# ...
|
||||
# }
|
||||
#
|
||||
# and could therefore be used in Ansible like:
|
||||
#
|
||||
# - debug: msg="From Foreman host {{ foreman['uuid'] }}"
|
||||
#
|
||||
# Which yields
|
||||
#
|
||||
# TASK [test_foreman : debug] ****************************************************
|
||||
# ok: [foo.example.com] => {
|
||||
# "msg": "From Foreman host 50190bd1-052a-a34a-3c9c-df37a39550bf"
|
||||
# }
|
||||
#
|
||||
# ## Automatic Ansible groups
|
||||
#
|
||||
# The inventory will provide a set of groups, by default prefixed by
|
||||
# 'foreman_'. If you want to customize this prefix, change the
|
||||
# group_prefix option in /etc/ansible/foreman.ini. The rest of this
|
||||
# guide will assume the default prefix of 'foreman'
|
||||
#
|
||||
# The hostgroup, location, organization, content view, and lifecycle
|
||||
# environment of each host are created as Ansible groups with a
|
||||
# foreman_<grouptype> prefix, all lowercase and problematic parameters
|
||||
# removed. So e.g. the foreman hostgroup
|
||||
#
|
||||
# myapp / webtier / datacenter1
|
||||
#
|
||||
# would turn into the Ansible group:
|
||||
#
|
||||
# foreman_hostgroup_myapp_webtier_datacenter1
|
||||
#
|
||||
# If the parameter want_hostcollections is set to true, the
|
||||
# collections each host is in are created as Ansible groups with a
|
||||
# foreman_hostcollection prefix, all lowercase and problematic
|
||||
# parameters removed. So e.g. the Foreman host collection
|
||||
#
|
||||
# Patch Window Thursday
|
||||
#
|
||||
# would turn into the Ansible group:
|
||||
#
|
||||
# foreman_hostcollection_patchwindowthursday
|
||||
#
|
||||
# If the parameter host_filters is set, it will be used as the
|
||||
# "search" parameter for the /api/v2/hosts call. This can be used to
|
||||
# restrict the list of returned host, as shown below.
|
||||
#
|
||||
# Furthermore Ansible groups can be created on the fly using the
|
||||
# *group_patterns* variable in *foreman.ini* so that you can build up
|
||||
# hierarchies using parameters on the hostgroup and host variables.
|
||||
#
|
||||
# Lets assume you have a host that is built using this nested hostgroup:
|
||||
#
|
||||
# myapp / webtier / datacenter1
|
||||
#
|
||||
# and each of the hostgroups defines a parameters respectively:
|
||||
#
|
||||
# myapp: app_param = myapp
|
||||
# webtier: tier_param = webtier
|
||||
# datacenter1: dc_param = datacenter1
|
||||
#
|
||||
# The host is also in a subnet called "mysubnet" and provisioned via an image
|
||||
# then *group_patterns* like:
|
||||
#
|
||||
# [ansible]
|
||||
# group_patterns = ["{app_param}-{tier_param}-{dc_param}",
|
||||
# "{app_param}-{tier_param}",
|
||||
# "{app_param}",
|
||||
# "{subnet_name}-{provision_method}"]
|
||||
#
|
||||
# would put the host into the additional Ansible groups:
|
||||
#
|
||||
# - myapp-webtier-datacenter1
|
||||
# - myapp-webtier
|
||||
# - myapp
|
||||
# - mysubnet-image
|
||||
#
|
||||
# by recursively resolving the hostgroups, getting the parameter keys
|
||||
# and values and doing a Python *string.format()* like replacement on
|
||||
# it.
|
||||
#
|
||||
[foreman]
|
||||
url = http://localhost:3000/
|
||||
user = foreman
|
||||
password = secret
|
||||
ssl_verify = True
|
||||
|
||||
# Retrieve only hosts from the organization "Web Engineering".
|
||||
# host_filters = organization="Web Engineering"
|
||||
|
||||
# Retrieve only hosts from the organization "Web Engineering" that are
|
||||
# also in the host collection "Apache Servers".
|
||||
# host_filters = organization="Web Engineering" and host_collection="Apache Servers"
|
||||
|
||||
[ansible]
|
||||
group_patterns = ["{app}-{tier}-{color}",
|
||||
"{app}-{color}",
|
||||
"{app}",
|
||||
"{tier}"]
|
||||
group_prefix = foreman_
|
||||
|
||||
# Whether to fetch facts from Foreman and store them on the host
|
||||
want_facts = True
|
||||
|
||||
# Whether to create Ansible groups for host collections. Only tested
|
||||
# with Katello (Red Hat Satellite). Disabled by default to not break
|
||||
# the script for stand-alone Foreman.
|
||||
want_hostcollections = False
|
||||
|
||||
# Whether to interpret global parameters value as JSON (if possible, else
|
||||
# take as is). Only tested with Katello (Red Hat Satellite).
|
||||
# This allows to define lists and dictionaries (and more complicated structures)
|
||||
# variables by entering them as JSON string in Foreman parameters.
|
||||
# Disabled by default as the change would else not be backward compatible.
|
||||
rich_params = False
|
||||
|
||||
[cache]
|
||||
path = .
|
||||
max_age = 60
|
||||
@ -1,436 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: set fileencoding=utf-8 :
|
||||
#
|
||||
# Copyright (C) 2016 Guido Günther <agx@sigxcpu.org>,
|
||||
# Daniel Lobato Garcia <dlobatog@redhat.com>
|
||||
#
|
||||
# This script is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with it. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This is somewhat based on cobbler inventory
|
||||
|
||||
# Stdlib imports
|
||||
# __future__ imports must occur at the beginning of file
|
||||
from __future__ import print_function
|
||||
try:
|
||||
# Python 2 version
|
||||
import ConfigParser
|
||||
except ImportError:
|
||||
# Python 3 version
|
||||
import configparser as ConfigParser
|
||||
import json
|
||||
import argparse
|
||||
import copy
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from time import time
|
||||
from collections import defaultdict
|
||||
from distutils.version import LooseVersion, StrictVersion
|
||||
|
||||
# 3rd party imports
|
||||
import requests
|
||||
if LooseVersion(requests.__version__) < LooseVersion('1.1.0'):
|
||||
print('This script requires python-requests 1.1 as a minimum version')
|
||||
sys.exit(1)
|
||||
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
|
||||
def json_format_dict(data, pretty=False):
|
||||
"""Converts a dict to a JSON object and dumps it as a formatted string"""
|
||||
|
||||
if pretty:
|
||||
return json.dumps(data, sort_keys=True, indent=2)
|
||||
else:
|
||||
return json.dumps(data)
|
||||
|
||||
|
||||
class ForemanInventory(object):
|
||||
|
||||
def __init__(self):
|
||||
self.inventory = defaultdict(list) # A list of groups and the hosts in that group
|
||||
self.cache = dict() # Details about hosts in the inventory
|
||||
self.params = dict() # Params of each host
|
||||
self.facts = dict() # Facts of each host
|
||||
self.hostgroups = dict() # host groups
|
||||
self.hostcollections = dict() # host collections
|
||||
self.session = None # Requests session
|
||||
self.config_paths = [
|
||||
"/etc/ansible/foreman.ini",
|
||||
os.path.dirname(os.path.realpath(__file__)) + '/foreman.ini',
|
||||
]
|
||||
env_value = os.environ.get('FOREMAN_INI_PATH')
|
||||
if env_value is not None:
|
||||
self.config_paths.append(os.path.expanduser(os.path.expandvars(env_value)))
|
||||
|
||||
def read_settings(self):
|
||||
"""Reads the settings from the foreman.ini file"""
|
||||
|
||||
config = ConfigParser.SafeConfigParser()
|
||||
config.read(self.config_paths)
|
||||
|
||||
# Foreman API related
|
||||
try:
|
||||
self.foreman_url = config.get('foreman', 'url')
|
||||
self.foreman_user = config.get('foreman', 'user')
|
||||
self.foreman_pw = config.get('foreman', 'password')
|
||||
self.foreman_ssl_verify = config.getboolean('foreman', 'ssl_verify')
|
||||
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError) as e:
|
||||
print("Error parsing configuration: %s" % e, file=sys.stderr)
|
||||
return False
|
||||
|
||||
# Ansible related
|
||||
try:
|
||||
group_patterns = config.get('ansible', 'group_patterns')
|
||||
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
|
||||
group_patterns = "[]"
|
||||
|
||||
self.group_patterns = json.loads(group_patterns)
|
||||
|
||||
try:
|
||||
self.group_prefix = config.get('ansible', 'group_prefix')
|
||||
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
|
||||
self.group_prefix = "foreman_"
|
||||
|
||||
try:
|
||||
self.want_facts = config.getboolean('ansible', 'want_facts')
|
||||
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
|
||||
self.want_facts = True
|
||||
|
||||
try:
|
||||
self.want_hostcollections = config.getboolean('ansible', 'want_hostcollections')
|
||||
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
|
||||
self.want_hostcollections = False
|
||||
|
||||
# Do we want parameters to be interpreted if possible as JSON? (no by default)
|
||||
try:
|
||||
self.rich_params = config.getboolean('ansible', 'rich_params')
|
||||
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
|
||||
self.rich_params = False
|
||||
|
||||
try:
|
||||
self.host_filters = config.get('foreman', 'host_filters')
|
||||
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
|
||||
self.host_filters = None
|
||||
|
||||
# Cache related
|
||||
try:
|
||||
cache_path = os.path.expanduser(config.get('cache', 'path'))
|
||||
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
|
||||
cache_path = '.'
|
||||
(script, ext) = os.path.splitext(os.path.basename(__file__))
|
||||
self.cache_path_cache = cache_path + "/%s.cache" % script
|
||||
self.cache_path_inventory = cache_path + "/%s.index" % script
|
||||
self.cache_path_params = cache_path + "/%s.params" % script
|
||||
self.cache_path_facts = cache_path + "/%s.facts" % script
|
||||
self.cache_path_hostcollections = cache_path + "/%s.hostcollections" % script
|
||||
try:
|
||||
self.cache_max_age = config.getint('cache', 'max_age')
|
||||
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
|
||||
self.cache_max_age = 60
|
||||
|
||||
return True
|
||||
|
||||
def parse_cli_args(self):
|
||||
"""Command line argument processing"""
|
||||
|
||||
parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on foreman')
|
||||
parser.add_argument('--list', action='store_true', default=True, help='List instances (default: True)')
|
||||
parser.add_argument('--host', action='store', help='Get all the variables about a specific instance')
|
||||
parser.add_argument('--refresh-cache', action='store_true', default=False,
|
||||
help='Force refresh of cache by making API requests to foreman (default: False - use cache files)')
|
||||
self.args = parser.parse_args()
|
||||
|
||||
def _get_session(self):
|
||||
if not self.session:
|
||||
self.session = requests.session()
|
||||
self.session.auth = HTTPBasicAuth(self.foreman_user, self.foreman_pw)
|
||||
self.session.verify = self.foreman_ssl_verify
|
||||
return self.session
|
||||
|
||||
def _get_json(self, url, ignore_errors=None, params=None):
|
||||
if params is None:
|
||||
params = {}
|
||||
params['per_page'] = 250
|
||||
|
||||
page = 1
|
||||
results = []
|
||||
s = self._get_session()
|
||||
while True:
|
||||
params['page'] = page
|
||||
ret = s.get(url, params=params)
|
||||
if ignore_errors and ret.status_code in ignore_errors:
|
||||
break
|
||||
ret.raise_for_status()
|
||||
json = ret.json()
|
||||
# /hosts/:id has not results key
|
||||
if 'results' not in json:
|
||||
return json
|
||||
# Facts are returned as dict in results not list
|
||||
if isinstance(json['results'], dict):
|
||||
return json['results']
|
||||
# List of all hosts is returned paginaged
|
||||
results = results + json['results']
|
||||
if len(results) >= json['subtotal']:
|
||||
break
|
||||
page += 1
|
||||
if len(json['results']) == 0:
|
||||
print("Did not make any progress during loop. "
|
||||
"expected %d got %d" % (json['total'], len(results)),
|
||||
file=sys.stderr)
|
||||
break
|
||||
return results
|
||||
|
||||
def _get_hosts(self):
|
||||
url = "%s/api/v2/hosts" % self.foreman_url
|
||||
|
||||
params = {}
|
||||
if self.host_filters:
|
||||
params['search'] = self.host_filters
|
||||
|
||||
return self._get_json(url, params=params)
|
||||
|
||||
def _get_host_data_by_id(self, hid):
|
||||
url = "%s/api/v2/hosts/%s" % (self.foreman_url, hid)
|
||||
return self._get_json(url)
|
||||
|
||||
def _get_facts_by_id(self, hid):
|
||||
url = "%s/api/v2/hosts/%s/facts" % (self.foreman_url, hid)
|
||||
return self._get_json(url)
|
||||
|
||||
def _resolve_params(self, host_params):
|
||||
"""Convert host params to dict"""
|
||||
params = {}
|
||||
|
||||
for param in host_params:
|
||||
name = param['name']
|
||||
if self.rich_params:
|
||||
try:
|
||||
params[name] = json.loads(param['value'])
|
||||
except ValueError:
|
||||
params[name] = param['value']
|
||||
else:
|
||||
params[name] = param['value']
|
||||
|
||||
return params
|
||||
|
||||
def _get_facts_by_id(self, hid):
|
||||
url = "%s/api/v2/hosts/%s/facts" % (self.foreman_url, hid)
|
||||
return self._get_json(url)
|
||||
|
||||
def _get_facts(self, host):
|
||||
"""Fetch all host facts of the host"""
|
||||
if not self.want_facts:
|
||||
return {}
|
||||
|
||||
ret = self._get_facts_by_id(host['id'])
|
||||
if len(ret.values()) == 0:
|
||||
facts = {}
|
||||
elif len(ret.values()) == 1:
|
||||
facts = list(ret.values())[0]
|
||||
else:
|
||||
raise ValueError("More than one set of facts returned for '%s'" % host)
|
||||
return facts
|
||||
|
||||
def write_to_cache(self, data, filename):
|
||||
"""Write data in JSON format to a file"""
|
||||
json_data = json_format_dict(data, True)
|
||||
cache = open(filename, 'w')
|
||||
cache.write(json_data)
|
||||
cache.close()
|
||||
|
||||
def _write_cache(self):
|
||||
self.write_to_cache(self.cache, self.cache_path_cache)
|
||||
self.write_to_cache(self.inventory, self.cache_path_inventory)
|
||||
self.write_to_cache(self.params, self.cache_path_params)
|
||||
self.write_to_cache(self.facts, self.cache_path_facts)
|
||||
self.write_to_cache(self.hostcollections, self.cache_path_hostcollections)
|
||||
|
||||
def to_safe(self, word):
|
||||
'''Converts 'bad' characters in a string to underscores
|
||||
so they can be used as Ansible groups
|
||||
|
||||
>>> ForemanInventory.to_safe("foo-bar baz")
|
||||
'foo_barbaz'
|
||||
'''
|
||||
regex = "[^A-Za-z0-9\_]"
|
||||
return re.sub(regex, "_", word.replace(" ", ""))
|
||||
|
||||
def update_cache(self):
|
||||
"""Make calls to foreman and save the output in a cache"""
|
||||
|
||||
self.groups = dict()
|
||||
self.hosts = dict()
|
||||
|
||||
for host in self._get_hosts():
|
||||
dns_name = host['name']
|
||||
|
||||
host_data = self._get_host_data_by_id(host['id'])
|
||||
host_params = host_data.get('all_parameters', {})
|
||||
|
||||
# Create ansible groups for hostgroup
|
||||
group = 'hostgroup'
|
||||
val = host.get('%s_title' % group) or host.get('%s_name' % group)
|
||||
if val:
|
||||
safe_key = self.to_safe('%s%s_%s' % (self.group_prefix, group, val.lower()))
|
||||
self.inventory[safe_key].append(dns_name)
|
||||
|
||||
# Create ansible groups for environment, location and organization
|
||||
for group in ['environment', 'location', 'organization']:
|
||||
val = host.get('%s_name' % group)
|
||||
if val:
|
||||
safe_key = self.to_safe('%s%s_%s' % (self.group_prefix, group, val.lower()))
|
||||
self.inventory[safe_key].append(dns_name)
|
||||
|
||||
for group in ['lifecycle_environment', 'content_view']:
|
||||
val = host.get('content_facet_attributes', {}).get('%s_name' % group)
|
||||
if val:
|
||||
safe_key = self.to_safe('%s%s_%s' % (self.group_prefix, group, val.lower()))
|
||||
self.inventory[safe_key].append(dns_name)
|
||||
|
||||
params = self._resolve_params(host_params)
|
||||
|
||||
# Ansible groups by parameters in host groups and Foreman host
|
||||
# attributes.
|
||||
groupby = dict()
|
||||
for k, v in params.items():
|
||||
groupby[k] = self.to_safe(str(v))
|
||||
|
||||
# The name of the ansible groups is given by group_patterns:
|
||||
for pattern in self.group_patterns:
|
||||
try:
|
||||
key = pattern.format(**groupby)
|
||||
self.inventory[key].append(dns_name)
|
||||
except KeyError:
|
||||
pass # Host not part of this group
|
||||
|
||||
if self.want_hostcollections:
|
||||
hostcollections = host_data.get('host_collections')
|
||||
|
||||
if hostcollections:
|
||||
# Create Ansible groups for host collections
|
||||
for hostcollection in hostcollections:
|
||||
safe_key = self.to_safe('%shostcollection_%s' % (self.group_prefix, hostcollection['name'].lower()))
|
||||
self.inventory[safe_key].append(dns_name)
|
||||
|
||||
self.hostcollections[dns_name] = hostcollections
|
||||
|
||||
self.cache[dns_name] = host
|
||||
self.params[dns_name] = params
|
||||
self.facts[dns_name] = self._get_facts(host)
|
||||
self.inventory['all'].append(dns_name)
|
||||
self._write_cache()
|
||||
|
||||
def is_cache_valid(self):
|
||||
"""Determines if the cache is still valid"""
|
||||
if os.path.isfile(self.cache_path_cache):
|
||||
mod_time = os.path.getmtime(self.cache_path_cache)
|
||||
current_time = time()
|
||||
if (mod_time + self.cache_max_age) > current_time:
|
||||
if (os.path.isfile(self.cache_path_inventory) and
|
||||
os.path.isfile(self.cache_path_params) and
|
||||
os.path.isfile(self.cache_path_facts)):
|
||||
return True
|
||||
return False
|
||||
|
||||
def load_inventory_from_cache(self):
|
||||
"""Read the index from the cache file sets self.index"""
|
||||
|
||||
with open(self.cache_path_inventory, 'r') as fp:
|
||||
self.inventory = json.load(fp)
|
||||
|
||||
def load_params_from_cache(self):
|
||||
"""Read the index from the cache file sets self.index"""
|
||||
|
||||
with open(self.cache_path_params, 'r') as fp:
|
||||
self.params = json.load(fp)
|
||||
|
||||
def load_facts_from_cache(self):
|
||||
"""Read the index from the cache file sets self.facts"""
|
||||
|
||||
if not self.want_facts:
|
||||
return
|
||||
with open(self.cache_path_facts, 'r') as fp:
|
||||
self.facts = json.load(fp)
|
||||
|
||||
def load_hostcollections_from_cache(self):
|
||||
"""Read the index from the cache file sets self.hostcollections"""
|
||||
|
||||
if not self.want_hostcollections:
|
||||
return
|
||||
with open(self.cache_path_hostcollections, 'r') as fp:
|
||||
self.hostcollections = json.load(fp)
|
||||
|
||||
def load_cache_from_cache(self):
|
||||
"""Read the cache from the cache file sets self.cache"""
|
||||
|
||||
with open(self.cache_path_cache, 'r') as fp:
|
||||
self.cache = json.load(fp)
|
||||
|
||||
def get_inventory(self):
|
||||
if self.args.refresh_cache or not self.is_cache_valid():
|
||||
self.update_cache()
|
||||
else:
|
||||
self.load_inventory_from_cache()
|
||||
self.load_params_from_cache()
|
||||
self.load_facts_from_cache()
|
||||
self.load_hostcollections_from_cache()
|
||||
self.load_cache_from_cache()
|
||||
|
||||
def get_host_info(self):
|
||||
"""Get variables about a specific host"""
|
||||
|
||||
if not self.cache or len(self.cache) == 0:
|
||||
# Need to load index from cache
|
||||
self.load_cache_from_cache()
|
||||
|
||||
if self.args.host not in self.cache:
|
||||
# try updating the cache
|
||||
self.update_cache()
|
||||
|
||||
if self.args.host not in self.cache:
|
||||
# host might not exist anymore
|
||||
return json_format_dict({}, True)
|
||||
|
||||
return json_format_dict(self.cache[self.args.host], True)
|
||||
|
||||
def _print_data(self):
|
||||
data_to_print = ""
|
||||
if self.args.host:
|
||||
data_to_print += self.get_host_info()
|
||||
else:
|
||||
self.inventory['_meta'] = {'hostvars': {}}
|
||||
for hostname in self.cache:
|
||||
self.inventory['_meta']['hostvars'][hostname] = {
|
||||
'foreman': self.cache[hostname],
|
||||
'foreman_params': self.params[hostname],
|
||||
}
|
||||
if self.want_facts:
|
||||
self.inventory['_meta']['hostvars'][hostname]['foreman_facts'] = self.facts[hostname]
|
||||
|
||||
data_to_print += json_format_dict(self.inventory, True)
|
||||
|
||||
print(data_to_print)
|
||||
|
||||
def run(self):
|
||||
# Read settings and parse CLI arguments
|
||||
if not self.read_settings():
|
||||
return False
|
||||
self.parse_cli_args()
|
||||
self.get_inventory()
|
||||
self._print_data()
|
||||
return True
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(not ForemanInventory().run())
|
||||
@ -1,17 +1,13 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
import argparse
|
||||
from ipalib import api
|
||||
import json
|
||||
from distutils.version import StrictVersion
|
||||
|
||||
|
||||
def initialize():
|
||||
'''
|
||||
This function initializes the FreeIPA/IPA API. This function requires
|
||||
no arguments. A kerberos key must be present in the users keyring in
|
||||
no arguments. A kerberos key must be present in the users keyring in
|
||||
order for this to work.
|
||||
'''
|
||||
|
||||
@ -20,47 +16,35 @@ def initialize():
|
||||
try:
|
||||
api.Backend.rpcclient.connect()
|
||||
except AttributeError:
|
||||
# FreeIPA < 4.0 compatibility
|
||||
#FreeIPA < 4.0 compatibility
|
||||
api.Backend.xmlclient.connect()
|
||||
|
||||
|
||||
return api
|
||||
|
||||
|
||||
def list_groups(api):
|
||||
'''
|
||||
This function prints a list of all host groups. This function requires
|
||||
This function returns a list of all host groups. This function requires
|
||||
one argument, the FreeIPA/IPA API object.
|
||||
'''
|
||||
|
||||
inventory = {}
|
||||
hostvars = {}
|
||||
hostvars={}
|
||||
meta={}
|
||||
|
||||
ipa_version = api.Command.env()['result']['version']
|
||||
result = api.Command.hostgroup_find()['result']
|
||||
|
||||
for hostgroup in result:
|
||||
# Get direct and indirect members (nested hostgroups) of hostgroup
|
||||
members = []
|
||||
if StrictVersion(ipa_version) >= StrictVersion('4.0.0'):
|
||||
hostgroup_name = hostgroup['cn'][0]
|
||||
hostgroup = api.Command.hostgroup_show(hostgroup_name)['result']
|
||||
inventory[hostgroup['cn'][0]] = { 'hosts': [host for host in hostgroup['member_host']]}
|
||||
|
||||
if 'member_host' in hostgroup:
|
||||
members = [host for host in hostgroup['member_host']]
|
||||
if 'memberindirect_host' in hostgroup:
|
||||
members += (host for host in hostgroup['memberindirect_host'])
|
||||
inventory[hostgroup['cn'][0]] = {'hosts': [host for host in members]}
|
||||
|
||||
for member in members:
|
||||
hostvars[member] = {}
|
||||
for host in hostgroup['member_host']:
|
||||
hostvars[host] = {}
|
||||
|
||||
inventory['_meta'] = {'hostvars': hostvars}
|
||||
inv_string = json.dumps(inventory, indent=1, sort_keys=True)
|
||||
print(inv_string)
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def parse_args():
|
||||
'''
|
||||
This function parses the arguments that were passed in via the command line.
|
||||
@ -76,11 +60,10 @@ def parse_args():
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def print_host(host):
|
||||
'''
|
||||
This function is really a stub, it could return variables to be used in
|
||||
a playbook. However, at this point there are no variables stored in
|
||||
This function is really a stub, it could return variables to be used in
|
||||
a playbook. However, at this point there are no variables stored in
|
||||
FreeIPA/IPA.
|
||||
|
||||
This function expects one string, this hostname to lookup variables for.
|
||||
@ -90,7 +73,6 @@ def print_host(host):
|
||||
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = parse_args()
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
#!/usr/bin/python
|
||||
# Copyright 2013 Google Inc.
|
||||
#
|
||||
# This file is part of Ansible
|
||||
@ -36,15 +37,13 @@
|
||||
# exist in your PYTHONPATH and be picked up automatically with an import
|
||||
# statement in the inventory script. However, you can specify an absolute
|
||||
# path to the secrets.py file with 'libcloud_secrets' parameter.
|
||||
# This option will be deprecated in a future release.
|
||||
libcloud_secrets =
|
||||
libcloud_secrets =
|
||||
|
||||
# If you are not going to use a 'secrets.py' file, you can set the necessary
|
||||
# authorization parameters here.
|
||||
gce_service_account_email_address =
|
||||
gce_service_account_pem_file_path =
|
||||
gce_project_id =
|
||||
gce_zone =
|
||||
gce_service_account_email_address =
|
||||
gce_service_account_pem_file_path =
|
||||
gce_project_id =
|
||||
|
||||
# Filter inventory based on on state. Leave undefined to return instances regardless of state.
|
||||
# example: Uncomment to only return inventory in the running or provisioning state
|
||||
@ -59,11 +58,3 @@ gce_zone =
|
||||
# The INVENTORY_IP_TYPE environment variable will override this value.
|
||||
inventory_ip_type =
|
||||
|
||||
[cache]
|
||||
# directory in which cache should be created
|
||||
cache_path = ~/.ansible/tmp
|
||||
|
||||
# The number of seconds a cache file is considered valid. After this many
|
||||
# seconds, a new API call will be made, and the cache file will be updated.
|
||||
# To disable the cache, set this value to 0
|
||||
cache_max_age = 300
|
||||
|
||||
@ -40,7 +40,6 @@ based on the data obtained from the libcloud Node object:
|
||||
- gce_tags
|
||||
- gce_metadata
|
||||
- gce_network
|
||||
- gce_subnetwork
|
||||
|
||||
When run in --list mode, instances are grouped by the following categories:
|
||||
- zone:
|
||||
@ -70,10 +69,11 @@ Examples:
|
||||
$ contrib/inventory/gce.py --host my_instance
|
||||
|
||||
Author: Eric Johnson <erjohnso@google.com>
|
||||
Contributors: Matt Hite <mhite@hotmail.com>, Tom Melendez <supertom@google.com>
|
||||
Version: 0.0.3
|
||||
Contributors: Matt Hite <mhite@hotmail.com>
|
||||
Version: 0.0.2
|
||||
'''
|
||||
|
||||
__requires__ = ['pycrypto>=2.6']
|
||||
try:
|
||||
import pkg_resources
|
||||
except ImportError:
|
||||
@ -83,19 +83,13 @@ except ImportError:
|
||||
# library is used.
|
||||
pass
|
||||
|
||||
USER_AGENT_PRODUCT = "Ansible-gce_inventory_plugin"
|
||||
USER_AGENT_VERSION = "v2"
|
||||
USER_AGENT_PRODUCT="Ansible-gce_inventory_plugin"
|
||||
USER_AGENT_VERSION="v2"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
|
||||
from time import time
|
||||
|
||||
if sys.version_info >= (3, 0):
|
||||
import configparser
|
||||
else:
|
||||
import ConfigParser as configparser
|
||||
import ConfigParser
|
||||
|
||||
import logging
|
||||
logging.getLogger('libcloud.common.google').addHandler(logging.NullHandler())
|
||||
@ -113,57 +107,8 @@ except:
|
||||
sys.exit("GCE inventory script requires libcloud >= 0.13")
|
||||
|
||||
|
||||
class CloudInventoryCache(object):
|
||||
def __init__(self, cache_name='ansible-cloud-cache', cache_path='/tmp',
|
||||
cache_max_age=300):
|
||||
cache_dir = os.path.expanduser(cache_path)
|
||||
if not os.path.exists(cache_dir):
|
||||
os.makedirs(cache_dir)
|
||||
self.cache_path_cache = os.path.join(cache_dir, cache_name)
|
||||
|
||||
self.cache_max_age = cache_max_age
|
||||
|
||||
def is_valid(self, max_age=None):
|
||||
''' Determines if the cache files have expired, or if it is still valid '''
|
||||
|
||||
if max_age is None:
|
||||
max_age = self.cache_max_age
|
||||
|
||||
if os.path.isfile(self.cache_path_cache):
|
||||
mod_time = os.path.getmtime(self.cache_path_cache)
|
||||
current_time = time()
|
||||
if (mod_time + max_age) > current_time:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_all_data_from_cache(self, filename=''):
|
||||
''' Reads the JSON inventory from the cache file. Returns Python dictionary. '''
|
||||
|
||||
data = ''
|
||||
if not filename:
|
||||
filename = self.cache_path_cache
|
||||
with open(filename, 'r') as cache:
|
||||
data = cache.read()
|
||||
return json.loads(data)
|
||||
|
||||
def write_to_cache(self, data, filename=''):
|
||||
''' Writes data to file as JSON. Returns True. '''
|
||||
if not filename:
|
||||
filename = self.cache_path_cache
|
||||
json_data = json.dumps(data)
|
||||
with open(filename, 'w') as cache:
|
||||
cache.write(json_data)
|
||||
return True
|
||||
|
||||
|
||||
class GceInventory(object):
|
||||
def __init__(self):
|
||||
# Cache object
|
||||
self.cache = None
|
||||
# dictionary containing inventory read from disk
|
||||
self.inventory = {}
|
||||
|
||||
# Read settings and parse CLI arguments
|
||||
self.parse_cli_args()
|
||||
self.config = self.get_config()
|
||||
@ -172,36 +117,22 @@ class GceInventory(object):
|
||||
if self.ip_type:
|
||||
self.ip_type = self.ip_type.lower()
|
||||
|
||||
# Cache management
|
||||
start_inventory_time = time()
|
||||
cache_used = False
|
||||
if self.args.refresh_cache or not self.cache.is_valid():
|
||||
self.do_api_calls_update_cache()
|
||||
else:
|
||||
self.load_inventory_from_cache()
|
||||
cache_used = True
|
||||
self.inventory['_meta']['stats'] = {'use_cache': True}
|
||||
self.inventory['_meta']['stats'] = {
|
||||
'inventory_load_time': time() - start_inventory_time,
|
||||
'cache_used': cache_used
|
||||
}
|
||||
|
||||
# Just display data for specific host
|
||||
if self.args.host:
|
||||
print(self.json_format_dict(
|
||||
self.inventory['_meta']['hostvars'][self.args.host],
|
||||
pretty=self.args.pretty))
|
||||
else:
|
||||
# Otherwise, assume user wants all instances grouped
|
||||
zones = self.parse_env_zones()
|
||||
print(self.json_format_dict(self.inventory,
|
||||
pretty=self.args.pretty))
|
||||
print(self.json_format_dict(self.node_to_dict(
|
||||
self.get_instance(self.args.host)),
|
||||
pretty=self.args.pretty))
|
||||
sys.exit(0)
|
||||
|
||||
zones = self.parse_env_zones()
|
||||
|
||||
# Otherwise, assume user wants all instances grouped
|
||||
print(self.json_format_dict(self.group_instances(zones),
|
||||
pretty=self.args.pretty))
|
||||
sys.exit(0)
|
||||
|
||||
def get_config(self):
|
||||
"""
|
||||
Reads the settings from the gce.ini file.
|
||||
|
||||
Populates a SafeConfigParser object with defaults and
|
||||
attempts to read an .ini-style configuration from the filename
|
||||
specified in GCE_INI_PATH. If the environment variable is
|
||||
@ -216,22 +147,17 @@ class GceInventory(object):
|
||||
# This provides empty defaults to each key, so that environment
|
||||
# variable configuration (as opposed to INI configuration) is able
|
||||
# to work.
|
||||
config = configparser.SafeConfigParser(defaults={
|
||||
config = ConfigParser.SafeConfigParser(defaults={
|
||||
'gce_service_account_email_address': '',
|
||||
'gce_service_account_pem_file_path': '',
|
||||
'gce_project_id': '',
|
||||
'gce_zone': '',
|
||||
'libcloud_secrets': '',
|
||||
'inventory_ip_type': '',
|
||||
'cache_path': '~/.ansible/tmp',
|
||||
'cache_max_age': '300'
|
||||
})
|
||||
if 'gce' not in config.sections():
|
||||
config.add_section('gce')
|
||||
if 'inventory' not in config.sections():
|
||||
config.add_section('inventory')
|
||||
if 'cache' not in config.sections():
|
||||
config.add_section('cache')
|
||||
|
||||
config.read(gce_ini_path)
|
||||
|
||||
@ -247,14 +173,6 @@ class GceInventory(object):
|
||||
if states:
|
||||
self.instance_states = states.split(',')
|
||||
|
||||
# Caching
|
||||
cache_path = config.get('cache', 'cache_path')
|
||||
cache_max_age = config.getint('cache', 'cache_max_age')
|
||||
# TOOD(supertom): support project-specific caches
|
||||
cache_name = 'ansible-gce.cache'
|
||||
self.cache = CloudInventoryCache(cache_path=cache_path,
|
||||
cache_max_age=cache_max_age,
|
||||
cache_name=cache_name)
|
||||
return config
|
||||
|
||||
def get_inventory_options(self):
|
||||
@ -274,11 +192,10 @@ class GceInventory(object):
|
||||
# exists.
|
||||
secrets_path = self.config.get('gce', 'libcloud_secrets')
|
||||
secrets_found = False
|
||||
|
||||
try:
|
||||
import secrets
|
||||
args = list(secrets.GCE_PARAMS)
|
||||
kwargs = secrets.GCE_KEYWORD_PARAMS
|
||||
args = list(getattr(secrets, 'GCE_PARAMS', []))
|
||||
kwargs = getattr(secrets, 'GCE_KEYWORD_PARAMS', {})
|
||||
secrets_found = True
|
||||
except:
|
||||
pass
|
||||
@ -296,23 +213,18 @@ class GceInventory(object):
|
||||
secrets_found = True
|
||||
except:
|
||||
pass
|
||||
|
||||
if not secrets_found:
|
||||
args = [
|
||||
self.config.get('gce', 'gce_service_account_email_address'),
|
||||
self.config.get('gce', 'gce_service_account_pem_file_path')
|
||||
self.config.get('gce','gce_service_account_email_address'),
|
||||
self.config.get('gce','gce_service_account_pem_file_path')
|
||||
]
|
||||
kwargs = {'project': self.config.get('gce', 'gce_project_id'),
|
||||
'datacenter': self.config.get('gce', 'gce_zone')}
|
||||
kwargs = {'project': self.config.get('gce', 'gce_project_id')}
|
||||
|
||||
# If the appropriate environment variables are set, they override
|
||||
# other configuration; process those into our args and kwargs.
|
||||
args[0] = os.environ.get('GCE_EMAIL', args[0])
|
||||
args[1] = os.environ.get('GCE_PEM_FILE_PATH', args[1])
|
||||
args[1] = os.environ.get('GCE_CREDENTIALS_FILE_PATH', args[1])
|
||||
|
||||
kwargs['project'] = os.environ.get('GCE_PROJECT', kwargs['project'])
|
||||
kwargs['datacenter'] = os.environ.get('GCE_ZONE', kwargs['datacenter'])
|
||||
|
||||
# Retrieve and return the GCE driver.
|
||||
gce = get_driver(Provider.GCE)(*args, **kwargs)
|
||||
@ -322,10 +234,10 @@ class GceInventory(object):
|
||||
return gce
|
||||
|
||||
def parse_env_zones(self):
|
||||
'''returns a list of comma separated zones parsed from the GCE_ZONE environment variable.
|
||||
'''returns a list of comma seperated zones parsed from the GCE_ZONE environment variable.
|
||||
If provided, this will be used to filter the results of the grouped_instances call'''
|
||||
import csv
|
||||
reader = csv.reader([os.environ.get('GCE_ZONE', "")], skipinitialspace=True)
|
||||
reader = csv.reader([os.environ.get('GCE_ZONE',"")], skipinitialspace=True)
|
||||
zones = [r for r in reader]
|
||||
return [z for z in zones[0]]
|
||||
|
||||
@ -333,18 +245,16 @@ class GceInventory(object):
|
||||
''' Command line argument processing '''
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Produce an Ansible Inventory file based on GCE')
|
||||
description='Produce an Ansible Inventory file based on GCE')
|
||||
parser.add_argument('--list', action='store_true', default=True,
|
||||
help='List instances (default: True)')
|
||||
help='List instances (default: True)')
|
||||
parser.add_argument('--host', action='store',
|
||||
help='Get all information about an instance')
|
||||
help='Get all information about an instance')
|
||||
parser.add_argument('--pretty', action='store_true', default=False,
|
||||
help='Pretty format (default: False)')
|
||||
parser.add_argument(
|
||||
'--refresh-cache', action='store_true', default=False,
|
||||
help='Force refresh of cache by making API requests (default: False - use cache files)')
|
||||
help='Pretty format (default: False)')
|
||||
self.args = parser.parse_args()
|
||||
|
||||
|
||||
def node_to_dict(self, inst):
|
||||
md = {}
|
||||
|
||||
@ -356,9 +266,6 @@ class GceInventory(object):
|
||||
md[entry['key']] = entry['value']
|
||||
|
||||
net = inst.extra['networkInterfaces'][0]['network'].split('/')[-1]
|
||||
subnet = None
|
||||
if 'subnetwork' in inst.extra['networkInterfaces'][0]:
|
||||
subnet = inst.extra['networkInterfaces'][0]['subnetwork'].split('/')[-1]
|
||||
# default to exernal IP unless user has specified they prefer internal
|
||||
if self.ip_type == 'internal':
|
||||
ssh_host = inst.private_ips[0]
|
||||
@ -379,38 +286,16 @@ class GceInventory(object):
|
||||
'gce_tags': inst.extra['tags'],
|
||||
'gce_metadata': md,
|
||||
'gce_network': net,
|
||||
'gce_subnetwork': subnet,
|
||||
# Hosts don't have a public name, so we add an IP
|
||||
'ansible_ssh_host': ssh_host
|
||||
}
|
||||
|
||||
def load_inventory_from_cache(self):
|
||||
''' Loads inventory from JSON on disk. '''
|
||||
|
||||
def get_instance(self, instance_name):
|
||||
'''Gets details about a specific instance '''
|
||||
try:
|
||||
self.inventory = self.cache.get_all_data_from_cache()
|
||||
hosts = self.inventory['_meta']['hostvars']
|
||||
return self.driver.ex_get_node(instance_name)
|
||||
except Exception as e:
|
||||
print(
|
||||
"Invalid inventory file %s. Please rebuild with -refresh-cache option."
|
||||
% (self.cache.cache_path_cache))
|
||||
raise
|
||||
|
||||
def do_api_calls_update_cache(self):
|
||||
''' Do API calls and save data in cache. '''
|
||||
zones = self.parse_env_zones()
|
||||
data = self.group_instances(zones)
|
||||
self.cache.write_to_cache(data)
|
||||
self.inventory = data
|
||||
|
||||
def list_nodes(self):
|
||||
all_nodes = []
|
||||
params, more_results = {'maxResults': 500}, True
|
||||
while more_results:
|
||||
self.driver.connection.gce_params = params
|
||||
all_nodes.extend(self.driver.list_nodes())
|
||||
more_results = 'pageToken' in params
|
||||
return all_nodes
|
||||
return None
|
||||
|
||||
def group_instances(self, zones=None):
|
||||
'''Group all instances'''
|
||||
@ -418,7 +303,7 @@ class GceInventory(object):
|
||||
meta = {}
|
||||
meta["hostvars"] = {}
|
||||
|
||||
for node in self.list_nodes():
|
||||
for node in self.driver.list_nodes():
|
||||
|
||||
# This check filters on the desired instance states defined in the
|
||||
# config file with the instance_states config option.
|
||||
@ -441,10 +326,8 @@ class GceInventory(object):
|
||||
if zones and zone not in zones:
|
||||
continue
|
||||
|
||||
if zone in groups:
|
||||
groups[zone].append(name)
|
||||
else:
|
||||
groups[zone] = [name]
|
||||
if zone in groups: groups[zone].append(name)
|
||||
else: groups[zone] = [name]
|
||||
|
||||
tags = node.extra['tags']
|
||||
for t in tags:
|
||||
@ -452,43 +335,26 @@ class GceInventory(object):
|
||||
tag = t[6:]
|
||||
else:
|
||||
tag = 'tag_%s' % t
|
||||
if tag in groups:
|
||||
groups[tag].append(name)
|
||||
else:
|
||||
groups[tag] = [name]
|
||||
if tag in groups: groups[tag].append(name)
|
||||
else: groups[tag] = [name]
|
||||
|
||||
net = node.extra['networkInterfaces'][0]['network'].split('/')[-1]
|
||||
net = 'network_%s' % net
|
||||
if net in groups:
|
||||
groups[net].append(name)
|
||||
else:
|
||||
groups[net] = [name]
|
||||
if net in groups: groups[net].append(name)
|
||||
else: groups[net] = [name]
|
||||
|
||||
machine_type = node.size
|
||||
if machine_type in groups:
|
||||
groups[machine_type].append(name)
|
||||
else:
|
||||
groups[machine_type] = [name]
|
||||
if machine_type in groups: groups[machine_type].append(name)
|
||||
else: groups[machine_type] = [name]
|
||||
|
||||
image = node.image and node.image or 'persistent_disk'
|
||||
if image in groups:
|
||||
groups[image].append(name)
|
||||
else:
|
||||
groups[image] = [name]
|
||||
if image in groups: groups[image].append(name)
|
||||
else: groups[image] = [name]
|
||||
|
||||
status = node.extra['status']
|
||||
stat = 'status_%s' % status.lower()
|
||||
if stat in groups:
|
||||
groups[stat].append(name)
|
||||
else:
|
||||
groups[stat] = [name]
|
||||
|
||||
for private_ip in node.private_ips:
|
||||
groups[private_ip] = [name]
|
||||
|
||||
if len(node.public_ips) >= 1:
|
||||
for public_ip in node.public_ips:
|
||||
groups[public_ip] = [name]
|
||||
if stat in groups: groups[stat].append(name)
|
||||
else: groups[stat] = [name]
|
||||
|
||||
groups["_meta"] = meta
|
||||
|
||||
@ -503,6 +369,6 @@ class GceInventory(object):
|
||||
else:
|
||||
return json.dumps(data)
|
||||
|
||||
|
||||
# Run the script
|
||||
if __name__ == '__main__':
|
||||
GceInventory()
|
||||
GceInventory()
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from subprocess import Popen,PIPE
|
||||
import sys
|
||||
import json
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from subprocess import Popen,PIPE
|
||||
import sys
|
||||
import json
|
||||
|
||||
|
||||
@ -113,15 +113,11 @@ load_chube_config()
|
||||
# Imports for ansible
|
||||
import ConfigParser
|
||||
|
||||
|
||||
class LinodeInventory(object):
|
||||
def _empty_inventory(self):
|
||||
return {"_meta": {"hostvars": {}}}
|
||||
|
||||
def __init__(self):
|
||||
"""Main execution path."""
|
||||
# Inventory grouped by display group
|
||||
self.inventory = self._empty_inventory()
|
||||
self.inventory = {}
|
||||
# Index of label to Linode ID
|
||||
self.index = {}
|
||||
# Local cache of Datacenter objects populated by populate_datacenter_cache()
|
||||
@ -142,7 +138,7 @@ class LinodeInventory(object):
|
||||
data_to_print = self.get_host_info()
|
||||
elif self.args.list:
|
||||
# Display list of nodes for inventory
|
||||
if len(self.inventory) == 1:
|
||||
if len(self.inventory) == 0:
|
||||
data_to_print = self.get_inventory_from_cache()
|
||||
else:
|
||||
data_to_print = self.json_format_dict(self.inventory, True)
|
||||
@ -175,11 +171,11 @@ class LinodeInventory(object):
|
||||
"""Command line argument processing"""
|
||||
parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on Linode')
|
||||
parser.add_argument('--list', action='store_true', default=True,
|
||||
help='List nodes (default: True)')
|
||||
help='List nodes (default: True)')
|
||||
parser.add_argument('--host', action='store',
|
||||
help='Get all the variables about a specific node')
|
||||
help='Get all the variables about a specific node')
|
||||
parser.add_argument('--refresh-cache', action='store_true', default=False,
|
||||
help='Force refresh of cache by making API requests to Linode (default: False - use cache files)')
|
||||
help='Force refresh of cache by making API requests to Linode (default: False - use cache files)')
|
||||
self.args = parser.parse_args()
|
||||
|
||||
def do_api_calls_update_cache(self):
|
||||
@ -201,7 +197,7 @@ class LinodeInventory(object):
|
||||
try:
|
||||
return Linode.find(api_id=linode_id)
|
||||
except chube_api.linode_api.ApiError as e:
|
||||
sys.exit("Looks like Linode's API is down:\n%s" % e)
|
||||
sys.exit("Looks like Linode's API is down:\n%" % e)
|
||||
|
||||
def populate_datacenter_cache(self):
|
||||
"""Creates self._datacenter_cache, containing all Datacenters indexed by ID."""
|
||||
@ -235,15 +231,9 @@ class LinodeInventory(object):
|
||||
# Inventory: Group by datacenter city
|
||||
self.push(self.inventory, self.get_datacenter_city(node), dest)
|
||||
|
||||
# Inventory: Group by display group
|
||||
# Inventory: Group by dipslay group
|
||||
self.push(self.inventory, node.display_group, dest)
|
||||
|
||||
# Inventory: Add a "linode" global tag group
|
||||
self.push(self.inventory, "linode", dest)
|
||||
|
||||
# Add host info to hostvars
|
||||
self.inventory["_meta"]["hostvars"][dest] = self.get_host_info(node)
|
||||
|
||||
def get_node_public_ip(self, node):
|
||||
"""Returns a the public IP address of the node"""
|
||||
return [addr.address for addr in node.ipaddresses if addr.is_public][0]
|
||||
@ -255,19 +245,16 @@ class LinodeInventory(object):
|
||||
# Need to load index from cache
|
||||
self.load_index_from_cache()
|
||||
|
||||
if self.args.host not in self.index:
|
||||
if not self.args.host in self.index:
|
||||
# try updating the cache
|
||||
self.do_api_calls_update_cache()
|
||||
if self.args.host not in self.index:
|
||||
if not self.args.host in self.index:
|
||||
# host might not exist anymore
|
||||
return self.json_format_dict({}, True)
|
||||
|
||||
node_id = self.index[self.args.host]
|
||||
|
||||
node = self.get_node(node_id)
|
||||
|
||||
return self.json_format_dict(self.get_host_info(node), True)
|
||||
|
||||
def get_host_info(self, node):
|
||||
node_vars = {}
|
||||
for direct_attr in [
|
||||
"api_id",
|
||||
@ -308,12 +295,12 @@ class LinodeInventory(object):
|
||||
if private_ips:
|
||||
node_vars["private_ip"] = private_ips[0]
|
||||
|
||||
return node_vars
|
||||
return self.json_format_dict(node_vars, True)
|
||||
|
||||
def push(self, my_dict, key, element):
|
||||
"""Pushed an element onto an array that may not have been defined in the dict."""
|
||||
if key in my_dict:
|
||||
my_dict[key].append(element)
|
||||
my_dict[key].append(element);
|
||||
else:
|
||||
my_dict[key] = [element]
|
||||
|
||||
|
||||
@ -35,7 +35,6 @@ import sys
|
||||
import lxc
|
||||
import json
|
||||
|
||||
|
||||
def build_dict():
|
||||
"""Returns a dictionary keyed to the defined LXC groups. All
|
||||
containers, including the ones not in any group, are included in the
|
||||
@ -52,8 +51,7 @@ def build_dict():
|
||||
|
||||
# Create a dictionary for each group (including the 'all' group
|
||||
return dict([(g, {'hosts': [k for k, v in containers.items() if g in v],
|
||||
'vars': {'ansible_connection': 'lxc'}}) for g in groups])
|
||||
|
||||
'vars': {'ansible_connection':'lxc'}}) for g in groups])
|
||||
|
||||
def main(argv):
|
||||
"""Returns a JSON dictionary as expected by Ansible"""
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
# LXD external inventory script settings
|
||||
|
||||
[lxd]
|
||||
|
||||
# The default resource
|
||||
#resource = local:
|
||||
|
||||
# The group name to add the hosts to
|
||||
#group = lxd
|
||||
|
||||
# The connection type to return for these hosts - lxd hasn't been tested yet
|
||||
#connection = lxd
|
||||
connection = smart
|
||||
@ -1,103 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# (c) 2013, Michael Scherer <misc@zarb.org>
|
||||
# (c) 2014, Hiroaki Nakamura <hnakamur@gmail.com>
|
||||
# (c) 2016, Andew Clarke <andrew@oscailte.org>
|
||||
#
|
||||
# This file is based on https://github.com/ansible/ansible/blob/devel/plugins/inventory/libvirt_lxc.py which is part of Ansible,
|
||||
# and https://github.com/hnakamur/lxc-ansible-playbooks/blob/master/provisioning/inventory-lxc.py
|
||||
#
|
||||
# NOTE, this file has some obvious limitations, improvements welcome
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
from subprocess import Popen, PIPE
|
||||
import distutils.spawn
|
||||
import sys
|
||||
import json
|
||||
try:
|
||||
import configparser
|
||||
except:
|
||||
from six.moves import configparser
|
||||
|
||||
# Set up defaults
|
||||
resource = 'local:'
|
||||
group = 'lxd'
|
||||
connection = 'lxd'
|
||||
hosts = {}
|
||||
result = {}
|
||||
|
||||
# Read the settings from the lxd.ini file
|
||||
config = configparser.SafeConfigParser()
|
||||
config.read(os.path.dirname(os.path.realpath(__file__)) + '/lxd.ini')
|
||||
if config.has_option('lxd', 'resource'):
|
||||
resource = config.get('lxd', 'resource')
|
||||
if config.has_option('lxd', 'group'):
|
||||
group = config.get('lxd', 'group')
|
||||
if config.has_option('lxd', 'connection'):
|
||||
connection = config.get('lxd', 'connection')
|
||||
|
||||
# Ensure executable exists
|
||||
if distutils.spawn.find_executable('lxc'):
|
||||
|
||||
# Set up containers result and hosts array
|
||||
result[group] = {}
|
||||
result[group]['hosts'] = []
|
||||
|
||||
# Run the command and load json result
|
||||
pipe = Popen(['lxc', 'list', resource, '--format', 'json'], stdout=PIPE, universal_newlines=True)
|
||||
lxdjson = json.load(pipe.stdout)
|
||||
|
||||
# Iterate the json lxd output
|
||||
for item in lxdjson:
|
||||
|
||||
# Check state and network
|
||||
if 'state' in item and item['state'] is not None and 'network' in item['state']:
|
||||
network = item['state']['network']
|
||||
|
||||
# Check for eth0 and addresses
|
||||
if 'eth0' in network and 'addresses' in network['eth0']:
|
||||
addresses = network['eth0']['addresses']
|
||||
|
||||
# Iterate addresses
|
||||
for address in addresses:
|
||||
|
||||
# Only return inet family addresses
|
||||
if 'family' in address and address['family'] == 'inet':
|
||||
if 'address' in address:
|
||||
ip = address['address']
|
||||
name = item['name']
|
||||
|
||||
# Add the host to the results and the host array
|
||||
result[group]['hosts'].append(name)
|
||||
hosts[name] = ip
|
||||
|
||||
# Set the other containers result values
|
||||
result[group]['vars'] = {}
|
||||
result[group]['vars']['ansible_connection'] = connection
|
||||
|
||||
# Process arguments
|
||||
if len(sys.argv) == 2 and sys.argv[1] == '--list':
|
||||
print(json.dumps(result))
|
||||
elif len(sys.argv) == 3 and sys.argv[1] == '--host':
|
||||
if sys.argv[2] == 'localhost':
|
||||
print(json.dumps({'ansible_connection': 'local'}))
|
||||
else:
|
||||
if connection == 'lxd':
|
||||
print(json.dumps({'ansible_connection': connection}))
|
||||
else:
|
||||
print(json.dumps({'ansible_connection': connection, 'ansible_host': hosts[sys.argv[2]]}))
|
||||
else:
|
||||
print("Need an argument, either --list or --host <host>")
|
||||
@ -1,17 +0,0 @@
|
||||
[mdt]
|
||||
|
||||
# Set the MDT server to connect to
|
||||
server = localhost.example.com
|
||||
|
||||
# Set the MDT Instance
|
||||
instance = EXAMPLEINSTANCE
|
||||
|
||||
# Set the MDT database
|
||||
database = MDTDB
|
||||
|
||||
# Configure login credentials
|
||||
user = local.domain\admin
|
||||
password = adminpassword
|
||||
|
||||
[tower]
|
||||
groupname = mdt
|
||||
@ -1,134 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# (c) 2016, Julian Barnett <jbarnett@tableau.com>
|
||||
#
|
||||
# This file is part of Ansible.
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
'''
|
||||
MDT external inventory script
|
||||
=================================
|
||||
author: J Barnett 06/23/2016 01:15
|
||||
maintainer: J Barnett (github @jbarnett1981)
|
||||
'''
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import pymssql
|
||||
try:
|
||||
import configparser
|
||||
except ImportError:
|
||||
import ConfigParser as configparser
|
||||
|
||||
|
||||
class MDTInventory(object):
|
||||
|
||||
def __init__(self):
|
||||
''' Main execution path '''
|
||||
self.conn = None
|
||||
|
||||
# Initialize empty inventory
|
||||
self.inventory = self._empty_inventory()
|
||||
|
||||
# Read CLI arguments
|
||||
self.read_settings()
|
||||
self.parse_cli_args()
|
||||
|
||||
# Get Hosts
|
||||
if self.args.list:
|
||||
self.get_hosts()
|
||||
|
||||
# Get specific host vars
|
||||
if self.args.host:
|
||||
self.get_hosts(self.args.host)
|
||||
|
||||
def _connect(self, query):
|
||||
'''
|
||||
Connect to MDT and dump contents of dbo.ComputerIdentity database
|
||||
'''
|
||||
if not self.conn:
|
||||
self.conn = pymssql.connect(server=self.mdt_server + "\\" + self.mdt_instance, user=self.mdt_user, password=self.mdt_password,
|
||||
database=self.mdt_database)
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute(query)
|
||||
self.mdt_dump = cursor.fetchall()
|
||||
self.conn.close()
|
||||
|
||||
def get_hosts(self, hostname=False):
|
||||
'''
|
||||
Gets host from MDT Database
|
||||
'''
|
||||
if hostname:
|
||||
query = ("SELECT t1.ID, t1.Description, t1.MacAddress, t2.Role "
|
||||
"FROM ComputerIdentity as t1 join Settings_Roles as t2 on t1.ID = t2.ID where t1.Description = '%s'" % hostname)
|
||||
else:
|
||||
query = 'SELECT t1.ID, t1.Description, t1.MacAddress, t2.Role FROM ComputerIdentity as t1 join Settings_Roles as t2 on t1.ID = t2.ID'
|
||||
self._connect(query)
|
||||
|
||||
# Configure to group name configured in Ansible Tower for this inventory
|
||||
groupname = self.mdt_groupname
|
||||
|
||||
# Initialize empty host list
|
||||
hostlist = []
|
||||
|
||||
# Parse through db dump and populate inventory
|
||||
for hosts in self.mdt_dump:
|
||||
self.inventory['_meta']['hostvars'][hosts[1]] = {'id': hosts[0], 'name': hosts[1], 'mac': hosts[2], 'role': hosts[3]}
|
||||
hostlist.append(hosts[1])
|
||||
self.inventory[groupname] = hostlist
|
||||
|
||||
# Print it all out
|
||||
print(json.dumps(self.inventory, indent=2))
|
||||
|
||||
def _empty_inventory(self):
|
||||
'''
|
||||
Create empty inventory dictionary
|
||||
'''
|
||||
return {"_meta": {"hostvars": {}}}
|
||||
|
||||
def read_settings(self):
|
||||
'''
|
||||
Reads the settings from the mdt.ini file
|
||||
'''
|
||||
config = configparser.SafeConfigParser()
|
||||
config.read('mdt.ini')
|
||||
|
||||
# MDT Server and instance and database
|
||||
self.mdt_server = config.get('mdt', 'server')
|
||||
self.mdt_instance = config.get('mdt', 'instance')
|
||||
self.mdt_database = config.get('mdt', 'database')
|
||||
|
||||
# MDT Login credentials
|
||||
if config.has_option('mdt', 'user'):
|
||||
self.mdt_user = config.get('mdt', 'user')
|
||||
if config.has_option('mdt', 'password'):
|
||||
self.mdt_password = config.get('mdt', 'password')
|
||||
|
||||
# Group name in Tower
|
||||
if config.has_option('tower', 'groupname'):
|
||||
self.mdt_groupname = config.get('tower', 'groupname')
|
||||
|
||||
def parse_cli_args(self):
|
||||
'''
|
||||
Command line argument processing
|
||||
'''
|
||||
parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on MDT')
|
||||
parser.add_argument('--list', action='store_true', default=False, help='List instances')
|
||||
parser.add_argument('--host', action='store', help='Get all the variables about a specific instance')
|
||||
self.args = parser.parse_args()
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run the script
|
||||
MDTInventory()
|
||||
@ -1,41 +0,0 @@
|
||||
# Ansible Nagios external inventory script settings
|
||||
#
|
||||
# To get all available possibilities, check following URL:
|
||||
# http://www.naemon.org/documentation/usersguide/livestatus.html
|
||||
# https://mathias-kettner.de/checkmk_livestatus.html
|
||||
#
|
||||
|
||||
[local]
|
||||
# Livestatus URI
|
||||
# Example for default naemon livestatus unix socket :
|
||||
# livestatus_uri=unix:/var/cache/naemon/live
|
||||
|
||||
[remote]
|
||||
|
||||
# default field name for host: name
|
||||
# Uncomment to override:
|
||||
# host_field=address
|
||||
#
|
||||
# default field group for host: groups
|
||||
# Uncomment to override:
|
||||
# group_field=state
|
||||
# default fields retrieved: address, alias, display_name, children, parents
|
||||
# To override, uncomment the following line
|
||||
# fields_to_retrieve=address,alias,display_name
|
||||
#
|
||||
# default variable prefix: livestatus_
|
||||
# To override, uncomment the following line
|
||||
# var_prefix=naemon_
|
||||
#
|
||||
# default filter: None
|
||||
#
|
||||
# Uncomment to override
|
||||
#
|
||||
# All host with state = OK
|
||||
# host_filter=state = 0
|
||||
# Warning: for the moment, you can use only one filter at a time. You cannot combine various conditions.
|
||||
#
|
||||
# All host in groups Linux
|
||||
# host_filter=groups >= Linux
|
||||
#
|
||||
livestatus_uri=tcp:192.168.66.137:6557
|
||||
@ -1,177 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# (c) 2015, Yannig Perre <yannig.perre@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible,
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
'''
|
||||
Nagios livestatus inventory script. Before using this script, please
|
||||
update nagios_livestatus.ini file.
|
||||
|
||||
Livestatus is a nagios/naemon/shinken module which let you retrieve
|
||||
informations stored in the monitoring core.
|
||||
|
||||
This plugin inventory need livestatus API for python. Please install it
|
||||
before using this script (apt/pip/yum/...).
|
||||
|
||||
Checkmk livestatus: https://mathias-kettner.de/checkmk_livestatus.html
|
||||
Livestatus API: http://www.naemon.org/documentation/usersguide/livestatus.html
|
||||
'''
|
||||
|
||||
import os
|
||||
import re
|
||||
import argparse
|
||||
try:
|
||||
import configparser
|
||||
except ImportError:
|
||||
import ConfigParser
|
||||
configparser = ConfigParser
|
||||
import json
|
||||
|
||||
try:
|
||||
from mk_livestatus import Socket
|
||||
except ImportError:
|
||||
print("Error: mk_livestatus is needed. Try something like: pip install python-mk-livestatus")
|
||||
exit(1)
|
||||
|
||||
|
||||
class NagiosLivestatusInventory(object):
|
||||
|
||||
def parse_ini_file(self):
|
||||
config = configparser.SafeConfigParser()
|
||||
config.read(os.path.dirname(os.path.realpath(__file__)) + '/nagios_livestatus.ini')
|
||||
for section in config.sections():
|
||||
if not config.has_option(section, 'livestatus_uri'):
|
||||
continue
|
||||
|
||||
# If fields_to_retrieve is not set, using default fields
|
||||
fields_to_retrieve = self.default_fields_to_retrieve
|
||||
if config.has_option(section, 'fields_to_retrieve'):
|
||||
fields_to_retrieve = [field.strip() for field in config.get(section, 'fields_to_retrieve').split(',')]
|
||||
fields_to_retrieve = tuple(fields_to_retrieve)
|
||||
|
||||
# default section values
|
||||
section_values = {
|
||||
'var_prefix': 'livestatus_',
|
||||
'host_filter': None,
|
||||
'host_field': 'name',
|
||||
'group_field': 'groups'
|
||||
}
|
||||
for key, value in section_values.items():
|
||||
if config.has_option(section, key):
|
||||
section_values[key] = config.get(section, key).strip()
|
||||
|
||||
# Retrieving livestatus string connection
|
||||
livestatus_uri = config.get(section, 'livestatus_uri')
|
||||
backend_definition = None
|
||||
|
||||
# Local unix socket
|
||||
unix_match = re.match('unix:(.*)', livestatus_uri)
|
||||
if unix_match is not None:
|
||||
backend_definition = {'connection': unix_match.group(1)}
|
||||
|
||||
# Remote tcp connection
|
||||
tcp_match = re.match('tcp:(.*):([^:]*)', livestatus_uri)
|
||||
if tcp_match is not None:
|
||||
backend_definition = {'connection': (tcp_match.group(1), int(tcp_match.group(2)))}
|
||||
|
||||
# No valid livestatus_uri => exiting
|
||||
if backend_definition is None:
|
||||
raise Exception('livestatus_uri field is invalid (%s). Expected: unix:/path/to/live or tcp:host:port' % livestatus_uri)
|
||||
|
||||
# Updating backend_definition with current value
|
||||
backend_definition['name'] = section
|
||||
backend_definition['fields'] = fields_to_retrieve
|
||||
for key, value in section_values.items():
|
||||
backend_definition[key] = value
|
||||
|
||||
self.backends.append(backend_definition)
|
||||
|
||||
def parse_options(self):
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--host', nargs=1)
|
||||
parser.add_argument('--list', action='store_true')
|
||||
parser.add_argument('--pretty', action='store_true')
|
||||
self.options = parser.parse_args()
|
||||
|
||||
def add_host(self, hostname, group):
|
||||
if group not in self.result:
|
||||
self.result[group] = {}
|
||||
self.result[group]['hosts'] = []
|
||||
if hostname not in self.result[group]['hosts']:
|
||||
self.result[group]['hosts'].append(hostname)
|
||||
|
||||
def query_backend(self, backend, host=None):
|
||||
'''Query a livestatus backend'''
|
||||
hosts_request = Socket(backend['connection']).hosts.columns(backend['host_field'], backend['group_field'])
|
||||
|
||||
if backend['host_filter'] is not None:
|
||||
hosts_request = hosts_request.filter(backend['host_filter'])
|
||||
|
||||
if host is not None:
|
||||
hosts_request = hosts_request.filter('name = ' + host[0])
|
||||
|
||||
hosts_request._columns += backend['fields']
|
||||
|
||||
hosts = hosts_request.call()
|
||||
for host in hosts:
|
||||
hostname = host[backend['host_field']]
|
||||
hostgroups = host[backend['group_field']]
|
||||
if not isinstance(hostgroups, list):
|
||||
hostgroups = [hostgroups]
|
||||
self.add_host(hostname, 'all')
|
||||
self.add_host(hostname, backend['name'])
|
||||
for group in hostgroups:
|
||||
self.add_host(hostname, group)
|
||||
for field in backend['fields']:
|
||||
var_name = backend['var_prefix'] + field
|
||||
if hostname not in self.result['_meta']['hostvars']:
|
||||
self.result['_meta']['hostvars'][hostname] = {}
|
||||
self.result['_meta']['hostvars'][hostname][var_name] = host[field]
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.defaultgroup = 'group_all'
|
||||
self.default_fields_to_retrieve = ('address', 'alias', 'display_name', 'childs', 'parents')
|
||||
self.backends = []
|
||||
self.options = None
|
||||
|
||||
self.parse_ini_file()
|
||||
self.parse_options()
|
||||
|
||||
self.result = {}
|
||||
self.result['_meta'] = {}
|
||||
self.result['_meta']['hostvars'] = {}
|
||||
self.json_indent = None
|
||||
if self.options.pretty:
|
||||
self.json_indent = 2
|
||||
|
||||
if len(self.backends) == 0:
|
||||
print("Error: Livestatus configuration is missing. See nagios_livestatus.ini.")
|
||||
exit(1)
|
||||
|
||||
for backend in self.backends:
|
||||
self.query_backend(backend, self.options.host)
|
||||
|
||||
if self.options.host:
|
||||
print(json.dumps(self.result['_meta']['hostvars'][self.options.host[0]], indent=self.json_indent))
|
||||
elif self.options.list:
|
||||
print(json.dumps(self.result, indent=self.json_indent))
|
||||
else:
|
||||
print("usage: --list or --host HOSTNAME [--pretty]")
|
||||
exit(1)
|
||||
|
||||
NagiosLivestatusInventory()
|
||||
@ -42,7 +42,6 @@ except ImportError:
|
||||
print("Error: SQLAlchemy is needed. Try something like: pip install sqlalchemy")
|
||||
exit(1)
|
||||
|
||||
|
||||
class NagiosNDOInventory(object):
|
||||
|
||||
def read_settings(self):
|
||||
|
||||
@ -142,7 +142,7 @@ def get_metadata(server):
|
||||
key = 'os_' + re.sub(r"[^A-Za-z0-9\-]", "_", key).lower()
|
||||
|
||||
# Att value to instance result (exclude manager class)
|
||||
# TODO: maybe use value.__class__ or similar inside of key_name
|
||||
#TODO: maybe use value.__class__ or similar inside of key_name
|
||||
if key != 'os_manager':
|
||||
results[key] = value
|
||||
return results
|
||||
|
||||
9
contrib/inventory/nsot.py
Executable file → Normal file
9
contrib/inventory/nsot.py
Executable file → Normal file
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/bin/env python
|
||||
|
||||
'''
|
||||
nsot
|
||||
@ -150,9 +150,8 @@ from click.exceptions import UsageError
|
||||
|
||||
from six import string_types
|
||||
|
||||
|
||||
def warning(*objs):
|
||||
print("WARNING: ", *objs, file=sys.stderr)
|
||||
print("WARNING: ", *objs, file=sys.stderr)
|
||||
|
||||
|
||||
class NSoTInventory(object):
|
||||
@ -217,7 +216,7 @@ class NSoTInventory(object):
|
||||
Depending on number of devices in NSoT, could be rather slow since this
|
||||
has to request every device resource to filter through
|
||||
'''
|
||||
device = [i for i in self.client.devices.get()
|
||||
device = [i for i in self.client.devices.get()['data']['devices']
|
||||
if host in i['hostname']][0]
|
||||
attributes = device['attributes']
|
||||
attributes.update({'site_id': device['site_id'], 'id': device['id']})
|
||||
@ -278,7 +277,7 @@ class NSoTInventory(object):
|
||||
|
||||
# Would do a list comprehension here, but would like to save code/time
|
||||
# and also acquire attributes in this step
|
||||
for host in devices:
|
||||
for host in devices['data']['devices']:
|
||||
# Iterate through each device that matches query, assign hostname
|
||||
# to the group's hosts array and then use this single iteration as
|
||||
# a chance to update self._meta which will be used in the final
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# The OpenStack Inventory module uses os-client-config for configuration.
|
||||
# https://github.com/openstack/os-client-config
|
||||
# https://github.com/stackforge/os-client-config
|
||||
# This means it will either:
|
||||
# - Respect normal OS_* environment variables like other OpenStack tools
|
||||
# - Read values from a clouds.yaml file.
|
||||
@ -29,11 +29,8 @@
|
||||
# - /etc/openstack/clouds.yaml
|
||||
# - /etc/ansible/openstack.yml
|
||||
# The clouds.yaml file can contain entries for multiple clouds and multiple
|
||||
# regions of those clouds. If it does, this inventory module will by default
|
||||
# connect to all of them and present them as one contiguous inventory. You
|
||||
# can limit to one cloud by passing the `--cloud` parameter, or use the
|
||||
# OS_CLOUD environment variable. If caching is enabled, and a cloud is
|
||||
# selected, then per-cloud cache folders will be used.
|
||||
# regions of those clouds. If it does, this inventory module will connect to
|
||||
# all of them and present them as one contiguous inventory.
|
||||
#
|
||||
# See the adjacent openstack.yml file for an example config file
|
||||
# There are two ansible inventory specific options that can be set in
|
||||
@ -47,9 +44,6 @@
|
||||
# has failed (for example, bad credentials or being offline).
|
||||
# When set to False, the inventory will return hosts from
|
||||
# whichever other clouds it can contact. (Default: True)
|
||||
#
|
||||
# Also it is possible to pass the correct user by setting an ansible_user: $myuser
|
||||
# metadata attribute.
|
||||
|
||||
import argparse
|
||||
import collections
|
||||
@ -114,8 +108,8 @@ def get_groups_from_server(server_vars, namegroup=True):
|
||||
return groups
|
||||
|
||||
|
||||
def get_host_groups(inventory, refresh=False, cloud=None):
|
||||
(cache_file, cache_expiration_time) = get_cache_settings(cloud)
|
||||
def get_host_groups(inventory, refresh=False):
|
||||
(cache_file, cache_expiration_time) = get_cache_settings()
|
||||
if is_cache_stale(cache_file, cache_expiration_time, refresh=refresh):
|
||||
groups = to_json(get_host_groups_from_cloud(inventory))
|
||||
open(cache_file, 'w').write(groups)
|
||||
@ -127,13 +121,7 @@ def get_host_groups(inventory, refresh=False, cloud=None):
|
||||
def append_hostvars(hostvars, groups, key, server, namegroup=False):
|
||||
hostvars[key] = dict(
|
||||
ansible_ssh_host=server['interface_ip'],
|
||||
ansible_host=server['interface_ip'],
|
||||
openstack=server)
|
||||
|
||||
metadata = server.get('metadata', {})
|
||||
if 'ansible_user' in metadata:
|
||||
hostvars[key]['ansible_user'] = metadata['ansible_user']
|
||||
|
||||
for group in get_groups_from_server(server, namegroup=namegroup):
|
||||
groups[group].append(key)
|
||||
|
||||
@ -188,14 +176,12 @@ def is_cache_stale(cache_file, cache_expiration_time, refresh=False):
|
||||
return True
|
||||
|
||||
|
||||
def get_cache_settings(cloud=None):
|
||||
def get_cache_settings():
|
||||
config = os_client_config.config.OpenStackConfig(
|
||||
config_files=os_client_config.config.CONFIG_FILES + CONFIG_FILES)
|
||||
# For inventory-wide caching
|
||||
cache_expiration_time = config.get_cache_expiration_time()
|
||||
cache_path = config.get_cache_path()
|
||||
if cloud:
|
||||
cache_path = '{0}_{1}'.format(cache_path, cloud)
|
||||
if not os.path.exists(cache_path):
|
||||
os.makedirs(cache_path)
|
||||
cache_file = os.path.join(cache_path, 'ansible-inventory.cache')
|
||||
@ -208,8 +194,6 @@ def to_json(in_dict):
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description='OpenStack Inventory Module')
|
||||
parser.add_argument('--cloud', default=os.environ.get('OS_CLOUD'),
|
||||
help='Cloud name (default: None')
|
||||
parser.add_argument('--private',
|
||||
action='store_true',
|
||||
help='Use private address for ansible host')
|
||||
@ -234,7 +218,6 @@ def main():
|
||||
refresh=args.refresh,
|
||||
config_files=config_files,
|
||||
private=args.private,
|
||||
cloud=args.cloud,
|
||||
)
|
||||
if hasattr(shade.inventory.OpenStackInventory, 'extra_config'):
|
||||
inventory_args.update(dict(
|
||||
@ -249,7 +232,7 @@ def main():
|
||||
inventory = shade.inventory.OpenStackInventory(**inventory_args)
|
||||
|
||||
if args.list:
|
||||
output = get_host_groups(inventory, refresh=args.refresh, cloud=args.cloud)
|
||||
output = get_host_groups(inventory, refresh=args.refresh)
|
||||
elif args.host:
|
||||
output = to_json(inventory.get_host(args.host))
|
||||
print(output)
|
||||
|
||||
@ -26,47 +26,39 @@
|
||||
# Groups are determined by the description field of openvz guests
|
||||
# multiple groups can be separated by commas: webserver,dbserver
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from subprocess import Popen,PIPE
|
||||
import sys
|
||||
import json
|
||||
|
||||
|
||||
# List openvz hosts
|
||||
vzhosts = ['vzhost1', 'vzhost2', 'vzhost3']
|
||||
# Add openvz hosts to the inventory and Add "_meta" trick
|
||||
#List openvz hosts
|
||||
vzhosts = ['vzhost1','vzhost2','vzhost3']
|
||||
#Add openvz hosts to the inventory and Add "_meta" trick
|
||||
inventory = {'vzhosts': {'hosts': vzhosts}, '_meta': {'hostvars': {}}}
|
||||
# default group, when description not defined
|
||||
#default group, when description not defined
|
||||
default_group = ['vzguest']
|
||||
|
||||
|
||||
def get_guests():
|
||||
# Loop through vzhosts
|
||||
#Loop through vzhosts
|
||||
for h in vzhosts:
|
||||
# SSH to vzhost and get the list of guests in json
|
||||
pipe = Popen(['ssh', h, 'vzlist', '-j'], stdout=PIPE, universal_newlines=True)
|
||||
#SSH to vzhost and get the list of guests in json
|
||||
pipe = Popen(['ssh', h,'vzlist','-j'], stdout=PIPE, universal_newlines=True)
|
||||
|
||||
# Load Json info of guests
|
||||
#Load Json info of guests
|
||||
json_data = json.loads(pipe.stdout.read())
|
||||
|
||||
# loop through guests
|
||||
#loop through guests
|
||||
for j in json_data:
|
||||
# Add information to host vars
|
||||
inventory['_meta']['hostvars'][j['hostname']] = {
|
||||
'ctid': j['ctid'],
|
||||
'veid': j['veid'],
|
||||
'vpsid': j['vpsid'],
|
||||
'private_path': j['private'],
|
||||
'root_path': j['root'],
|
||||
'ip': j['ip']
|
||||
}
|
||||
#Add information to host vars
|
||||
inventory['_meta']['hostvars'][j['hostname']] = {'ctid': j['ctid'], 'veid': j['veid'], 'vpsid': j['vpsid'], 'private_path': j['private'], 'root_path': j['root'], 'ip': j['ip']}
|
||||
|
||||
# determine group from guest description
|
||||
#determine group from guest description
|
||||
if j['description'] is not None:
|
||||
groups = j['description'].split(",")
|
||||
else:
|
||||
groups = default_group
|
||||
|
||||
# add guest to inventory
|
||||
#add guest to inventory
|
||||
for g in groups:
|
||||
if g not in inventory:
|
||||
inventory[g] = {'hosts': []}
|
||||
|
||||
@ -31,4 +31,3 @@ ovirt_api_secrets =
|
||||
ovirt_url =
|
||||
ovirt_username =
|
||||
ovirt_password =
|
||||
ovirt_ca_file =
|
||||
|
||||
@ -230,7 +230,7 @@ class OVirtInventory(object):
|
||||
"""
|
||||
return [x.get_name() for x in inst.get_tags().list()]
|
||||
|
||||
def get_machine_type(self, inst):
|
||||
def get_machine_type(self,inst):
|
||||
inst_type = inst.get_instance_type()
|
||||
if inst_type:
|
||||
return self.driver.instancetypes.get(id=inst_type.id).name
|
||||
|
||||
@ -1,262 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2016 Red Hat, Inc.
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
"""
|
||||
oVirt dynamic inventory script
|
||||
=================================
|
||||
|
||||
Generates dynamic inventory file for oVirt.
|
||||
|
||||
Script will return following attributes for each virtual machine:
|
||||
- id
|
||||
- name
|
||||
- host
|
||||
- cluster
|
||||
- status
|
||||
- description
|
||||
- fqdn
|
||||
- os_type
|
||||
- template
|
||||
- tags
|
||||
- statistics
|
||||
- devices
|
||||
|
||||
When run in --list mode, virtual machines are grouped by the following categories:
|
||||
- cluster
|
||||
- tag
|
||||
- status
|
||||
|
||||
Note: If there is some virtual machine which has has more tags it will be in both tag
|
||||
records.
|
||||
|
||||
Examples:
|
||||
# Execute update of system on webserver virtual machine:
|
||||
|
||||
$ ansible -i contrib/inventory/ovirt4.py webserver -m yum -a "name=* state=latest"
|
||||
|
||||
# Get webserver virtual machine information:
|
||||
|
||||
$ contrib/inventory/ovirt4.py --host webserver
|
||||
|
||||
Author: Ondra Machacek (@machacekondra)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
try:
|
||||
import ConfigParser as configparser
|
||||
except ImportError:
|
||||
import configparser
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
try:
|
||||
import ovirtsdk4 as sdk
|
||||
import ovirtsdk4.types as otypes
|
||||
except ImportError:
|
||||
print('oVirt inventory script requires ovirt-engine-sdk-python >= 4.0.0')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""
|
||||
Create command line parser for oVirt dynamic inventory script.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Ansible dynamic inventory script for oVirt.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--list',
|
||||
action='store_true',
|
||||
default=True,
|
||||
help='Get data of all virtual machines (default: True).',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--host',
|
||||
help='Get data of virtual machines running on specified host.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--pretty',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Pretty format (default: False).',
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def create_connection():
|
||||
"""
|
||||
Create a connection to oVirt engine API.
|
||||
"""
|
||||
# Get the path of the configuration file, by default use
|
||||
# 'ovirt.ini' file in script directory:
|
||||
default_path = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)),
|
||||
'ovirt.ini',
|
||||
)
|
||||
config_path = os.environ.get('OVIRT_INI_PATH', default_path)
|
||||
|
||||
# Create parser and add ovirt section if it doesn't exist:
|
||||
config = configparser.SafeConfigParser(
|
||||
defaults={
|
||||
'ovirt_url': None,
|
||||
'ovirt_username': None,
|
||||
'ovirt_password': None,
|
||||
'ovirt_ca_file': None,
|
||||
}
|
||||
)
|
||||
if not config.has_section('ovirt'):
|
||||
config.add_section('ovirt')
|
||||
config.read(config_path)
|
||||
|
||||
# Create a connection with options defined in ini file:
|
||||
return sdk.Connection(
|
||||
url=config.get('ovirt', 'ovirt_url'),
|
||||
username=config.get('ovirt', 'ovirt_username'),
|
||||
password=config.get('ovirt', 'ovirt_password'),
|
||||
ca_file=config.get('ovirt', 'ovirt_ca_file'),
|
||||
insecure=config.get('ovirt', 'ovirt_ca_file') is None,
|
||||
)
|
||||
|
||||
|
||||
def get_dict_of_struct(connection, vm):
|
||||
"""
|
||||
Transform SDK Vm Struct type to Python dictionary.
|
||||
"""
|
||||
if vm is None:
|
||||
return dict()
|
||||
|
||||
vms_service = connection.system_service().vms_service()
|
||||
clusters_service = connection.system_service().clusters_service()
|
||||
vm_service = vms_service.vm_service(vm.id)
|
||||
devices = vm_service.reported_devices_service().list()
|
||||
tags = vm_service.tags_service().list()
|
||||
stats = vm_service.statistics_service().list()
|
||||
labels = vm_service.affinity_labels_service().list()
|
||||
groups = clusters_service.cluster_service(
|
||||
vm.cluster.id
|
||||
).affinity_groups_service().list()
|
||||
|
||||
return {
|
||||
'id': vm.id,
|
||||
'name': vm.name,
|
||||
'host': connection.follow_link(vm.host).name if vm.host else None,
|
||||
'cluster': connection.follow_link(vm.cluster).name,
|
||||
'status': str(vm.status),
|
||||
'description': vm.description,
|
||||
'fqdn': vm.fqdn,
|
||||
'os_type': vm.os.type,
|
||||
'template': connection.follow_link(vm.template).name,
|
||||
'tags': [tag.name for tag in tags],
|
||||
'affinity_labels': [label.name for label in labels],
|
||||
'affinity_groups': [
|
||||
group.name for group in groups
|
||||
if vm.name in [vm.name for vm in connection.follow_link(group.vms)]
|
||||
],
|
||||
'statistics': dict(
|
||||
(stat.name, stat.values[0].datum) for stat in stats
|
||||
),
|
||||
'devices': dict(
|
||||
(device.name, [ip.address for ip in device.ips]) for device in devices if device.ips
|
||||
),
|
||||
'ansible_host': next((device.ips[0].address for device in devices if device.ips), None)
|
||||
}
|
||||
|
||||
|
||||
def get_data(connection, vm_name=None):
|
||||
"""
|
||||
Obtain data of `vm_name` if specified, otherwise obtain data of all vms.
|
||||
"""
|
||||
vms_service = connection.system_service().vms_service()
|
||||
clusters_service = connection.system_service().clusters_service()
|
||||
|
||||
if vm_name:
|
||||
vm = vms_service.list(search='name=%s' % vm_name) or [None]
|
||||
data = get_dict_of_struct(
|
||||
connection=connection,
|
||||
vm=vm[0],
|
||||
)
|
||||
else:
|
||||
vms = dict()
|
||||
data = defaultdict(list)
|
||||
for vm in vms_service.list():
|
||||
name = vm.name
|
||||
vm_service = vms_service.vm_service(vm.id)
|
||||
cluster_service = clusters_service.cluster_service(vm.cluster.id)
|
||||
|
||||
# Add vm to vms dict:
|
||||
vms[name] = get_dict_of_struct(connection, vm)
|
||||
|
||||
# Add vm to cluster group:
|
||||
cluster_name = connection.follow_link(vm.cluster).name
|
||||
data['cluster_%s' % cluster_name].append(name)
|
||||
|
||||
# Add vm to tag group:
|
||||
tags_service = vm_service.tags_service()
|
||||
for tag in tags_service.list():
|
||||
data['tag_%s' % tag.name].append(name)
|
||||
|
||||
# Add vm to status group:
|
||||
data['status_%s' % vm.status].append(name)
|
||||
|
||||
# Add vm to affinity group:
|
||||
for group in cluster_service.affinity_groups_service().list():
|
||||
if vm.name in [
|
||||
v.name for v in connection.follow_link(group.vms)
|
||||
]:
|
||||
data['affinity_group_%s' % group.name].append(vm.name)
|
||||
|
||||
# Add vm to affinity label group:
|
||||
affinity_labels_service = vm_service.affinity_labels_service()
|
||||
for label in affinity_labels_service.list():
|
||||
data['affinity_label_%s' % label.name].append(name)
|
||||
|
||||
data["_meta"] = {
|
||||
'hostvars': vms,
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
connection = create_connection()
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
obj=get_data(
|
||||
connection=connection,
|
||||
vm_name=args.host,
|
||||
),
|
||||
sort_keys=args.pretty,
|
||||
indent=args.pretty * 2,
|
||||
)
|
||||
)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -1,53 +0,0 @@
|
||||
# Ansible Packet.net external inventory script settings
|
||||
#
|
||||
|
||||
[packet]
|
||||
|
||||
# Packet projects to get info for. Set this to 'all' to get info for all
|
||||
# projects in Packet and merge the results together. Alternatively, set
|
||||
# this to a comma separated list of projects. E.g. 'project-1,project-3,project-4'
|
||||
projects = all
|
||||
projects_exclude =
|
||||
|
||||
# By default, packet devices in all state are returned. Specify
|
||||
# packet device states to return as a comma-separated list.
|
||||
# device_states = active, inactive, queued, provisioning
|
||||
|
||||
# items per page to retrieve from packet api at a time
|
||||
items_per_page = 999
|
||||
|
||||
# API calls to Packet are costly. For this reason, we cache the results of an API
|
||||
# call. Set this to the path you want cache files to be written to. Two files
|
||||
# will be written to this directory:
|
||||
# - ansible-packet.cache
|
||||
# - ansible-packet.index
|
||||
cache_path = ~/.ansible/tmp
|
||||
|
||||
# The number of seconds a cache file is considered valid. After this many
|
||||
# seconds, a new API call will be made, and the cache file will be updated.
|
||||
# To disable the cache, set this value to 0
|
||||
cache_max_age = 300
|
||||
|
||||
# Organize groups into a nested/hierarchy instead of a flat namespace.
|
||||
nested_groups = False
|
||||
|
||||
# Replace - tags when creating groups to avoid issues with ansible
|
||||
replace_dash_in_groups = True
|
||||
|
||||
# The packet inventory output can become very large. To manage its size,
|
||||
# configure which groups should be created.
|
||||
group_by_device_id = True
|
||||
group_by_hostname = True
|
||||
group_by_facility = True
|
||||
group_by_project = True
|
||||
group_by_operating_system = True
|
||||
group_by_plan_type = True
|
||||
group_by_tags = True
|
||||
group_by_tag_none = True
|
||||
|
||||
# If you only want to include hosts that match a certain regular expression
|
||||
# pattern_include = staging-*
|
||||
|
||||
# If you want to exclude any hosts that match a certain regular expression
|
||||
# pattern_exclude = staging-*
|
||||
|
||||
@ -1,499 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
'''
|
||||
Packet.net external inventory script
|
||||
=================================
|
||||
|
||||
Generates inventory that Ansible can understand by making API request to
|
||||
Packet.net using the Packet library.
|
||||
|
||||
NOTE: This script assumes Ansible is being executed where the environment
|
||||
variable needed for Packet API Token already been set:
|
||||
export PACKET_API_TOKEN=Bfse9F24SFtfs423Gsd3ifGsd43sSdfs
|
||||
|
||||
This script also assumes there is a packet_net.ini file alongside it. To specify a
|
||||
different path to packet_net.ini, define the PACKET_NET_INI_PATH environment variable:
|
||||
|
||||
export PACKET_NET_INI_PATH=/path/to/my_packet_net.ini
|
||||
|
||||
'''
|
||||
|
||||
# (c) 2016, Peter Sankauskas
|
||||
# (c) 2017, Tomas Karasek
|
||||
#
|
||||
# This file is part of Ansible,
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
######################################################################
|
||||
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import re
|
||||
from time import time
|
||||
import six
|
||||
|
||||
from six.moves import configparser
|
||||
|
||||
try:
|
||||
import packet
|
||||
except ImportError as e:
|
||||
sys.exit("failed=True msg='`packet-python` library required for this script'")
|
||||
|
||||
import traceback
|
||||
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
|
||||
ini_section = 'packet'
|
||||
|
||||
|
||||
class PacketInventory(object):
|
||||
|
||||
def _empty_inventory(self):
|
||||
return {"_meta": {"hostvars": {}}}
|
||||
|
||||
def __init__(self):
|
||||
''' Main execution path '''
|
||||
|
||||
# Inventory grouped by device IDs, tags, security groups, regions,
|
||||
# and availability zones
|
||||
self.inventory = self._empty_inventory()
|
||||
|
||||
# Index of hostname (address) to device ID
|
||||
self.index = {}
|
||||
|
||||
# Read settings and parse CLI arguments
|
||||
self.parse_cli_args()
|
||||
self.read_settings()
|
||||
|
||||
# Cache
|
||||
if self.args.refresh_cache:
|
||||
self.do_api_calls_update_cache()
|
||||
elif not self.is_cache_valid():
|
||||
self.do_api_calls_update_cache()
|
||||
|
||||
# Data to print
|
||||
if self.args.host:
|
||||
data_to_print = self.get_host_info()
|
||||
|
||||
elif self.args.list:
|
||||
# Display list of devices for inventory
|
||||
if self.inventory == self._empty_inventory():
|
||||
data_to_print = self.get_inventory_from_cache()
|
||||
else:
|
||||
data_to_print = self.json_format_dict(self.inventory, True)
|
||||
|
||||
print(data_to_print)
|
||||
|
||||
def is_cache_valid(self):
|
||||
''' Determines if the cache files have expired, or if it is still valid '''
|
||||
|
||||
if os.path.isfile(self.cache_path_cache):
|
||||
mod_time = os.path.getmtime(self.cache_path_cache)
|
||||
current_time = time()
|
||||
if (mod_time + self.cache_max_age) > current_time:
|
||||
if os.path.isfile(self.cache_path_index):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def read_settings(self):
|
||||
''' Reads the settings from the packet_net.ini file '''
|
||||
if six.PY3:
|
||||
config = configparser.ConfigParser()
|
||||
else:
|
||||
config = configparser.SafeConfigParser()
|
||||
|
||||
_ini_path_raw = os.environ.get('PACKET_NET_INI_PATH')
|
||||
|
||||
if _ini_path_raw:
|
||||
packet_ini_path = os.path.expanduser(os.path.expandvars(_ini_path_raw))
|
||||
else:
|
||||
packet_ini_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'packet_net.ini')
|
||||
config.read(packet_ini_path)
|
||||
|
||||
# items per page
|
||||
self.items_per_page = 999
|
||||
if config.has_option(ini_section, 'items_per_page'):
|
||||
config.get(ini_section, 'items_per_page')
|
||||
|
||||
# Instance states to be gathered in inventory. Default is all of them.
|
||||
packet_valid_device_states = [
|
||||
'active',
|
||||
'inactive',
|
||||
'queued',
|
||||
'provisioning'
|
||||
]
|
||||
self.packet_device_states = []
|
||||
if config.has_option(ini_section, 'device_states'):
|
||||
for device_state in config.get(ini_section, 'device_states').split(','):
|
||||
device_state = device_state.strip()
|
||||
if device_state not in packet_valid_device_states:
|
||||
continue
|
||||
self.packet_device_states.append(device_state)
|
||||
else:
|
||||
self.packet_device_states = packet_valid_device_states
|
||||
|
||||
# Cache related
|
||||
cache_dir = os.path.expanduser(config.get(ini_section, 'cache_path'))
|
||||
if not os.path.exists(cache_dir):
|
||||
os.makedirs(cache_dir)
|
||||
|
||||
self.cache_path_cache = cache_dir + "/ansible-packet.cache"
|
||||
self.cache_path_index = cache_dir + "/ansible-packet.index"
|
||||
self.cache_max_age = config.getint(ini_section, 'cache_max_age')
|
||||
|
||||
# Configure nested groups instead of flat namespace.
|
||||
if config.has_option(ini_section, 'nested_groups'):
|
||||
self.nested_groups = config.getboolean(ini_section, 'nested_groups')
|
||||
else:
|
||||
self.nested_groups = False
|
||||
|
||||
# Replace dash or not in group names
|
||||
if config.has_option(ini_section, 'replace_dash_in_groups'):
|
||||
self.replace_dash_in_groups = config.getboolean(ini_section, 'replace_dash_in_groups')
|
||||
else:
|
||||
self.replace_dash_in_groups = True
|
||||
|
||||
# Configure which groups should be created.
|
||||
group_by_options = [
|
||||
'group_by_device_id',
|
||||
'group_by_hostname',
|
||||
'group_by_facility',
|
||||
'group_by_project',
|
||||
'group_by_operating_system',
|
||||
'group_by_plan_type',
|
||||
'group_by_tags',
|
||||
'group_by_tag_none',
|
||||
]
|
||||
for option in group_by_options:
|
||||
if config.has_option(ini_section, option):
|
||||
setattr(self, option, config.getboolean(ini_section, option))
|
||||
else:
|
||||
setattr(self, option, True)
|
||||
|
||||
# Do we need to just include hosts that match a pattern?
|
||||
try:
|
||||
pattern_include = config.get(ini_section, 'pattern_include')
|
||||
if pattern_include and len(pattern_include) > 0:
|
||||
self.pattern_include = re.compile(pattern_include)
|
||||
else:
|
||||
self.pattern_include = None
|
||||
except configparser.NoOptionError:
|
||||
self.pattern_include = None
|
||||
|
||||
# Do we need to exclude hosts that match a pattern?
|
||||
try:
|
||||
pattern_exclude = config.get(ini_section, 'pattern_exclude')
|
||||
if pattern_exclude and len(pattern_exclude) > 0:
|
||||
self.pattern_exclude = re.compile(pattern_exclude)
|
||||
else:
|
||||
self.pattern_exclude = None
|
||||
except configparser.NoOptionError:
|
||||
self.pattern_exclude = None
|
||||
|
||||
# Projects
|
||||
self.projects = []
|
||||
configProjects = config.get(ini_section, 'projects')
|
||||
configProjects_exclude = config.get(ini_section, 'projects_exclude')
|
||||
if (configProjects == 'all'):
|
||||
for projectInfo in self.get_projects():
|
||||
if projectInfo.name not in configProjects_exclude:
|
||||
self.projects.append(projectInfo.name)
|
||||
else:
|
||||
self.projects = configProjects.split(",")
|
||||
|
||||
def parse_cli_args(self):
|
||||
''' Command line argument processing '''
|
||||
|
||||
parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on Packet')
|
||||
parser.add_argument('--list', action='store_true', default=True,
|
||||
help='List Devices (default: True)')
|
||||
parser.add_argument('--host', action='store',
|
||||
help='Get all the variables about a specific device')
|
||||
parser.add_argument('--refresh-cache', action='store_true', default=False,
|
||||
help='Force refresh of cache by making API requests to Packet (default: False - use cache files)')
|
||||
self.args = parser.parse_args()
|
||||
|
||||
def do_api_calls_update_cache(self):
|
||||
''' Do API calls to each region, and save data in cache files '''
|
||||
|
||||
for projectInfo in self.get_projects():
|
||||
if projectInfo.name in self.projects:
|
||||
self.get_devices_by_project(projectInfo)
|
||||
|
||||
self.write_to_cache(self.inventory, self.cache_path_cache)
|
||||
self.write_to_cache(self.index, self.cache_path_index)
|
||||
|
||||
def connect(self):
|
||||
''' create connection to api server'''
|
||||
token = os.environ.get('PACKET_API_TOKEN')
|
||||
if token is None:
|
||||
raise Exception("Error reading token from environment (PACKET_API_TOKEN)!")
|
||||
manager = packet.Manager(auth_token=token)
|
||||
return manager
|
||||
|
||||
def get_projects(self):
|
||||
'''Makes a Packet API call to get the list of projects'''
|
||||
try:
|
||||
manager = self.connect()
|
||||
projects = manager.list_projects()
|
||||
return projects
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
self.fail_with_error(e, 'getting Packet projects')
|
||||
|
||||
def get_devices_by_project(self, project):
|
||||
''' Makes an Packet API call to the list of devices in a particular
|
||||
project '''
|
||||
|
||||
params = {
|
||||
'per_page': self.items_per_page
|
||||
}
|
||||
|
||||
try:
|
||||
manager = self.connect()
|
||||
devices = manager.list_devices(project_id=project.id, params=params)
|
||||
|
||||
for device in devices:
|
||||
self.add_device(device, project)
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
self.fail_with_error(e, 'getting Packet devices')
|
||||
|
||||
def fail_with_error(self, err_msg, err_operation=None):
|
||||
'''log an error to std err for ansible-playbook to consume and exit'''
|
||||
if err_operation:
|
||||
err_msg = 'ERROR: "{err_msg}", while: {err_operation}\n'.format(
|
||||
err_msg=err_msg, err_operation=err_operation)
|
||||
sys.stderr.write(err_msg)
|
||||
sys.exit(1)
|
||||
|
||||
def get_device(self, device_id):
|
||||
manager = self.connect()
|
||||
|
||||
device = manager.get_device(device_id)
|
||||
return device
|
||||
|
||||
def add_device(self, device, project):
|
||||
''' Adds a device to the inventory and index, as long as it is
|
||||
addressable '''
|
||||
|
||||
# Only return devices with desired device states
|
||||
if device.state not in self.packet_device_states:
|
||||
return
|
||||
|
||||
# Select the best destination address
|
||||
dest = None
|
||||
for ip_address in device.ip_addresses:
|
||||
if ip_address['public'] is True and ip_address['address_family'] == 4:
|
||||
dest = ip_address['address']
|
||||
|
||||
if not dest:
|
||||
# Skip devices we cannot address (e.g. private VPC subnet)
|
||||
return
|
||||
|
||||
# if we only want to include hosts that match a pattern, skip those that don't
|
||||
if self.pattern_include and not self.pattern_include.match(device.hostname):
|
||||
return
|
||||
|
||||
# if we need to exclude hosts that match a pattern, skip those
|
||||
if self.pattern_exclude and self.pattern_exclude.match(device.hostname):
|
||||
return
|
||||
|
||||
# Add to index
|
||||
self.index[dest] = [project.id, device.id]
|
||||
|
||||
# Inventory: Group by device ID (always a group of 1)
|
||||
if self.group_by_device_id:
|
||||
self.inventory[device.id] = [dest]
|
||||
if self.nested_groups:
|
||||
self.push_group(self.inventory, 'devices', device.id)
|
||||
|
||||
# Inventory: Group by device name (hopefully a group of 1)
|
||||
if self.group_by_hostname:
|
||||
self.push(self.inventory, device.hostname, dest)
|
||||
if self.nested_groups:
|
||||
self.push_group(self.inventory, 'hostnames', project.name)
|
||||
|
||||
# Inventory: Group by project
|
||||
if self.group_by_project:
|
||||
self.push(self.inventory, project.name, dest)
|
||||
if self.nested_groups:
|
||||
self.push_group(self.inventory, 'projects', project.name)
|
||||
|
||||
# Inventory: Group by facility
|
||||
if self.group_by_facility:
|
||||
self.push(self.inventory, device.facility['code'], dest)
|
||||
if self.nested_groups:
|
||||
if self.group_by_facility:
|
||||
self.push_group(self.inventory, project.name, device.facility['code'])
|
||||
|
||||
# Inventory: Group by OS
|
||||
if self.group_by_operating_system:
|
||||
self.push(self.inventory, device.operating_system.slug, dest)
|
||||
if self.nested_groups:
|
||||
self.push_group(self.inventory, 'operating_systems', device.operating_system.slug)
|
||||
|
||||
# Inventory: Group by plan type
|
||||
if self.group_by_plan_type:
|
||||
self.push(self.inventory, device.plan['slug'], dest)
|
||||
if self.nested_groups:
|
||||
self.push_group(self.inventory, 'plans', device.plan['slug'])
|
||||
|
||||
# Inventory: Group by tag keys
|
||||
if self.group_by_tags:
|
||||
for k in device.tags:
|
||||
key = self.to_safe("tag_" + k)
|
||||
self.push(self.inventory, key, dest)
|
||||
if self.nested_groups:
|
||||
self.push_group(self.inventory, 'tags', self.to_safe("tag_" + k))
|
||||
|
||||
# Global Tag: devices without tags
|
||||
if self.group_by_tag_none and len(device.tags) == 0:
|
||||
self.push(self.inventory, 'tag_none', dest)
|
||||
if self.nested_groups:
|
||||
self.push_group(self.inventory, 'tags', 'tag_none')
|
||||
|
||||
# Global Tag: tag all Packet devices
|
||||
self.push(self.inventory, 'packet', dest)
|
||||
|
||||
self.inventory["_meta"]["hostvars"][dest] = self.get_host_info_dict_from_device(device)
|
||||
|
||||
def get_host_info_dict_from_device(self, device):
|
||||
device_vars = {}
|
||||
for key in vars(device):
|
||||
value = getattr(device, key)
|
||||
key = self.to_safe('packet_' + key)
|
||||
|
||||
# Handle complex types
|
||||
if key == 'packet_state':
|
||||
device_vars[key] = device.state or ''
|
||||
elif key == 'packet_hostname':
|
||||
device_vars[key] = value
|
||||
elif isinstance(value, (int, bool)):
|
||||
device_vars[key] = value
|
||||
elif isinstance(value, six.string_types):
|
||||
device_vars[key] = value.strip()
|
||||
elif value is None:
|
||||
device_vars[key] = ''
|
||||
elif key == 'packet_facility':
|
||||
device_vars[key] = value['code']
|
||||
elif key == 'packet_operating_system':
|
||||
device_vars[key] = value.slug
|
||||
elif key == 'packet_plan':
|
||||
device_vars[key] = value['slug']
|
||||
elif key == 'packet_tags':
|
||||
for k in value:
|
||||
key = self.to_safe('packet_tag_' + k)
|
||||
device_vars[key] = k
|
||||
else:
|
||||
pass
|
||||
# print key
|
||||
# print type(value)
|
||||
# print value
|
||||
|
||||
return device_vars
|
||||
|
||||
def get_host_info(self):
|
||||
''' Get variables about a specific host '''
|
||||
|
||||
if len(self.index) == 0:
|
||||
# Need to load index from cache
|
||||
self.load_index_from_cache()
|
||||
|
||||
if self.args.host not in self.index:
|
||||
# try updating the cache
|
||||
self.do_api_calls_update_cache()
|
||||
if self.args.host not in self.index:
|
||||
# host might not exist anymore
|
||||
return self.json_format_dict({}, True)
|
||||
|
||||
(project_id, device_id) = self.index[self.args.host]
|
||||
|
||||
device = self.get_device(device_id)
|
||||
return self.json_format_dict(self.get_host_info_dict_from_device(device), True)
|
||||
|
||||
def push(self, my_dict, key, element):
|
||||
''' Push an element onto an array that may not have been defined in
|
||||
the dict '''
|
||||
group_info = my_dict.setdefault(key, [])
|
||||
if isinstance(group_info, dict):
|
||||
host_list = group_info.setdefault('hosts', [])
|
||||
host_list.append(element)
|
||||
else:
|
||||
group_info.append(element)
|
||||
|
||||
def push_group(self, my_dict, key, element):
|
||||
''' Push a group as a child of another group. '''
|
||||
parent_group = my_dict.setdefault(key, {})
|
||||
if not isinstance(parent_group, dict):
|
||||
parent_group = my_dict[key] = {'hosts': parent_group}
|
||||
child_groups = parent_group.setdefault('children', [])
|
||||
if element not in child_groups:
|
||||
child_groups.append(element)
|
||||
|
||||
def get_inventory_from_cache(self):
|
||||
''' Reads the inventory from the cache file and returns it as a JSON
|
||||
object '''
|
||||
|
||||
cache = open(self.cache_path_cache, 'r')
|
||||
json_inventory = cache.read()
|
||||
return json_inventory
|
||||
|
||||
def load_index_from_cache(self):
|
||||
''' Reads the index from the cache file sets self.index '''
|
||||
|
||||
cache = open(self.cache_path_index, 'r')
|
||||
json_index = cache.read()
|
||||
self.index = json.loads(json_index)
|
||||
|
||||
def write_to_cache(self, data, filename):
|
||||
''' Writes data in JSON format to a file '''
|
||||
|
||||
json_data = self.json_format_dict(data, True)
|
||||
cache = open(filename, 'w')
|
||||
cache.write(json_data)
|
||||
cache.close()
|
||||
|
||||
def uncammelize(self, key):
|
||||
temp = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', key)
|
||||
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', temp).lower()
|
||||
|
||||
def to_safe(self, word):
|
||||
''' Converts 'bad' characters in a string to underscores so they can be used as Ansible groups '''
|
||||
regex = "[^A-Za-z0-9\_"
|
||||
if not self.replace_dash_in_groups:
|
||||
regex += "\-"
|
||||
return re.sub(regex + "]", "_", word)
|
||||
|
||||
def json_format_dict(self, data, pretty=False):
|
||||
''' Converts a dict to a JSON object and dumps it as a formatted
|
||||
string '''
|
||||
|
||||
if pretty:
|
||||
return json.dumps(data, sort_keys=True, indent=2)
|
||||
else:
|
||||
return json.dumps(data)
|
||||
|
||||
|
||||
# Run the script
|
||||
PacketInventory()
|
||||
@ -25,6 +25,7 @@
|
||||
#
|
||||
# { "groups": ["utility", "databases"], "a": false, "b": true }
|
||||
|
||||
import urllib
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
@ -34,16 +35,13 @@ import sys
|
||||
from optparse import OptionParser
|
||||
|
||||
from six import iteritems
|
||||
from six.moves.urllib.parse import urlencode
|
||||
|
||||
from ansible.module_utils.urls import open_url
|
||||
|
||||
|
||||
class ProxmoxNodeList(list):
|
||||
def get_names(self):
|
||||
return [node['node'] for node in self]
|
||||
|
||||
|
||||
class ProxmoxVM(dict):
|
||||
def get_variables(self):
|
||||
variables = {}
|
||||
@ -51,11 +49,8 @@ class ProxmoxVM(dict):
|
||||
variables['proxmox_' + key] = value
|
||||
return variables
|
||||
|
||||
|
||||
class ProxmoxVMList(list):
|
||||
def __init__(self, data=None):
|
||||
data = [] if data is None else data
|
||||
|
||||
def __init__(self, data=[]):
|
||||
for item in data:
|
||||
self.append(ProxmoxVM(item))
|
||||
|
||||
@ -73,17 +68,14 @@ class ProxmoxVMList(list):
|
||||
|
||||
return variables
|
||||
|
||||
|
||||
class ProxmoxPoolList(list):
|
||||
def get_names(self):
|
||||
return [pool['poolid'] for pool in self]
|
||||
|
||||
|
||||
class ProxmoxPool(dict):
|
||||
def get_members_name(self):
|
||||
return [member['name'] for member in self['members'] if member['template'] != 1]
|
||||
|
||||
|
||||
class ProxmoxAPI(object):
|
||||
def __init__(self, options):
|
||||
self.options = options
|
||||
@ -99,7 +91,7 @@ class ProxmoxAPI(object):
|
||||
def auth(self):
|
||||
request_path = '{}api2/json/access/ticket'.format(self.options.url)
|
||||
|
||||
request_params = urlencode({
|
||||
request_params = urllib.urlencode({
|
||||
'username': self.options.username,
|
||||
'password': self.options.password,
|
||||
})
|
||||
@ -147,7 +139,6 @@ class ProxmoxAPI(object):
|
||||
def pool(self, poolid):
|
||||
return ProxmoxPool(self.get('api2/json/pools/{}'.format(poolid)))
|
||||
|
||||
|
||||
def main_list(options):
|
||||
results = {
|
||||
'all': {
|
||||
@ -208,7 +199,6 @@ def main_list(options):
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main_host(options):
|
||||
proxmox_api = ProxmoxAPI(options)
|
||||
proxmox_api.auth()
|
||||
@ -221,7 +211,6 @@ def main_host(options):
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def main():
|
||||
parser = OptionParser(usage='%prog [options] --list | --host HOSTNAME')
|
||||
parser.add_option('--list', action="store_true", default=False, dest="list")
|
||||
@ -246,6 +235,5 @@ def main():
|
||||
|
||||
print(json.dumps(data, indent=indent))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@ -1,46 +1,31 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
#!/usr/bin/python
|
||||
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
import os
|
||||
import argparse
|
||||
import types
|
||||
|
||||
RACKHD_URL = 'http://localhost:8080'
|
||||
|
||||
|
||||
class RackhdInventory(object):
|
||||
def __init__(self, nodeids):
|
||||
self._inventory = {}
|
||||
for nodeid in nodeids:
|
||||
self._load_inventory_data(nodeid)
|
||||
inventory = {}
|
||||
for (nodeid, info) in self._inventory.items():
|
||||
inventory[nodeid] = (self._format_output(nodeid, info))
|
||||
for nodeid,info in self._inventory.items():
|
||||
inventory[nodeid]= (self._format_output(nodeid, info))
|
||||
print(json.dumps(inventory))
|
||||
|
||||
def _load_inventory_data(self, nodeid):
|
||||
info = {}
|
||||
info['ohai'] = RACKHD_URL + '/api/common/nodes/{0}/catalogs/ohai'.format(nodeid)
|
||||
info['ohai'] = RACKHD_URL + '/api/common/nodes/{0}/catalogs/ohai'.format(nodeid )
|
||||
info['lookup'] = RACKHD_URL + '/api/common/lookups/?q={0}'.format(nodeid)
|
||||
|
||||
results = {}
|
||||
for (key, url) in info.items():
|
||||
r = requests.get(url, verify=False)
|
||||
for key,url in info.items():
|
||||
r = requests.get( url, verify=False)
|
||||
results[key] = r.text
|
||||
self._inventory[nodeid] = results
|
||||
|
||||
@ -50,8 +35,8 @@ class RackhdInventory(object):
|
||||
ipaddress = ''
|
||||
if len(node_info) > 0:
|
||||
ipaddress = node_info[0]['ipAddress']
|
||||
output = {'hosts': [ipaddress], 'vars': {}}
|
||||
for (key, result) in info.items():
|
||||
output = { 'hosts':[ipaddress],'vars':{}}
|
||||
for key,result in info.items():
|
||||
output['vars'][key] = json.loads(result)
|
||||
output['vars']['ansible_ssh_user'] = 'monorail'
|
||||
except KeyError:
|
||||
@ -65,12 +50,11 @@ def parse_args():
|
||||
parser.add_argument('--list', action='store_true')
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
try:
|
||||
# check if rackhd url(ie:10.1.1.45:8080) is specified in the environment
|
||||
#check if rackhd url(ie:10.1.1.45:8080) is specified in the environment
|
||||
RACKHD_URL = 'http://' + str(os.environ['RACKHD_URL'])
|
||||
except:
|
||||
# use default values
|
||||
#use default values
|
||||
pass
|
||||
|
||||
# Use the nodeid specified in the environment to limit the data returned
|
||||
@ -86,7 +70,7 @@ if (parse_args().host):
|
||||
if (parse_args().list):
|
||||
try:
|
||||
url = RACKHD_URL + '/api/common/nodes'
|
||||
r = requests.get(url, verify=False)
|
||||
r = requests.get( url, verify=False)
|
||||
data = json.loads(r.text)
|
||||
for entry in data:
|
||||
if entry['type'] == 'compute':
|
||||
|
||||
@ -155,6 +155,8 @@ import ConfigParser
|
||||
|
||||
from six import iteritems
|
||||
|
||||
from ansible.constants import get_config, mk_boolean
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
@ -168,11 +170,8 @@ except ImportError:
|
||||
|
||||
from time import time
|
||||
|
||||
from ansible.constants import get_config
|
||||
from ansible.module_utils.parsing.convert_bool import boolean
|
||||
from ansible.module_utils.six import text_type
|
||||
|
||||
NON_CALLABLES = (text_type, str, bool, dict, int, list, type(None))
|
||||
NON_CALLABLES = (basestring, bool, dict, int, list, type(None))
|
||||
|
||||
|
||||
def load_config_file():
|
||||
@ -227,25 +226,12 @@ def _list_into_cache(regions):
|
||||
|
||||
prefix = get_config(p, 'rax', 'meta_prefix', 'RAX_META_PREFIX', 'meta')
|
||||
|
||||
networks = get_config(p, 'rax', 'access_network', 'RAX_ACCESS_NETWORK',
|
||||
'public', islist=True)
|
||||
try:
|
||||
# Ansible 2.3+
|
||||
networks = get_config(p, 'rax', 'access_network',
|
||||
'RAX_ACCESS_NETWORK', 'public', value_type='list')
|
||||
except TypeError:
|
||||
# Ansible 2.2.x and below
|
||||
# pylint: disable=unexpected-keyword-arg
|
||||
networks = get_config(p, 'rax', 'access_network',
|
||||
'RAX_ACCESS_NETWORK', 'public', islist=True)
|
||||
try:
|
||||
try:
|
||||
# Ansible 2.3+
|
||||
ip_versions = map(int, get_config(p, 'rax', 'access_ip_version',
|
||||
'RAX_ACCESS_IP_VERSION', 4, value_type='list'))
|
||||
except TypeError:
|
||||
# Ansible 2.2.x and below
|
||||
# pylint: disable=unexpected-keyword-arg
|
||||
ip_versions = map(int, get_config(p, 'rax', 'access_ip_version',
|
||||
'RAX_ACCESS_IP_VERSION', 4, islist=True))
|
||||
ip_versions = map(int, get_config(p, 'rax', 'access_ip_version',
|
||||
'RAX_ACCESS_IP_VERSION', 4,
|
||||
islist=True))
|
||||
except:
|
||||
ip_versions = [4]
|
||||
else:
|
||||
@ -293,7 +279,7 @@ def _list_into_cache(regions):
|
||||
if not cbs_attachments[region]:
|
||||
cbs = pyrax.connect_to_cloud_blockstorage(region)
|
||||
for vol in cbs.list():
|
||||
if boolean(vol.bootable, strict=False):
|
||||
if mk_boolean(vol.bootable):
|
||||
for attachment in vol.attachments:
|
||||
metadata = vol.volume_image_metadata
|
||||
server_id = attachment['server_id']
|
||||
@ -372,8 +358,8 @@ def _list(regions, refresh_cache=True):
|
||||
'RAX_CACHE_MAX_AGE', 600))
|
||||
|
||||
if (not os.path.exists(get_cache_file_path(regions)) or
|
||||
refresh_cache or
|
||||
(time() - os.stat(get_cache_file_path(regions))[-1]) > cache_max_age):
|
||||
refresh_cache or
|
||||
(time() - os.stat(get_cache_file_path(regions))[-1]) > cache_max_age):
|
||||
# Cache file doesn't exist or older than 10m or refresh cache requested
|
||||
_list_into_cache(regions)
|
||||
|
||||
@ -436,16 +422,8 @@ def setup():
|
||||
if region:
|
||||
regions.append(region)
|
||||
else:
|
||||
try:
|
||||
# Ansible 2.3+
|
||||
region_list = get_config(p, 'rax', 'regions', 'RAX_REGION', 'all',
|
||||
value_type='list')
|
||||
except TypeError:
|
||||
# Ansible 2.2.x and below
|
||||
# pylint: disable=unexpected-keyword-arg
|
||||
region_list = get_config(p, 'rax', 'regions', 'RAX_REGION', 'all',
|
||||
islist=True)
|
||||
|
||||
region_list = get_config(p, 'rax', 'regions', 'RAX_REGION', 'all',
|
||||
islist=True)
|
||||
for region in region_list:
|
||||
region = region.strip().upper()
|
||||
if region == 'ALL':
|
||||
|
||||
@ -1 +0,0 @@
|
||||
ovirt4.py
|
||||
@ -14,7 +14,7 @@ disable_ssl_certificate_validation = True
|
||||
# Your Rudder API token, created in the Web interface.
|
||||
token = aaabbbccc
|
||||
|
||||
# Rudder API version to use, use "latest" for latest available
|
||||
# Rudder API version to use, use "latest" for lastest available
|
||||
# version.
|
||||
version = latest
|
||||
|
||||
@ -23,7 +23,7 @@ version = latest
|
||||
group_name = displayName
|
||||
|
||||
# Fail if there are two groups with the same name or two hosts with the
|
||||
# same hostname in the output.
|
||||
# same hostname in the output.
|
||||
fail_if_name_collision = True
|
||||
|
||||
# We cache the results of Rudder API in a local file
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
SoftLayer external inventory script.
|
||||
|
||||
The SoftLayer Python API client is required. Use `pip install softlayer` to install it.
|
||||
You have a few different options for configuring your username and api_key. You can pass
|
||||
You have a few different options for configuring your username and api_key. You can pass
|
||||
environment variables (SL_USERNAME and SL_API_KEY). You can also write INI file to
|
||||
~/.softlayer or /etc/softlayer.conf. For more information see the SL API at:
|
||||
- https://softlayer-python.readthedocs.org/en/latest/config_file.html
|
||||
@ -35,44 +35,14 @@ via the command `sl config setup`.
|
||||
import SoftLayer
|
||||
import re
|
||||
import argparse
|
||||
import itertools
|
||||
try:
|
||||
import json
|
||||
except:
|
||||
import simplejson as json
|
||||
|
||||
|
||||
class SoftLayerInventory(object):
|
||||
common_items = [
|
||||
'id',
|
||||
'globalIdentifier',
|
||||
'hostname',
|
||||
'domain',
|
||||
'fullyQualifiedDomainName',
|
||||
'primaryBackendIpAddress',
|
||||
'primaryIpAddress',
|
||||
'datacenter',
|
||||
'tagReferences.tag.name',
|
||||
'userData.value',
|
||||
]
|
||||
|
||||
vs_items = [
|
||||
'lastKnownPowerState.name',
|
||||
'powerState',
|
||||
'maxCpu',
|
||||
'maxMemory',
|
||||
'activeTransaction.transactionStatus[friendlyName,name]',
|
||||
'status',
|
||||
]
|
||||
|
||||
hw_items = [
|
||||
'hardwareStatusId',
|
||||
'processorPhysicalCoreAmount',
|
||||
'memoryCapacity',
|
||||
]
|
||||
|
||||
def _empty_inventory(self):
|
||||
return {"_meta": {"hostvars": {}}}
|
||||
return {"_meta" : {"hostvars" : {}}}
|
||||
|
||||
def __init__(self):
|
||||
'''Main path'''
|
||||
@ -97,7 +67,7 @@ class SoftLayerInventory(object):
|
||||
'''Push an element onto an array that may not have been defined in the dict'''
|
||||
|
||||
if key in my_dict:
|
||||
my_dict[key].append(element)
|
||||
my_dict[key].append(element);
|
||||
else:
|
||||
my_dict[key] = [element]
|
||||
|
||||
@ -106,9 +76,9 @@ class SoftLayerInventory(object):
|
||||
|
||||
parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on SoftLayer')
|
||||
parser.add_argument('--list', action='store_true', default=False,
|
||||
help='List instances (default: False)')
|
||||
help='List instances (default: False)')
|
||||
parser.add_argument('--host', action='store',
|
||||
help='Get all the variables about a specific instance')
|
||||
help='Get all the variables about a specific instance')
|
||||
self.args = parser.parse_args()
|
||||
|
||||
def json_format_dict(self, data, pretty=False):
|
||||
@ -138,8 +108,6 @@ class SoftLayerInventory(object):
|
||||
if 'primaryIpAddress' not in instance:
|
||||
return
|
||||
|
||||
instance['userData'] = instance['userData'][0]['value'] if instance['userData'] else ''
|
||||
|
||||
dest = instance['primaryIpAddress']
|
||||
|
||||
self.inventory["_meta"]["hostvars"][dest] = instance
|
||||
@ -171,15 +139,10 @@ class SoftLayerInventory(object):
|
||||
# Inventory: group by type (hardware/virtual)
|
||||
self.push(self.inventory, instance_type, dest)
|
||||
|
||||
# Inventory: group by tag
|
||||
for tag in instance['tagReferences']:
|
||||
self.push(self.inventory, tag['tag']['name'], dest)
|
||||
|
||||
def get_virtual_servers(self):
|
||||
'''Get all the CCI instances'''
|
||||
vs = SoftLayer.VSManager(self.client)
|
||||
mask = "mask[%s]" % ','.join(itertools.chain(self.common_items, self.vs_items))
|
||||
instances = vs.list_instances(mask=mask)
|
||||
instances = vs.list_instances()
|
||||
|
||||
for instance in instances:
|
||||
self.process_instance(instance)
|
||||
@ -187,8 +150,7 @@ class SoftLayerInventory(object):
|
||||
def get_physical_servers(self):
|
||||
'''Get all the hardware instances'''
|
||||
hw = SoftLayer.HardwareManager(self.client)
|
||||
mask = "mask[%s]" % ','.join(itertools.chain(self.common_items, self.hw_items))
|
||||
instances = hw.list_hardware(mask=mask)
|
||||
instances = hw.list_hardware()
|
||||
|
||||
for instance in instances:
|
||||
self.process_instance(instance, 'hardware')
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/bin/env python
|
||||
|
||||
"""
|
||||
Spacewalk external inventory script
|
||||
@ -18,10 +18,10 @@ output of "spacewalk-report system-groups-systems|inventory".
|
||||
|
||||
Tested with Ansible 1.9.2 and spacewalk 2.3
|
||||
"""
|
||||
#
|
||||
#
|
||||
# Author:: Jon Miller <jonEbird@gmail.com>
|
||||
# Copyright:: Copyright (c) 2013, Jon Miller
|
||||
#
|
||||
#
|
||||
# Extended for support of multiple organizations and
|
||||
# adding the "_meta" dictionary to --list output by
|
||||
# Bernhard Lichtinger <bernhard.lichtinger@lrz.de> 2015
|
||||
@ -30,15 +30,15 @@ Tested with Ansible 1.9.2 and spacewalk 2.3
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or (at
|
||||
# your option) any later version.
|
||||
#
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# General Public License for more details.
|
||||
#
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
@ -56,16 +56,13 @@ try:
|
||||
except:
|
||||
import simplejson as json
|
||||
|
||||
base_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
default_ini_file = os.path.join(base_dir, "spacewalk.ini")
|
||||
|
||||
base_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
SW_REPORT = '/usr/bin/spacewalk-report'
|
||||
CACHE_DIR = os.path.join(base_dir, ".spacewalk_reports")
|
||||
CACHE_AGE = 300 # 5min
|
||||
INI_FILE = os.path.expanduser(os.path.expandvars(os.environ.get("SPACEWALK_INI_PATH", default_ini_file)))
|
||||
CACHE_AGE = 300 # 5min
|
||||
INI_FILE = os.path.join(base_dir, "spacewalk.ini")
|
||||
|
||||
|
||||
# Sanity check
|
||||
# Sanity check
|
||||
if not os.path.exists(SW_REPORT):
|
||||
print('Error: %s is required for operation.' % (SW_REPORT), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@ -76,8 +73,7 @@ if not os.path.exists(CACHE_DIR):
|
||||
os.chmod(CACHE_DIR, 0o2775)
|
||||
|
||||
# Helper functions
|
||||
# ------------------------------
|
||||
|
||||
#------------------------------
|
||||
|
||||
def spacewalk_report(name):
|
||||
"""Yield a dictionary form of each CSV output produced by the specified
|
||||
@ -95,7 +91,7 @@ def spacewalk_report(name):
|
||||
lines = open(cache_filename, 'r').readlines()
|
||||
keys = lines[0].strip().split(',')
|
||||
# add 'spacewalk_' prefix to the keys
|
||||
keys = ['spacewalk_' + key for key in keys]
|
||||
keys = [ 'spacewalk_' + key for key in keys ]
|
||||
for line in lines[1:]:
|
||||
values = line.strip().split(',')
|
||||
if len(keys) == len(values):
|
||||
@ -103,7 +99,7 @@ def spacewalk_report(name):
|
||||
|
||||
|
||||
# Options
|
||||
# ------------------------------
|
||||
#------------------------------
|
||||
|
||||
parser = OptionParser(usage="%prog [options] --list | --host <machine>")
|
||||
parser.add_option('--list', default=False, dest="list", action="store_true",
|
||||
@ -113,48 +109,48 @@ parser.add_option('--host', default=None, dest="host",
|
||||
parser.add_option('-H', '--human', dest="human",
|
||||
default=False, action="store_true",
|
||||
help="Produce a friendlier version of either server list or host detail")
|
||||
parser.add_option('-o', '--org', default=None, dest="org_number",
|
||||
help="Limit to spacewalk organization number")
|
||||
parser.add_option('-o', '--org', default=None, dest="org_number",
|
||||
help="Limit to spacewalk organization number")
|
||||
parser.add_option('-p', default=False, dest="prefix_org_name", action="store_true",
|
||||
help="Prefix the group name with the organization number")
|
||||
help="Prefix the group name with the organization number")
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
|
||||
# read spacewalk.ini if present
|
||||
# ------------------------------
|
||||
#------------------------------
|
||||
if os.path.exists(INI_FILE):
|
||||
config = ConfigParser.SafeConfigParser()
|
||||
config.read(INI_FILE)
|
||||
if config.has_option('spacewalk', 'cache_age'):
|
||||
CACHE_AGE = config.get('spacewalk', 'cache_age')
|
||||
if not options.org_number and config.has_option('spacewalk', 'org_number'):
|
||||
options.org_number = config.get('spacewalk', 'org_number')
|
||||
if not options.prefix_org_name and config.has_option('spacewalk', 'prefix_org_name'):
|
||||
options.prefix_org_name = config.getboolean('spacewalk', 'prefix_org_name')
|
||||
if config.has_option('spacewalk' , 'cache_age'):
|
||||
CACHE_AGE = config.get('spacewalk' , 'cache_age')
|
||||
if not options.org_number and config.has_option('spacewalk' , 'org_number'):
|
||||
options.org_number = config.get('spacewalk' , 'org_number')
|
||||
if not options.prefix_org_name and config.has_option('spacewalk' , 'prefix_org_name'):
|
||||
options.prefix_org_name = config.getboolean('spacewalk' , 'prefix_org_name')
|
||||
|
||||
|
||||
# Generate dictionary for mapping group_id to org_id
|
||||
# ------------------------------
|
||||
#------------------------------
|
||||
org_groups = {}
|
||||
try:
|
||||
for group in spacewalk_report('system-groups'):
|
||||
org_groups[group['spacewalk_group_id']] = group['spacewalk_org_id']
|
||||
for group in spacewalk_report('system-groups'):
|
||||
org_groups[group['spacewalk_group_id']] = group['spacewalk_org_id']
|
||||
|
||||
except (OSError) as e:
|
||||
print('Problem executing the command "%s system-groups": %s' %
|
||||
print('Problem executing the command "%s system-groups": %s' %
|
||||
(SW_REPORT, str(e)), file=sys.stderr)
|
||||
sys.exit(2)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
# List out the known server from Spacewalk
|
||||
# ------------------------------
|
||||
#------------------------------
|
||||
if options.list:
|
||||
|
||||
# to build the "_meta"-Group with hostvars first create dictionary for later use
|
||||
host_vars = {}
|
||||
try:
|
||||
for item in spacewalk_report('inventory'):
|
||||
host_vars[item['spacewalk_profile_name']] = dict((key, (value.split(';') if ';' in value else value)) for key, value in item.items())
|
||||
host_vars[ item['spacewalk_profile_name'] ] = dict( ( key, ( value.split(';') if ';' in value else value) ) for key, value in item.items() )
|
||||
|
||||
except (OSError) as e:
|
||||
print('Problem executing the command "%s inventory": %s' %
|
||||
@ -162,11 +158,11 @@ if options.list:
|
||||
sys.exit(2)
|
||||
|
||||
groups = {}
|
||||
meta = {"hostvars": {}}
|
||||
meta = { "hostvars" : {} }
|
||||
try:
|
||||
for system in spacewalk_report('system-groups-systems'):
|
||||
# first get org_id of system
|
||||
org_id = org_groups[system['spacewalk_group_id']]
|
||||
org_id = org_groups[ system['spacewalk_group_id'] ]
|
||||
|
||||
# shall we add the org_id as prefix to the group name:
|
||||
if options.prefix_org_name:
|
||||
@ -182,16 +178,16 @@ if options.list:
|
||||
groups[group_name] = set()
|
||||
|
||||
groups[group_name].add(system['spacewalk_server_name'])
|
||||
if system['spacewalk_server_name'] in host_vars and not system['spacewalk_server_name'] in meta["hostvars"]:
|
||||
meta["hostvars"][system['spacewalk_server_name']] = host_vars[system['spacewalk_server_name']]
|
||||
if system['spacewalk_server_name'] in host_vars and not system['spacewalk_server_name'] in meta[ "hostvars" ]:
|
||||
meta[ "hostvars" ][ system['spacewalk_server_name'] ] = host_vars[ system['spacewalk_server_name'] ]
|
||||
# or we list all groups and systems:
|
||||
else:
|
||||
if group_name not in groups:
|
||||
groups[group_name] = set()
|
||||
|
||||
groups[group_name].add(system['spacewalk_server_name'])
|
||||
if system['spacewalk_server_name'] in host_vars and not system['spacewalk_server_name'] in meta["hostvars"]:
|
||||
meta["hostvars"][system['spacewalk_server_name']] = host_vars[system['spacewalk_server_name']]
|
||||
if system['spacewalk_server_name'] in host_vars and not system['spacewalk_server_name'] in meta[ "hostvars" ]:
|
||||
meta[ "hostvars" ][ system['spacewalk_server_name'] ] = host_vars[ system['spacewalk_server_name'] ]
|
||||
|
||||
except (OSError) as e:
|
||||
print('Problem executing the command "%s system-groups-systems": %s' %
|
||||
@ -202,15 +198,15 @@ if options.list:
|
||||
for group, systems in iteritems(groups):
|
||||
print('[%s]\n%s\n' % (group, '\n'.join(systems)))
|
||||
else:
|
||||
final = dict([(k, list(s)) for k, s in iteritems(groups)])
|
||||
final = dict( [ (k, list(s)) for k, s in iteritems(groups) ] )
|
||||
final["_meta"] = meta
|
||||
print(json.dumps(final))
|
||||
# print(json.dumps(groups))
|
||||
print(json.dumps( final ))
|
||||
#print(json.dumps(groups))
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
# Return a details information concerning the spacewalk server
|
||||
# ------------------------------
|
||||
#------------------------------
|
||||
elif options.host:
|
||||
|
||||
host_details = {}
|
||||
@ -224,13 +220,13 @@ elif options.host:
|
||||
print('Problem executing the command "%s inventory": %s' %
|
||||
(SW_REPORT, str(e)), file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
if options.human:
|
||||
print('Host: %s' % options.host)
|
||||
for k, v in iteritems(host_details):
|
||||
print(' %s: %s' % (k, '\n '.join(v.split(';'))))
|
||||
else:
|
||||
print(json.dumps(dict((key, (value.split(';') if ';' in value else value)) for key, value in host_details.items())))
|
||||
print( json.dumps( dict( ( key, ( value.split(';') if ';' in value else value) ) for key, value in host_details.items() ) ) )
|
||||
sys.exit(0)
|
||||
|
||||
else:
|
||||
|
||||
@ -43,24 +43,21 @@
|
||||
import argparse
|
||||
import os.path
|
||||
import sys
|
||||
from collections import MutableSequence
|
||||
import paramiko
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
import paramiko
|
||||
|
||||
|
||||
SSH_CONF = '~/.ssh/config'
|
||||
|
||||
_key = 'ssh_config'
|
||||
|
||||
_ssh_to_ansible = [('user', 'ansible_ssh_user'),
|
||||
('hostname', 'ansible_ssh_host'),
|
||||
('identityfile', 'ansible_ssh_private_key_file'),
|
||||
('port', 'ansible_ssh_port')]
|
||||
('hostname', 'ansible_ssh_host'),
|
||||
('identityfile', 'ansible_ssh_private_key_file'),
|
||||
('port', 'ansible_ssh_port')]
|
||||
|
||||
|
||||
def get_config():
|
||||
@ -71,7 +68,7 @@ def get_config():
|
||||
cfg.parse(f)
|
||||
ret_dict = {}
|
||||
for d in cfg._config:
|
||||
if isinstance(d['host'], MutableSequence):
|
||||
if type(d['host']) is list:
|
||||
alias = d['host'][0]
|
||||
else:
|
||||
alias = d['host']
|
||||
@ -96,7 +93,7 @@ def print_list():
|
||||
# If the attribute is a list, just take the first element.
|
||||
# Private key is returned in a list for some reason.
|
||||
attr = attributes[ssh_opt]
|
||||
if isinstance(attr, MutableSequence):
|
||||
if type(attr) is list:
|
||||
attr = attr[0]
|
||||
tmp_dict[ans_opt] = attr
|
||||
if tmp_dict:
|
||||
@ -112,7 +109,7 @@ def print_host(host):
|
||||
|
||||
def get_args(args_list):
|
||||
parser = argparse.ArgumentParser(
|
||||
description='ansible inventory script parsing .ssh/config')
|
||||
description='ansible inventory script parsing .ssh/config')
|
||||
mutex_group = parser.add_mutually_exclusive_group(required=True)
|
||||
help_list = 'list all hosts from .ssh/config inventory'
|
||||
mutex_group.add_argument('--list', action='store_true', help=help_list)
|
||||
|
||||
@ -1,191 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright (c) 2016, Hugh Ma <hugh.ma@flextronics.com>
|
||||
#
|
||||
# This module is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This software is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Stacki inventory script
|
||||
# Configure stacki.yml with proper auth information and place in the following:
|
||||
# - ../inventory/stacki.yml
|
||||
# - /etc/stacki/stacki.yml
|
||||
# - /etc/ansible/stacki.yml
|
||||
# The stacki.yml file can contain entries for authentication information
|
||||
# regarding the Stacki front-end node.
|
||||
#
|
||||
# use_hostnames uses hostname rather than interface ip as connection
|
||||
#
|
||||
#
|
||||
|
||||
"""
|
||||
Example Usage:
|
||||
List Stacki Nodes
|
||||
$ ./stack.py --list
|
||||
|
||||
|
||||
Example Configuration:
|
||||
---
|
||||
stacki:
|
||||
auth:
|
||||
stacki_user: admin
|
||||
stacki_password: abc12345678910
|
||||
stacki_endpoint: http://192.168.200.50/stack
|
||||
use_hostnames: false
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
from distutils.version import StrictVersion
|
||||
|
||||
try:
|
||||
import json
|
||||
except:
|
||||
import simplejson as json
|
||||
|
||||
try:
|
||||
import requests
|
||||
except:
|
||||
sys.exit('requests package is required for this inventory script')
|
||||
|
||||
|
||||
CONFIG_FILES = ['/etc/stacki/stacki.yml', '/etc/ansible/stacki.yml']
|
||||
|
||||
|
||||
def stack_auth(params):
|
||||
endpoint = params['stacki_endpoint']
|
||||
auth_creds = {'USERNAME': params['stacki_user'],
|
||||
'PASSWORD': params['stacki_password']}
|
||||
|
||||
client = requests.session()
|
||||
client.get(endpoint)
|
||||
|
||||
init_csrf = client.cookies['csrftoken']
|
||||
|
||||
header = {'csrftoken': init_csrf, 'X-CSRFToken': init_csrf,
|
||||
'Content-type': 'application/x-www-form-urlencoded'}
|
||||
|
||||
login_endpoint = endpoint + "/login"
|
||||
|
||||
login_req = client.post(login_endpoint, data=auth_creds, headers=header)
|
||||
|
||||
csrftoken = login_req.cookies['csrftoken']
|
||||
sessionid = login_req.cookies['sessionid']
|
||||
|
||||
auth_creds.update(CSRFTOKEN=csrftoken, SESSIONID=sessionid)
|
||||
|
||||
return client, auth_creds
|
||||
|
||||
|
||||
def stack_build_header(auth_creds):
|
||||
header = {'csrftoken': auth_creds['CSRFTOKEN'],
|
||||
'X-CSRFToken': auth_creds['CSRFTOKEN'],
|
||||
'sessionid': auth_creds['SESSIONID'],
|
||||
'Content-type': 'application/json'}
|
||||
|
||||
return header
|
||||
|
||||
|
||||
def stack_host_list(endpoint, header, client):
|
||||
|
||||
stack_r = client.post(endpoint, data=json.dumps({"cmd": "list host"}),
|
||||
headers=header)
|
||||
return json.loads(stack_r.json())
|
||||
|
||||
|
||||
def stack_net_list(endpoint, header, client):
|
||||
|
||||
stack_r = client.post(endpoint, data=json.dumps({"cmd": "list host interface"}),
|
||||
headers=header)
|
||||
return json.loads(stack_r.json())
|
||||
|
||||
|
||||
def format_meta(hostdata, intfdata, config):
|
||||
use_hostnames = config['use_hostnames']
|
||||
meta = dict(all=dict(hosts=list()),
|
||||
frontends=dict(hosts=list()),
|
||||
backends=dict(hosts=list()),
|
||||
_meta=dict(hostvars=dict()))
|
||||
|
||||
# Iterate through list of dicts of hosts and remove
|
||||
# environment key as it causes conflicts
|
||||
for host in hostdata:
|
||||
del host['environment']
|
||||
meta['_meta']['hostvars'][host['host']] = host
|
||||
meta['_meta']['hostvars'][host['host']]['interfaces'] = list()
|
||||
|
||||
# @bbyhuy to improve readability in next iteration
|
||||
|
||||
for intf in intfdata:
|
||||
if intf['host'] in meta['_meta']['hostvars']:
|
||||
meta['_meta']['hostvars'][intf['host']]['interfaces'].append(intf)
|
||||
if intf['default'] is True:
|
||||
meta['_meta']['hostvars'][intf['host']]['ansible_host'] = intf['ip']
|
||||
if not use_hostnames:
|
||||
meta['all']['hosts'].append(intf['ip'])
|
||||
if meta['_meta']['hostvars'][intf['host']]['appliance'] != 'frontend':
|
||||
meta['backends']['hosts'].append(intf['ip'])
|
||||
else:
|
||||
meta['frontends']['hosts'].append(intf['ip'])
|
||||
else:
|
||||
meta['all']['hosts'].append(intf['host'])
|
||||
if meta['_meta']['hostvars'][intf['host']]['appliance'] != 'frontend':
|
||||
meta['backends']['hosts'].append(intf['host'])
|
||||
else:
|
||||
meta['frontends']['hosts'].append(intf['host'])
|
||||
return meta
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description='Stacki Inventory Module')
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument('--list', action='store_true',
|
||||
help='List active hosts')
|
||||
group.add_argument('--host', help='List details about the specific host')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
if StrictVersion(requests.__version__) < StrictVersion("2.4.3"):
|
||||
sys.exit('requests>=2.4.3 is required for this inventory script')
|
||||
|
||||
try:
|
||||
config_files = CONFIG_FILES
|
||||
config_files.append(os.path.dirname(os.path.realpath(__file__)) + '/stacki.yml')
|
||||
config = None
|
||||
for cfg_file in config_files:
|
||||
if os.path.isfile(cfg_file):
|
||||
stream = open(cfg_file, 'r')
|
||||
config = yaml.safe_load(stream)
|
||||
break
|
||||
if not config:
|
||||
sys.stderr.write("No config file found at {}\n".format(config_files))
|
||||
sys.exit(1)
|
||||
client, auth_creds = stack_auth(config['stacki']['auth'])
|
||||
header = stack_build_header(auth_creds)
|
||||
host_list = stack_host_list(config['stacki']['auth']['stacki_endpoint'], header, client)
|
||||
intf_list = stack_net_list(config['stacki']['auth']['stacki_endpoint'], header, client)
|
||||
final_meta = format_meta(host_list, intf_list, config)
|
||||
print(json.dumps(final_meta, indent=4))
|
||||
except Exception as e:
|
||||
sys.stderr.write('%s\n' % e.message)
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -1,7 +0,0 @@
|
||||
---
|
||||
stacki:
|
||||
auth:
|
||||
stacki_user: admin
|
||||
stacki_password: GhYgWut1hfGbbnstmbW3m-bJbeME-3EvC20rF1LHrDM
|
||||
stacki_endpoint: http://192.168.200.50/stack
|
||||
use_hostnames: false
|
||||
@ -69,7 +69,7 @@ parser.add_option('--host', default=None, dest="host",
|
||||
|
||||
# get all the ssh configs for all boxes in an array of dictionaries.
|
||||
def get_ssh_config():
|
||||
return dict((k, get_a_ssh_config(k)) for k in list_running_boxes())
|
||||
return {k: get_a_ssh_config(k) for k in list_running_boxes()}
|
||||
|
||||
|
||||
# list all the running boxes
|
||||
@ -102,7 +102,7 @@ def get_a_ssh_config(box_name):
|
||||
if os.path.isfile(id):
|
||||
host_config['identityfile'] = id
|
||||
|
||||
return dict((v, host_config[k]) for k, v in _ssh_to_ansible)
|
||||
return {v: host_config[k] for k, v in _ssh_to_ansible}
|
||||
|
||||
# List out servers that vagrant has running
|
||||
# ------------------------------
|
||||
|
||||
@ -16,21 +16,20 @@
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
from subprocess import Popen, PIPE
|
||||
from subprocess import Popen,PIPE
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
|
||||
class SetEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, set):
|
||||
return list(obj)
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
def default(self, obj):
|
||||
if isinstance(obj, set):
|
||||
return list(obj)
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
VBOX = "VBoxManage"
|
||||
VBOX="VBoxManage"
|
||||
|
||||
|
||||
def get_hosts(host=None):
|
||||
@ -40,7 +39,7 @@ def get_hosts(host=None):
|
||||
if host:
|
||||
p = Popen([VBOX, 'showvminfo', host], stdout=PIPE)
|
||||
else:
|
||||
returned = {'all': set(), '_metadata': {}}
|
||||
returned = { 'all': set(), '_metadata': {} }
|
||||
p = Popen([VBOX, 'list', '-l', 'vms'], stdout=PIPE)
|
||||
except:
|
||||
sys.exit(1)
|
||||
@ -51,7 +50,7 @@ def get_hosts(host=None):
|
||||
for line in p.stdout.readlines():
|
||||
|
||||
try:
|
||||
k, v = line.split(':', 1)
|
||||
k,v = line.split(':',1)
|
||||
except:
|
||||
continue
|
||||
|
||||
@ -63,11 +62,11 @@ def get_hosts(host=None):
|
||||
if v not in hostvars:
|
||||
curname = v
|
||||
hostvars[curname] = {}
|
||||
try: # try to get network info
|
||||
x = Popen([VBOX, 'guestproperty', 'get', curname, "/VirtualBox/GuestInfo/Net/0/V4/IP"], stdout=PIPE)
|
||||
try: # try to get network info
|
||||
x = Popen([VBOX, 'guestproperty', 'get', curname,"/VirtualBox/GuestInfo/Net/0/V4/IP"],stdout=PIPE)
|
||||
ipinfo = x.stdout.read()
|
||||
if 'Value' in ipinfo:
|
||||
a, ip = ipinfo.split(':', 1)
|
||||
a,ip = ipinfo.split(':',1)
|
||||
hostvars[curname]['ansible_ssh_host'] = ip.strip()
|
||||
except:
|
||||
pass
|
||||
@ -84,11 +83,11 @@ def get_hosts(host=None):
|
||||
returned['all'].add(curname)
|
||||
continue
|
||||
|
||||
pref_k = 'vbox_' + k.strip().replace(' ', '_')
|
||||
pref_k = 'vbox_' + k.strip().replace(' ','_')
|
||||
if k.startswith(' '):
|
||||
if prevkey not in hostvars[curname]:
|
||||
hostvars[curname][prevkey] = {}
|
||||
hostvars[curname][prevkey][pref_k] = v
|
||||
hostvars[curname][prevkey][pref_k]= v
|
||||
else:
|
||||
if v != '':
|
||||
hostvars[curname][pref_k] = v
|
||||
|
||||
@ -38,20 +38,18 @@ import os
|
||||
import ssl
|
||||
import sys
|
||||
import time
|
||||
import ConfigParser
|
||||
|
||||
from six import integer_types, text_type, string_types
|
||||
from six.moves import configparser
|
||||
from six import text_type, string_types
|
||||
|
||||
# Disable logging message trigged by pSphere/suds.
|
||||
try:
|
||||
from logging import NullHandler
|
||||
except ImportError:
|
||||
from logging import Handler
|
||||
|
||||
class NullHandler(Handler):
|
||||
def emit(self, record):
|
||||
pass
|
||||
|
||||
logging.getLogger('psphere').addHandler(NullHandler())
|
||||
logging.getLogger('suds').addHandler(NullHandler())
|
||||
|
||||
@ -64,11 +62,11 @@ from suds.sudsobject import Object as SudsObject
|
||||
class VMwareInventory(object):
|
||||
|
||||
def __init__(self, guests_only=None):
|
||||
self.config = configparser.SafeConfigParser()
|
||||
self.config = ConfigParser.SafeConfigParser()
|
||||
if os.environ.get('VMWARE_INI', ''):
|
||||
config_files = [os.environ['VMWARE_INI']]
|
||||
else:
|
||||
config_files = [os.path.abspath(sys.argv[0]).rstrip('.py') + '.ini', 'vmware.ini']
|
||||
config_files = [os.path.abspath(sys.argv[0]).rstrip('.py') + '.ini', 'vmware.ini']
|
||||
for config_file in config_files:
|
||||
if os.path.exists(config_file):
|
||||
self.config.read(config_file)
|
||||
@ -210,7 +208,7 @@ class VMwareInventory(object):
|
||||
if obj_info != ():
|
||||
l.append(obj_info)
|
||||
return l
|
||||
elif isinstance(obj, (type(None), bool, float) + string_types + integer_types):
|
||||
elif isinstance(obj, (type(None), bool, int, long, float, string_types)):
|
||||
return obj
|
||||
else:
|
||||
return ()
|
||||
@ -364,7 +362,7 @@ class VMwareInventory(object):
|
||||
# Loop through all VMs on physical host.
|
||||
for vm in host.vm:
|
||||
if prefix_filter:
|
||||
if vm.name.startswith(prefix_filter):
|
||||
if vm.name.startswith( prefix_filter ):
|
||||
continue
|
||||
self._add_host(inv, 'all', vm.name)
|
||||
self._add_host(inv, vm_group, vm.name)
|
||||
|
||||
@ -8,12 +8,10 @@ server=vcenter
|
||||
# The port for the vsphere API
|
||||
#port=443
|
||||
|
||||
# The username with access to the vsphere API. This setting
|
||||
# may also be defined via the VMWARE_USERNAME environment variable.
|
||||
# The username with access to the vsphere API
|
||||
username=administrator@vsphere.local
|
||||
|
||||
# The password for the vsphere API. This setting
|
||||
# may also be defined via the VMWARE_PASSWORD environment variable.
|
||||
# The password for the vsphere API
|
||||
password=vmware
|
||||
|
||||
# Verify the server's SSL certificate
|
||||
@ -30,8 +28,8 @@ password=vmware
|
||||
|
||||
|
||||
# Max object level refers to the level of recursion the script will delve into
|
||||
# the objects returned from pyvomi to find serializable facts. The default
|
||||
# level of 0 is sufficient for most tasks and will be the most performant.
|
||||
# the objects returned from pyvomi to find serializable facts. The default
|
||||
# level of 0 is sufficient for most tasks and will be the most performant.
|
||||
# Beware that the recursion can exceed python's limit (causing traceback),
|
||||
# cause sluggish script performance and return huge blobs of facts.
|
||||
# If you do not know what you are doing, leave this set to 1.
|
||||
@ -42,16 +40,10 @@ password=vmware
|
||||
#lower_var_keys=True
|
||||
|
||||
|
||||
# Don't retrieve and process some VMware attribute keys
|
||||
# Default values permit to sanitize inventory meta and to improve a little bit
|
||||
# performance by removing non-common group attributes.
|
||||
#skip_keys = declaredalarmstate,disabledmethod,dynamicproperty,dynamictype,environmentbrowser,managedby,parent,childtype,resourceconfig
|
||||
|
||||
|
||||
# Host alias for objects in the inventory. VMWare allows duplicate VM names
|
||||
# so they can not be considered unique. Use this setting to alter the alias
|
||||
# returned for the hosts. Any atributes for the guest can be used to build
|
||||
# this alias. The default combines the config name and the config uuid and
|
||||
# returned for the hosts. Any atributes for the guest can be used to build
|
||||
# this alias. The default combines the config name and the config uuid and
|
||||
# expects that the ansible_host will be set by the host_pattern.
|
||||
#alias_pattern={{ config.name + '_' + config.uuid }}
|
||||
|
||||
@ -61,7 +53,7 @@ password=vmware
|
||||
#host_pattern={{ guest.ipaddress }}
|
||||
|
||||
|
||||
# Host filters are a comma separated list of jinja patterns to remove
|
||||
# Host filters are a comma separated list of jinja patterns to remove
|
||||
# non-matching hosts from the final result.
|
||||
# EXAMPLES:
|
||||
# host_filters={{ config.guestid == 'rhel7_64Guest' }}
|
||||
@ -77,37 +69,3 @@ password=vmware
|
||||
# because those values will become the literal group name. The patterns can be
|
||||
# comma delimited to create as many groups as necessary
|
||||
#groupby_patterns={{ guest.guestid }},{{ 'templates' if config.template else 'guests'}}
|
||||
|
||||
# Group by custom fields will use VMware custom fields to generate hostgroups
|
||||
# based on {{ custom_field_group_prefix }} + field_name + _ + field_value
|
||||
# Set groupby_custom_field to True will enable this feature
|
||||
# If custom field value is comma separated, multiple groups are created.
|
||||
# Warning: This required max_object_level to be set to 2 or greater.
|
||||
#groupby_custom_field = False
|
||||
|
||||
# You can customize prefix used by custom field hostgroups generation here.
|
||||
# vmware_tag_ prefix is the default and consistent with ec2_tag_
|
||||
#custom_field_group_prefix = 'vmware_tag_'
|
||||
|
||||
# The script attempts to recurse into virtualmachine objects and serialize
|
||||
# all available data. The serialization is comprehensive but slow. If the
|
||||
# vcenter environment is large and the desired properties are known, create
|
||||
# a 'properties' section in this config and make an arbitrary list of
|
||||
# key=value settings where the value is a path to a specific property. If
|
||||
# If this feature is enabled, be sure to fetch every property that is used
|
||||
# in the jinja expressions defined above. For performance tuning, reduce
|
||||
# the number of properties to the smallest amount possible and limit the
|
||||
# use of properties that are not direct attributes of vim.VirtualMachine
|
||||
#[properties]
|
||||
#prop01=name
|
||||
#prop02=config.cpuHotAddEnabled
|
||||
#prop03=config.cpuHotRemoveEnabled
|
||||
#prop04=config.instanceUuid
|
||||
#prop05=config.hardware.numCPU
|
||||
#prop06=config.template
|
||||
#prop07=config.name
|
||||
#prop08=guest.hostName
|
||||
#prop09=guest.ipAddress
|
||||
#prop10=guest.guestId
|
||||
#prop11=guest.guestState
|
||||
#prop12=runtime.maxMemoryUsage
|
||||
|
||||
@ -1,26 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# This file is part of Ansible,
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Requirements
|
||||
# - pyvmomi >= 6.0.0.2016.4
|
||||
|
||||
# TODO:
|
||||
# * more jq examples
|
||||
# * optional folder heriarchy
|
||||
# * optional folder heirarchy
|
||||
|
||||
"""
|
||||
$ jq '._meta.hostvars[].config' data.json | head
|
||||
@ -38,69 +23,46 @@ $ jq '._meta.hostvars[].config' data.json | head
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import atexit
|
||||
import datetime
|
||||
import getpass
|
||||
import itertools
|
||||
import json
|
||||
import jinja2
|
||||
import os
|
||||
import re
|
||||
import six
|
||||
import ssl
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
from collections import defaultdict
|
||||
from six.moves import configparser
|
||||
from time import time
|
||||
|
||||
import six
|
||||
from jinja2 import Environment
|
||||
from six import integer_types, string_types
|
||||
from six.moves import configparser
|
||||
|
||||
HAS_PYVMOMI = False
|
||||
try:
|
||||
import argparse
|
||||
except ImportError:
|
||||
sys.exit('Error: This inventory script required "argparse" python module. Please install it or upgrade to python-2.7')
|
||||
|
||||
try:
|
||||
from pyVmomi import vim, vmodl
|
||||
from pyVmomi import vim
|
||||
from pyVim.connect import SmartConnect, Disconnect
|
||||
HAS_PYVMOMI = True
|
||||
except ImportError:
|
||||
sys.exit("ERROR: This inventory script required 'pyVmomi' Python module, it was not able to load it")
|
||||
pass
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
hasvcr = False
|
||||
try:
|
||||
import vcr
|
||||
|
||||
hasvcr = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def regex_match(s, pattern):
|
||||
'''Custom filter for regex matching'''
|
||||
reg = re.compile(pattern)
|
||||
if reg.match(s):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def select_chain_match(inlist, key, pattern):
|
||||
'''Get a key from a list of dicts, squash values to a single list, then filter'''
|
||||
outlist = [x[key] for x in inlist]
|
||||
outlist = list(itertools.chain(*outlist))
|
||||
outlist = [x for x in outlist if regex_match(x, pattern)]
|
||||
return outlist
|
||||
|
||||
|
||||
class VMwareMissingHostException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class VMWareInventory(object):
|
||||
|
||||
__name__ = 'VMWareInventory'
|
||||
|
||||
guest_props = False
|
||||
instances = []
|
||||
debug = False
|
||||
load_dumpfile = None
|
||||
@ -111,47 +73,28 @@ class VMWareInventory(object):
|
||||
cache_max_age = None
|
||||
cache_path_cache = None
|
||||
cache_path_index = None
|
||||
cache_dir = None
|
||||
server = None
|
||||
port = None
|
||||
username = None
|
||||
password = None
|
||||
validate_certs = True
|
||||
host_filters = []
|
||||
skip_keys = []
|
||||
groupby_patterns = []
|
||||
|
||||
safe_types = [bool, str, float, None] + list(integer_types)
|
||||
iter_types = [dict, list]
|
||||
|
||||
bad_types = ['Array', 'disabledMethod', 'declaredAlarmState']
|
||||
if (sys.version_info > (3, 0)):
|
||||
safe_types = [int, bool, str, float, None]
|
||||
else:
|
||||
safe_types = [int, long, bool, str, float, None]
|
||||
iter_types = [dict, list]
|
||||
skip_keys = ['dynamicproperty', 'dynamictype', 'managedby', 'childtype']
|
||||
|
||||
vimTableMaxDepth = {
|
||||
"vim.HostSystem": 2,
|
||||
"vim.VirtualMachine": 2,
|
||||
}
|
||||
|
||||
custom_fields = {}
|
||||
def _empty_inventory(self):
|
||||
return {"_meta" : {"hostvars" : {}}}
|
||||
|
||||
# use jinja environments to allow for custom filters
|
||||
env = Environment()
|
||||
env.filters['regex_match'] = regex_match
|
||||
env.filters['select_chain_match'] = select_chain_match
|
||||
|
||||
# translation table for attributes to fetch for known vim types
|
||||
|
||||
vimTable = {
|
||||
vim.Datastore: ['_moId', 'name'],
|
||||
vim.ResourcePool: ['_moId', 'name'],
|
||||
vim.HostSystem: ['_moId', 'name'],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _empty_inventory():
|
||||
return {"_meta": {"hostvars": {}}}
|
||||
|
||||
def __init__(self, load=True):
|
||||
self.inventory = VMWareInventory._empty_inventory()
|
||||
self.inventory = self._empty_inventory()
|
||||
|
||||
if load:
|
||||
# Read settings and parse CLI arguments
|
||||
@ -165,7 +108,7 @@ class VMWareInventory(object):
|
||||
if self.args.refresh_cache or not cache_valid:
|
||||
self.do_api_calls_update_cache()
|
||||
else:
|
||||
self.debugl('loading inventory from cache')
|
||||
self.debugl('# loading inventory from cache')
|
||||
self.inventory = self.get_inventory_from_cache()
|
||||
|
||||
def debugl(self, text):
|
||||
@ -173,12 +116,11 @@ class VMWareInventory(object):
|
||||
try:
|
||||
text = str(text)
|
||||
except UnicodeEncodeError:
|
||||
text = text.encode('ascii', 'ignore')
|
||||
print('%s %s' % (datetime.datetime.now(), text))
|
||||
text = text.encode('ascii','ignore')
|
||||
print(text)
|
||||
|
||||
def show(self):
|
||||
# Data to print
|
||||
self.debugl('dumping results')
|
||||
data_to_print = None
|
||||
if self.args.host:
|
||||
data_to_print = self.get_host_info(self.args.host)
|
||||
@ -187,6 +129,7 @@ class VMWareInventory(object):
|
||||
data_to_print = self.inventory
|
||||
return json.dumps(data_to_print, indent=2)
|
||||
|
||||
|
||||
def is_cache_valid(self):
|
||||
|
||||
''' Determines if the cache files have expired, or if it is still valid '''
|
||||
@ -201,20 +144,25 @@ class VMWareInventory(object):
|
||||
|
||||
return valid
|
||||
|
||||
|
||||
def do_api_calls_update_cache(self):
|
||||
|
||||
''' Get instances and cache the data '''
|
||||
|
||||
self.inventory = self.instances_to_inventory(self.get_instances())
|
||||
self.write_to_cache(self.inventory)
|
||||
instances = self.get_instances()
|
||||
self.instances = instances
|
||||
self.inventory = self.instances_to_inventory(instances)
|
||||
self.write_to_cache(self.inventory, self.cache_path_cache)
|
||||
|
||||
def write_to_cache(self, data):
|
||||
|
||||
def write_to_cache(self, data, cache_path):
|
||||
|
||||
''' Dump inventory to json file '''
|
||||
|
||||
with open(self.cache_path_cache, 'wb') as f:
|
||||
f.write(json.dumps(data))
|
||||
|
||||
|
||||
def get_inventory_from_cache(self):
|
||||
|
||||
''' Read in jsonified inventory '''
|
||||
@ -224,6 +172,7 @@ class VMWareInventory(object):
|
||||
jdata = f.read()
|
||||
return json.loads(jdata)
|
||||
|
||||
|
||||
def read_settings(self):
|
||||
|
||||
''' Reads the settings from the vmware_inventory.ini file '''
|
||||
@ -242,24 +191,13 @@ class VMWareInventory(object):
|
||||
'cache_name': 'ansible-vmware',
|
||||
'cache_path': '~/.ansible/tmp',
|
||||
'cache_max_age': 3600,
|
||||
'max_object_level': 1,
|
||||
'skip_keys': 'declaredalarmstate,'
|
||||
'disabledmethod,'
|
||||
'dynamicproperty,'
|
||||
'dynamictype,'
|
||||
'environmentbrowser,'
|
||||
'managedby,'
|
||||
'parent,'
|
||||
'childtype,'
|
||||
'resourceconfig',
|
||||
'alias_pattern': '{{ config.name + "_" + config.uuid }}',
|
||||
'host_pattern': '{{ guest.ipaddress }}',
|
||||
'host_filters': '{{ guest.gueststate == "running" }}',
|
||||
'groupby_patterns': '{{ guest.guestid }},{{ "templates" if config.template else "guests"}}',
|
||||
'lower_var_keys': True,
|
||||
'custom_field_group_prefix': 'vmware_tag_',
|
||||
'groupby_custom_field': False}
|
||||
}
|
||||
'max_object_level': 1,
|
||||
'alias_pattern': '{{ config.name + "_" + config.uuid }}',
|
||||
'host_pattern': '{{ guest.ipaddress }}',
|
||||
'host_filters': '{{ guest.gueststate == "running" }}',
|
||||
'groupby_patterns': '{{ guest.guestid }},{{ "templates" if config.template else "guests"}}',
|
||||
'lower_var_keys': True }
|
||||
}
|
||||
|
||||
if six.PY3:
|
||||
config = configparser.ConfigParser()
|
||||
@ -272,9 +210,9 @@ class VMWareInventory(object):
|
||||
config.read(vmware_ini_path)
|
||||
|
||||
# apply defaults
|
||||
for k, v in defaults['vmware'].items():
|
||||
for k,v in defaults['vmware'].items():
|
||||
if not config.has_option('vmware', k):
|
||||
config.set('vmware', k, str(v))
|
||||
config.set('vmware', k, str(v))
|
||||
|
||||
# where is the cache?
|
||||
self.cache_dir = os.path.expanduser(config.get('vmware', 'cache_path'))
|
||||
@ -284,49 +222,34 @@ class VMWareInventory(object):
|
||||
# set the cache filename and max age
|
||||
cache_name = config.get('vmware', 'cache_name')
|
||||
self.cache_path_cache = self.cache_dir + "/%s.cache" % cache_name
|
||||
self.debugl('cache path is %s' % self.cache_path_cache)
|
||||
self.cache_max_age = int(config.getint('vmware', 'cache_max_age'))
|
||||
|
||||
# mark the connection info
|
||||
self.server = os.environ.get('VMWARE_SERVER', config.get('vmware', 'server'))
|
||||
self.debugl('server is %s' % self.server)
|
||||
# mark the connection info
|
||||
self.server = os.environ.get('VMWARE_SERVER', config.get('vmware', 'server'))
|
||||
self.port = int(os.environ.get('VMWARE_PORT', config.get('vmware', 'port')))
|
||||
self.username = os.environ.get('VMWARE_USERNAME', config.get('vmware', 'username'))
|
||||
self.debugl('username is %s' % self.username)
|
||||
self.password = os.environ.get('VMWARE_PASSWORD', config.get('vmware', 'password'))
|
||||
self.validate_certs = os.environ.get('VMWARE_VALIDATE_CERTS', config.get('vmware', 'validate_certs'))
|
||||
if self.validate_certs in ['no', 'false', 'False', False]:
|
||||
self.validate_certs = False
|
||||
|
||||
self.debugl('cert validation is %s' % self.validate_certs)
|
||||
else:
|
||||
self.validate_certs = True
|
||||
|
||||
# behavior control
|
||||
self.maxlevel = int(config.get('vmware', 'max_object_level'))
|
||||
self.debugl('max object level is %s' % self.maxlevel)
|
||||
self.lowerkeys = config.get('vmware', 'lower_var_keys')
|
||||
if type(self.lowerkeys) != bool:
|
||||
if str(self.lowerkeys).lower() in ['yes', 'true', '1']:
|
||||
self.lowerkeys = True
|
||||
else:
|
||||
else:
|
||||
self.lowerkeys = False
|
||||
self.debugl('lower keys is %s' % self.lowerkeys)
|
||||
self.skip_keys = list(config.get('vmware', 'skip_keys').split(','))
|
||||
self.debugl('skip keys is %s' % self.skip_keys)
|
||||
self.host_filters = list(config.get('vmware', 'host_filters').split(','))
|
||||
self.debugl('host filters are %s' % self.host_filters)
|
||||
self.groupby_patterns = list(config.get('vmware', 'groupby_patterns').split(','))
|
||||
self.debugl('groupby patterns are %s' % self.groupby_patterns)
|
||||
|
||||
# Special feature to disable the brute force serialization of the
|
||||
# virtulmachine objects. The key name for these properties does not
|
||||
# matter because the values are just items for a larger list.
|
||||
if config.has_section('properties'):
|
||||
self.guest_props = []
|
||||
for prop in config.items('properties'):
|
||||
self.guest_props.append(prop[1])
|
||||
self.host_filters = list(config.get('vmware', 'host_filters').split(','))
|
||||
self.groupby_patterns = list(config.get('vmware', 'groupby_patterns').split(','))
|
||||
|
||||
# save the config
|
||||
self.config = config
|
||||
self.config = config
|
||||
|
||||
|
||||
def parse_cli_args(self):
|
||||
|
||||
@ -334,57 +257,54 @@ class VMWareInventory(object):
|
||||
|
||||
parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on PyVmomi')
|
||||
parser.add_argument('--debug', action='store_true', default=False,
|
||||
help='show debug info')
|
||||
help='show debug info')
|
||||
parser.add_argument('--list', action='store_true', default=True,
|
||||
help='List instances (default: True)')
|
||||
help='List instances (default: True)')
|
||||
parser.add_argument('--host', action='store',
|
||||
help='Get all the variables about a specific instance')
|
||||
help='Get all the variables about a specific instance')
|
||||
parser.add_argument('--refresh-cache', action='store_true', default=False,
|
||||
help='Force refresh of cache by making API requests to VSphere (default: False - use cache files)')
|
||||
help='Force refresh of cache by making API requests to VSphere (default: False - use cache files)')
|
||||
parser.add_argument('--max-instances', default=None, type=int,
|
||||
help='maximum number of instances to retrieve')
|
||||
help='maximum number of instances to retrieve')
|
||||
self.args = parser.parse_args()
|
||||
|
||||
|
||||
def get_instances(self):
|
||||
|
||||
''' Get a list of vm instances with pyvmomi '''
|
||||
|
||||
instances = []
|
||||
|
||||
kwargs = {'host': self.server,
|
||||
'user': self.username,
|
||||
'pwd': self.password,
|
||||
'port': int(self.port)}
|
||||
'port': int(self.port) }
|
||||
|
||||
if hasattr(ssl, 'SSLContext') and not self.validate_certs:
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
kwargs['sslContext'] = context
|
||||
|
||||
return self._get_instances(kwargs)
|
||||
instances = self._get_instances(kwargs)
|
||||
return instances
|
||||
|
||||
|
||||
def _get_instances(self, inkwargs):
|
||||
|
||||
''' Make API calls '''
|
||||
|
||||
instances = []
|
||||
try:
|
||||
si = SmartConnect(**inkwargs)
|
||||
except ssl.SSLError as connection_error:
|
||||
if '[SSL: CERTIFICATE_VERIFY_FAILED]' in str(connection_error) and self.validate_certs:
|
||||
sys.exit("Unable to connect to ESXi server due to %s, "
|
||||
"please specify validate_certs=False and try again" % connection_error)
|
||||
si = SmartConnect(**inkwargs)
|
||||
|
||||
except Exception as exc:
|
||||
self.debugl("Unable to connect to ESXi server due to %s" % exc)
|
||||
sys.exit("Unable to connect to ESXi server due to %s" % exc)
|
||||
|
||||
self.debugl('retrieving all instances')
|
||||
self.debugl('# retrieving instances')
|
||||
if not si:
|
||||
sys.exit("Could not connect to the specified host using specified "
|
||||
"username and password")
|
||||
print("Could not connect to the specified host using specified "
|
||||
"username and password")
|
||||
return -1
|
||||
atexit.register(Disconnect, si)
|
||||
content = si.RetrieveContent()
|
||||
|
||||
# Create a search container for virtualmachines
|
||||
self.debugl('creating containerview for virtualmachines')
|
||||
container = content.rootFolder
|
||||
viewType = [vim.VirtualMachine]
|
||||
recursive = True
|
||||
@ -393,44 +313,29 @@ class VMWareInventory(object):
|
||||
for child in children:
|
||||
# If requested, limit the total number of instances
|
||||
if self.args.max_instances:
|
||||
if len(instances) >= self.args.max_instances:
|
||||
if len(instances) >= (self.args.max_instances):
|
||||
break
|
||||
instances.append(child)
|
||||
self.debugl("%s total instances in container view" % len(instances))
|
||||
self.debugl("# total instances retrieved %s" % len(instances))
|
||||
|
||||
if self.args.host:
|
||||
instances = [x for x in instances if x.name == self.args.host]
|
||||
|
||||
instance_tuples = []
|
||||
for instance in sorted(instances):
|
||||
if self.guest_props:
|
||||
ifacts = self.facts_from_proplist(instance)
|
||||
else:
|
||||
ifacts = self.facts_from_vobj(instance)
|
||||
instance_tuples = []
|
||||
for instance in sorted(instances):
|
||||
ifacts = self.facts_from_vobj(instance)
|
||||
instance_tuples.append((instance, ifacts))
|
||||
self.debugl('facts collected for all instances')
|
||||
|
||||
try:
|
||||
cfm = content.customFieldsManager
|
||||
if cfm is not None and cfm.field:
|
||||
for f in cfm.field:
|
||||
if f.managedObjectType == vim.VirtualMachine:
|
||||
self.custom_fields[f.key] = f.name
|
||||
self.debugl('%d custom fields collected' % len(self.custom_fields))
|
||||
except vmodl.RuntimeFault as exc:
|
||||
self.debugl("Unable to gather custom fields due to %s" % exc.msg)
|
||||
|
||||
return instance_tuples
|
||||
|
||||
|
||||
def instances_to_inventory(self, instances):
|
||||
|
||||
''' Convert a list of vm objects into a json compliant inventory '''
|
||||
|
||||
self.debugl('re-indexing instances based on ini settings')
|
||||
inventory = VMWareInventory._empty_inventory()
|
||||
inventory = self._empty_inventory()
|
||||
inventory['all'] = {}
|
||||
inventory['all']['hosts'] = []
|
||||
for idx, instance in enumerate(instances):
|
||||
last_idata = None
|
||||
total = len(instances)
|
||||
for idx,instance in enumerate(instances):
|
||||
|
||||
# make a unique id for this object to avoid vmware's
|
||||
# numerous uuid's which aren't all unique.
|
||||
thisid = str(uuid.uuid4())
|
||||
@ -442,21 +347,18 @@ class VMWareInventory(object):
|
||||
inventory['_meta']['hostvars'][thisid]['ansible_uuid'] = thisid
|
||||
|
||||
# Make a map of the uuid to the alias the user wants
|
||||
name_mapping = self.create_template_mapping(
|
||||
inventory,
|
||||
self.config.get('vmware', 'alias_pattern')
|
||||
)
|
||||
name_mapping = self.create_template_mapping(inventory,
|
||||
self.config.get('vmware', 'alias_pattern'))
|
||||
|
||||
# Make a map of the uuid to the ssh hostname the user wants
|
||||
host_mapping = self.create_template_mapping(
|
||||
inventory,
|
||||
self.config.get('vmware', 'host_pattern')
|
||||
)
|
||||
host_mapping = self.create_template_mapping(inventory,
|
||||
self.config.get('vmware', 'host_pattern'))
|
||||
|
||||
|
||||
# Reset the inventory keys
|
||||
for k, v in name_mapping.items():
|
||||
for k,v in name_mapping.items():
|
||||
|
||||
if not host_mapping or k not in host_mapping:
|
||||
if not host_mapping or not k in host_mapping:
|
||||
continue
|
||||
|
||||
# set ansible_host (2.x)
|
||||
@ -464,7 +366,7 @@ class VMWareInventory(object):
|
||||
inventory['_meta']['hostvars'][k]['ansible_host'] = host_mapping[k]
|
||||
# 1.9.x backwards compliance
|
||||
inventory['_meta']['hostvars'][k]['ansible_ssh_host'] = host_mapping[k]
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
if k == v:
|
||||
@ -478,73 +380,47 @@ class VMWareInventory(object):
|
||||
inventory['all']['hosts'].remove(k)
|
||||
inventory['_meta']['hostvars'].pop(k, None)
|
||||
|
||||
self.debugl('pre-filtered hosts:')
|
||||
self.debugl('# pre-filtered hosts:')
|
||||
for i in inventory['all']['hosts']:
|
||||
self.debugl(' * %s' % i)
|
||||
self.debugl('# * %s' % i)
|
||||
# Apply host filters
|
||||
for hf in self.host_filters:
|
||||
if not hf:
|
||||
continue
|
||||
self.debugl('filter: %s' % hf)
|
||||
self.debugl('# filter: %s' % hf)
|
||||
filter_map = self.create_template_mapping(inventory, hf, dtype='boolean')
|
||||
for k, v in filter_map.items():
|
||||
for k,v in filter_map.items():
|
||||
if not v:
|
||||
# delete this host
|
||||
inventory['all']['hosts'].remove(k)
|
||||
inventory['_meta']['hostvars'].pop(k, None)
|
||||
|
||||
self.debugl('post-filter hosts:')
|
||||
self.debugl('# post-filter hosts:')
|
||||
for i in inventory['all']['hosts']:
|
||||
self.debugl(' * %s' % i)
|
||||
self.debugl('# * %s' % i)
|
||||
|
||||
# Create groups
|
||||
for gbp in self.groupby_patterns:
|
||||
groupby_map = self.create_template_mapping(inventory, gbp)
|
||||
for k, v in groupby_map.items():
|
||||
for k,v in groupby_map.items():
|
||||
if v not in inventory:
|
||||
inventory[v] = {}
|
||||
inventory[v]['hosts'] = []
|
||||
if k not in inventory[v]['hosts']:
|
||||
inventory[v]['hosts'].append(k)
|
||||
|
||||
if self.config.get('vmware', 'groupby_custom_field'):
|
||||
for k, v in inventory['_meta']['hostvars'].items():
|
||||
if 'customvalue' in v:
|
||||
for tv in v['customvalue']:
|
||||
if not isinstance(tv['value'], string_types):
|
||||
continue
|
||||
|
||||
newkey = None
|
||||
field_name = self.custom_fields[tv['key']] if tv['key'] in self.custom_fields else tv['key']
|
||||
values = []
|
||||
keylist = map(lambda x: x.strip(), tv['value'].split(','))
|
||||
for kl in keylist:
|
||||
try:
|
||||
newkey = self.config.get('vmware', 'custom_field_group_prefix') + field_name + '_' + kl
|
||||
newkey = newkey.strip()
|
||||
except Exception as e:
|
||||
self.debugl(e)
|
||||
values.append(newkey)
|
||||
for tag in values:
|
||||
if not tag:
|
||||
continue
|
||||
if tag not in inventory:
|
||||
inventory[tag] = {}
|
||||
inventory[tag]['hosts'] = []
|
||||
if k not in inventory[tag]['hosts']:
|
||||
inventory[tag]['hosts'].append(k)
|
||||
inventory[v]['hosts'].append(k)
|
||||
|
||||
return inventory
|
||||
|
||||
|
||||
def create_template_mapping(self, inventory, pattern, dtype='string'):
|
||||
|
||||
''' Return a hash of uuid to templated string from pattern '''
|
||||
|
||||
mapping = {}
|
||||
for k, v in inventory['_meta']['hostvars'].items():
|
||||
t = self.env.from_string(pattern)
|
||||
for k,v in inventory['_meta']['hostvars'].items():
|
||||
t = jinja2.Template(pattern)
|
||||
newkey = None
|
||||
try:
|
||||
try:
|
||||
newkey = t.render(v)
|
||||
newkey = newkey.strip()
|
||||
except Exception as e:
|
||||
@ -557,72 +433,12 @@ class VMWareInventory(object):
|
||||
if newkey.lower() == 'false':
|
||||
newkey = False
|
||||
elif newkey.lower() == 'true':
|
||||
newkey = True
|
||||
newkey = True
|
||||
elif dtype == 'string':
|
||||
pass
|
||||
pass
|
||||
mapping[k] = newkey
|
||||
return mapping
|
||||
|
||||
def facts_from_proplist(self, vm):
|
||||
'''Get specific properties instead of serializing everything'''
|
||||
|
||||
rdata = {}
|
||||
for prop in self.guest_props:
|
||||
self.debugl('getting %s property for %s' % (prop, vm.name))
|
||||
key = prop
|
||||
if self.lowerkeys:
|
||||
key = key.lower()
|
||||
|
||||
if '.' not in prop:
|
||||
# props without periods are direct attributes of the parent
|
||||
rdata[key] = getattr(vm, prop)
|
||||
else:
|
||||
# props with periods are subkeys of parent attributes
|
||||
parts = prop.split('.')
|
||||
total = len(parts) - 1
|
||||
|
||||
# pointer to the current object
|
||||
val = None
|
||||
# pointer to the current result key
|
||||
lastref = rdata
|
||||
|
||||
for idx, x in enumerate(parts):
|
||||
|
||||
if isinstance(val, dict):
|
||||
if x in val:
|
||||
val = val.get(x)
|
||||
elif x.lower() in val:
|
||||
val = val.get(x.lower())
|
||||
else:
|
||||
# if the val wasn't set yet, get it from the parent
|
||||
if not val:
|
||||
try:
|
||||
val = getattr(vm, x)
|
||||
except AttributeError as e:
|
||||
self.debugl(e)
|
||||
else:
|
||||
# in a subkey, get the subprop from the previous attrib
|
||||
try:
|
||||
val = getattr(val, x)
|
||||
except AttributeError as e:
|
||||
self.debugl(e)
|
||||
|
||||
# make sure it serializes
|
||||
val = self._process_object_types(val)
|
||||
|
||||
# lowercase keys if requested
|
||||
if self.lowerkeys:
|
||||
x = x.lower()
|
||||
|
||||
# change the pointer or set the final value
|
||||
if idx != total:
|
||||
if x not in lastref:
|
||||
lastref[x] = {}
|
||||
lastref = lastref[x]
|
||||
else:
|
||||
lastref[x] = val
|
||||
|
||||
return rdata
|
||||
|
||||
def facts_from_vobj(self, vobj, level=0):
|
||||
|
||||
@ -638,7 +454,7 @@ class VMWareInventory(object):
|
||||
|
||||
if level == 0:
|
||||
try:
|
||||
self.debugl("get facts for %s" % vobj.name)
|
||||
self.debugl("# get facts: %s" % vobj.name)
|
||||
except Exception as e:
|
||||
self.debugl(e)
|
||||
|
||||
@ -646,8 +462,7 @@ class VMWareInventory(object):
|
||||
|
||||
methods = dir(vobj)
|
||||
methods = [str(x) for x in methods if not x.startswith('_')]
|
||||
methods = [x for x in methods if x not in self.bad_types]
|
||||
methods = [x for x in methods if not x.lower() in self.skip_keys]
|
||||
methods = [x for x in methods if not x in self.bad_types]
|
||||
methods = sorted(methods)
|
||||
|
||||
for method in methods:
|
||||
@ -656,39 +471,22 @@ class VMWareInventory(object):
|
||||
methodToCall = getattr(vobj, method)
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
# Skip callable methods
|
||||
if callable(methodToCall):
|
||||
continue
|
||||
|
||||
if self.lowerkeys:
|
||||
method = method.lower()
|
||||
|
||||
rdata[method] = self._process_object_types(
|
||||
methodToCall,
|
||||
thisvm=vobj,
|
||||
inkey=method,
|
||||
)
|
||||
rdata[method] = self._process_object_types(methodToCall)
|
||||
|
||||
return rdata
|
||||
|
||||
def _process_object_types(self, vobj, thisvm=None, inkey='', level=0):
|
||||
|
||||
def _process_object_types(self, vobj, level=0):
|
||||
''' Serialize an object '''
|
||||
rdata = {}
|
||||
|
||||
if type(vobj).__name__ in self.vimTableMaxDepth and level >= self.vimTableMaxDepth[type(vobj).__name__]:
|
||||
return rdata
|
||||
|
||||
if vobj is None:
|
||||
rdata = None
|
||||
elif type(vobj) in self.vimTable:
|
||||
rdata = {}
|
||||
for key in self.vimTable[type(vobj)]:
|
||||
try:
|
||||
rdata[key] = getattr(vobj, key)
|
||||
except Exception as e:
|
||||
self.debugl(e)
|
||||
|
||||
elif issubclass(type(vobj), str) or isinstance(vobj, str):
|
||||
if vobj.isalnum():
|
||||
rdata = vobj
|
||||
@ -696,37 +494,30 @@ class VMWareInventory(object):
|
||||
rdata = vobj.decode('ascii', 'ignore')
|
||||
elif issubclass(type(vobj), bool) or isinstance(vobj, bool):
|
||||
rdata = vobj
|
||||
elif issubclass(type(vobj), integer_types) or isinstance(vobj, integer_types):
|
||||
elif issubclass(type(vobj), int) or isinstance(vobj, int):
|
||||
rdata = vobj
|
||||
elif issubclass(type(vobj), float) or isinstance(vobj, float):
|
||||
rdata = vobj
|
||||
elif issubclass(type(vobj), long) or isinstance(vobj, long):
|
||||
rdata = vobj
|
||||
elif issubclass(type(vobj), list) or issubclass(type(vobj), tuple):
|
||||
rdata = []
|
||||
try:
|
||||
vobj = sorted(vobj)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
for idv, vii in enumerate(vobj):
|
||||
if level + 1 <= self.maxlevel:
|
||||
vid = self._process_object_types(
|
||||
vii,
|
||||
thisvm=thisvm,
|
||||
inkey=inkey + '[' + str(idv) + ']',
|
||||
level=(level + 1)
|
||||
)
|
||||
|
||||
for vi in vobj:
|
||||
if (level+1 <= self.maxlevel):
|
||||
#vid = self.facts_from_vobj(vi, level=(level+1))
|
||||
vid = self._process_object_types(vi, level=(level+1))
|
||||
if vid:
|
||||
rdata.append(vid)
|
||||
|
||||
elif issubclass(type(vobj), dict):
|
||||
pass
|
||||
|
||||
elif issubclass(type(vobj), object):
|
||||
methods = dir(vobj)
|
||||
methods = [str(x) for x in methods if not x.startswith('_')]
|
||||
methods = [x for x in methods if x not in self.bad_types]
|
||||
methods = [x for x in methods if not inkey + '.' + x.lower() in self.skip_keys]
|
||||
methods = [x for x in methods if not x in self.bad_types]
|
||||
methods = sorted(methods)
|
||||
|
||||
for method in methods:
|
||||
@ -735,47 +526,28 @@ class VMWareInventory(object):
|
||||
methodToCall = getattr(vobj, method)
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
if callable(methodToCall):
|
||||
continue
|
||||
|
||||
if self.lowerkeys:
|
||||
method = method.lower()
|
||||
if level + 1 <= self.maxlevel:
|
||||
try:
|
||||
rdata[method] = self._process_object_types(
|
||||
methodToCall,
|
||||
thisvm=thisvm,
|
||||
inkey=inkey + '.' + method,
|
||||
level=(level + 1)
|
||||
)
|
||||
except vim.fault.NoPermission:
|
||||
self.debugl("Skipping method %s (NoPermission)" % method)
|
||||
if (level+1 <= self.maxlevel):
|
||||
rdata[method] = self._process_object_types(methodToCall, level=(level+1))
|
||||
else:
|
||||
pass
|
||||
|
||||
if not rdata:
|
||||
rdata = None
|
||||
return rdata
|
||||
|
||||
def get_host_info(self, host):
|
||||
|
||||
|
||||
''' Return hostvars for a single host '''
|
||||
|
||||
if host in self.inventory['_meta']['hostvars']:
|
||||
return self.inventory['_meta']['hostvars'][host]
|
||||
elif self.args.host and self.inventory['_meta']['hostvars']:
|
||||
match = None
|
||||
for k, v in self.inventory['_meta']['hostvars'].items():
|
||||
if self.inventory['_meta']['hostvars'][k]['name'] == self.args.host:
|
||||
match = k
|
||||
break
|
||||
if match:
|
||||
return self.inventory['_meta']['hostvars'][match]
|
||||
else:
|
||||
raise VMwareMissingHostException('%s not found' % host)
|
||||
else:
|
||||
raise VMwareMissingHostException('%s not found' % host)
|
||||
return self.inventory['_meta']['hostvars'][host]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run the script
|
||||
print(VMWareInventory().show())
|
||||
|
||||
|
||||
|
||||
@ -54,7 +54,6 @@ except ImportError as e:
|
||||
# Imports for ansible
|
||||
import ConfigParser
|
||||
|
||||
|
||||
class AzureInventory(object):
|
||||
def __init__(self):
|
||||
"""Main execution path."""
|
||||
@ -172,9 +171,10 @@ class AzureInventory(object):
|
||||
parser.add_argument('--list-images', action='store',
|
||||
help='Get all available images.')
|
||||
parser.add_argument('--refresh-cache',
|
||||
action='store_true', default=False,
|
||||
help='Force refresh of thecache by making API requests to Azure '
|
||||
'(default: False - use cache files)')
|
||||
action='store_true', default=False,
|
||||
help='Force refresh of thecache by making API requests to Azure '
|
||||
'(default: False - use cache files)',
|
||||
)
|
||||
parser.add_argument('--host', action='store',
|
||||
help='Get all information about an instance.')
|
||||
self.args = parser.parse_args()
|
||||
@ -198,7 +198,7 @@ class AzureInventory(object):
|
||||
associated with a cloud service.
|
||||
"""
|
||||
try:
|
||||
for deployment in self.sms.get_hosted_service_properties(cloud_service.service_name, embed_detail=True).deployments.deployments:
|
||||
for deployment in self.sms.get_hosted_service_properties(cloud_service.service_name,embed_detail=True).deployments.deployments:
|
||||
self.add_deployment(cloud_service, deployment)
|
||||
except Exception as e:
|
||||
sys.exit("Error: Failed to access deployments - {0}".format(str(e)))
|
||||
@ -246,7 +246,7 @@ class AzureInventory(object):
|
||||
def push(self, my_dict, key, element):
|
||||
"""Pushed an element onto an array that may not have been defined in the dict."""
|
||||
if key in my_dict:
|
||||
my_dict[key].append(element)
|
||||
my_dict[key].append(element);
|
||||
else:
|
||||
my_dict[key] = [element]
|
||||
|
||||
|
||||
@ -24,23 +24,17 @@ Zabbix Server external inventory script.
|
||||
========================================
|
||||
|
||||
Returns hosts and hostgroups from Zabbix Server.
|
||||
If you want to run with --limit against a host group with space in the
|
||||
name, use asterisk. For example --limit="Linux*servers".
|
||||
|
||||
Configuration is read from `zabbix.ini`.
|
||||
|
||||
Tested with Zabbix Server 2.0.6 and 3.2.3.
|
||||
Tested with Zabbix Server 2.0.6.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import sys
|
||||
import os, sys
|
||||
import argparse
|
||||
try:
|
||||
import ConfigParser as configparser
|
||||
except ImportError:
|
||||
import configparser
|
||||
import ConfigParser
|
||||
|
||||
try:
|
||||
from zabbix_api import ZabbixAPI
|
||||
@ -54,16 +48,15 @@ try:
|
||||
except:
|
||||
import simplejson as json
|
||||
|
||||
|
||||
class ZabbixInventory(object):
|
||||
|
||||
def read_settings(self):
|
||||
config = configparser.SafeConfigParser()
|
||||
config = ConfigParser.SafeConfigParser()
|
||||
conf_path = './zabbix.ini'
|
||||
if not os.path.exists(conf_path):
|
||||
conf_path = os.path.dirname(os.path.realpath(__file__)) + '/zabbix.ini'
|
||||
conf_path = os.path.dirname(os.path.realpath(__file__)) + '/zabbix.ini'
|
||||
if os.path.exists(conf_path):
|
||||
config.read(conf_path)
|
||||
config.read(conf_path)
|
||||
# server
|
||||
if config.has_option('zabbix', 'server'):
|
||||
self.zabbix_server = config.get('zabbix', 'server')
|
||||
@ -86,7 +79,7 @@ class ZabbixInventory(object):
|
||||
}
|
||||
|
||||
def get_host(self, api, name):
|
||||
data = {'ansible_ssh_host': name}
|
||||
data = {}
|
||||
return data
|
||||
|
||||
def get_list(self, api):
|
||||
@ -102,14 +95,11 @@ class ZabbixInventory(object):
|
||||
for group in host['groups']:
|
||||
groupname = group['name']
|
||||
|
||||
if groupname not in data:
|
||||
if not groupname in data:
|
||||
data[groupname] = self.hoststub()
|
||||
|
||||
data[groupname]['hosts'].append(hostname)
|
||||
|
||||
# Prevents Ansible from calling this script for each server with --host
|
||||
data['_meta'] = {'hostvars': self.meta}
|
||||
|
||||
return data
|
||||
|
||||
def __init__(self):
|
||||
@ -118,7 +108,6 @@ class ZabbixInventory(object):
|
||||
self.zabbix_server = None
|
||||
self.zabbix_username = None
|
||||
self.zabbix_password = None
|
||||
self.meta = {}
|
||||
|
||||
self.read_settings()
|
||||
self.read_cli()
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from subprocess import Popen,PIPE
|
||||
import sys
|
||||
import json
|
||||
|
||||
@ -27,10 +27,10 @@ result['all'] = {}
|
||||
pipe = Popen(['zoneadm', 'list', '-ip'], stdout=PIPE, universal_newlines=True)
|
||||
result['all']['hosts'] = []
|
||||
for l in pipe.stdout.readlines():
|
||||
# 1:work:running:/zones/work:3126dc59-9a07-4829-cde9-a816e4c5040e:native:shared
|
||||
s = l.split(':')
|
||||
if s[1] != 'global':
|
||||
result['all']['hosts'].append(s[1])
|
||||
# 1:work:running:/zones/work:3126dc59-9a07-4829-cde9-a816e4c5040e:native:shared
|
||||
s = l.split(':')
|
||||
if s[1] != 'global':
|
||||
result['all']['hosts'].append(s[1])
|
||||
|
||||
result['all']['vars'] = {}
|
||||
result['all']['vars']['ansible_connection'] = 'zone'
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# (c) 2014, Matt Martz <matt@sivel.net>
|
||||
# (c) 2016, Justin Mayer <https://justinmayer.com/>
|
||||
#
|
||||
# This file is part of Ansible.
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@ -18,81 +17,59 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# =============================================================================
|
||||
#
|
||||
# This script is to be used with vault_password_file or --vault-password-file
|
||||
# to retrieve the vault password via your OS's native keyring application.
|
||||
# Script to be used with vault_password_file or --vault-password-file
|
||||
# to retrieve the vault password via your OSes native keyring application
|
||||
#
|
||||
# This file *MUST* be saved with executable permissions. Otherwise, Ansible
|
||||
# will try to parse as a password file and display: "ERROR! Decryption failed"
|
||||
# This script requires the ``keyring`` python module
|
||||
#
|
||||
# The `keyring` Python module is required: https://pypi.python.org/pypi/keyring
|
||||
#
|
||||
# By default, this script will store the specified password in the keyring of
|
||||
# the user that invokes the script. To specify a user keyring, add a [vault]
|
||||
# section to your ansible.cfg file with a 'username' option. Example:
|
||||
# Add a [vault] section to your ansible.cfg file,
|
||||
# the only option is 'username'. Example:
|
||||
#
|
||||
# [vault]
|
||||
# username = 'ansible-vault'
|
||||
# username = 'ansible_vault'
|
||||
#
|
||||
# Another optional setting is for the key name, which allows you to use this
|
||||
# script to handle multiple project vaults with different passwords:
|
||||
#
|
||||
# [vault]
|
||||
# keyname = 'ansible-vault-yourproject'
|
||||
#
|
||||
# You can configure the `vault_password_file` option in ansible.cfg:
|
||||
# Additionally, it would be a good idea to configure vault_password_file in
|
||||
# ansible.cfg
|
||||
#
|
||||
# [defaults]
|
||||
# ...
|
||||
# vault_password_file = /path/to/vault-keyring.py
|
||||
# ...
|
||||
#
|
||||
# To set your password, `cd` to your project directory and run:
|
||||
# To set your password: python /path/to/vault-keyring.py set
|
||||
#
|
||||
# python /path/to/vault-keyring.py set
|
||||
#
|
||||
# If you choose not to configure the path to `vault_password_file` in
|
||||
# ansible.cfg, your `ansible-playbook` command might look like:
|
||||
# If you choose to not configure the path to vault_password_file in ansible.cfg
|
||||
# your ansible-playbook command may look like:
|
||||
#
|
||||
# ansible-playbook --vault-password-file=/path/to/vault-keyring.py site.yml
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
import sys
|
||||
import getpass
|
||||
import keyring
|
||||
import ConfigParser
|
||||
|
||||
|
||||
import ansible.constants as C
|
||||
|
||||
|
||||
def main():
|
||||
(parser, config_path) = C.load_config_file()
|
||||
if parser.has_option('vault', 'username'):
|
||||
(parser,config_path) = C.load_config_file()
|
||||
try:
|
||||
username = parser.get('vault', 'username')
|
||||
else:
|
||||
username = getpass.getuser()
|
||||
|
||||
if parser.has_option('vault', 'keyname'):
|
||||
keyname = parser.get('vault', 'keyname')
|
||||
else:
|
||||
keyname = 'ansible'
|
||||
except ConfigParser.NoSectionError:
|
||||
sys.stderr.write('No [vault] section configured in config file: %s\n' % config_path)
|
||||
sys.exit(1)
|
||||
|
||||
if len(sys.argv) == 2 and sys.argv[1] == 'set':
|
||||
intro = 'Storing password in "{}" user keyring using key name: {}\n'
|
||||
sys.stdout.write(intro.format(username, keyname))
|
||||
password = getpass.getpass()
|
||||
confirm = getpass.getpass('Confirm password: ')
|
||||
if password == confirm:
|
||||
keyring.set_password(keyname, username, password)
|
||||
keyring.set_password('ansible', username, password)
|
||||
else:
|
||||
sys.stderr.write('Passwords do not match\n')
|
||||
sys.exit(1)
|
||||
else:
|
||||
sys.stdout.write('{}\n'.format(keyring.get_password(keyname,
|
||||
username)))
|
||||
sys.stdout.write('%s\n' % keyring.get_password('ansible', username))
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
234
docs-api/Makefile
Normal file
234
docs-api/Makefile
Normal file
@ -0,0 +1,234 @@
|
||||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
LOGFILE = sphinx.log
|
||||
FULL_TRACEBACKS = -T
|
||||
CPUS ?= 4
|
||||
VERBOSITY ?= -v
|
||||
FORCE_REBUILD = -a -E
|
||||
NITPICK ?=
|
||||
SPHINXOPTS = -j $(CPUS) -w $(LOGFILE) $(FULL_TRACEBACKS) $(FORCE_REBUILD) $(NITPICK) $(VERBOSITY)
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
RSTDIR = rst
|
||||
MODULES_PATH = ../lib
|
||||
EXCLUDE_PATHS = ../lib/ansible/modules ../lib/ansible/utils/module_docs_fragments ../lib/ansible/compat/six ../lib/ansible/module_utils/six.py
|
||||
DOC_PROJECTS = "Ansible API"
|
||||
|
||||
# User-friendly check for sphinx-build
|
||||
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
|
||||
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
|
||||
endif
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " applehelp to make an Apple Help Book"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " xml to make Docutils-native XML files"
|
||||
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
@echo " coverage to run coverage check of the documentation (if enabled)"
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)/*
|
||||
rm -rf $(RSTDIR)/*.rst
|
||||
|
||||
.PHONY: apidoc
|
||||
apidoc:
|
||||
sphinx-apidoc --module-first --doc-project $(DOC_PROJECT) --force --maxdepth 7 -o $(RSTDIR) $(MODULES_PATH) $(EXCLUDE_PATHS)
|
||||
|
||||
.PHONY: html
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
.PHONY: docs
|
||||
docs: clean apidoc html
|
||||
|
||||
.PHONY: dirhtml
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
.PHONY: singlehtml
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
.PHONY: pickle
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
.PHONY: json
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
.PHONY: htmlhelp
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
.PHONY: qthelp
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Ansible.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Ansible.qhc"
|
||||
|
||||
.PHONY: applehelp
|
||||
applehelp:
|
||||
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
|
||||
@echo
|
||||
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
|
||||
@echo "N.B. You won't be able to view it unless you put it in" \
|
||||
"~/Library/Documentation/Help or install it in your application" \
|
||||
"bundle."
|
||||
|
||||
.PHONY: devhelp
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/Ansible"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Ansible"
|
||||
@echo "# devhelp"
|
||||
|
||||
.PHONY: epub
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
.PHONY: latex
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
.PHONY: latexpdf
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
.PHONY: latexpdfja
|
||||
latexpdfja:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through platex and dvipdfmx..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
.PHONY: text
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
.PHONY: man
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
.PHONY: texinfo
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
.PHONY: info
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
.PHONY: gettext
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
.PHONY: changes
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
.PHONY: linkcheck
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
.PHONY: doctest
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
|
||||
.PHONY: coverage
|
||||
coverage:
|
||||
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
|
||||
@echo "Testing of coverage in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/coverage/python.txt."
|
||||
|
||||
.PHONY: xml
|
||||
xml:
|
||||
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||
@echo
|
||||
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
||||
|
||||
.PHONY: pseudoxml
|
||||
pseudoxml:
|
||||
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||
@echo
|
||||
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
||||
309
docs-api/conf.py
Normal file
309
docs-api/conf.py
Normal file
@ -0,0 +1,309 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Ansible documentation build configuration file, created by
|
||||
# sphinx-quickstart on Fri Jun 3 17:34:17 2016.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('../bin'))
|
||||
sys.path.insert(0, os.path.abspath('../lib/ansible'))
|
||||
import sphinx_rtd_theme
|
||||
import alabaster
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.napoleon',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.graphviz',
|
||||
'sphinx.ext.inheritance_diagram',
|
||||
'alabaster',
|
||||
]
|
||||
|
||||
#autodoc_default_flags = ['members', 'show-inheritance', 'inherited-members', 'undoc-members',]
|
||||
autodoc_default_flags = ['members', 'show-inheritance', 'undoc-members',]
|
||||
autoclass_content = 'both'
|
||||
autodoc_member_order = 'bysource'
|
||||
autodoc_mock_imports = ['xmltodict', 'winrm', 'redis', 'StricRedis']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'Ansible'
|
||||
copyright = u'2016, Red Hat'
|
||||
author = u'Red Hat'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = u'2.2'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = u'1'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
#keep_warnings = False
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = True
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#html_theme = 'alabaster'
|
||||
#html_theme_path = ['../docsite/_themes']
|
||||
#html_theme = 'srtd'
|
||||
html_short_title = 'Ansible Documentation'
|
||||
|
||||
#html_theme = "sphinx_rtd_theme"
|
||||
#html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
|
||||
html_theme_path = [alabaster.get_path()]
|
||||
html_theme = 'alabaster'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (relative to this directory) to use as a favicon of
|
||||
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
#html_extra_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Language to be used for generating the HTML full-text search index.
|
||||
# Sphinx supports the following languages:
|
||||
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
|
||||
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
|
||||
#html_search_language = 'en'
|
||||
|
||||
# A dictionary with options for the search language support, empty by default.
|
||||
# Now only 'ja' uses this config value
|
||||
#html_search_options = {'type': 'default'}
|
||||
|
||||
# The name of a javascript file (relative to the configuration directory) that
|
||||
# implements a search results scorer. If empty, the default will be used.
|
||||
#html_search_scorer = 'scorer.js'
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'Ansibledoc'
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'Ansible.tex', u'Ansible Documentation',
|
||||
u'Red Hat', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'ansible', u'Ansible Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'Ansible', u'Ansible Documentation',
|
||||
author, 'Ansible', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
#texinfo_no_detailmenu = False
|
||||
@ -1,232 +0,0 @@
|
||||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
LOGFILE = sphinx.log
|
||||
FULL_TRACEBACKS = -T
|
||||
CPUS ?= 4
|
||||
VERBOSITY ?= -v
|
||||
FORCE_REBUILD = -a -E
|
||||
NITPICK ?=
|
||||
SPHINXOPTS = -j $(CPUS) -w $(LOGFILE) $(FULL_TRACEBACKS) $(FORCE_REBUILD) $(NITPICK) $(VERBOSITY)
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
RSTDIR = rst
|
||||
MODULES_PATH = ../../lib
|
||||
EXCLUDE_PATHS = ../../lib/ansible/modules ../../lib/ansible/utils/module_docs_fragments ../../lib/ansible/module_utils/six
|
||||
DOC_PROJECTS = "Ansible API"
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " applehelp to make an Apple Help Book"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " xml to make Docutils-native XML files"
|
||||
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
@echo " coverage to run coverage check of the documentation (if enabled)"
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)/*
|
||||
rm -rf $(RSTDIR)/*.rst
|
||||
|
||||
.PHONY: apidoc
|
||||
apidoc:
|
||||
sphinx-apidoc --module-first --doc-project $(DOC_PROJECT) --force --maxdepth 7 -o $(RSTDIR) $(MODULES_PATH) $(EXCLUDE_PATHS)
|
||||
|
||||
.PHONY: html
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
.PHONY: docs
|
||||
docs: clean apidoc html
|
||||
|
||||
.PHONY: webdocs
|
||||
webdocs: clean apidoc html
|
||||
|
||||
.PHONY: dirhtml
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
.PHONY: singlehtml
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
.PHONY: pickle
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
.PHONY: json
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
.PHONY: htmlhelp
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
.PHONY: qthelp
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Ansible.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Ansible.qhc"
|
||||
|
||||
.PHONY: applehelp
|
||||
applehelp:
|
||||
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
|
||||
@echo
|
||||
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
|
||||
@echo "N.B. You won't be able to view it unless you put it in" \
|
||||
"~/Library/Documentation/Help or install it in your application" \
|
||||
"bundle."
|
||||
|
||||
.PHONY: devhelp
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/Ansible"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Ansible"
|
||||
@echo "# devhelp"
|
||||
|
||||
.PHONY: epub
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
.PHONY: latex
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
.PHONY: latexpdf
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
.PHONY: latexpdfja
|
||||
latexpdfja:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through platex and dvipdfmx..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
.PHONY: text
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
.PHONY: man
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
.PHONY: texinfo
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
.PHONY: info
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
.PHONY: gettext
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
.PHONY: changes
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
.PHONY: linkcheck
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
.PHONY: doctest
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
|
||||
.PHONY: coverage
|
||||
coverage:
|
||||
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
|
||||
@echo "Testing of coverage in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/coverage/python.txt."
|
||||
|
||||
.PHONY: xml
|
||||
xml:
|
||||
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||
@echo
|
||||
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
||||
|
||||
.PHONY: pseudoxml
|
||||
pseudoxml:
|
||||
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||
@echo
|
||||
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
||||
309
docs/api/conf.py
309
docs/api/conf.py
@ -1,309 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Ansible documentation build configuration file, created by
|
||||
# sphinx-quickstart on Fri Jun 3 17:34:17 2016.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
# sys.path.insert(0, os.path.abspath('../bin'))
|
||||
sys.path.insert(0, os.path.abspath('../lib/ansible'))
|
||||
import sphinx_rtd_theme
|
||||
import alabaster
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.napoleon',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.graphviz',
|
||||
'sphinx.ext.inheritance_diagram',
|
||||
'alabaster',
|
||||
]
|
||||
|
||||
# autodoc_default_flags = ['members', 'show-inheritance', 'inherited-members', 'undoc-members', ]
|
||||
autodoc_default_flags = ['members', 'show-inheritance', 'undoc-members', ]
|
||||
autoclass_content = 'both'
|
||||
autodoc_member_order = 'bysource'
|
||||
autodoc_mock_imports = ['xmltodict', 'winrm', 'redis', 'StricRedis']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'Ansible'
|
||||
copyright = u'2016, Red Hat'
|
||||
author = u'Red Hat'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = u'2.3'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = u'1'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
# today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
# today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
# default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
# add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
# modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
# keep_warnings = False
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = True
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
# html_theme = 'alabaster'
|
||||
# html_theme_path = ['../docsite/_themes']
|
||||
# html_theme = 'srtd'
|
||||
html_short_title = 'Ansible Documentation'
|
||||
|
||||
# html_theme = "sphinx_rtd_theme"
|
||||
# html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
|
||||
html_theme_path = [alabaster.get_path()]
|
||||
html_theme = 'alabaster'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
# html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
# html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
# html_logo = None
|
||||
|
||||
# The name of an image file (relative to this directory) to use as a favicon of
|
||||
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
# html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
# html_extra_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
# html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
# html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
# html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
# html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
# html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
# html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
# html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
# html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
# html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
# html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
# html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
# html_file_suffix = None
|
||||
|
||||
# Language to be used for generating the HTML full-text search index.
|
||||
# Sphinx supports the following languages:
|
||||
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
|
||||
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
|
||||
# html_search_language = 'en'
|
||||
|
||||
# A dictionary with options for the search language support, empty by default.
|
||||
# Now only 'ja' uses this config value
|
||||
# html_search_options = {'type': 'default'}
|
||||
|
||||
# The name of a javascript file (relative to the configuration directory) that
|
||||
# implements a search results scorer. If empty, the default will be used.
|
||||
# html_search_scorer = 'scorer.js'
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'Ansibledoc'
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'Ansible.tex', u'Ansible Documentation',
|
||||
u'Red Hat', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
# latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
# latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
# latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
# latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
# latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
# latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'ansible', u'Ansible Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
# man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'Ansible', u'Ansible Documentation',
|
||||
author, 'Ansible', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
# texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
# texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
# texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
# texinfo_no_detailmenu = False
|
||||
@ -1,72 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
DEFAULT_TEMPLATE_FILE = 'config.rst.j2'
|
||||
|
||||
|
||||
def generate_parser():
|
||||
p = optparse.OptionParser(
|
||||
version='%prog 1.0',
|
||||
usage='usage: %prog [options]',
|
||||
description='Generate module documentation from metadata',
|
||||
)
|
||||
p.add_option("-t", "--template-file", action="store", dest="template_file", default=DEFAULT_TEMPLATE_FILE, help="directory containing Jinja2 templates")
|
||||
p.add_option("-o", "--output-dir", action="store", dest="output_dir", default='/tmp/', help="Output directory for rst files")
|
||||
p.add_option("-d", "--docs-source", action="store", dest="docs", default=None, help="Source for attribute docs")
|
||||
|
||||
(options, args) = p.parse_args()
|
||||
|
||||
return p
|
||||
|
||||
|
||||
def fix_description(config_options):
|
||||
'''some descriptions are strings, some are lists. workaround it...'''
|
||||
|
||||
for config_key in config_options:
|
||||
description = config_options[config_key].get('description', [])
|
||||
if isinstance(description, list):
|
||||
desc_list = description
|
||||
else:
|
||||
desc_list = [description]
|
||||
config_options[config_key]['description'] = desc_list
|
||||
return config_options
|
||||
|
||||
|
||||
def main(args):
|
||||
|
||||
parser = generate_parser()
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
output_dir = os.path.abspath(options.output_dir)
|
||||
template_file_full_path = os.path.abspath(options.template_file)
|
||||
template_file = os.path.basename(template_file_full_path)
|
||||
template_dir = os.path.dirname(os.path.abspath(template_file_full_path))
|
||||
|
||||
if options.docs:
|
||||
with open(options.docs) as f:
|
||||
docs = yaml.safe_load(f)
|
||||
else:
|
||||
docs = {}
|
||||
|
||||
config_options = docs
|
||||
config_options = fix_description(config_options)
|
||||
|
||||
env = Environment(loader=FileSystemLoader(template_dir), trim_blocks=True,)
|
||||
template = env.get_template(template_file)
|
||||
output_name = os.path.join(output_dir, template_file.replace('.j2', ''))
|
||||
temp_vars = {'config_options': config_options}
|
||||
|
||||
with open(output_name, 'w') as f:
|
||||
f.write(template.render(temp_vars).encode('utf-8'))
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv[:]))
|
||||
@ -1,69 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import optparse
|
||||
import yaml
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
from ansible.playbook import Play
|
||||
from ansible.playbook.block import Block
|
||||
from ansible.playbook.role import Role
|
||||
from ansible.playbook.task import Task
|
||||
|
||||
template_file = 'playbooks_keywords.rst.j2'
|
||||
oblist = {}
|
||||
clist = []
|
||||
class_list = [Play, Role, Block, Task]
|
||||
|
||||
p = optparse.OptionParser(
|
||||
version='%prog 1.0',
|
||||
usage='usage: %prog [options]',
|
||||
description='Generate module documentation from metadata',
|
||||
)
|
||||
p.add_option("-T", "--template-dir", action="store", dest="template_dir", default="../templates", help="directory containing Jinja2 templates")
|
||||
p.add_option("-o", "--output-dir", action="store", dest="output_dir", default='/tmp/', help="Output directory for rst files")
|
||||
p.add_option("-d", "--docs-source", action="store", dest="docs", default=None, help="Source for attribute docs")
|
||||
|
||||
(options, args) = p.parse_args()
|
||||
|
||||
for aclass in class_list:
|
||||
aobj = aclass()
|
||||
name = type(aobj).__name__
|
||||
|
||||
if options.docs:
|
||||
with open(options.docs) as f:
|
||||
docs = yaml.safe_load(f)
|
||||
else:
|
||||
docs = {}
|
||||
|
||||
# build ordered list to loop over and dict with attributes
|
||||
clist.append(name)
|
||||
oblist[name] = dict((x, aobj.__dict__['_attributes'][x]) for x in aobj.__dict__['_attributes'] if 'private' not in x or not x.private)
|
||||
|
||||
# pick up docs if they exist
|
||||
for a in oblist[name]:
|
||||
if a in docs:
|
||||
oblist[name][a] = docs[a]
|
||||
else:
|
||||
oblist[name][a] = ' UNDOCUMENTED!! '
|
||||
|
||||
# loop is really with_ for users
|
||||
if name == 'Task':
|
||||
oblist[name]['with_<lookup_plugin>'] = 'with_ is how loops are defined, it can use any available lookup plugin to generate the item list'
|
||||
|
||||
# local_action is implicit with action
|
||||
if 'action' in oblist[name]:
|
||||
oblist[name]['local_action'] = 'Same as action but also implies ``delegate_to: localhost``'
|
||||
|
||||
# remove unusable (used to be private?)
|
||||
for nouse in ('loop', 'loop_args'):
|
||||
if nouse in oblist[name]:
|
||||
del oblist[name][nouse]
|
||||
|
||||
env = Environment(loader=FileSystemLoader(options.template_dir), trim_blocks=True,)
|
||||
template = env.get_template(template_file)
|
||||
outputname = options.output_dir + template_file.replace('.j2', '')
|
||||
tempvars = {'oblist': oblist, 'clist': clist}
|
||||
|
||||
with open(outputname, 'w') as f:
|
||||
f.write(template.render(tempvars))
|
||||
@ -1,280 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import pprint
|
||||
import sys
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
from ansible.module_utils._text import to_bytes
|
||||
|
||||
|
||||
def generate_parser():
|
||||
p = optparse.OptionParser(
|
||||
version='%prog 1.0',
|
||||
usage='usage: %prog [options]',
|
||||
description='Generate cli documentation from cli docstrings',
|
||||
)
|
||||
|
||||
p.add_option("-t", "--template-file", action="store", dest="template_file", default="../templates/man.j2", help="path to jinja2 template")
|
||||
p.add_option("-o", "--output-dir", action="store", dest="output_dir", default='/tmp/', help="Output directory for rst files")
|
||||
p.add_option("-f", "--output-format", action="store", dest="output_format", default='man', help="Output format for docs (the default 'man' or 'rst')")
|
||||
return p
|
||||
|
||||
|
||||
# from https://www.python.org/dev/peps/pep-0257/
|
||||
def trim_docstring(docstring):
|
||||
if not docstring:
|
||||
return ''
|
||||
# Convert tabs to spaces (following the normal Python rules)
|
||||
# and split into a list of lines:
|
||||
lines = docstring.expandtabs().splitlines()
|
||||
# Determine minimum indentation (first line doesn't count):
|
||||
indent = sys.maxsize
|
||||
for line in lines[1:]:
|
||||
stripped = line.lstrip()
|
||||
if stripped:
|
||||
indent = min(indent, len(line) - len(stripped))
|
||||
# Remove indentation (first line is special):
|
||||
trimmed = [lines[0].strip()]
|
||||
if indent < sys.maxsize:
|
||||
for line in lines[1:]:
|
||||
trimmed.append(line[indent:].rstrip())
|
||||
# Strip off trailing and leading blank lines:
|
||||
while trimmed and not trimmed[-1]:
|
||||
trimmed.pop()
|
||||
while trimmed and not trimmed[0]:
|
||||
trimmed.pop(0)
|
||||
# Return a single string:
|
||||
return '\n'.join(trimmed)
|
||||
|
||||
|
||||
def get_options(optlist):
|
||||
''' get actual options '''
|
||||
|
||||
opts = []
|
||||
for opt in optlist:
|
||||
res = {
|
||||
'desc': opt.help,
|
||||
'options': opt._short_opts + opt._long_opts
|
||||
}
|
||||
if opt.action == 'store':
|
||||
res['arg'] = opt.dest.upper()
|
||||
opts.append(res)
|
||||
|
||||
return opts
|
||||
|
||||
|
||||
def get_option_groups(option_parser):
|
||||
groups = []
|
||||
for option_group in option_parser.option_groups:
|
||||
group_info = {}
|
||||
group_info['desc'] = option_group.get_description()
|
||||
group_info['options'] = option_group.option_list
|
||||
group_info['group_obj'] = option_group
|
||||
groups.append(group_info)
|
||||
return groups
|
||||
|
||||
|
||||
def opt_doc_list(cli):
|
||||
''' iterate over options lists '''
|
||||
|
||||
results = []
|
||||
for option_group in cli.parser.option_groups:
|
||||
results.extend(get_options(option_group.option_list))
|
||||
|
||||
results.extend(get_options(cli.parser.option_list))
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# def opts_docs(cli, name):
|
||||
def opts_docs(cli_class_name, cli_module_name):
|
||||
''' generate doc structure from options '''
|
||||
|
||||
cli_name = 'ansible-%s' % cli_module_name
|
||||
if cli_module_name == 'adhoc':
|
||||
cli_name = 'ansible'
|
||||
|
||||
# WIth no action/subcommand
|
||||
# shared opts set
|
||||
# instantiate each cli and ask its options
|
||||
cli_klass = getattr(__import__("ansible.cli.%s" % cli_module_name,
|
||||
fromlist=[cli_class_name]), cli_class_name)
|
||||
cli = cli_klass([])
|
||||
|
||||
# parse the common options
|
||||
try:
|
||||
cli.parse()
|
||||
except:
|
||||
pass
|
||||
|
||||
# base/common cli info
|
||||
docs = {
|
||||
'cli': cli_module_name,
|
||||
'cli_name': cli_name,
|
||||
'usage': cli.parser.usage,
|
||||
'short_desc': cli.parser.description,
|
||||
'long_desc': trim_docstring(cli.__doc__),
|
||||
'actions': {},
|
||||
}
|
||||
option_info = {'option_names': [],
|
||||
'options': [],
|
||||
'groups': []}
|
||||
|
||||
for extras in ('ARGUMENTS'):
|
||||
if hasattr(cli, extras):
|
||||
docs[extras.lower()] = getattr(cli, extras)
|
||||
|
||||
common_opts = opt_doc_list(cli)
|
||||
groups_info = get_option_groups(cli.parser)
|
||||
shared_opt_names = []
|
||||
for opt in common_opts:
|
||||
shared_opt_names.extend(opt.get('options', []))
|
||||
|
||||
option_info['options'] = common_opts
|
||||
option_info['option_names'] = shared_opt_names
|
||||
|
||||
option_info['groups'].extend(groups_info)
|
||||
|
||||
docs.update(option_info)
|
||||
|
||||
# now for each action/subcommand
|
||||
# force populate parser with per action options
|
||||
|
||||
# use class attrs not the attrs on a instance (not that it matters here...)
|
||||
for action in getattr(cli_klass, 'VALID_ACTIONS', ()):
|
||||
# instantiate each cli and ask its options
|
||||
action_cli_klass = getattr(__import__("ansible.cli.%s" % cli_module_name,
|
||||
fromlist=[cli_class_name]), cli_class_name)
|
||||
# init with args with action added?
|
||||
cli = action_cli_klass([])
|
||||
cli.args.append(action)
|
||||
|
||||
try:
|
||||
cli.parse()
|
||||
except:
|
||||
pass
|
||||
|
||||
# FIXME/TODO: needed?
|
||||
# avoid dupe errors
|
||||
cli.parser.set_conflict_handler('resolve')
|
||||
|
||||
cli.set_action()
|
||||
|
||||
action_info = {'option_names': [],
|
||||
'options': []}
|
||||
# docs['actions'][action] = {}
|
||||
# docs['actions'][action]['name'] = action
|
||||
action_info['name'] = action
|
||||
action_info['desc'] = trim_docstring(getattr(cli, 'execute_%s' % action).__doc__)
|
||||
|
||||
# docs['actions'][action]['desc'] = getattr(cli, 'execute_%s' % action).__doc__.strip()
|
||||
action_doc_list = opt_doc_list(cli)
|
||||
|
||||
uncommon_options = []
|
||||
for action_doc in action_doc_list:
|
||||
# uncommon_options = []
|
||||
|
||||
option_aliases = action_doc.get('options', [])
|
||||
for option_alias in option_aliases:
|
||||
|
||||
if option_alias in shared_opt_names:
|
||||
continue
|
||||
|
||||
# TODO: use set
|
||||
if option_alias not in action_info['option_names']:
|
||||
action_info['option_names'].append(option_alias)
|
||||
|
||||
if action_doc in action_info['options']:
|
||||
continue
|
||||
|
||||
uncommon_options.append(action_doc)
|
||||
|
||||
action_info['options'] = uncommon_options
|
||||
|
||||
docs['actions'][action] = action_info
|
||||
|
||||
docs['options'] = opt_doc_list(cli)
|
||||
return docs
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
parser = generate_parser()
|
||||
|
||||
options, args = parser.parse_args()
|
||||
|
||||
template_file = options.template_file
|
||||
template_path = os.path.expanduser(template_file)
|
||||
template_dir = os.path.abspath(os.path.dirname(template_path))
|
||||
template_basename = os.path.basename(template_file)
|
||||
|
||||
output_dir = os.path.abspath(options.output_dir)
|
||||
output_format = options.output_format
|
||||
|
||||
cli_modules = args
|
||||
|
||||
# various cli parsing things checks sys.argv if the 'args' that are passed in are []
|
||||
# so just remove any args so the cli modules dont try to parse them resulting in warnings
|
||||
sys.argv = [sys.argv[0]]
|
||||
# need to be in right dir
|
||||
os.chdir(os.path.dirname(__file__))
|
||||
|
||||
allvars = {}
|
||||
output = {}
|
||||
cli_list = []
|
||||
cli_bin_name_list = []
|
||||
|
||||
# for binary in os.listdir('../../lib/ansible/cli'):
|
||||
for cli_module_name in cli_modules:
|
||||
binary = os.path.basename(os.path.expanduser(cli_module_name))
|
||||
|
||||
if not binary.endswith('.py'):
|
||||
continue
|
||||
elif binary == '__init__.py':
|
||||
continue
|
||||
|
||||
cli_name = os.path.splitext(binary)[0]
|
||||
|
||||
if cli_name == 'adhoc':
|
||||
cli_class_name = 'AdHocCLI'
|
||||
# myclass = 'AdHocCLI'
|
||||
output[cli_name] = 'ansible.1.asciidoc.in'
|
||||
cli_bin_name = 'ansible'
|
||||
else:
|
||||
# myclass = "%sCLI" % libname.capitalize()
|
||||
cli_class_name = "%sCLI" % cli_name.capitalize()
|
||||
output[cli_name] = 'ansible-%s.1.asciidoc.in' % cli_name
|
||||
cli_bin_name = 'ansible-%s' % cli_name
|
||||
|
||||
# FIXME:
|
||||
allvars[cli_name] = opts_docs(cli_class_name, cli_name)
|
||||
cli_bin_name_list.append(cli_bin_name)
|
||||
|
||||
cli_list = allvars.keys()
|
||||
|
||||
doc_name_formats = {'man': '%s.1.asciidoc.in',
|
||||
'rst': '%s.rst'}
|
||||
|
||||
for cli_name in cli_list:
|
||||
|
||||
# template it!
|
||||
env = Environment(loader=FileSystemLoader(template_dir))
|
||||
template = env.get_template(template_basename)
|
||||
|
||||
# add rest to vars
|
||||
tvars = allvars[cli_name]
|
||||
tvars['cli_list'] = cli_list
|
||||
tvars['cli_bin_name_list'] = cli_bin_name_list
|
||||
tvars['cli'] = cli_name
|
||||
if '-i' in tvars['options']:
|
||||
print('uses inventory')
|
||||
|
||||
manpage = template.render(tvars)
|
||||
filename = os.path.join(output_dir, doc_name_formats[output_format] % tvars['cli_name'])
|
||||
|
||||
with open(filename, 'wb') as f:
|
||||
f.write(to_bytes(manpage))
|
||||
print("Wrote doc to %s" % filename)
|
||||
@ -1,629 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# (c) 2012, Jan-Piet Mens <jpmens () gmail.com>
|
||||
# (c) 2012-2014, Michael DeHaan <michael@ansible.com> and others
|
||||
# (c) 2017 Ansible Project
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
import datetime
|
||||
import glob
|
||||
import optparse
|
||||
import os
|
||||
import pprint
|
||||
import re
|
||||
import sys
|
||||
import warnings
|
||||
from collections import defaultdict
|
||||
try:
|
||||
from html import escape as html_escape
|
||||
except ImportError:
|
||||
# Python-3.2 or later
|
||||
import cgi
|
||||
|
||||
def html_escape(text, quote=True):
|
||||
return cgi.escape(text, quote)
|
||||
|
||||
import yaml
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from six import iteritems, string_types
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils._text import to_bytes
|
||||
from ansible.utils import plugin_docs
|
||||
|
||||
|
||||
#####################################################################################
|
||||
# constants and paths
|
||||
|
||||
# if a module is added in a version of Ansible older than this, don't print the version added information
|
||||
# in the module documentation because everyone is assumed to be running something newer than this already.
|
||||
TO_OLD_TO_BE_NOTABLE = 1.3
|
||||
|
||||
# Get parent directory of the directory this script lives in
|
||||
MODULEDIR = os.path.abspath(os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)), os.pardir, 'lib', 'ansible', 'modules'
|
||||
))
|
||||
|
||||
# The name of the DOCUMENTATION template
|
||||
EXAMPLE_YAML = os.path.abspath(os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)), os.pardir, 'examples', 'DOCUMENTATION.yml'
|
||||
))
|
||||
|
||||
_ITALIC = re.compile(r"I\(([^)]+)\)")
|
||||
_BOLD = re.compile(r"B\(([^)]+)\)")
|
||||
_MODULE = re.compile(r"M\(([^)]+)\)")
|
||||
_URL = re.compile(r"U\(([^)]+)\)")
|
||||
_CONST = re.compile(r"C\(([^)]+)\)")
|
||||
|
||||
DEPRECATED = b" (D)"
|
||||
|
||||
|
||||
def rst_ify(text):
|
||||
''' convert symbols like I(this is in italics) to valid restructured text '''
|
||||
|
||||
try:
|
||||
t = _ITALIC.sub(r'*' + r"\1" + r"*", text)
|
||||
t = _BOLD.sub(r'**' + r"\1" + r"**", t)
|
||||
t = _MODULE.sub(r':ref:`' + r"\1 <\1>" + r"`", t)
|
||||
t = _URL.sub(r"\1", t)
|
||||
t = _CONST.sub(r'``' + r"\1" + r"``", t)
|
||||
except Exception as e:
|
||||
raise AnsibleError("Could not process (%s) : %s" % (str(text), str(e)))
|
||||
|
||||
return t
|
||||
|
||||
|
||||
def html_ify(text):
|
||||
''' convert symbols like I(this is in italics) to valid HTML '''
|
||||
|
||||
t = html_escape(text)
|
||||
t = _ITALIC.sub("<em>" + r"\1" + "</em>", t)
|
||||
t = _BOLD.sub("<b>" + r"\1" + "</b>", t)
|
||||
t = _MODULE.sub("<span class='module'>" + r"\1" + "</span>", t)
|
||||
t = _URL.sub("<a href='" + r"\1" + "'>" + r"\1" + "</a>", t)
|
||||
t = _CONST.sub("<code>" + r"\1" + "</code>", t)
|
||||
|
||||
return t
|
||||
|
||||
|
||||
def rst_fmt(text, fmt):
|
||||
''' helper for Jinja2 to do format strings '''
|
||||
|
||||
return fmt % (text)
|
||||
|
||||
|
||||
def rst_xline(width, char="="):
|
||||
''' return a restructured text line of a given length '''
|
||||
|
||||
return char * width
|
||||
|
||||
|
||||
def write_data(text, output_dir, outputname, module=None):
|
||||
''' dumps module output to a file or the screen, as requested '''
|
||||
|
||||
if output_dir is not None:
|
||||
if module:
|
||||
outputname = outputname % module
|
||||
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
fname = os.path.join(output_dir, outputname)
|
||||
fname = fname.replace(".py", "")
|
||||
with open(fname, 'wb') as f:
|
||||
f.write(to_bytes(text))
|
||||
else:
|
||||
print(text)
|
||||
|
||||
|
||||
def get_module_info(module_dir, limit_to_modules=None, verbose=False):
|
||||
'''
|
||||
Returns information about modules and the categories that they belong to
|
||||
|
||||
:arg module_dir: file system path to the top of the modules directory
|
||||
:kwarg limit_to_modules: If given, this is a list of module names to
|
||||
generate information for. All other modules will be ignored.
|
||||
:returns: Tuple of two dicts containing module_info, categories, and
|
||||
aliases and a set listing deprecated modules:
|
||||
|
||||
:module_info: mapping of module names to information about them. The fields of the dict are:
|
||||
|
||||
:path: filesystem path to the module
|
||||
:deprecated: boolean. True means the module is deprecated otherwise not.
|
||||
:aliases: set of aliases to this module name
|
||||
:metadata: The modules metadata (as recorded in the module)
|
||||
:doc: The documentation structure for the module
|
||||
:examples: The module's examples
|
||||
:returndocs: The module's returndocs
|
||||
|
||||
:categories: maps category names to a dict. The dict contains at
|
||||
least one key, '_modules' which contains a list of module names in
|
||||
that category. Any other keys in the dict are subcategories with
|
||||
the same structure.
|
||||
|
||||
'''
|
||||
|
||||
categories = dict()
|
||||
module_info = defaultdict(dict)
|
||||
|
||||
# * windows powershell modules have documentation stubs in python docstring
|
||||
# format (they are not executed) so skip the ps1 format files
|
||||
# * One glob level for every module level that we're going to traverse
|
||||
files = (
|
||||
glob.glob("%s/*.py" % module_dir) +
|
||||
glob.glob("%s/*/*.py" % module_dir) +
|
||||
glob.glob("%s/*/*/*.py" % module_dir) +
|
||||
glob.glob("%s/*/*/*/*.py" % module_dir)
|
||||
)
|
||||
|
||||
for module_path in files:
|
||||
# Do not list __init__.py files
|
||||
if module_path.endswith('__init__.py'):
|
||||
continue
|
||||
|
||||
# Do not list blacklisted modules
|
||||
module = os.path.splitext(os.path.basename(module_path))[0]
|
||||
if module in plugin_docs.BLACKLIST['MODULE'] or module == 'base':
|
||||
continue
|
||||
|
||||
# If requested, limit module documentation building only to passed-in
|
||||
# modules.
|
||||
if limit_to_modules is not None and module.lower() not in limit_to_modules:
|
||||
continue
|
||||
|
||||
deprecated = False
|
||||
if module.startswith("_"):
|
||||
if os.path.islink(module_path):
|
||||
# Handle aliases
|
||||
source = os.path.splitext(os.path.basename(os.path.realpath(module_path)))[0]
|
||||
module = module.replace("_", "", 1)
|
||||
aliases = module_info[source].get('aliases', set())
|
||||
aliases.add(module)
|
||||
# In case we just created this via get()'s fallback
|
||||
module_info[source]['aliases'] = aliases
|
||||
continue
|
||||
else:
|
||||
# Handle deprecations
|
||||
module = module.replace("_", "", 1)
|
||||
deprecated = True
|
||||
|
||||
#
|
||||
# Regular module to process
|
||||
#
|
||||
|
||||
category = categories
|
||||
|
||||
# Start at the second directory because we don't want the "vendor"
|
||||
mod_path_only = os.path.dirname(module_path[len(module_dir):])
|
||||
|
||||
module_categories = []
|
||||
# build up the categories that this module belongs to
|
||||
for new_cat in mod_path_only.split('/')[1:]:
|
||||
if new_cat not in category:
|
||||
category[new_cat] = dict()
|
||||
category[new_cat]['_modules'] = []
|
||||
module_categories.append(new_cat)
|
||||
category = category[new_cat]
|
||||
|
||||
category['_modules'].append(module)
|
||||
|
||||
# the category we will use in links (so list_of_all_plugins can point to plugins/action_plugins/*'
|
||||
if module_categories:
|
||||
primary_category = module_categories[0]
|
||||
|
||||
# use ansible core library to parse out doc metadata YAML and plaintext examples
|
||||
doc, examples, returndocs, metadata = plugin_docs.get_docstring(module_path, verbose=verbose)
|
||||
|
||||
# save all the information
|
||||
module_info[module] = {'path': module_path,
|
||||
'deprecated': deprecated,
|
||||
'aliases': set(),
|
||||
'metadata': metadata,
|
||||
'doc': doc,
|
||||
'examples': examples,
|
||||
'returndocs': returndocs,
|
||||
'categories': module_categories,
|
||||
'primary_category': primary_category,
|
||||
}
|
||||
|
||||
# keep module tests out of becoming module docs
|
||||
if 'test' in categories:
|
||||
del categories['test']
|
||||
|
||||
return module_info, categories
|
||||
|
||||
|
||||
def generate_parser():
|
||||
''' generate an optparse parser '''
|
||||
|
||||
p = optparse.OptionParser(
|
||||
version='%prog 1.0',
|
||||
usage='usage: %prog [options] arg1 arg2',
|
||||
description='Generate module documentation from metadata',
|
||||
)
|
||||
|
||||
p.add_option("-A", "--ansible-version", action="store", dest="ansible_version", default="unknown", help="Ansible version number")
|
||||
p.add_option("-M", "--module-dir", action="store", dest="module_dir", default=MODULEDIR, help="Ansible library path")
|
||||
p.add_option("-P", "--plugin-type", action="store", dest="plugin_type", default='modules', help="The type of plugin (plugins, modules)")
|
||||
p.add_option("-T", "--template-dir", action="store", dest="template_dir", default="hacking/templates", help="directory containing Jinja2 templates")
|
||||
p.add_option("-t", "--type", action='store', dest='type', choices=['rst'], default='rst', help="Document type")
|
||||
p.add_option("-v", "--verbose", action='store_true', default=False, help="Verbose")
|
||||
p.add_option("-o", "--output-dir", action="store", dest="output_dir", default=None, help="Output directory for module files")
|
||||
p.add_option("-I", "--includes-file", action="store", dest="includes_file", default=None, help="Create a file containing list of processed modules")
|
||||
p.add_option("-l", "--limit-to-modules", action="store", dest="limit_to_modules", default=None,
|
||||
help="Limit building module documentation to comma-separated list of modules. Specify non-existing module name for no modules.")
|
||||
p.add_option('-V', action='version', help='Show version number and exit')
|
||||
return p
|
||||
|
||||
|
||||
def jinja2_environment(template_dir, typ, plugin_type):
|
||||
|
||||
env = Environment(loader=FileSystemLoader(template_dir),
|
||||
variable_start_string="@{",
|
||||
variable_end_string="}@",
|
||||
trim_blocks=True)
|
||||
env.globals['xline'] = rst_xline
|
||||
|
||||
templates = {}
|
||||
if typ == 'rst':
|
||||
env.filters['convert_symbols_to_format'] = rst_ify
|
||||
env.filters['html_ify'] = html_ify
|
||||
env.filters['fmt'] = rst_fmt
|
||||
env.filters['xline'] = rst_xline
|
||||
templates['plugin'] = env.get_template('plugin.rst.j2')
|
||||
templates['category_list'] = env.get_template('%s_by_category.rst.j2' % plugin_type)
|
||||
templates['support_list'] = env.get_template('%s_by_support.rst.j2' % plugin_type)
|
||||
templates['list_of_CATEGORY_modules'] = env.get_template('list_of_CATEGORY_%s.rst.j2' % plugin_type)
|
||||
else:
|
||||
raise Exception("unknown module format type: %s" % typ)
|
||||
|
||||
return templates
|
||||
|
||||
|
||||
def too_old(added):
|
||||
if not added:
|
||||
return False
|
||||
try:
|
||||
added_tokens = str(added).split(".")
|
||||
readded = added_tokens[0] + "." + added_tokens[1]
|
||||
added_float = float(readded)
|
||||
except ValueError as e:
|
||||
warnings.warn("Could not parse %s: %s" % (added, str(e)))
|
||||
return False
|
||||
return added_float < TO_OLD_TO_BE_NOTABLE
|
||||
|
||||
|
||||
def process_modules(module_map, templates, outputname, output_dir, ansible_version, plugin_type):
|
||||
for module in module_map:
|
||||
# print("rendering: %s" % module)
|
||||
|
||||
# pprint.pprint(('process_modules module:', module))
|
||||
|
||||
fname = module_map[module]['path']
|
||||
|
||||
# pprint.pprint(('process_modules module_info: ', module_map[module]))
|
||||
|
||||
module_categories = module_map[module].get('categories', [])
|
||||
|
||||
# crash if module is missing documentation and not explicitly hidden from docs index
|
||||
if module_map[module]['doc'] is None:
|
||||
print("%s: ERROR: MODULE MISSING DOCUMENTATION" % (fname,))
|
||||
_doc = {'module': module,
|
||||
'version_added': '2.4',
|
||||
'filename': fname}
|
||||
module_map[module]['doc'] = _doc
|
||||
# continue
|
||||
|
||||
# Going to reference this heavily so make a short name to reference it by
|
||||
doc = module_map[module]['doc']
|
||||
|
||||
# pprint.pprint(('process_modules doc: ', doc))
|
||||
|
||||
# add some defaults for plugins that dont have most of the info
|
||||
doc['module'] = doc.get('module', module)
|
||||
doc['version_added'] = doc.get('version_added', 'historical')
|
||||
|
||||
doc['plugin_type'] = plugin_type
|
||||
|
||||
if module_map[module]['deprecated'] and 'deprecated' not in doc:
|
||||
print("%s: WARNING: MODULE MISSING DEPRECATION DOCUMENTATION: %s" % (fname, 'deprecated'))
|
||||
|
||||
required_fields = ('short_description',)
|
||||
for field in required_fields:
|
||||
if field not in doc:
|
||||
print("%s: WARNING: MODULE MISSING field '%s'" % (fname, field))
|
||||
|
||||
not_nullable_fields = ('short_description',)
|
||||
for field in not_nullable_fields:
|
||||
if field in doc and doc[field] in (None, ''):
|
||||
print("%s: WARNING: MODULE field '%s' DOCUMENTATION is null/empty value=%s" % (fname, field, doc[field]))
|
||||
|
||||
if 'version_added' not in doc:
|
||||
pprint.pprint(doc)
|
||||
# sys.exit("*** ERROR: missing version_added in: %s ***\n" % module)
|
||||
|
||||
#
|
||||
# The present template gets everything from doc so we spend most of this
|
||||
# function moving data into doc for the template to reference
|
||||
#
|
||||
|
||||
if module_map[module]['aliases']:
|
||||
doc['aliases'] = module_map[module]['aliases']
|
||||
|
||||
# don't show version added information if it's too old to be called out
|
||||
added = 0
|
||||
if doc['version_added'] == 'historical':
|
||||
del doc['version_added']
|
||||
else:
|
||||
added = doc['version_added']
|
||||
|
||||
# Strip old version_added for the module
|
||||
if too_old(added):
|
||||
del doc['version_added']
|
||||
|
||||
option_names = []
|
||||
|
||||
if 'options' in doc and doc['options']:
|
||||
for (k, v) in iteritems(doc['options']):
|
||||
# Error out if there's no description
|
||||
if 'description' not in doc['options'][k]:
|
||||
raise AnsibleError("Missing required description for option %s in %s " % (k, module))
|
||||
|
||||
# Error out if required isn't a boolean (people have been putting
|
||||
# information on when something is required in here. Those need
|
||||
# to go in the description instead).
|
||||
required_value = doc['options'][k].get('required', False)
|
||||
if not isinstance(required_value, bool):
|
||||
raise AnsibleError("Invalid required value '%s' for option '%s' in '%s' (must be truthy)" % (required_value, k, module))
|
||||
|
||||
# Strip old version_added information for options
|
||||
if 'version_added' in doc['options'][k] and too_old(doc['options'][k]['version_added']):
|
||||
del doc['options'][k]['version_added']
|
||||
|
||||
# Make sure description is a list of lines for later formatting
|
||||
if not isinstance(doc['options'][k]['description'], list):
|
||||
doc['options'][k]['description'] = [doc['options'][k]['description']]
|
||||
|
||||
option_names.append(k)
|
||||
|
||||
option_names.sort()
|
||||
|
||||
doc['option_keys'] = option_names
|
||||
doc['filename'] = fname
|
||||
doc['docuri'] = doc['module'].replace('_', '-')
|
||||
doc['now_date'] = datetime.date.today().strftime('%Y-%m-%d')
|
||||
doc['ansible_version'] = ansible_version
|
||||
|
||||
# check the 'deprecated' field in doc. We expect a dict potentially with 'why', 'version', and 'alternative' fields
|
||||
# examples = module_map[module]['examples']
|
||||
# print('\n\n%s: type of examples: %s\n' % (module, type(examples)))
|
||||
# if examples and not isinstance(examples, (str, unicode, list)):
|
||||
# raise TypeError('module %s examples is wrong type (%s): %s' % (module, type(examples), examples))
|
||||
|
||||
# use 'examples' for 'plainexamples' if 'examples' is a string
|
||||
if isinstance(module_map[module]['examples'], string_types):
|
||||
doc['plainexamples'] = module_map[module]['examples'] # plain text
|
||||
else:
|
||||
doc['plainexamples'] = ''
|
||||
|
||||
doc['metadata'] = module_map[module]['metadata']
|
||||
|
||||
# pprint.pprint(module_map[module]
|
||||
if module_map[module]['returndocs']:
|
||||
try:
|
||||
doc['returndocs'] = yaml.safe_load(module_map[module]['returndocs'])
|
||||
except Exception as e:
|
||||
print("%s:%s:yaml error:%s:returndocs=%s" % (fname, module, e, module_map[module]['returndocs']))
|
||||
doc['returndocs'] = None
|
||||
else:
|
||||
doc['returndocs'] = None
|
||||
|
||||
doc['author'] = doc.get('author', ['UNKNOWN'])
|
||||
if isinstance(doc['author'], string_types):
|
||||
doc['author'] = [doc['author']]
|
||||
|
||||
# print('about to template')
|
||||
# pprint.pprint(doc)
|
||||
text = templates['plugin'].render(doc)
|
||||
|
||||
# plugins get namespace dirs but modules do not
|
||||
if plugin_type == 'plugins':
|
||||
for module_category in module_categories:
|
||||
category_output_dir = os.path.join(output_dir, 'plugins', '%s' % module_category)
|
||||
write_data(text, category_output_dir, outputname, module)
|
||||
else:
|
||||
write_data(text, output_dir, outputname, module)
|
||||
|
||||
|
||||
def process_categories(mod_info, categories, templates,
|
||||
output_dir, output_name, plugin_type):
|
||||
for category in sorted(categories.keys()):
|
||||
if (plugin_type, category) == ('plugins', ''):
|
||||
print('skipping unknown cat: %s' % category)
|
||||
continue
|
||||
module_map = categories[category]
|
||||
category_filename = output_name % category
|
||||
|
||||
print("*** recording category %s in %s ***" % (category, category_filename))
|
||||
|
||||
# start a new category file
|
||||
|
||||
category_name = category.replace("_", " ")
|
||||
category_title = category_name.title()
|
||||
|
||||
subcategories = dict((k, v) for k, v in module_map.items() if k != '_modules')
|
||||
template_data = {'title': category_title,
|
||||
'category_name': category_name,
|
||||
'category': module_map,
|
||||
'subcategories': subcategories,
|
||||
'module_info': mod_info,
|
||||
'plugin_type': plugin_type
|
||||
}
|
||||
|
||||
text = templates['list_of_CATEGORY_modules'].render(template_data)
|
||||
write_data(text, output_dir, category_filename)
|
||||
|
||||
|
||||
def process_support_levels(mod_info, templates, output_dir, plugin_type):
|
||||
supported_by = {'Ansible Core Team': {'slug': 'core_supported',
|
||||
'modules': [],
|
||||
'output': 'core_maintained.rst',
|
||||
'blurb': "These are :doc:`modules maintained by the"
|
||||
" Ansible Core Team<core_maintained>` and will always ship"
|
||||
" with Ansible itself."},
|
||||
'Ansible Network Team': {'slug': 'network_supported',
|
||||
'modules': [],
|
||||
'output': 'network_maintained.rst',
|
||||
'blurb': "These are :doc:`modules maintained by the"
|
||||
" Ansible Network Team<network_maintained>` in"
|
||||
" a relationship similar to how the Ansible Core Team"
|
||||
" maintains the Core modules."},
|
||||
'Ansible Partners': {'slug': 'partner_supported',
|
||||
'modules': [],
|
||||
'output': 'partner_maintained.rst',
|
||||
'blurb': """
|
||||
Some examples of :doc:`Certified Modules<partner_maintained>` are those submitted by other
|
||||
companies. Maintainers of these types of modules must watch for any issues reported or pull requests
|
||||
raised against the module.
|
||||
|
||||
The Ansible Core Team will review all modules becoming certified. Core committers will review
|
||||
proposed changes to existing Certified Modules once the community maintainers of the module have
|
||||
approved the changes. Core committers will also ensure that any issues that arise due to Ansible
|
||||
engine changes will be remediated. Also, it is strongly recommended (but not presently required)
|
||||
for these types of modules to have unit tests.
|
||||
|
||||
These modules are currently shipped with Ansible, but might be shipped separately in the future.
|
||||
"""},
|
||||
'Ansible Community': {'slug': 'community_supported',
|
||||
'modules': [],
|
||||
'output': 'community_maintained.rst',
|
||||
'blurb': """
|
||||
These are :doc:`modules maintained by the Ansible Community<community_maintained>`. They **are
|
||||
not** supported by the Ansible Core Team or by companies/partners associated to the module.
|
||||
|
||||
They are still fully usable, but the response rate to issues is purely up to the community. Best
|
||||
effort support will be provided but is not covered under any support contracts.
|
||||
|
||||
These modules are currently shipped with Ansible, but will most likely be shipped separately in the future.
|
||||
"""},
|
||||
}
|
||||
|
||||
# only gen support pages for modules for now, need to split and namespace templates and generated docs
|
||||
if plugin_type == 'plugins':
|
||||
return
|
||||
# Separate the modules by support_level
|
||||
for module, info in mod_info.items():
|
||||
if not info.get('metadata', None):
|
||||
print('no metadata for %s' % module)
|
||||
continue
|
||||
if info['metadata']['supported_by'] == 'core':
|
||||
supported_by['Ansible Core Team']['modules'].append(module)
|
||||
elif info['metadata']['supported_by'] == 'network':
|
||||
supported_by['Ansible Network Team']['modules'].append(module)
|
||||
elif info['metadata']['supported_by'] == 'certified':
|
||||
supported_by['Ansible Partners']['modules'].append(module)
|
||||
elif info['metadata']['supported_by'] == 'community':
|
||||
supported_by['Ansible Community']['modules'].append(module)
|
||||
else:
|
||||
raise AnsibleError('Unknown supported_by value: %s' % info['metadata']['supported_by'])
|
||||
|
||||
# Render the module lists
|
||||
for maintainers, data in supported_by.items():
|
||||
template_data = {'maintainers': maintainers,
|
||||
'modules': data['modules'],
|
||||
'slug': data['slug'],
|
||||
'module_info': mod_info,
|
||||
}
|
||||
text = templates['support_list'].render(template_data)
|
||||
write_data(text, output_dir, data['output'])
|
||||
|
||||
|
||||
def validate_options(options):
|
||||
''' validate option parser options '''
|
||||
|
||||
if not options.module_dir:
|
||||
sys.exit("--module-dir is required", file=sys.stderr)
|
||||
if not os.path.exists(options.module_dir):
|
||||
sys.exit("--module-dir does not exist: %s" % options.module_dir, file=sys.stderr)
|
||||
if not options.template_dir:
|
||||
sys.exit("--template-dir must be specified")
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
p = generate_parser()
|
||||
|
||||
(options, args) = p.parse_args()
|
||||
validate_options(options)
|
||||
|
||||
plugin_type = options.plugin_type
|
||||
|
||||
if plugin_type == 'modules':
|
||||
templates = jinja2_environment(options.template_dir, options.type, plugin_type)
|
||||
output_dir = options.output_dir
|
||||
# trim trailing s off of plugin_type for plugin_type=='modules'. ie 'copy_module.rst'
|
||||
outputname = '%s_' + '%s.rst' % plugin_type[:-1]
|
||||
else:
|
||||
templates = jinja2_environment(options.template_dir, options.type, 'plugins')
|
||||
# for plugins, just use 'ssh.rst' vs 'ssh_module.rst'
|
||||
outputname = '%s.rst'
|
||||
output_dir = '%s/plugins/%s' % (options.output_dir, plugin_type)
|
||||
|
||||
# Convert passed-in limit_to_modules to None or list of modules.
|
||||
if options.limit_to_modules is not None:
|
||||
options.limit_to_modules = [s.lower() for s in options.limit_to_modules.split(",")]
|
||||
|
||||
mod_info, categories = get_module_info(options.module_dir, limit_to_modules=options.limit_to_modules, verbose=options.verbose)
|
||||
|
||||
categories['all'] = {'_modules': mod_info.keys()}
|
||||
|
||||
# pprint.pprint(categories)
|
||||
# pprint.pprint(mod_info)
|
||||
# pprint.pprint(dict(mod_info))
|
||||
# Transform the data
|
||||
if options.type == 'rst':
|
||||
for key, record in mod_info.items():
|
||||
# pprint.pprint(('record', record))
|
||||
if record.get('doc', None):
|
||||
short_desc = record['doc']['short_description']
|
||||
if short_desc is None:
|
||||
print('WARNING: short_description for %s is None' % key)
|
||||
short_desc = ''
|
||||
record['doc']['short_description'] = rst_ify(short_desc)
|
||||
|
||||
if plugin_type == 'modules':
|
||||
# Write master category list
|
||||
category_list_text = templates['category_list'].render(categories=sorted(categories.keys()))
|
||||
category_index_name = '%s_by_category.rst' % plugin_type
|
||||
write_data(category_list_text, output_dir, category_index_name)
|
||||
|
||||
# Render all the individual module pages
|
||||
process_modules(mod_info, templates, outputname, output_dir, options.ansible_version, plugin_type)
|
||||
|
||||
# Render all the categories for modules
|
||||
if plugin_type == 'modules':
|
||||
category_list_name_template = 'list_of_%s_' + '%s.rst' % plugin_type
|
||||
process_categories(mod_info, categories, templates, output_dir, category_list_name_template, plugin_type)
|
||||
|
||||
# Render all the categories for modules
|
||||
process_support_levels(mod_info, templates, output_dir, plugin_type)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -1,12 +0,0 @@
|
||||
#!/bin/bash -eu
|
||||
|
||||
cat <<- EOF > ../docsite/rst/dev_guide/testing/sanity/index.rst
|
||||
Sanity Tests
|
||||
============
|
||||
|
||||
The following sanity tests are available as \`\`--test\`\` options for \`\`ansible-test sanity\`\`:
|
||||
|
||||
$(for test in $(../../test/runner/ansible-test sanity --list-tests); do echo "- :doc:\`${test} <${test}>\`"; done)
|
||||
|
||||
This list is also available using \`\`ansible-test sanity --list-tests\`\`.
|
||||
EOF
|
||||
@ -1,105 +0,0 @@
|
||||
OS := $(shell uname -s)
|
||||
SITELIB = $(shell python -c "from distutils.sysconfig import get_python_lib; print get_python_lib()"):
|
||||
FORMATTER=../bin/plugin_formatter.py
|
||||
TESTING_FORMATTER=../bin/testing_formatter.sh
|
||||
DUMPER=../bin/dump_keywords.py
|
||||
CONFIG_DUMPER=../bin/dump_config.py
|
||||
GENERATE_CLI=../bin/generate_man.py
|
||||
ifeq ($(shell echo $(OS) | egrep -ic 'Darwin|FreeBSD|OpenBSD|DragonFly'),1)
|
||||
CPUS ?= $(shell sysctl hw.ncpu|awk '{print $$2}')
|
||||
else
|
||||
CPUS ?= $(shell nproc)
|
||||
endif
|
||||
|
||||
MODULE_ARGS=
|
||||
ifdef MODULES
|
||||
MODULE_ARGS = -l $(MODULES)
|
||||
endif
|
||||
|
||||
PLUGIN_ARGS=
|
||||
ifdef PLUGINS
|
||||
PLUGIN_ARGS = -l $(PLUGINS)
|
||||
endif
|
||||
|
||||
DOC_PLUGINS ?= cache callback connection inventory lookup strategy vars
|
||||
|
||||
assertrst:
|
||||
ifndef rst
|
||||
$(error specify document or pattern with rst=somefile.rst)
|
||||
endif
|
||||
|
||||
all: docs
|
||||
|
||||
docs: clean htmldocs
|
||||
|
||||
generate_rst: testing keywords modules plugins staticmin cli config
|
||||
|
||||
htmldocs: generate_rst
|
||||
CPUS=$(CPUS) $(MAKE) -f Makefile.sphinx html
|
||||
|
||||
singlehtmldocs: generate_rst
|
||||
CPUS=$(CPUS) $(MAKE) -f Makefile.sphinx singlehtml
|
||||
|
||||
webdocs: docs
|
||||
|
||||
#TODO: leaving htmlout removal for those having older versions, should eventually be removed also
|
||||
clean:
|
||||
-rm -rf htmlout
|
||||
-rm -rf _build
|
||||
-rm -f .buildinfo
|
||||
-rm -f objects.inv
|
||||
-rm -rf *.doctrees
|
||||
@echo "Cleaning up minified css files"
|
||||
find . -type f -name "*.min.css" -delete
|
||||
@echo "Cleaning up byte compiled python stuff"
|
||||
find . -regex ".*\.py[co]$$" -delete
|
||||
@echo "Cleaning up editor backup files"
|
||||
find . -type f \( -name "*~" -or -name "#*" \) -delete
|
||||
find . -type f \( -name "*.swp" \) -delete
|
||||
@echo "Cleaning up generated rst"
|
||||
-rm rst/list_of_*.rst
|
||||
-rm rst/*_by_category.rst
|
||||
-rm rst/*_module.rst
|
||||
-rm rst/*_plugin.rst
|
||||
-rm rst/*_maintained.rst
|
||||
-rm rst/playbooks_directives.rst
|
||||
-rm rst/playbooks_keywords.rst
|
||||
-rm rst/plugins/*/*.rst
|
||||
-rm rst/config.rst
|
||||
-rm rst/ansible-*.rst
|
||||
-rm rst/ansible.rst
|
||||
|
||||
.PHONEY: docs clean
|
||||
|
||||
# TODO: make generate_man output dir cli option
|
||||
cli: $(GENERATE_CLI)
|
||||
PYTHONPATH=../../lib $(GENERATE_CLI) --template-file=../templates/cli_rst.j2 --output-dir=rst/ --output-format rst ../../lib/ansible/cli/*.py
|
||||
|
||||
keywords: $(FORMATTER) ../templates/playbooks_keywords.rst.j2
|
||||
PYTHONPATH=../../lib $(DUMPER) --template-dir=../templates --output-dir=rst/ -d ./keyword_desc.yml
|
||||
|
||||
config:
|
||||
PYTHONPATH=../../lib $(CONFIG_DUMPER) --template-file=../templates/config.rst.j2 --output-dir=rst/ -d ../../lib/ansible/config/base.yml
|
||||
|
||||
modules: $(FORMATTER) ../templates/plugin.rst.j2
|
||||
PYTHONPATH=../../lib $(FORMATTER) -t rst --template-dir=../templates --module-dir=../../lib/ansible/modules -o rst/ $(MODULE_ARGS)
|
||||
|
||||
plugins: $(FORMATTER) ../templates/plugin.rst.j2
|
||||
@echo "looping over doc plugins"
|
||||
for plugin in $(DOC_PLUGINS); \
|
||||
do \
|
||||
PYTHONPATH=../../lib $(FORMATTER) -t rst --plugin-type $$plugin --template-dir=../templates --module-dir=../../lib/ansible/plugins/$$plugin -o rst/ $(PLUGIN_ARGS); \
|
||||
done
|
||||
|
||||
testing:
|
||||
$(TESTING_FORMATTER)
|
||||
|
||||
staticmin:
|
||||
cat _themes/srtd/static/css/theme.css | sed -e 's/^[ ]*//g; s/[ ]*$$//g; s/\([:{;,]\) /\1/g; s/ {/{/g; s/\/\*.*\*\///g; /^$$/d' | sed -e :a -e '$$!N; s/\n\(.\)/\1/; ta' > _themes/srtd/static/css/theme.min.css
|
||||
|
||||
epub:
|
||||
(CPUS=$(CPUS) make -f Makefile.sphinx epub)
|
||||
|
||||
htmlsingle: assertrst
|
||||
sphinx-build -j $(CPUS) -b html -d _build/doctrees ./rst _build/html rst/$(rst)
|
||||
@echo "Output is in _build/html/$(rst:.rst=.html)"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user