Compare commits
570 Commits
v2.4.2.0-1
...
stable-2.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 49f7e6a12c | |||
| d70fd17cf9 | |||
| 5d5abbb898 | |||
| 3f7b7a1bb9 | |||
| 858a0a2339 | |||
| 61989f72cc | |||
| b3da42a669 | |||
| e15daf186a | |||
| 9c4467303f | |||
| df3f9345e1 | |||
| 5c7c13b9c1 | |||
| 0ef6a7ae98 | |||
| 1fd5bcdc09 | |||
| d178c4be2a | |||
| be846c7053 | |||
| fa2834d863 | |||
| 320e099c12 | |||
| f0ee61925a | |||
| d024517d4b | |||
| 651842a4ef | |||
| ee7de10285 | |||
| bec698052f | |||
| 729dbe7e67 | |||
| 032d917e68 | |||
| 9ecdec0141 | |||
| 028c710380 | |||
| 8ee8872d6b | |||
| caf1537510 | |||
| 6f54b1ae2b | |||
| f77690e652 | |||
| 5336a51a9e | |||
| 9c049b757f | |||
| 0ccbbe0c09 | |||
| 43bbd4b6dc | |||
| 517eec8c49 | |||
| b85d9acc09 | |||
| ba5a3f2f67 | |||
| eafc46512d | |||
| 39a53c5623 | |||
| 9fae8501e2 | |||
| 724761d2a1 | |||
| 3e2b2b359c | |||
| 5ebd5c548c | |||
| 18ffa8a4da | |||
| fa88d0770c | |||
| 24cae22e0b | |||
| 4f2a2ec884 | |||
| 7d44063c1e | |||
| dcc0a2d596 | |||
| c97afd0c48 | |||
| 7c726d197b | |||
| 13c379dcd0 | |||
| 9196a72c3f | |||
| 0b4a259444 | |||
| 5353b8c3b6 | |||
| 6869ced587 | |||
| 87414e4fd1 | |||
| ef9c27e280 | |||
| 76ab1d2afc | |||
| a316891968 | |||
| 3303f25a0c | |||
| bee699a33a | |||
| 4133cf924a | |||
| 53559433ef | |||
| 8ca8459af5 | |||
| 6c559e904a | |||
| 901beeec55 | |||
| 4604deca92 | |||
| d001c73699 | |||
| 53cfde5096 | |||
| 6c9d2cc2b4 | |||
| 4de1e26d5c | |||
| 8ac8f4c2ab | |||
| 5035e53e80 | |||
| 2126e9fee1 | |||
| 93cab88663 | |||
| c5f857ec39 | |||
| 07dd83bc6e | |||
| acb556b3d4 | |||
| 48b20684f2 | |||
| d7a6ccc4c2 | |||
| 7f2bc082fa | |||
| d975f32243 | |||
| e8e388a742 | |||
| a6c25f143a | |||
| e957ab5a7f | |||
| 46002111bb | |||
| 05bb1b3cb0 | |||
| 8d7056220d | |||
| 76c941305b | |||
| 48da1b8e85 | |||
| 1e56daf66c | |||
| 506b0a8b61 | |||
| ea1339de18 | |||
| d60764bc81 | |||
| 951fa36fca | |||
| 43cf527fe7 | |||
| a074ebedbc | |||
| d29dfdc84b | |||
| aac8ea0262 | |||
| 3db2073909 | |||
| 0fddd90f14 | |||
| 5fe16bef6f | |||
| 86b179d3d7 | |||
| 7a8520ac1a | |||
| 8f7868aead | |||
| 9ddb4a985c | |||
| 55db4d9e23 | |||
| 849def8d6e | |||
| c0d6a0c11a | |||
| 97cd0bc803 | |||
| 7c7e849a27 | |||
| 9eaa5fecc1 | |||
| a0b7620b8e | |||
| 3906614538 | |||
| 03a89e17b9 | |||
| c52a35ff34 | |||
| 16ee2090bd | |||
| 7e1a3da97c | |||
| 625e8f2030 | |||
| 826f42a153 | |||
| 1fdf8bca2a | |||
| 6eaf9b9fa2 | |||
| 9a3515357b | |||
| cd0812a187 | |||
| 864c67cec5 | |||
| 7fe91e1848 | |||
| 084bacc4a4 | |||
| e6c9d9c81b | |||
| 0c250f2b07 | |||
| a516f883ac | |||
| 95f7aa2b53 | |||
| da4a9d6da0 | |||
| d18cc79873 | |||
| 2b3cd2577a | |||
| 50bb0d08f1 | |||
| 391420d88f | |||
| 4627b3efbb | |||
| 7d8794daa6 | |||
| 2021362386 | |||
| bb564075b5 | |||
| 09b26b6373 | |||
| 8647fdc7f2 | |||
| 7fe0bfd4f9 | |||
| 2b5caa23fb | |||
| 0d2a9d3f53 | |||
| 34e3a8e6cb | |||
| 1fc35117d3 | |||
| fb81447581 | |||
| 2dfcb218dc | |||
| 44ab6db535 | |||
| d9f03cf014 | |||
| c5e46efbd9 | |||
| 92579eb48f | |||
| d56e2fb80e | |||
| 1f930f098f | |||
| 63404821da | |||
| cf75521bc8 | |||
| 50d34f90e7 | |||
| 089f17bfc8 | |||
| 27bf701260 | |||
| 299e42bc28 | |||
| 2f40903397 | |||
| 2ec31c2c79 | |||
| 7afd8e9640 | |||
| 93781e0b05 | |||
| edff4a9219 | |||
| 69b8a48ed1 | |||
| 8f45660398 | |||
| c8ffe94bf4 | |||
| c06b2e4622 | |||
| 1394cdf6b9 | |||
| 38a272b004 | |||
| 97a5804a2c | |||
| 8436a88b5d | |||
| 0bf20fab84 | |||
| 22272fe5b8 | |||
| 7d8afbd83d | |||
| f11e54bcff | |||
| 46a3a6a03e | |||
| e29ae99b50 | |||
| 363b74e498 | |||
| 17b60f4fbf | |||
| b89f211ed4 | |||
| cde5d8444d | |||
| 6cf0386a52 | |||
| b09f756195 | |||
| 27f9b9dc43 | |||
| 31a951c4f7 | |||
| fe55e87ab4 | |||
| 2ed31fc818 | |||
| be087575cc | |||
| 79931041e2 | |||
| 832a739331 | |||
| b83ef89f09 | |||
| 419bd389d8 | |||
| e497e2b5c0 | |||
| 16ae952888 | |||
| 954eba8605 | |||
| e4b701c00a | |||
| 21c22a2416 | |||
| 4e98cc351c | |||
| f53cb5f63e | |||
| 1cae019c79 | |||
| f3470c1062 | |||
| 21a4c1380c | |||
| 4811504a6e | |||
| 08fee9bfc5 | |||
| 2cb2994371 | |||
| 2eb59bedcf | |||
| 14bf6b016c | |||
| 28fecc9ce1 | |||
| 2c647f18e5 | |||
| 4e087bb14f | |||
| 1f7fbf29f9 | |||
| 0681c2fd8f | |||
| 4a6235e320 | |||
| 94f13c7271 | |||
| 93eb60161c | |||
| 2f1fc85002 | |||
| 91f35363a2 | |||
| f76633b347 | |||
| 9c9bfb962e | |||
| 0e27fa4540 | |||
| 3a972c9170 | |||
| 91c7691a92 | |||
| 502ad88506 | |||
| 80f0380312 | |||
| a8f160d2fd | |||
| 6be48dd14c | |||
| fcd074d40f | |||
| 21775d7866 | |||
| ab536c8aa8 | |||
| 93f37f5969 | |||
| ec3b7b7de8 | |||
| 7a4914aa9b | |||
| 9ca5da82ff | |||
| 3d608ef9fa | |||
| c14e099dd7 | |||
| 7e318e8398 | |||
| add2e9cbd1 | |||
| 9f93c9c84b | |||
| b4e0b5503c | |||
| 5536ddd118 | |||
| 021ed1aa8b | |||
| a703f3a6d2 | |||
| f7ea8b32a9 | |||
| ba7d2db8ad | |||
| b50ed10a84 | |||
| b4fae25a96 | |||
| 6142736946 | |||
| 25fc4217df | |||
| d71e8fb870 | |||
| 369ed9feed | |||
| ebd3b35d02 | |||
| 82df9041e7 | |||
| dd59fc176e | |||
| 9af054addf | |||
| 7f29cb9dc6 | |||
| 2786908bac | |||
| 43331d8c31 | |||
| 579a2ff739 | |||
| bfc082fb07 | |||
| 725e40c5e6 | |||
| 8216a659fa | |||
| a2120a3d63 | |||
| 6d76cb40c5 | |||
| b0e7ea78af | |||
| 6a0d2116b8 | |||
| 37908735d4 | |||
| c0248873da | |||
| 75695f5c70 | |||
| 0a6bc57fa5 | |||
| 1d18964daa | |||
| 8eb3963cd9 | |||
| c605cd37f6 | |||
| e1698fb4bf | |||
| 81f09f3fbd | |||
| b77d834239 | |||
| 7607e55a5a | |||
| d512717cd2 | |||
| ce17557b84 | |||
| 80e109e1ad | |||
| fdc562e3c3 | |||
| 1dcfd7ba02 | |||
| 9cfa2d7e28 | |||
| 5bd01c09d6 | |||
| 61bd0e1310 | |||
| bb1047c483 | |||
| c2a29a01a2 | |||
| 6a1ebaa1bc | |||
| 1d1a04008e | |||
| 1cde02058f | |||
| 48675550bf | |||
| 3955ea5a8a | |||
| 454c8ff5b8 | |||
| ba0b4afb6b | |||
| 76507bd567 | |||
| d2bf615780 | |||
| 3840fb267c | |||
| fbccfc8b61 | |||
| 2d179b376b | |||
| a0add9dda5 | |||
| de9517dcc8 | |||
| bef2b70eae | |||
| df08ba37fe | |||
| 6dc42e376f | |||
| 9689f00bb1 | |||
| 2f0e8d9654 | |||
| 6d9235e36d | |||
| eb606cc18b | |||
| 85ef768d51 | |||
| 457e32128f | |||
| e8ce341f9c | |||
| 87214005d2 | |||
| dc0a4e74d6 | |||
| 43409fa16c | |||
| b912923ef2 | |||
| 46bc8253e1 | |||
| 1f70fc6424 | |||
| ed4ad5f6fb | |||
| 69eb22c652 | |||
| dca880d04a | |||
| 0c6364c771 | |||
| c9cf07e3b5 | |||
| bcd66059ae | |||
| 72e6dbcd12 | |||
| 184c2985fc | |||
| 4abbe8a989 | |||
| ce159ce2eb | |||
| fdb94b9a7a | |||
| be0e826091 | |||
| f12ed5eba8 | |||
| b1d5e9ff3c | |||
| dcb732b416 | |||
| ad6ec9610e | |||
| fcceac5ff2 | |||
| 0a319a28f2 | |||
| 17fe156016 | |||
| 5824a6d9bb | |||
| 59cac03d3b | |||
| cc2e0c1e3d | |||
| 624556eb16 | |||
| 64737c80f9 | |||
| 5e9d182229 | |||
| 1f7ed5d8ee | |||
| f41dd578b3 | |||
| 407d76b8d5 | |||
| d2c43c421f | |||
| 6c509b5364 | |||
| 4bce9947e5 | |||
| 4ad7135690 | |||
| bf9f6ce09c | |||
| 84a6b543a9 | |||
| 6cf7b792a3 | |||
| abec9331d8 | |||
| ba16f2ae95 | |||
| 1e605d727c | |||
| 6c48d52b4a | |||
| 8bb598daa2 | |||
| 21edd6a5ab | |||
| e10e03f4d9 | |||
| d5bdea0bab | |||
| cd1bdaa61d | |||
| 9f69b6b585 | |||
| 9e52a7c769 | |||
| 8fd438000a | |||
| 54ce8327cb | |||
| 82519ab2a6 | |||
| 5761c333a6 | |||
| 72b852a814 | |||
| 897569c1ca | |||
| 8e2cb2abb2 | |||
| b8d9b106de | |||
| f68c0043de | |||
| 48fb0cdecb | |||
| 28602cddb0 | |||
| 5011d63f7e | |||
| 5ab25e2dfe | |||
| 6e1bb6c87d | |||
| ac7dc6bd81 | |||
| b6dac26224 | |||
| a7a3a34987 | |||
| 6df187da84 | |||
| cf95619229 | |||
| 17a37844d4 | |||
| 0697c6e59d | |||
| 66be9d06c4 | |||
| 62b2c9eb4e | |||
| 887319f95c | |||
| fc233bf3f4 | |||
| 8dcba63022 | |||
| 60f5fe76a1 | |||
| 0b66ec0ddd | |||
| e7bf204db4 | |||
| c8e6461dee | |||
| 3214ef8832 | |||
| 6a5444c2fe | |||
| d4e06695fd | |||
| d369e3ec72 | |||
| bff8db3c8f | |||
| 8c6010cc86 | |||
| 13ffd3ee2d | |||
| f9bff8e488 | |||
| 6d83bf8ca9 | |||
| 17f06dd310 | |||
| 82f893d65a | |||
| 40b55eda4c | |||
| 7678ab05d8 | |||
| 5b801afa38 | |||
| 71cc791677 | |||
| 3d1b3e2bb8 | |||
| b0e5556d9a | |||
| 75e6fb30d5 | |||
| 9b5ec8f359 | |||
| 2dbcd8fc9e | |||
| 82789df231 | |||
| 3fafc6d655 | |||
| 9b864811f8 | |||
| ba62a8d559 | |||
| b793fbc60a | |||
| 7e6d546632 | |||
| 45d2f59858 | |||
| dd6e10ce43 | |||
| 6694e97a31 | |||
| 699bb847a0 | |||
| c02ca22894 | |||
| 1f9d66ef60 | |||
| 8f6f2fc920 | |||
| 4d7710806e | |||
| c2665da3e8 | |||
| 186259eb80 | |||
| e571bf94d4 | |||
| 565b5097da | |||
| 0a6f8b2bf3 | |||
| f649dc71e7 | |||
| d93e1f4ccf | |||
| 9eb9f55a31 | |||
| dbcfce03d2 | |||
| b3cfb630dc | |||
| 058e02137a | |||
| 666cb07614 | |||
| df04955572 | |||
| 422092b8bc | |||
| 5ae850c3b2 | |||
| 7c8e1b41bb | |||
| 43bfd16666 | |||
| 45670eff81 | |||
| 50e5b0f8e9 | |||
| e69064d0fc | |||
| 1aa775196b | |||
| b07451eef8 | |||
| cc98528ecb | |||
| f241c70740 | |||
| 2ed2c12f60 | |||
| d85b8adba6 | |||
| 8e16b481d0 | |||
| d89dbf19fb | |||
| 40c01f3739 | |||
| 71ffa5abdc | |||
| 0533e0bc96 | |||
| 3974b13a5a | |||
| d04d5bf0d5 | |||
| 8a733d990f | |||
| 895fc48700 | |||
| 2068ff8926 | |||
| da6670cca4 | |||
| d0337a8928 | |||
| 4c21d58f4c | |||
| 86ca0bf3b1 | |||
| 627576a955 | |||
| 3aa4db5083 | |||
| cd76552724 | |||
| f630e140d2 | |||
| c03b8ef0c2 | |||
| 46718ac3f4 | |||
| 3e5c7c540b | |||
| 7950f09d19 | |||
| f8911adbbc | |||
| 6aa1b6d9b1 | |||
| 013ace9ab2 | |||
| 0d0ed35ba4 | |||
| b9fbfaf64e | |||
| 2c5c7b54f6 | |||
| c1aeda59bd | |||
| 0f813fd76a | |||
| 38c11e2239 | |||
| ed4a06d8ef | |||
| 65f4cbf487 | |||
| 48a3922d56 | |||
| ca838d75e3 | |||
| f339184e29 | |||
| 91f71b0ace | |||
| f2f310472f | |||
| de7dc5d07f | |||
| ae5cfb2898 | |||
| 381409140e | |||
| be92f909ee | |||
| 974a0ce3fb | |||
| 1f1febaa0d | |||
| 3c25ae2e10 | |||
| d9218ce33f | |||
| 346a9fe87d | |||
| 5b5c6c4f47 | |||
| 02d059271c | |||
| a6771b2255 | |||
| a9e8b54246 | |||
| d2108e9ff3 | |||
| 59dadc4f6b | |||
| 927d28e5d5 | |||
| a61718cfc5 | |||
| 93ef35e6a9 | |||
| b0e22d7701 | |||
| 1b7db6316e | |||
| ca8c6e8e1c | |||
| 204e27ca66 | |||
| f96730003b | |||
| 4f3f79d37b | |||
| 89f0207007 | |||
| f8ed1c003a | |||
| c5cd908c33 | |||
| 958da26d18 | |||
| a5d6be6dd2 | |||
| 7af506e7cf | |||
| 3a0f2475b2 | |||
| 2db3f12027 | |||
| bb52b45ea0 | |||
| 4114a3097f | |||
| 73a269f9a5 | |||
| 737e467b8a | |||
| cac0eea291 | |||
| 54843d88ee | |||
| 47651e6c22 | |||
| 9e6ec4c6b0 | |||
| 6457f88aab | |||
| e210da3659 | |||
| e0ecaac90d | |||
| dbedcd3538 | |||
| 4e6442fd19 | |||
| 126249d69a | |||
| 056372690f | |||
| 9cee982a62 | |||
| b69942a6d2 | |||
| d9858ee73a | |||
| 64bcab9253 | |||
| fc4326dc0c | |||
| 92ea5c9f7b | |||
| 90021104d5 | |||
| 8bd5abaf1e | |||
| 1ab60564ae | |||
| af2e94e3c7 | |||
| 5378a6003a | |||
| 2859933a79 | |||
| 42bffeec7c | |||
| 9a8e95bff3 | |||
| 54ec2a0b84 | |||
| 1e9e6339d1 | |||
| 5bc3efe34b | |||
| 7d19ad82eb | |||
| c1bb3aea06 | |||
| 011df4ad24 | |||
| c71ef9e3d7 | |||
| e9ed190f7b | |||
| fe090b0fb9 | |||
| 50a924bb04 | |||
| 1e968d34cb | |||
| c99fffa936 | |||
| f2225395f9 | |||
| c33f60435b | |||
| ab8bb57f5e |
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
|
||||
|
||||
1105
.github/BOTMETA.yml
vendored
1105
.github/BOTMETA.yml
vendored
File diff suppressed because it is too large
Load Diff
60
.github/ISSUE_TEMPLATE.md
vendored
60
.github/ISSUE_TEMPLATE.md
vendored
@ -1,60 +0,0 @@
|
||||
<!---
|
||||
Verify first that your issue/request is not already reported on GitHub.
|
||||
Also test if the latest release, and master branch are affected too.
|
||||
-->
|
||||
|
||||
##### ISSUE TYPE
|
||||
<!--- Pick one below and delete the rest: -->
|
||||
- Bug Report
|
||||
- Feature Idea
|
||||
- Documentation Report
|
||||
|
||||
##### COMPONENT NAME
|
||||
<!--- Name of the module/plugin/task/feature -->
|
||||
|
||||
##### ANSIBLE VERSION
|
||||
<!--- Paste verbatim output from "ansible --version" between quotes below -->
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
##### CONFIGURATION
|
||||
<!---
|
||||
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.
|
||||
-->
|
||||
|
||||
##### SUMMARY
|
||||
<!--- Explain the problem briefly -->
|
||||
|
||||
##### STEPS TO REPRODUCE
|
||||
<!---
|
||||
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
|
||||
For new features, show how the feature would be used.
|
||||
-->
|
||||
|
||||
<!--- Paste example playbooks or commands between quotes below -->
|
||||
```yaml
|
||||
|
||||
```
|
||||
|
||||
<!--- You can also paste gist.github.com links for larger files -->
|
||||
|
||||
##### EXPECTED RESULTS
|
||||
<!--- What did you expect to happen when running the steps above? -->
|
||||
|
||||
##### ACTUAL RESULTS
|
||||
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->
|
||||
|
||||
<!--- Paste verbatim command output between quotes below -->
|
||||
```
|
||||
|
||||
```
|
||||
37
.github/PULL_REQUEST_TEMPLATE.md
vendored
37
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,37 +0,0 @@
|
||||
##### SUMMARY
|
||||
<!--- Describe the change, including rationale and design decisions -->
|
||||
|
||||
<!---
|
||||
If you are fixing an existing issue, please include "Fixes #nnn" in your
|
||||
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 -->
|
||||
```
|
||||
|
||||
```
|
||||
77
.gitignore
vendored
77
.gitignore
vendored
@ -2,10 +2,9 @@
|
||||
*.py[co]
|
||||
build
|
||||
AUTHORS.TXT
|
||||
# Emacs backup and autosave files...
|
||||
# Emacs backup files...
|
||||
*~
|
||||
.\#*
|
||||
\#*
|
||||
# RPM stuff...
|
||||
MANIFEST
|
||||
dist
|
||||
@ -20,82 +19,42 @@ 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/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/*_module.rst
|
||||
docs/docsite/rst/modules_by_category.rst
|
||||
docs/docsite/rst/network_maintained.rst
|
||||
docs/docsite/rst/partner_maintained.rst
|
||||
docs/docsite/rst/playbooks_keywords.rst
|
||||
docs/docsite/rst/playbooks_directives.rst
|
||||
docs/docsite/*.html
|
||||
docs/docsite/rst/ansible.rst
|
||||
docs/docsite/rst/ansible-*.rst
|
||||
docs/docsite/_static/*.gif
|
||||
docs/docsite/_static/*.png
|
||||
docs/docsite/_static/websupport.js
|
||||
docs/docsite/searchindex.js
|
||||
docs/docsite/htmlout
|
||||
docs/docsite/_build
|
||||
docs/api/rst/
|
||||
docs/api/_build/
|
||||
docsite/rst/modules_by_category.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
|
||||
# deb building stuff...
|
||||
/debian/
|
||||
debian/
|
||||
deb-build
|
||||
# Vim swap files
|
||||
*.swp
|
||||
*.swo
|
||||
credentials.yml
|
||||
# test output
|
||||
*.retry
|
||||
*.out
|
||||
.coverage
|
||||
.tox
|
||||
.cache
|
||||
results.xml
|
||||
coverage.xml
|
||||
/test/units/cover-html
|
||||
/test/integration/targets/*/backup/
|
||||
/test/cache/*
|
||||
# Development
|
||||
/test/develop
|
||||
venv
|
||||
Vagrantfile
|
||||
.vagrant
|
||||
ansible.egg-info/
|
||||
/shippable/
|
||||
/test/integration/cloud-config-azure.yml
|
||||
# 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
|
||||
|
||||
36
.mailmap
36
.mailmap
@ -1,36 +0,0 @@
|
||||
Michael DeHaan <michael.dehaan@gmail.com> <michael@ansibleworks.com>
|
||||
Michael DeHaan <michael.dehaan@gmail.com> Michael DeHaan <Michael DeHaan michael@ansibleworks.com>
|
||||
Michael DeHaan <michael.dehaan@gmail.com> <michael@ansible.com>
|
||||
<bcoca@ansible.com> <brian.coca+git@gmail.com>
|
||||
<bcoca@ansible.com> <briancoca+ansible@gmail.com>
|
||||
<bcoca@ansible.com> <briancoca+dev@gmail.com>
|
||||
<bcoca@ansible.com> <bcoca@tablethotels.com>
|
||||
<a.badger@gmail.com> <toshio@fedoraproject.org>
|
||||
James Tanner <tanner.jc@gmail.com>
|
||||
<jimi@sngx.net> <jcammarata@ansibleworks.com>
|
||||
Michael Scherer <misc@zarb.org> mscherer <misc@zarb.org>
|
||||
<misc@zarb.org> <misc@ephaone.org>
|
||||
<misc@zarb.org> <mscherer@users.noreply.github.com>
|
||||
James Martin <jmartin@ansible.com> <jmartin@basho.com>
|
||||
James Laska <jlaska@ansible.com> <jlaska@redhat.com>
|
||||
James Laska <jlaska@ansible.com> <jlaska@ansibleworks.com>
|
||||
James Laska <jlaska@ansible.com> <jlaska@James-Laskas-MacBook-Pro.local>
|
||||
James Laska <jlaska@ansible.com> <jlaska@users.noreply.github.com>
|
||||
Jason McKerr <mckerrj@gmail.com> <jmckerr@jmckerr-OSX.local>
|
||||
Chris Houseknecht <chouseknecht@ansible.com>
|
||||
Chris Houseknecht <chouseknecht@ansible.com> chouseknecht <chouse@ansible.com>
|
||||
Chris Houseknecht <chouseknecht@ansible.com> Chris Houseknecht <chouse@ansibleworks.com>
|
||||
Stephen Fromm <sfromm@gmail.com> Stephen Fromm <stephenf@nero.net>
|
||||
Sandra Wills <docschick@ansible.com> Sandra Wills <swills@ansible.com>
|
||||
Will Thames <will@thames.id.au> willthames <will@thames.id.au>
|
||||
Will Thames <will@thames.id.au> Will Thames <willdthames@gmail.com>
|
||||
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>
|
||||
33
.travis.yml
Normal file
33
.travis.yml
Normal file
@ -0,0 +1,33 @@
|
||||
sudo: false
|
||||
language: python
|
||||
matrix:
|
||||
include:
|
||||
- env: TOXENV=py24 INTEGRATION=no
|
||||
- env: TOXENV=py26 INTEGRATION=yes
|
||||
python: 2.6
|
||||
- env: TOXENV=py27 INTEGRATION=yes
|
||||
python: 2.7
|
||||
- env: TOXENV=py34 INTEGRATION=no
|
||||
python: 3.4
|
||||
- env: TOXENV=py35 INTEGRATION=no
|
||||
python: 3.5
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- deadsnakes
|
||||
packages:
|
||||
- python2.4
|
||||
install:
|
||||
- pip install tox PyYAML Jinja2 sphinx
|
||||
script:
|
||||
# urllib2's defaults are not secure enough for us
|
||||
- ./test/code-smell/replace-urlopen.sh .
|
||||
- ./test/code-smell/use-compat-six.sh lib
|
||||
- ./test/code-smell/boilerplate.sh
|
||||
- ./test/code-smell/required-and-default-attributes.sh
|
||||
- if test x"$TOXENV" != x'py24' ; then tox ; fi
|
||||
- if test x"$TOXENV" = x'py24' ; then python2.4 -V && python2.4 -m compileall -fq -x 'module_utils/(a10|rax|openstack|ec2|gce).py' lib/ansible/module_utils ; fi
|
||||
#- make -C docsite all
|
||||
- if test x"$INTEGRATION" = x'yes' ; then source ./hacking/env-setup && cd test/integration/ && make parsing && make test_var_precedence && make unicode ; fi
|
||||
after_success:
|
||||
- coveralls
|
||||
19
.yamllint
19
.yamllint
@ -1,19 +0,0 @@
|
||||
extends: default
|
||||
|
||||
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}
|
||||
comments: disable
|
||||
comments-indentation: disable
|
||||
document-start: disable
|
||||
empty-lines: {max: 3, level: error}
|
||||
hyphens: {level: error}
|
||||
indentation: disable
|
||||
key-duplicates: enable
|
||||
line-length: disable
|
||||
new-line-at-end-of-file: disable
|
||||
new-lines: {type: unix}
|
||||
trailing-spaces: disable
|
||||
truthy: disable
|
||||
3020
CHANGELOG.md
3020
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,8 +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
|
||||
if isinstance(x, string_types):
|
||||
if isinstance(x, basestring):
|
||||
|
||||
Cleverness
|
||||
==========
|
||||
@ -314,3 +313,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!
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,29 +1,27 @@
|
||||
# WELCOME TO ANSIBLE GITHUB
|
||||
Welcome To Ansible GitHub
|
||||
=========================
|
||||
|
||||
Hi! Nice to see you here!
|
||||
|
||||
If you'd like to ask a question
|
||||
===============================
|
||||
|
||||
## QUESTIONS ?
|
||||
Please see [this web page ](http://docs.ansible.com/community.html) for community information, which includes pointers on how to ask questions on the [mailing lists](http://docs.ansible.com/community.html#mailing-list-information) and IRC.
|
||||
|
||||
Please see the [community page](http://docs.ansible.com/community.html) for information on how to ask questions on the [mailing lists](http://docs.ansible.com/community.html#mailing-list-information) and IRC.
|
||||
The github issue tracker is not the best place for questions for various reasons, but both IRC and the mailing list are very helpful places for those things, and that page has the pointers to those.
|
||||
|
||||
The GitHub issue tracker is not the best place for questions for various reasons, but both IRC and the mailing list are very helpful places for those things, as the community page explains best.
|
||||
If you'd like to contribute code
|
||||
================================
|
||||
|
||||
Please see [this web page](http://docs.ansible.com/community.html) for information about the contribution process. Important license agreement information is also included on that page.
|
||||
|
||||
## CONTRIBUTING ?
|
||||
If you'd like to file a bug
|
||||
===========================
|
||||
|
||||
Please see the [community page](http://docs.ansible.com/community.html) for information regarding the contribution process. Important license agreement information is also included on that page.
|
||||
I'd also read the community page above, but in particular, make sure you copy [this issue template](https://github.com/ansible/ansible/blob/devel/ISSUE_TEMPLATE.md) into your ticket description. We have a friendly neighborhood bot that will remind you if you forget :) This template helps us organize tickets faster and prevents asking some repeated questions, so it's very helpful to us and we appreciate your help with it.
|
||||
|
||||
|
||||
## BUG TO REPORT ?
|
||||
|
||||
First and foremost, also check the [community page](http://docs.ansible.com/community.html).
|
||||
|
||||
You can report bugs or make enhancement requests at the [Ansible GitHub issue page](http://github.com/ansible/ansible/issues/new) by filling out the issue template that will be presented.
|
||||
|
||||
Also please make sure you are testing on the latest released version of Ansible or the development branch. You can find the latest releases and development branch at:
|
||||
|
||||
- https://github.com/ansible/ansible/releases
|
||||
- https://github.com/ansible/ansible/archive/devel.tar.gz
|
||||
Also please make sure you are testing on the latest released version of Ansible or the development branch.
|
||||
|
||||
Thanks!
|
||||
|
||||
|
||||
|
||||
39
ISSUE_TEMPLATE.md
Normal file
39
ISSUE_TEMPLATE.md
Normal file
@ -0,0 +1,39 @@
|
||||
##### Issue Type:
|
||||
|
||||
Can you help us out in labelling this by telling us what kind of ticket this this? You can say:
|
||||
- Bug Report
|
||||
- Feature Idea
|
||||
- Feature Pull Request
|
||||
- New Module Pull Request
|
||||
- Bugfix Pull Request
|
||||
- Documentation Report
|
||||
- Docs Pull Request
|
||||
|
||||
##### Ansible Version:
|
||||
|
||||
Let us know what version of Ansible you are using. Please supply the verbatim output from running “ansible --version”. If you're filing a ticket on a version of Ansible which is not the latest, we'd greatly appreciate it if you could retest on the latest version first. We don't expect you to test against the development branch most of the time, but we may ask for that if you have cycles. Thanks!
|
||||
|
||||
##### Ansible Configuration:
|
||||
|
||||
What have you changed about your Ansible installation? What configuration settings have you changed/added/removed? Compare your /etc/ansible/ansible.cfg against a clean version from Github and let us know what's different.
|
||||
|
||||
##### Environment:
|
||||
|
||||
What OS are you running Ansible from and what OS are you managing? Examples include RHEL 5/6, Centos 5/6, Ubuntu 12.04/13.10, *BSD, Solaris. If this is a generic feature request or it doesn’t apply, just say “N/A”. Not all tickets may be about operating system related things and we understand that.
|
||||
|
||||
##### Summary:
|
||||
|
||||
Please summarize your request in this space. You will earn bonus points for being succinct, but please add enough detail so we can understand the request. Thanks!
|
||||
|
||||
##### Steps To Reproduce:
|
||||
|
||||
If this is a bug ticket, please enter the steps you use to reproduce the problem in the space below. If this is a feature request, please enter the steps you would use to use the feature. If an example playbook is useful, please include a short reproducer inline, indented by four spaces. If a longer one is necessary, linking to one uploaded to gist.github.com would be great. Much appreciated!
|
||||
|
||||
##### Expected Results:
|
||||
|
||||
Please enter your expected results in this space. When running the steps supplied above in the previous section, what did you expect to happen? If showing example output, please indent your output by four spaces so it will render correctly in GitHub's viewer thingy.
|
||||
|
||||
##### Actual Results:
|
||||
|
||||
Please enter your actual results in this space. When running the steps supplied above, what actually happened? If you are showing example output, please indent your output by four spaces so it will render correctly in GitHub. Thanks again!
|
||||
|
||||
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
|
||||
|
||||
***
|
||||
|
||||
# Extras maintainers list
|
||||
|
||||
The full list of maintainers for modules is located here: https://github.com/ansible/ansibullbot/blob/master/MAINTAINERS.txt
|
||||
|
||||
## 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
|
||||
144
Makefile
144
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
|
||||
ASCII2MAN = a2x -D $(dir $@) -d manpage -f manpage $<
|
||||
ASCII2HTMLMAN = a2x -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()")
|
||||
@ -48,12 +47,10 @@ else
|
||||
GITINFO = ""
|
||||
endif
|
||||
|
||||
ifeq ($(shell echo $(OS) | egrep -c 'Darwin|FreeBSD|OpenBSD|DragonFly'),1)
|
||||
ifeq ($(shell echo $(OS) | egrep -c 'Darwin|FreeBSD|OpenBSD'),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
|
||||
@ -70,7 +67,7 @@ ifeq ($(OFFICIAL),yes)
|
||||
DEBUILD_OPTS += -k$(DEBSIGN_KEYID)
|
||||
endif
|
||||
else
|
||||
DEB_RELEASE = 100.git$(DATE)$(GITINFO)
|
||||
DEB_RELEASE = 0.git$(DATE)$(GITINFO)
|
||||
# Do not sign unofficial builds
|
||||
DEBUILD_OPTS += -uc -us
|
||||
DPUT_OPTS += -u
|
||||
@ -80,23 +77,13 @@ DEB_PPA ?= ppa
|
||||
# Choose the desired Ubuntu release: lucid precise saucy trusty
|
||||
DEB_DIST ?= unstable
|
||||
|
||||
# pbuilder parameters
|
||||
PBUILDER_ARCH ?= amd64
|
||||
PBUILDER_CACHE_DIR = /var/cache/pbuilder
|
||||
PBUILDER_BIN ?= pbuilder
|
||||
PBUILDER_OPTS ?= --debootstrapopts --variant=buildd --architecture $(PBUILDER_ARCH) --debbuildopts -b
|
||||
|
||||
# RPM build parameters
|
||||
RPMSPECDIR= packaging/rpm
|
||||
RPMSPEC = $(RPMSPECDIR)/ansible.spec
|
||||
RPMDIST = $(shell rpm --eval '%{?dist}')
|
||||
RPMRELEASE = $(RELEASE)
|
||||
ifneq ($(OFFICIAL),yes)
|
||||
RPMRELEASE = 100.git$(DATE)$(GITINFO)
|
||||
endif
|
||||
ifeq ($(PUBLISH),nightly)
|
||||
# https://fedoraproject.org/wiki/Packaging:Versioning#Snapshots
|
||||
RPMRELEASE = $(RELEASE).$(DATE)git.$(GIT_HASH)
|
||||
RPMRELEASE = 0.git$(DATE)$(GITINFO)
|
||||
endif
|
||||
RPMNVR = "$(NAME)-$(VERSION)-$(RPMRELEASE)$(RPMDIST)"
|
||||
|
||||
@ -104,40 +91,20 @@ 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.4
|
||||
|
||||
########################################################
|
||||
|
||||
.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
|
||||
|
||||
.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
|
||||
|
||||
.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)
|
||||
|
||||
.PHONY: authors
|
||||
authors:
|
||||
sh hacking/authors.sh
|
||||
|
||||
@ -151,19 +118,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 +138,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 +145,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 -f 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,49 +156,39 @@ 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
|
||||
@sed -e 's#^Version:.*#Version: $(VERSION)#' -e 's#^Release:.*#Release: $(RPMRELEASE)%{?dist}#' $(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/
|
||||
$(MOCK_BIN) -r $(MOCK_CFG) --resultdir rpm-build/ --buildsrpm --spec rpm-build/$(NAME).spec --sources rpm-build/
|
||||
@echo "#############################################"
|
||||
@echo "Ansible SRPM is built:"
|
||||
@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
|
||||
$(MOCK_BIN) -r $(MOCK_CFG) --resultdir rpm-build/ --rebuild rpm-build/$(NAME)-*.src.rpm
|
||||
@echo "#############################################"
|
||||
@echo "Ansible RPM is built:"
|
||||
@echo rpm-build/*.noarch.rpm
|
||||
@echo "#############################################"
|
||||
|
||||
.PHONY: srpm
|
||||
srpm: rpmcommon
|
||||
@rpmbuild --define "_topdir %(pwd)/rpm-build" \
|
||||
--define "_builddir %{_topdir}" \
|
||||
@ -254,7 +203,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 +219,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,25 +227,7 @@ 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}" ; \
|
||||
$(PBUILDER_BIN) create $${PBUILDER_OPTS} --othermirror "deb http://archive.ubuntu.com/ubuntu $${DIST} universe" ; \
|
||||
$(PBUILDER_BIN) update $${PBUILDER_OPTS} ; \
|
||||
$(PBUILDER_BIN) build $${PBUILDER_OPTS} deb-build/$${DIST}/$(NAME)_$(VERSION)-$(DEB_RELEASE)~$${DIST}.dsc ; \
|
||||
done
|
||||
@echo "#############################################"
|
||||
@echo "Ansible DEB artifacts:"
|
||||
@for DIST in $(DEB_DIST) ; do \
|
||||
echo deb-build/$${DIST}/$(NAME)_$(VERSION)-$(DEB_RELEASE)~$${DIST}_amd64.changes ; \
|
||||
done
|
||||
@echo "#############################################"
|
||||
|
||||
# Build package outside of pbuilder, with locally installed dependencies.
|
||||
# Install BuildRequires as noted in packaging/debian/control.
|
||||
.PHONY: local_deb
|
||||
local_deb: debian
|
||||
deb: debian
|
||||
@for DIST in $(DEB_DIST) ; do \
|
||||
(cd deb-build/$${DIST}/$(NAME)-$(VERSION)/ && $(DEBUILD) -b) ; \
|
||||
done
|
||||
@ -309,7 +238,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 +249,19 @@ 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)
|
||||
|
||||
.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: generate_asciidoc
|
||||
make $(MANPAGES)
|
||||
|
||||
.PHONY: alldocs
|
||||
alldocs: docs webdocs
|
||||
|
||||
version:
|
||||
@echo $(VERSION)
|
||||
webdocs: $(MANPAGES)
|
||||
(cd docsite/; make docs)
|
||||
|
||||
docs: $(MANPAGES)
|
||||
|
||||
32
README.md
32
README.md
@ -1,5 +1,6 @@
|
||||
[](https://pypi.python.org/pypi/ansible)
|
||||
[](https://app.shippable.com/projects/573f79d02a8192902e20e34b)
|
||||
[](https://pypi.python.org/pypi/ansible)
|
||||
[](https://travis-ci.org/ansible/ansible)
|
||||
|
||||
|
||||
Ansible
|
||||
@ -7,11 +8,13 @@ Ansible
|
||||
|
||||
Ansible is a radically simple IT automation system. It handles configuration-management, application deployment, cloud provisioning, ad-hoc task-execution, and multinode orchestration - including trivializing things like zero downtime rolling updates with load balancers.
|
||||
|
||||
Read the documentation and more at https://ansible.com/
|
||||
Read the documentation and more at http://ansible.com/
|
||||
|
||||
Many users run straight from the development branch (it's generally fine to do so), but you might also wish to consume a release.
|
||||
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/ansible/intro_installation.html) for a variety of platforms.
|
||||
You can find instructions [here](http://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](http://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
|
||||
=================
|
||||
@ -25,15 +28,15 @@ 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](http://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)
|
||||
* Announcement list: [ansible-announce](https://groups.google.com/group/ansible-announce) - read only
|
||||
* Users list: [ansible-project](http://groups.google.com/group/ansible-project)
|
||||
* Development list: [ansible-devel](http://groups.google.com/group/ansible-devel)
|
||||
* Announcement list: [ansible-announce](http://groups.google.com/group/ansible-announce) - read only
|
||||
* irc.freenode.net: #ansible
|
||||
|
||||
Branch Info
|
||||
@ -41,20 +44,15 @@ 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.
|
||||
* For releases 1.8 - 2.2, 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)
|
||||
* 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.
|
||||
* We'd love to have your contributions, read [Community Information](http://docs.ansible.com/community.html) for notes on how to get started.
|
||||
|
||||
Authors
|
||||
=======
|
||||
|
||||
Ansible was created by [Michael DeHaan](https://github.com/mpdehaan) (michael.dehaan/gmail/com) and has contributions from over 1000 users (and growing). Thanks everyone!
|
||||
|
||||
Ansible is sponsored by [Ansible, Inc](https://ansible.com)
|
||||
|
||||
Licence
|
||||
=======
|
||||
GNU
|
||||
Click on the [Link](COPYING) to see the full text.
|
||||
Ansible is sponsored by [Ansible, Inc](http://ansible.com)
|
||||
|
||||
|
||||
|
||||
149
RELEASES.txt
149
RELEASES.txt
@ -1,87 +1,70 @@
|
||||
Ansible Releases at a Glance
|
||||
============================
|
||||
|
||||
VERSION RELEASE CODE NAME
|
||||
++++++++++++++++++++++++++++++
|
||||
Active Development
|
||||
++++++++++++++++++
|
||||
|
||||
2.1 "TBD" - in progress
|
||||
|
||||
Released
|
||||
++++++++
|
||||
|
||||
2.0.1 "Over the Hills and Far Away" TBD
|
||||
2.0.0 "Over the Hills and Far Away" 01-12-2015
|
||||
1.9.4 "Dancing In the Streets" 10-09-2015
|
||||
1.9.3 "Dancing In the Streets" 09-03-2015
|
||||
1.9.2 "Dancing In the Streets" 06-24-2015
|
||||
1.9.1 "Dancing In the Streets" 04-27-2015
|
||||
1.9.0 "Dancing In the Streets" 03-25-2015
|
||||
1.8.4 "You Really Got Me" ---- 02-19-2015
|
||||
1.8.3 "You Really Got Me" ---- 02-17-2015
|
||||
1.8.2 "You Really Got Me" ---- 12-04-2014
|
||||
1.8.1 "You Really Got Me" ---- 11-26-2014
|
||||
1.7.2 "Summer Nights" -------- 09-24-2014
|
||||
1.7.1 "Summer Nights" -------- 08-14-2014
|
||||
1.7 "Summer Nights" -------- 08-06-2014
|
||||
1.6.10 "The Cradle Will Rock" - 07-25-2014
|
||||
1.6.9 "The Cradle Will Rock" - 07-24-2014
|
||||
1.6.8 "The Cradle Will Rock" - 07-22-2014
|
||||
1.6.7 "The Cradle Will Rock" - 07-21-2014
|
||||
1.6.6 "The Cradle Will Rock" - 07-01-2014
|
||||
1.6.5 "The Cradle Will Rock" - 06-25-2014
|
||||
1.6.4 "The Cradle Will Rock" - 06-25-2014
|
||||
1.6.3 "The Cradle Will Rock" - 06-09-2014
|
||||
1.6.2 "The Cradle Will Rock" - 05-23-2014
|
||||
1.6.1 "The Cradle Will Rock" - 05-07-2014
|
||||
1.6 "The Cradle Will Rock" - 05-05-2014
|
||||
1.5.5 "Love Walks In" -------- 04-18-2014
|
||||
1.5.4 "Love Walks In" -------- 04-01-2014
|
||||
1.5.3 "Love Walks In" -------- 03-13-2014
|
||||
1.5.2 "Love Walks In" -------- 03-11-2014
|
||||
1.5.1 "Love Walks In" -------- 03-10-2014
|
||||
1.5 "Love Walks In" -------- 02-28-2014
|
||||
1.4.5 "Could This Be Magic?" - 02-12-2014
|
||||
1.4.4 "Could This Be Magic?" - 01-06-2014
|
||||
1.4.3 "Could This Be Magic?" - 12-20-2013
|
||||
1.4.2 "Could This Be Magic?" - 12-18-2013
|
||||
1.4.1 "Could This Be Magic?" - 11-27-2013
|
||||
1.4 "Could This Be Magic?" - 11-21-2013
|
||||
1.3.4 "Top of the World" ----- 10-29-2013
|
||||
1.3.3 "Top of the World" ----- 10-09-2013
|
||||
1.3.2 "Top of the World" ----- 09-19-2013
|
||||
1.3.1 "Top of the World" ----- 09-16-2013
|
||||
1.3 "Top of the World" ----- 09-13-2013
|
||||
1.2.3 "Hear About It Later" -- 08-21-2013
|
||||
1.2.2 "Hear About It Later" -- 07-05-2013
|
||||
1.2.1 "Hear About It Later" -- 07-04-2013
|
||||
1.2 "Right Now" ------------ 06-10-2013
|
||||
1.1 "Mean Street" ---------- 04-02-2013
|
||||
1.0 "Eruption" ------------- 02-01-2013
|
||||
0.9 "Dreams" --------------- 11-30-2012
|
||||
0.8 "Cathedral" ------------ 10-19-2012
|
||||
0.7 "Panama" --------------- 09-06-2012
|
||||
0.6 "Cabo" ----------------- 08-06-2012
|
||||
0.5 "Amsterdam" ------------ 07-04-2012
|
||||
0.4 "Unchained" ------------ 05-23-2012
|
||||
0.3 "Baluchitherium" ------- 04-23-2012
|
||||
0.0.2 Untitled
|
||||
0.0.1 Untitled
|
||||
|
||||
|
||||
2.5.0 TBD "Kashmir"
|
||||
2.4.2 11-29-2017 "Dancing Days"
|
||||
2.4.1 10-25-2017 "Dancing Days"
|
||||
2.4.0 09-19-2017 "Dancing Days"
|
||||
2.3.2 08-04-2017 "Ramble On"
|
||||
2.3.1 06-01-2017 "Ramble On"
|
||||
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.6 06-01-2017 "The Song Remains the Same"
|
||||
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"
|
||||
0.0.1 ? "Untitled"
|
||||
|
||||
28
ROADMAP.rst
28
ROADMAP.rst
@ -1,28 +0,0 @@
|
||||
*******************************
|
||||
Roadmap For Ansible by Red Hat
|
||||
*******************************
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
=======================================
|
||||
Current 2.4 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>`_
|
||||
|
||||
|
||||
|
||||
**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>`_
|
||||
File diff suppressed because it is too large
Load Diff
83
bin/ansible
83
bin/ansible
@ -33,18 +33,16 @@ except Exception:
|
||||
pass
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
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
|
||||
from ansible.utils.unicode import to_unicode
|
||||
|
||||
|
||||
########################################
|
||||
# OUTPUT OF LAST RESORT
|
||||
### OUTPUT OF LAST RESORT ###
|
||||
class LastResort(object):
|
||||
def display(self, msg):
|
||||
print(msg, file=sys.stderr)
|
||||
@ -66,77 +64,52 @@ 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)
|
||||
except ImportError as e:
|
||||
# ImportError members have changed in py3
|
||||
if 'msg' in dir(e):
|
||||
msg = e.msg
|
||||
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:
|
||||
msg = e.message
|
||||
if msg.endswith(' %s' % sub):
|
||||
raise AnsibleError("Unknown Ansible alias: %s" % me)
|
||||
except ImportError as e:
|
||||
if e.message.endswith(' %s' % sub):
|
||||
raise AnsibleError("Ansible sub-program not implemented: %s" % me)
|
||||
else:
|
||||
raise
|
||||
|
||||
try:
|
||||
args = [to_text(a, errors='surrogate_or_strict') for a in sys.argv]
|
||||
except UnicodeError:
|
||||
display.error('Command line args are not in utf-8, unable to continue. Ansible currently only understands utf-8')
|
||||
display.display(u"The full traceback was:\n\n%s" % to_text(traceback.format_exc()))
|
||||
exit_code = 6
|
||||
else:
|
||||
cli = mycli(args)
|
||||
cli.parse()
|
||||
exit_code = cli.run()
|
||||
cli = mycli(sys.argv)
|
||||
cli.parse()
|
||||
sys.exit(cli.run())
|
||||
|
||||
except AnsibleOptionsError as e:
|
||||
cli.parser.print_help()
|
||||
display.error(to_text(e), wrap_text=False)
|
||||
exit_code = 5
|
||||
display.error(to_unicode(e), wrap_text=False)
|
||||
sys.exit(5)
|
||||
except AnsibleParserError as e:
|
||||
display.error(to_text(e), wrap_text=False)
|
||||
exit_code = 4
|
||||
display.error(to_unicode(e), wrap_text=False)
|
||||
sys.exit(4)
|
||||
# TQM takes care of these, but leaving comment to reserve the exit codes
|
||||
# except AnsibleHostUnreachable as e:
|
||||
# display.error(str(e))
|
||||
# exit_code = 3
|
||||
# sys.exit(3)
|
||||
# except AnsibleHostFailed as e:
|
||||
# display.error(str(e))
|
||||
# exit_code = 2
|
||||
# sys.exit(2)
|
||||
except AnsibleError as e:
|
||||
display.error(to_text(e), wrap_text=False)
|
||||
exit_code = 1
|
||||
display.error(to_unicode(e), wrap_text=False)
|
||||
sys.exit(1)
|
||||
except KeyboardInterrupt:
|
||||
display.error("User interrupted execution")
|
||||
exit_code = 99
|
||||
sys.exit(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_unicode(e), wrap_text=False)
|
||||
if not have_cli_options or have_cli_options and cli.options.verbosity > 2:
|
||||
log_only = False
|
||||
display.display(u"the full traceback was:\n\n%s" % to_unicode(traceback.format_exc()))
|
||||
else:
|
||||
display.display("to see the full traceback, use -vvv")
|
||||
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:
|
||||
# Remove ansible tempdir
|
||||
shutil.rmtree(C.DEFAULT_LOCAL_TMP, True)
|
||||
|
||||
sys.exit(exit_code)
|
||||
sys.exit(250)
|
||||
|
||||
@ -1 +0,0 @@
|
||||
ansible
|
||||
@ -1,388 +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]
|
||||
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 +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!
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
#
|
||||
|
||||
# Define an Abiquo user with access to Abiquo API which will be used to
|
||||
# perform required queries to obtain information to generate the Ansible
|
||||
# perform required queries to obtain infromation to generate the Ansible
|
||||
# inventory output.
|
||||
#
|
||||
[auth]
|
||||
|
||||
@ -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,593 +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']
|
||||
pass
|
||||
|
||||
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']
|
||||
pass
|
||||
|
||||
try:
|
||||
self.aos_username = config.get('aos', 'username')
|
||||
except:
|
||||
if 'AOS_USERNAME' in os.environ.keys():
|
||||
self.aos_username = os.environ['AOS_USERNAME']
|
||||
pass
|
||||
|
||||
try:
|
||||
self.aos_password = config.get('aos', 'password')
|
||||
except:
|
||||
if 'AOS_PASSWORD' in os.environ.keys():
|
||||
self.aos_password = os.environ['AOS_PASSWORD']
|
||||
pass
|
||||
|
||||
try:
|
||||
self.aos_blueprint = config.get('aos', 'blueprint')
|
||||
except:
|
||||
if 'AOS_BLUEPRINT' in os.environ.keys():
|
||||
self.aos_blueprint = os.environ['AOS_BLUEPRINT']
|
||||
pass
|
||||
|
||||
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()
|
||||
@ -1,22 +0,0 @@
|
||||
#
|
||||
# Configuration file for azure_rm.py
|
||||
#
|
||||
[azure]
|
||||
# Control which resource groups are included. By default all resources groups are included.
|
||||
# Set resource_groups to a comma separated list of resource groups names.
|
||||
#resource_groups=
|
||||
|
||||
# Control which tags are included. Set tags to a comma separated list of keys or key:value pairs
|
||||
#tags=
|
||||
|
||||
# Control which locations are included. Set locations to a comma separated list (e.g. eastus,eastus2,westus)
|
||||
#locations=
|
||||
|
||||
# Include powerstate. If you don't need powerstate information, turning it off improves runtime performance.
|
||||
include_powerstate=yes
|
||||
|
||||
# Control grouping with the following boolean flags. Valid values: yes, no, true, false, True, False, 0, 1.
|
||||
group_by_resource_group=yes
|
||||
group_by_location=yes
|
||||
group_by_security_group=yes
|
||||
group_by_tag=yes
|
||||
@ -1,859 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) 2016 Matt Davis, <mdavis@ansible.com>
|
||||
# Chris Houseknecht, <house@redhat.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/>.
|
||||
#
|
||||
|
||||
'''
|
||||
Azure External Inventory Script
|
||||
===============================
|
||||
Generates dynamic inventory by making API requests to the Azure Resource
|
||||
Manager using the Azure Python SDK. For instruction on installing the
|
||||
Azure Python SDK see http://azure-sdk-for-python.readthedocs.org/
|
||||
|
||||
Authentication
|
||||
--------------
|
||||
The order of precedence is command line arguments, environment variables,
|
||||
and finally the [default] profile found in ~/.azure/credentials.
|
||||
|
||||
If using a credentials file, it should be an ini formatted file with one or
|
||||
more sections, which we refer to as profiles. The script looks for a
|
||||
[default] section, if a profile is not specified either on the command line
|
||||
or with an environment variable. The keys in a profile will match the
|
||||
list of command line arguments below.
|
||||
|
||||
For command line arguments and environment variables specify a profile found
|
||||
in your ~/.azure/credentials file, or a service principal or Active Directory
|
||||
user.
|
||||
|
||||
Command line arguments:
|
||||
- profile
|
||||
- client_id
|
||||
- secret
|
||||
- subscription_id
|
||||
- tenant
|
||||
- ad_user
|
||||
- password
|
||||
- cloud_environment
|
||||
|
||||
Environment variables:
|
||||
- AZURE_PROFILE
|
||||
- AZURE_CLIENT_ID
|
||||
- AZURE_SECRET
|
||||
- AZURE_SUBSCRIPTION_ID
|
||||
- AZURE_TENANT
|
||||
- AZURE_AD_USER
|
||||
- AZURE_PASSWORD
|
||||
- AZURE_CLOUD_ENVIRONMENT
|
||||
|
||||
Run for Specific Host
|
||||
-----------------------
|
||||
When run for a specific host using the --host option, a resource group is
|
||||
required. For a specific host, this script returns the following variables:
|
||||
|
||||
{
|
||||
"ansible_host": "XXX.XXX.XXX.XXX",
|
||||
"computer_name": "computer_name2",
|
||||
"fqdn": null,
|
||||
"id": "/subscriptions/subscription-id/resourceGroups/galaxy-production/providers/Microsoft.Compute/virtualMachines/object-name",
|
||||
"image": {
|
||||
"offer": "CentOS",
|
||||
"publisher": "OpenLogic",
|
||||
"sku": "7.1",
|
||||
"version": "latest"
|
||||
},
|
||||
"location": "westus",
|
||||
"mac_address": "00-00-5E-00-53-FE",
|
||||
"name": "object-name",
|
||||
"network_interface": "interface-name",
|
||||
"network_interface_id": "/subscriptions/subscription-id/resourceGroups/galaxy-production/providers/Microsoft.Network/networkInterfaces/object-name1",
|
||||
"network_security_group": null,
|
||||
"network_security_group_id": null,
|
||||
"os_disk": {
|
||||
"name": "object-name",
|
||||
"operating_system_type": "Linux"
|
||||
},
|
||||
"plan": null,
|
||||
"powerstate": "running",
|
||||
"private_ip": "172.26.3.6",
|
||||
"private_ip_alloc_method": "Static",
|
||||
"provisioning_state": "Succeeded",
|
||||
"public_ip": "XXX.XXX.XXX.XXX",
|
||||
"public_ip_alloc_method": "Static",
|
||||
"public_ip_id": "/subscriptions/subscription-id/resourceGroups/galaxy-production/providers/Microsoft.Network/publicIPAddresses/object-name",
|
||||
"public_ip_name": "object-name",
|
||||
"resource_group": "galaxy-production",
|
||||
"security_group": "object-name",
|
||||
"security_group_id": "/subscriptions/subscription-id/resourceGroups/galaxy-production/providers/Microsoft.Network/networkSecurityGroups/object-name",
|
||||
"tags": {
|
||||
"db": "database"
|
||||
},
|
||||
"type": "Microsoft.Compute/virtualMachines",
|
||||
"virtual_machine_size": "Standard_DS4"
|
||||
}
|
||||
|
||||
Groups
|
||||
------
|
||||
When run in --list mode, instances are grouped by the following categories:
|
||||
- azure
|
||||
- location
|
||||
- resource_group
|
||||
- security_group
|
||||
- tag key
|
||||
- tag key_value
|
||||
|
||||
Control groups using azure_rm.ini or set environment variables:
|
||||
|
||||
AZURE_GROUP_BY_RESOURCE_GROUP=yes
|
||||
AZURE_GROUP_BY_LOCATION=yes
|
||||
AZURE_GROUP_BY_SECURITY_GROUP=yes
|
||||
AZURE_GROUP_BY_TAG=yes
|
||||
|
||||
Select hosts within specific resource groups by assigning a comma separated list to:
|
||||
|
||||
AZURE_RESOURCE_GROUPS=resource_group_a,resource_group_b
|
||||
|
||||
Select hosts for specific tag key by assigning a comma separated list of tag keys to:
|
||||
|
||||
AZURE_TAGS=key1,key2,key3
|
||||
|
||||
Select hosts for specific locations:
|
||||
|
||||
AZURE_LOCATIONS=eastus,westus,eastus2
|
||||
|
||||
Or, select hosts for specific tag key:value pairs by assigning a comma separated list key:value pairs to:
|
||||
|
||||
AZURE_TAGS=key1:value1,key2:value2
|
||||
|
||||
If you don't need the powerstate, you can improve performance by turning off powerstate fetching:
|
||||
AZURE_INCLUDE_POWERSTATE=no
|
||||
|
||||
azure_rm.ini
|
||||
------------
|
||||
As mentioned above, you can control execution using environment variables or a .ini file. A sample
|
||||
azure_rm.ini is included. The name of the .ini file is the basename of the inventory script (in this case
|
||||
'azure_rm') with a .ini extension. It also assumes the .ini file is alongside the script. To specify
|
||||
a different path for the .ini file, define the AZURE_INI_PATH environment variable:
|
||||
|
||||
export AZURE_INI_PATH=/path/to/custom.ini
|
||||
|
||||
Powerstate:
|
||||
-----------
|
||||
The powerstate attribute indicates whether or not a host is running. If the value is 'running', the machine is
|
||||
up. If the value is anything other than 'running', the machine is down, and will be unreachable.
|
||||
|
||||
Examples:
|
||||
---------
|
||||
Execute /bin/uname on all instances in the galaxy-qa resource group
|
||||
$ ansible -i azure_rm.py galaxy-qa -m shell -a "/bin/uname -a"
|
||||
|
||||
Use the inventory script to print instance specific information
|
||||
$ contrib/inventory/azure_rm.py --host my_instance_host_name --pretty
|
||||
|
||||
Use with a playbook
|
||||
$ ansible-playbook -i contrib/inventory/azure_rm.py my_playbook.yml --limit galaxy-qa
|
||||
|
||||
|
||||
Insecure Platform Warning
|
||||
-------------------------
|
||||
If you receive InsecurePlatformWarning from urllib3, install the
|
||||
requests security packages:
|
||||
|
||||
pip install requests[security]
|
||||
|
||||
|
||||
author:
|
||||
- Chris Houseknecht (@chouseknecht)
|
||||
- Matt Davis (@nitzmahone)
|
||||
|
||||
Company: Ansible by Red Hat
|
||||
|
||||
Version: 1.0.0
|
||||
'''
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import inspect
|
||||
|
||||
try:
|
||||
# python2
|
||||
import ConfigParser as cp
|
||||
except ImportError:
|
||||
# python3
|
||||
import configparser as cp
|
||||
|
||||
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
|
||||
except ImportError as exc:
|
||||
HAS_AZURE_EXC = exc
|
||||
HAS_AZURE = False
|
||||
|
||||
|
||||
AZURE_CREDENTIAL_ENV_MAPPING = dict(
|
||||
profile='AZURE_PROFILE',
|
||||
subscription_id='AZURE_SUBSCRIPTION_ID',
|
||||
client_id='AZURE_CLIENT_ID',
|
||||
secret='AZURE_SECRET',
|
||||
tenant='AZURE_TENANT',
|
||||
ad_user='AZURE_AD_USER',
|
||||
password='AZURE_PASSWORD',
|
||||
cloud_environment='AZURE_CLOUD_ENVIRONMENT',
|
||||
)
|
||||
|
||||
AZURE_CONFIG_SETTINGS = dict(
|
||||
resource_groups='AZURE_RESOURCE_GROUPS',
|
||||
tags='AZURE_TAGS',
|
||||
locations='AZURE_LOCATIONS',
|
||||
include_powerstate='AZURE_INCLUDE_POWERSTATE',
|
||||
group_by_resource_group='AZURE_GROUP_BY_RESOURCE_GROUP',
|
||||
group_by_location='AZURE_GROUP_BY_LOCATION',
|
||||
group_by_security_group='AZURE_GROUP_BY_SECURITY_GROUP',
|
||||
group_by_tag='AZURE_GROUP_BY_TAG'
|
||||
)
|
||||
|
||||
AZURE_MIN_VERSION = "2.0.0"
|
||||
|
||||
|
||||
def azure_id_to_dict(id):
|
||||
pieces = re.sub(r'^\/', '', id).split('/')
|
||||
result = {}
|
||||
index = 0
|
||||
while index < len(pieces) - 1:
|
||||
result[pieces[index]] = pieces[index + 1]
|
||||
index += 1
|
||||
return result
|
||||
|
||||
|
||||
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
|
||||
|
||||
self.debug = False
|
||||
if args.debug:
|
||||
self.debug = True
|
||||
|
||||
self.credentials = self._get_credentials(args)
|
||||
if not self.credentials:
|
||||
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")
|
||||
self.subscription_id = self.credentials['subscription_id']
|
||||
|
||||
if self.credentials.get('client_id') is not None and \
|
||||
self.credentials.get('secret') is not None and \
|
||||
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)
|
||||
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)
|
||||
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')
|
||||
|
||||
def fail(self, msg):
|
||||
raise Exception(msg)
|
||||
|
||||
def _get_profile(self, profile="default"):
|
||||
path = expanduser("~")
|
||||
path += "/.azure/credentials"
|
||||
try:
|
||||
config = cp.ConfigParser()
|
||||
config.read(path)
|
||||
except Exception as exc:
|
||||
self.fail("Failed to access {0}. Check that the file exists and you have read "
|
||||
"access. {1}".format(path, str(exc)))
|
||||
credentials = dict()
|
||||
for key in AZURE_CREDENTIAL_ENV_MAPPING:
|
||||
try:
|
||||
credentials[key] = config.get(profile, key, raw=True)
|
||||
except:
|
||||
pass
|
||||
|
||||
if credentials.get('client_id') is not None or credentials.get('ad_user') is not None:
|
||||
return credentials
|
||||
|
||||
return None
|
||||
|
||||
def _get_env_credentials(self):
|
||||
env_credentials = dict()
|
||||
for attribute, env_variable in AZURE_CREDENTIAL_ENV_MAPPING.items():
|
||||
env_credentials[attribute] = os.environ.get(env_variable, None)
|
||||
|
||||
if env_credentials['profile'] is not None:
|
||||
credentials = self._get_profile(env_credentials['profile'])
|
||||
return credentials
|
||||
|
||||
if env_credentials['client_id'] is not None or env_credentials['ad_user'] is not None:
|
||||
return env_credentials
|
||||
|
||||
return None
|
||||
|
||||
def _get_credentials(self, params):
|
||||
# Get authentication credentials.
|
||||
# Precedence: cmd line parameters-> environment variables-> default profile in ~/.azure/credentials.
|
||||
|
||||
self.log('Getting credentials')
|
||||
|
||||
arg_credentials = dict()
|
||||
for attribute, env_variable in AZURE_CREDENTIAL_ENV_MAPPING.items():
|
||||
arg_credentials[attribute] = getattr(params, attribute)
|
||||
|
||||
# try module params
|
||||
if arg_credentials['profile'] is not None:
|
||||
self.log('Retrieving credentials with profile parameter.')
|
||||
credentials = self._get_profile(arg_credentials['profile'])
|
||||
return credentials
|
||||
|
||||
if arg_credentials['client_id'] is not None:
|
||||
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:
|
||||
self.log('Received credentials from env.')
|
||||
return env_credentials
|
||||
|
||||
# try default profile from ~./azure/credentials
|
||||
default_credentials = self._get_profile()
|
||||
if default_credentials:
|
||||
self.log('Retrieved default profile credentials from ~/.azure/credentials.')
|
||||
return default_credentials
|
||||
|
||||
return None
|
||||
|
||||
def _register(self, key):
|
||||
try:
|
||||
# We have to perform the one-time registration here. Otherwise, we receive an error the first
|
||||
# time we attempt to use the requested client.
|
||||
resource_client = self.rm_client
|
||||
resource_client.providers.register(key)
|
||||
except Exception as exc:
|
||||
self.log("One-time registration of {0} failed - {1}".format(key, str(exc)))
|
||||
self.log("You might need to register {0} using an admin account".format(key))
|
||||
self.log(("To register a provider using the Python CLI: "
|
||||
"https://docs.microsoft.com/azure/azure-resource-manager/"
|
||||
"resource-manager-common-deployment-errors#noregisteredproviderfound"))
|
||||
|
||||
@property
|
||||
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._register('Microsoft.Network')
|
||||
return self._network_client
|
||||
|
||||
@property
|
||||
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'
|
||||
)
|
||||
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._register('Microsoft.Compute')
|
||||
return self._compute_client
|
||||
|
||||
|
||||
class AzureInventory(object):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self._args = self._parse_cli_args()
|
||||
|
||||
try:
|
||||
rm = AzureRM(self._args)
|
||||
except Exception as e:
|
||||
sys.exit("{0}".format(str(e)))
|
||||
|
||||
self._compute_client = rm.compute_client
|
||||
self._network_client = rm.network_client
|
||||
self._resource_client = rm.rm_client
|
||||
self._security_groups = None
|
||||
|
||||
self.resource_groups = []
|
||||
self.tags = None
|
||||
self.locations = None
|
||||
self.replace_dash_in_groups = False
|
||||
self.group_by_resource_group = True
|
||||
self.group_by_location = True
|
||||
self.group_by_security_group = True
|
||||
self.group_by_tag = True
|
||||
self.include_powerstate = True
|
||||
|
||||
self._inventory = dict(
|
||||
_meta=dict(
|
||||
hostvars=dict()
|
||||
),
|
||||
azure=[]
|
||||
)
|
||||
|
||||
self._get_settings()
|
||||
|
||||
if self._args.resource_groups:
|
||||
self.resource_groups = self._args.resource_groups.split(',')
|
||||
|
||||
if self._args.tags:
|
||||
self.tags = self._args.tags.split(',')
|
||||
|
||||
if self._args.locations:
|
||||
self.locations = self._args.locations.split(',')
|
||||
|
||||
if self._args.no_powerstate:
|
||||
self.include_powerstate = False
|
||||
|
||||
self.get_inventory()
|
||||
print(self._json_format_dict(pretty=self._args.pretty))
|
||||
sys.exit(0)
|
||||
|
||||
def _parse_cli_args(self):
|
||||
# Parse command line arguments
|
||||
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)')
|
||||
parser.add_argument('--debug', action='store_true', default=False,
|
||||
help='Send debug messages to STDOUT')
|
||||
parser.add_argument('--host', action='store',
|
||||
help='Get all information about an instance')
|
||||
parser.add_argument('--pretty', action='store_true', 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',
|
||||
help='Azure Subscription Id')
|
||||
parser.add_argument('--client_id', action='store',
|
||||
help='Azure Client Id ')
|
||||
parser.add_argument('--secret', action='store',
|
||||
help='Azure Client Secret')
|
||||
parser.add_argument('--tenant', action='store',
|
||||
help='Azure Tenant Id')
|
||||
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',
|
||||
help='Return inventory for comma separated list of tag key:value pairs')
|
||||
parser.add_argument('--locations', action='store',
|
||||
help='Return inventory for comma separated list of locations')
|
||||
parser.add_argument('--no-powerstate', action='store_true', default=False,
|
||||
help='Do not include the power state of each virtual host')
|
||||
return parser.parse_args()
|
||||
|
||||
def get_inventory(self):
|
||||
if len(self.resource_groups) > 0:
|
||||
# get VMs for requested resource groups
|
||||
for resource_group in self.resource_groups:
|
||||
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)))
|
||||
if self._args.host or self.tags:
|
||||
selected_machines = self._selected_machines(virtual_machines)
|
||||
self._load_machines(selected_machines)
|
||||
else:
|
||||
self._load_machines(virtual_machines)
|
||||
else:
|
||||
# get all VMs within the subscription
|
||||
try:
|
||||
virtual_machines = self._compute_client.virtual_machines.list_all()
|
||||
except Exception as exc:
|
||||
sys.exit("Error: fetching virtual machines - {0}".format(str(exc)))
|
||||
|
||||
if self._args.host or self.tags or self.locations:
|
||||
selected_machines = self._selected_machines(virtual_machines)
|
||||
self._load_machines(selected_machines)
|
||||
else:
|
||||
self._load_machines(virtual_machines)
|
||||
|
||||
def _load_machines(self, machines):
|
||||
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
|
||||
# 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()
|
||||
|
||||
if self.group_by_security_group:
|
||||
self._get_security_groups(resource_group)
|
||||
|
||||
host_vars = dict(
|
||||
ansible_host=None,
|
||||
private_ip=None,
|
||||
private_ip_alloc_method=None,
|
||||
public_ip=None,
|
||||
public_ip_name=None,
|
||||
public_ip_id=None,
|
||||
public_ip_alloc_method=None,
|
||||
fqdn=None,
|
||||
location=machine.location,
|
||||
name=machine.name,
|
||||
type=machine.type,
|
||||
id=machine.id,
|
||||
tags=machine.tags,
|
||||
network_interface_id=None,
|
||||
network_interface=None,
|
||||
resource_group=resource_group,
|
||||
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),
|
||||
provisioning_state=machine.provisioning_state,
|
||||
)
|
||||
|
||||
host_vars['os_disk'] = dict(
|
||||
name=machine.storage_profile.os_disk.name,
|
||||
operating_system_type=machine.storage_profile.os_disk.os_type.value
|
||||
)
|
||||
|
||||
if self.include_powerstate:
|
||||
host_vars['powerstate'] = self._get_powerstate(resource_group, machine.name)
|
||||
|
||||
if machine.storage_profile.image_reference:
|
||||
host_vars['image'] = dict(
|
||||
offer=machine.storage_profile.image_reference.offer,
|
||||
publisher=machine.storage_profile.image_reference.publisher,
|
||||
sku=machine.storage_profile.image_reference.sku,
|
||||
version=machine.storage_profile.image_reference.version
|
||||
)
|
||||
|
||||
# Add windows details
|
||||
if machine.os_profile is not None and 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
|
||||
host_vars['windows_rm'] = None
|
||||
if machine.os_profile.windows_configuration.win_rm is not None:
|
||||
host_vars['windows_rm'] = dict(listeners=None)
|
||||
if machine.os_profile.windows_configuration.win_rm.listeners is not None:
|
||||
host_vars['windows_rm']['listeners'] = []
|
||||
for listener in machine.os_profile.windows_configuration.win_rm.listeners:
|
||||
host_vars['windows_rm']['listeners'].append(dict(protocol=listener.protocol,
|
||||
certificate_url=listener.certificate_url))
|
||||
|
||||
for interface in machine.network_profile.network_interfaces:
|
||||
interface_reference = self._parse_ref_id(interface.id)
|
||||
network_interface = self._network_client.network_interfaces.get(
|
||||
interface_reference['resourceGroups'],
|
||||
interface_reference['networkInterfaces'])
|
||||
if network_interface.primary:
|
||||
if self.group_by_security_group and \
|
||||
self._security_groups[resource_group].get(network_interface.id, None):
|
||||
host_vars['security_group'] = \
|
||||
self._security_groups[resource_group][network_interface.id]['name']
|
||||
host_vars['security_group_id'] = \
|
||||
self._security_groups[resource_group][network_interface.id]['id']
|
||||
host_vars['network_interface'] = network_interface.name
|
||||
host_vars['network_interface_id'] = network_interface.id
|
||||
host_vars['mac_address'] = network_interface.mac_address
|
||||
for ip_config in network_interface.ip_configurations:
|
||||
host_vars['private_ip'] = ip_config.private_ip_address
|
||||
host_vars['private_ip_alloc_method'] = ip_config.private_ip_allocation_method
|
||||
if ip_config.public_ip_address:
|
||||
public_ip_reference = self._parse_ref_id(ip_config.public_ip_address.id)
|
||||
public_ip_address = self._network_client.public_ip_addresses.get(
|
||||
public_ip_reference['resourceGroups'],
|
||||
public_ip_reference['publicIPAddresses'])
|
||||
host_vars['ansible_host'] = public_ip_address.ip_address
|
||||
host_vars['public_ip'] = public_ip_address.ip_address
|
||||
host_vars['public_ip_name'] = public_ip_address.name
|
||||
host_vars['public_ip_alloc_method'] = public_ip_address.public_ip_allocation_method
|
||||
host_vars['public_ip_id'] = public_ip_address.id
|
||||
if public_ip_address.dns_settings:
|
||||
host_vars['fqdn'] = public_ip_address.dns_settings.fqdn
|
||||
|
||||
self._add_host(host_vars)
|
||||
|
||||
def _selected_machines(self, virtual_machines):
|
||||
selected_machines = []
|
||||
for machine in virtual_machines:
|
||||
if self._args.host and self._args.host == machine.name:
|
||||
selected_machines.append(machine)
|
||||
if self.tags and self._tags_match(machine.tags, self.tags):
|
||||
selected_machines.append(machine)
|
||||
if self.locations and machine.location in self.locations:
|
||||
selected_machines.append(machine)
|
||||
return selected_machines
|
||||
|
||||
def _get_security_groups(self, resource_group):
|
||||
''' For a given resource_group build a mapping of network_interface.id to security_group name '''
|
||||
if not self._security_groups:
|
||||
self._security_groups = dict()
|
||||
if not self._security_groups.get(resource_group):
|
||||
self._security_groups[resource_group] = dict()
|
||||
for group in self._network_client.network_security_groups.list(resource_group):
|
||||
if group.network_interfaces:
|
||||
for interface in group.network_interfaces:
|
||||
self._security_groups[resource_group][interface.id] = dict(
|
||||
name=group.name,
|
||||
id=group.id
|
||||
)
|
||||
|
||||
def _get_powerstate(self, resource_group, name):
|
||||
try:
|
||||
vm = self._compute_client.virtual_machines.get(resource_group,
|
||||
name,
|
||||
expand='instanceview')
|
||||
except Exception as exc:
|
||||
sys.exit("Error: fetching instanceview for host {0} - {1}".format(name, str(exc)))
|
||||
|
||||
return next((s.code.replace('PowerState/', '')
|
||||
for s in vm.instance_view.statuses if s.code.startswith('PowerState')), None)
|
||||
|
||||
def _add_host(self, vars):
|
||||
|
||||
host_name = self._to_safe(vars['name'])
|
||||
resource_group = self._to_safe(vars['resource_group'])
|
||||
security_group = None
|
||||
if vars.get('security_group'):
|
||||
security_group = self._to_safe(vars['security_group'])
|
||||
|
||||
if self.group_by_resource_group:
|
||||
if not self._inventory.get(resource_group):
|
||||
self._inventory[resource_group] = []
|
||||
self._inventory[resource_group].append(host_name)
|
||||
|
||||
if self.group_by_location:
|
||||
if not self._inventory.get(vars['location']):
|
||||
self._inventory[vars['location']] = []
|
||||
self._inventory[vars['location']].append(host_name)
|
||||
|
||||
if self.group_by_security_group and security_group:
|
||||
if not self._inventory.get(security_group):
|
||||
self._inventory[security_group] = []
|
||||
self._inventory[security_group].append(host_name)
|
||||
|
||||
self._inventory['_meta']['hostvars'][host_name] = vars
|
||||
self._inventory['azure'].append(host_name)
|
||||
|
||||
if self.group_by_tag and vars.get('tags'):
|
||||
for key, value in vars['tags'].items():
|
||||
safe_key = self._to_safe(key)
|
||||
safe_value = safe_key + '_' + self._to_safe(value)
|
||||
if not self._inventory.get(safe_key):
|
||||
self._inventory[safe_key] = []
|
||||
if not self._inventory.get(safe_value):
|
||||
self._inventory[safe_value] = []
|
||||
self._inventory[safe_key].append(host_name)
|
||||
self._inventory[safe_value].append(host_name)
|
||||
|
||||
def _json_format_dict(self, pretty=False):
|
||||
# convert inventory to json
|
||||
if pretty:
|
||||
return json.dumps(self._inventory, sort_keys=True, indent=2)
|
||||
else:
|
||||
return json.dumps(self._inventory)
|
||||
|
||||
def _get_settings(self):
|
||||
# Load settings from the .ini, if it exists. Otherwise,
|
||||
# look for environment values.
|
||||
file_settings = self._load_settings()
|
||||
if file_settings:
|
||||
for key in AZURE_CONFIG_SETTINGS:
|
||||
if key in ('resource_groups', 'tags', 'locations') and file_settings.get(key):
|
||||
values = file_settings.get(key).split(',')
|
||||
if len(values) > 0:
|
||||
setattr(self, key, values)
|
||||
elif file_settings.get(key):
|
||||
val = self._to_boolean(file_settings[key])
|
||||
setattr(self, key, val)
|
||||
else:
|
||||
env_settings = self._get_env_settings()
|
||||
for key in AZURE_CONFIG_SETTINGS:
|
||||
if key in('resource_groups', 'tags', 'locations') and env_settings.get(key):
|
||||
values = env_settings.get(key).split(',')
|
||||
if len(values) > 0:
|
||||
setattr(self, key, values)
|
||||
elif env_settings.get(key, None) is not None:
|
||||
val = self._to_boolean(env_settings[key])
|
||||
setattr(self, key, val)
|
||||
|
||||
def _parse_ref_id(self, reference):
|
||||
response = {}
|
||||
keys = reference.strip('/').split('/')
|
||||
for index in range(len(keys)):
|
||||
if index < len(keys) - 1 and index % 2 == 0:
|
||||
response[keys[index]] = keys[index + 1]
|
||||
return response
|
||||
|
||||
def _to_boolean(self, value):
|
||||
if value in ['Yes', 'yes', 1, 'True', 'true', True]:
|
||||
result = True
|
||||
elif value in ['No', 'no', 0, 'False', 'false', False]:
|
||||
result = False
|
||||
else:
|
||||
result = True
|
||||
return result
|
||||
|
||||
def _get_env_settings(self):
|
||||
env_settings = dict()
|
||||
for attribute, env_variable in AZURE_CONFIG_SETTINGS.items():
|
||||
env_settings[attribute] = os.environ.get(env_variable, None)
|
||||
return env_settings
|
||||
|
||||
def _load_settings(self):
|
||||
basename = os.path.splitext(os.path.basename(__file__))[0]
|
||||
default_path = os.path.join(os.path.dirname(__file__), (basename + '.ini'))
|
||||
path = os.path.expanduser(os.path.expandvars(os.environ.get('AZURE_INI_PATH', default_path)))
|
||||
config = None
|
||||
settings = None
|
||||
try:
|
||||
config = cp.ConfigParser()
|
||||
config.read(path)
|
||||
except:
|
||||
pass
|
||||
|
||||
if config is not None:
|
||||
settings = dict()
|
||||
for key in AZURE_CONFIG_SETTINGS:
|
||||
try:
|
||||
settings[key] = config.get('azure', key, raw=True)
|
||||
except:
|
||||
pass
|
||||
|
||||
return settings
|
||||
|
||||
def _tags_match(self, tag_obj, tag_args):
|
||||
'''
|
||||
Return True if the tags object from a VM contains the requested tag values.
|
||||
|
||||
:param tag_obj: Dictionary of string:string pairs
|
||||
:param tag_args: List of strings in the form key=value
|
||||
:return: boolean
|
||||
'''
|
||||
|
||||
if not tag_obj:
|
||||
return False
|
||||
|
||||
matches = 0
|
||||
for arg in tag_args:
|
||||
arg_key = arg
|
||||
arg_value = None
|
||||
if re.search(r':', arg):
|
||||
arg_key, arg_value = arg.split(':')
|
||||
if arg_value and tag_obj.get(arg_key, None) == arg_value:
|
||||
matches += 1
|
||||
elif not arg_value and tag_obj.get(arg_key, None) is not None:
|
||||
matches += 1
|
||||
if matches == len(tag_args):
|
||||
return True
|
||||
return False
|
||||
|
||||
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 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))
|
||||
|
||||
AzureInventory()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -1,39 +0,0 @@
|
||||
# Copyright 2016 Doalitic.
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
# The Brook.io inventory script has the following dependencies:
|
||||
# 1. A working Brook.io account
|
||||
# See https://brook.io
|
||||
# 2. A valid token generated through the 'API token' panel of Brook.io
|
||||
# 3. The libbrook python libray.
|
||||
# See https://github.com/doalitic/libbrook
|
||||
#
|
||||
# Author: Francisco Ros <fjros@doalitic.com>
|
||||
|
||||
[brook]
|
||||
# Valid API token (required).
|
||||
# E.g. 'Aed342a12A60433697281FeEe1a4037C'
|
||||
#
|
||||
api_token =
|
||||
|
||||
# Project id within Brook.io, as obtained from the project settings (optional). If provided, the
|
||||
# generated inventory will just include the hosts that belong to such project. Otherwise, it will
|
||||
# include all hosts in projects the requesting user has access to. The response includes groups
|
||||
# 'project_x', being 'x' the project name.
|
||||
# E.g. '2e8e099e1bc34cc0979d97ac34e9577b'
|
||||
#
|
||||
project_id =
|
||||
@ -1,264 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright 2016 Doalitic.
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
"""
|
||||
Brook.io external inventory script
|
||||
==================================
|
||||
|
||||
Generates inventory that Ansible can understand by making API requests to Brook.io via the libbrook
|
||||
library. Hence, such dependency must be installed in the system to run this script.
|
||||
|
||||
The default configuration file is named 'brook.ini' and is located alongside this script. You can
|
||||
choose any other file by setting the BROOK_INI_PATH environment variable.
|
||||
|
||||
If param 'project_id' is left blank in 'brook.ini', the inventory includes all the instances in
|
||||
projects where the requesting user belongs. Otherwise, only instances from the given project are
|
||||
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)
|
||||
- brook_external_ips: list(str)
|
||||
- brook_created_at
|
||||
- brook_updated_at
|
||||
- ansible_ssh_host
|
||||
|
||||
Instances are grouped by the following categories:
|
||||
- tag:
|
||||
A group is created for each tag. E.g. groups 'tag_foo' and 'tag_bar' are created if there exist
|
||||
instances with tags 'foo' and/or 'bar'.
|
||||
- project:
|
||||
A group is created for each project. E.g. group 'project_test' is created if a project named
|
||||
'test' exist.
|
||||
- status:
|
||||
A group is created for each instance state. E.g. groups 'status_RUNNING' and 'status_PENDING'
|
||||
are created if there are instances in running and pending state.
|
||||
|
||||
Examples:
|
||||
Execute uname on all instances in project 'test'
|
||||
$ ansible -i brook.py project_test -m shell -a "/bin/uname -a"
|
||||
|
||||
Install nginx on all debian web servers tagged with 'www'
|
||||
$ ansible -i brook.py tag_www -m apt -a "name=nginx state=present"
|
||||
|
||||
Run site.yml playbook on web servers
|
||||
$ ansible-playbook -i brook.py site.yml -l tag_www
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
try:
|
||||
from ConfigParser import SafeConfigParser as ConfigParser
|
||||
except ImportError:
|
||||
from configparser import ConfigParser
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
try:
|
||||
import libbrook
|
||||
except:
|
||||
sys.exit('Brook.io inventory script requires libbrook. See https://github.com/doalitic/libbrook')
|
||||
|
||||
|
||||
class BrookInventory:
|
||||
|
||||
_API_ENDPOINT = 'https://api.brook.io'
|
||||
|
||||
def __init__(self):
|
||||
self._configure_from_file()
|
||||
self.client = self.get_api_client()
|
||||
self.inventory = self.get_inventory()
|
||||
|
||||
def _configure_from_file(self):
|
||||
"""Initialize from .ini file.
|
||||
|
||||
Configuration file is assumed to be named 'brook.ini' and to be located on the same
|
||||
directory than this file, unless the environment variable BROOK_INI_PATH says otherwise.
|
||||
"""
|
||||
|
||||
brook_ini_default_path = \
|
||||
os.path.join(os.path.dirname(os.path.realpath(__file__)), 'brook.ini')
|
||||
brook_ini_path = os.environ.get('BROOK_INI_PATH', brook_ini_default_path)
|
||||
|
||||
config = ConfigParser(defaults={
|
||||
'api_token': '',
|
||||
'project_id': ''
|
||||
})
|
||||
config.read(brook_ini_path)
|
||||
self.api_token = config.get('brook', 'api_token')
|
||||
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.')
|
||||
|
||||
def get_api_client(self):
|
||||
"""Authenticate user via the provided credentials and return the corresponding API client.
|
||||
"""
|
||||
|
||||
# Get JWT token from API token
|
||||
#
|
||||
unauthenticated_client = libbrook.ApiClient(host=self._API_ENDPOINT)
|
||||
auth_api = libbrook.AuthApi(unauthenticated_client)
|
||||
api_token = libbrook.AuthTokenRequest()
|
||||
api_token.token = self.api_token
|
||||
jwt = auth_api.auth_token(token=api_token)
|
||||
|
||||
# Create authenticated API client
|
||||
#
|
||||
return libbrook.ApiClient(host=self._API_ENDPOINT,
|
||||
header_name='Authorization',
|
||||
header_value='Bearer %s' % jwt.token)
|
||||
|
||||
def get_inventory(self):
|
||||
"""Generate Ansible inventory.
|
||||
"""
|
||||
|
||||
groups = dict()
|
||||
meta = dict()
|
||||
meta['hostvars'] = dict()
|
||||
|
||||
instances_api = libbrook.InstancesApi(self.client)
|
||||
projects_api = libbrook.ProjectsApi(self.client)
|
||||
templates_api = libbrook.TemplatesApi(self.client)
|
||||
|
||||
# If no project is given, get all projects the requesting user has access to
|
||||
#
|
||||
if not self.project_id:
|
||||
projects = [project.id for project in projects_api.index_projects()]
|
||||
else:
|
||||
projects = [self.project_id]
|
||||
|
||||
# Build inventory from instances in all projects
|
||||
#
|
||||
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
|
||||
|
||||
# Update hostvars
|
||||
try:
|
||||
meta['hostvars'][instance.name] = \
|
||||
self.hostvars(project, instance, template, instances_api)
|
||||
except libbrook.rest.ApiException:
|
||||
continue
|
||||
|
||||
# Group by project
|
||||
project_group = 'project_%s' % project.name
|
||||
if project_group in groups:
|
||||
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:
|
||||
groups[status_group].append(instance.name)
|
||||
else:
|
||||
groups[status_group] = [instance.name]
|
||||
|
||||
# Group by tags
|
||||
tags = meta['hostvars'][instance.name]['brook_tags']
|
||||
for tag in tags:
|
||||
tag_group = 'tag_%s' % tag
|
||||
if tag_group in groups:
|
||||
groups[tag_group].append(instance.name)
|
||||
else:
|
||||
groups[tag_group] = [instance.name]
|
||||
|
||||
groups['_meta'] = meta
|
||||
return groups
|
||||
|
||||
def hostvars(self, project, instance, template, api):
|
||||
"""Return the hostvars dictionary for the given instance.
|
||||
|
||||
Raise libbrook.rest.ApiException if it cannot retrieve all required information from the
|
||||
Brook.io API.
|
||||
"""
|
||||
|
||||
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']
|
||||
del hostvars['key']
|
||||
del hostvars['provider']
|
||||
del hostvars['image']
|
||||
|
||||
# Substitute identifiers for names
|
||||
#
|
||||
hostvars['brook_project'] = project.name
|
||||
hostvars['brook_template'] = template.name if template else None
|
||||
|
||||
# Retrieve instance state
|
||||
#
|
||||
status = api.status_instance(project_id=project.id, instance_id=instance.id)
|
||||
hostvars.update({'brook_status': status.state})
|
||||
|
||||
# Retrieve instance tags
|
||||
#
|
||||
tags = api.instance_tags(project_id=project.id, instance_id=instance.id)
|
||||
hostvars.update({'brook_tags': tags})
|
||||
|
||||
# Retrieve instance addresses
|
||||
#
|
||||
addresses = api.instance_addresses(project_id=project.id, instance_id=instance.id)
|
||||
internal_ips = [address.address for address in addresses if address.scope == 'internal']
|
||||
external_ips = [address.address for address in addresses
|
||||
if address.address and address.scope == 'external']
|
||||
hostvars.update({'brook_internal_ips': internal_ips})
|
||||
hostvars.update({'brook_external_ips': external_ips})
|
||||
try:
|
||||
hostvars.update({'ansible_ssh_host': external_ips[0]})
|
||||
except IndexError:
|
||||
raise libbrook.rest.ApiException(status='502', reason='Instance without public IP')
|
||||
|
||||
return hostvars
|
||||
|
||||
|
||||
# Run the script
|
||||
#
|
||||
brook = BrookInventory()
|
||||
print(json.dumps(brook.inventory))
|
||||
@ -1,33 +0,0 @@
|
||||
[cloudforms]
|
||||
|
||||
# the version of CloudForms ; currently not used, but tested with
|
||||
version = 4.1
|
||||
|
||||
# This should be the hostname of the CloudForms server
|
||||
url = https://cfme.example.com
|
||||
|
||||
# This will more than likely need to be a local CloudForms username
|
||||
username = <set your username here>
|
||||
|
||||
# The password for said username
|
||||
password = <set your password here>
|
||||
|
||||
# True = verify SSL certificate / False = trust anything
|
||||
ssl_verify = True
|
||||
|
||||
# limit the number of vms returned per request
|
||||
limit = 100
|
||||
|
||||
# purge the CloudForms actions from hosts
|
||||
purge_actions = True
|
||||
|
||||
# Clean up group names (from tags and other groupings so Ansible doesn't complain)
|
||||
clean_group_keys = True
|
||||
|
||||
# Explode tags into nested groups / subgroups
|
||||
nest_tags = False
|
||||
|
||||
[cache]
|
||||
|
||||
# Maximum time to trust the cache in seconds
|
||||
max_age = 600
|
||||
@ -1,461 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: set fileencoding=utf-8 :
|
||||
#
|
||||
# Copyright (C) 2016 Guido Günther <agx@sigxcpu.org>
|
||||
#
|
||||
# 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 loosely based on the foreman inventory script
|
||||
# -- Josh Preston <jpreston@redhat.com>
|
||||
#
|
||||
|
||||
from __future__ import print_function
|
||||
import argparse
|
||||
import ConfigParser
|
||||
import os
|
||||
import re
|
||||
from time import time
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
import warnings
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
|
||||
class CloudFormsInventory(object):
|
||||
def __init__(self):
|
||||
"""
|
||||
Main execution path
|
||||
"""
|
||||
self.inventory = dict() # A list of groups and the hosts in that group
|
||||
self.hosts = dict() # Details about hosts in the inventory
|
||||
|
||||
# Parse CLI arguments
|
||||
self.parse_cli_args()
|
||||
|
||||
# Read settings
|
||||
self.read_settings()
|
||||
|
||||
# Cache
|
||||
if self.args.refresh_cache or not self.is_cache_valid():
|
||||
self.update_cache()
|
||||
else:
|
||||
self.load_inventory_from_cache()
|
||||
self.load_hosts_from_cache()
|
||||
|
||||
data_to_print = ""
|
||||
|
||||
# Data to print
|
||||
if self.args.host:
|
||||
if self.args.debug:
|
||||
print("Fetching host [%s]" % self.args.host)
|
||||
data_to_print += self.get_host_info(self.args.host)
|
||||
else:
|
||||
self.inventory['_meta'] = {'hostvars': {}}
|
||||
for hostname in self.hosts:
|
||||
self.inventory['_meta']['hostvars'][hostname] = {
|
||||
'cloudforms': self.hosts[hostname],
|
||||
}
|
||||
# include the ansible_ssh_host in the top level
|
||||
if 'ansible_ssh_host' in self.hosts[hostname]:
|
||||
self.inventory['_meta']['hostvars'][hostname]['ansible_ssh_host'] = self.hosts[hostname]['ansible_ssh_host']
|
||||
|
||||
data_to_print += self.json_format_dict(self.inventory, self.args.pretty)
|
||||
|
||||
print(data_to_print)
|
||||
|
||||
def is_cache_valid(self):
|
||||
"""
|
||||
Determines if the cache files have expired, or if it is still valid
|
||||
"""
|
||||
if self.args.debug:
|
||||
print("Determining if cache [%s] is still valid (< %s seconds old)" % (self.cache_path_hosts, self.cache_max_age))
|
||||
|
||||
if os.path.isfile(self.cache_path_hosts):
|
||||
mod_time = os.path.getmtime(self.cache_path_hosts)
|
||||
current_time = time()
|
||||
if (mod_time + self.cache_max_age) > current_time:
|
||||
if os.path.isfile(self.cache_path_inventory):
|
||||
if self.args.debug:
|
||||
print("Cache is still valid!")
|
||||
return True
|
||||
|
||||
if self.args.debug:
|
||||
print("Cache is stale or does not exist.")
|
||||
|
||||
return False
|
||||
|
||||
def read_settings(self):
|
||||
"""
|
||||
Reads the settings from the cloudforms.ini file
|
||||
"""
|
||||
config = ConfigParser.SafeConfigParser()
|
||||
config_paths = [
|
||||
os.path.dirname(os.path.realpath(__file__)) + '/cloudforms.ini',
|
||||
"/etc/ansible/cloudforms.ini",
|
||||
]
|
||||
|
||||
env_value = os.environ.get('CLOUDFORMS_INI_PATH')
|
||||
if env_value is not None:
|
||||
config_paths.append(os.path.expanduser(os.path.expandvars(env_value)))
|
||||
|
||||
if self.args.debug:
|
||||
for config_path in config_paths:
|
||||
print("Reading from configuration file [%s]" % config_path)
|
||||
|
||||
config.read(config_paths)
|
||||
|
||||
# CloudForms API related
|
||||
if config.has_option('cloudforms', 'url'):
|
||||
self.cloudforms_url = config.get('cloudforms', 'url')
|
||||
else:
|
||||
self.cloudforms_url = None
|
||||
|
||||
if not self.cloudforms_url:
|
||||
warnings.warn("No url specified, expected something like 'https://cfme.example.com'")
|
||||
|
||||
if config.has_option('cloudforms', 'username'):
|
||||
self.cloudforms_username = config.get('cloudforms', 'username')
|
||||
else:
|
||||
self.cloudforms_username = None
|
||||
|
||||
if not self.cloudforms_username:
|
||||
warnings.warn("No username specified, you need to specify a CloudForms username.")
|
||||
|
||||
if config.has_option('cloudforms', 'password'):
|
||||
self.cloudforms_pw = config.get('cloudforms', 'password')
|
||||
else:
|
||||
self.cloudforms_pw = None
|
||||
|
||||
if not self.cloudforms_pw:
|
||||
warnings.warn("No password specified, you need to specify a password for the CloudForms user.")
|
||||
|
||||
if config.has_option('cloudforms', 'ssl_verify'):
|
||||
self.cloudforms_ssl_verify = config.getboolean('cloudforms', 'ssl_verify')
|
||||
else:
|
||||
self.cloudforms_ssl_verify = True
|
||||
|
||||
if config.has_option('cloudforms', 'version'):
|
||||
self.cloudforms_version = config.get('cloudforms', 'version')
|
||||
else:
|
||||
self.cloudforms_version = None
|
||||
|
||||
if config.has_option('cloudforms', 'limit'):
|
||||
self.cloudforms_limit = config.getint('cloudforms', 'limit')
|
||||
else:
|
||||
self.cloudforms_limit = 100
|
||||
|
||||
if config.has_option('cloudforms', 'purge_actions'):
|
||||
self.cloudforms_purge_actions = config.getboolean('cloudforms', 'purge_actions')
|
||||
else:
|
||||
self.cloudforms_purge_actions = True
|
||||
|
||||
if config.has_option('cloudforms', 'clean_group_keys'):
|
||||
self.cloudforms_clean_group_keys = config.getboolean('cloudforms', 'clean_group_keys')
|
||||
else:
|
||||
self.cloudforms_clean_group_keys = True
|
||||
|
||||
if config.has_option('cloudforms', 'nest_tags'):
|
||||
self.cloudforms_nest_tags = config.getboolean('cloudforms', 'nest_tags')
|
||||
else:
|
||||
self.cloudforms_nest_tags = False
|
||||
|
||||
# Ansible related
|
||||
try:
|
||||
group_patterns = config.get('ansible', 'group_patterns')
|
||||
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
|
||||
group_patterns = "[]"
|
||||
|
||||
self.group_patterns = eval(group_patterns)
|
||||
|
||||
# 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_hosts = cache_path + "/%s.hosts" % script
|
||||
self.cache_path_inventory = cache_path + "/%s.inventory" % script
|
||||
self.cache_max_age = config.getint('cache', 'max_age')
|
||||
|
||||
if self.args.debug:
|
||||
print("CloudForms settings:")
|
||||
print("cloudforms_url = %s" % self.cloudforms_url)
|
||||
print("cloudforms_username = %s" % self.cloudforms_username)
|
||||
print("cloudforms_pw = %s" % self.cloudforms_pw)
|
||||
print("cloudforms_ssl_verify = %s" % self.cloudforms_ssl_verify)
|
||||
print("cloudforms_version = %s" % self.cloudforms_version)
|
||||
print("cloudforms_limit = %s" % self.cloudforms_limit)
|
||||
print("cloudforms_purge_actions = %s" % self.cloudforms_purge_actions)
|
||||
print("Cache settings:")
|
||||
print("cache_max_age = %s" % self.cache_max_age)
|
||||
print("cache_path_hosts = %s" % self.cache_path_hosts)
|
||||
print("cache_path_inventory = %s" % self.cache_path_inventory)
|
||||
|
||||
def parse_cli_args(self):
|
||||
"""
|
||||
Command line argument processing
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on CloudForms managed VMs')
|
||||
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('--pretty', action='store_true', default=False, help='Pretty print JSON output (default: False)')
|
||||
parser.add_argument('--refresh-cache', action='store_true', default=False,
|
||||
help='Force refresh of cache by making API requests to CloudForms (default: False - use cache files)')
|
||||
parser.add_argument('--debug', action='store_true', default=False, help='Show debug output while running (default: False)')
|
||||
self.args = parser.parse_args()
|
||||
|
||||
def _get_json(self, url):
|
||||
"""
|
||||
Make a request and return the JSON
|
||||
"""
|
||||
results = []
|
||||
|
||||
ret = requests.get(url,
|
||||
auth=HTTPBasicAuth(self.cloudforms_username, self.cloudforms_pw),
|
||||
verify=self.cloudforms_ssl_verify)
|
||||
|
||||
ret.raise_for_status()
|
||||
|
||||
try:
|
||||
results = json.loads(ret.text)
|
||||
except ValueError:
|
||||
warnings.warn("Unexpected response from {0} ({1}): {2}".format(self.cloudforms_url, ret.status_code, ret.reason))
|
||||
results = {}
|
||||
|
||||
if self.args.debug:
|
||||
print("=======================================================================")
|
||||
print("=======================================================================")
|
||||
print("=======================================================================")
|
||||
print(ret.text)
|
||||
print("=======================================================================")
|
||||
print("=======================================================================")
|
||||
print("=======================================================================")
|
||||
|
||||
return results
|
||||
|
||||
def _get_hosts(self):
|
||||
"""
|
||||
Get all hosts by paging through the results
|
||||
"""
|
||||
limit = self.cloudforms_limit
|
||||
|
||||
page = 0
|
||||
last_page = False
|
||||
|
||||
results = []
|
||||
|
||||
while not last_page:
|
||||
offset = page * limit
|
||||
ret = self._get_json("%s/api/vms?offset=%s&limit=%s&expand=resources,tags,hosts,&attributes=ipaddresses" % (self.cloudforms_url, offset, limit))
|
||||
results += ret['resources']
|
||||
if ret['subcount'] < limit:
|
||||
last_page = True
|
||||
page += 1
|
||||
|
||||
return results
|
||||
|
||||
def update_cache(self):
|
||||
"""
|
||||
Make calls to cloudforms and save the output in a cache
|
||||
"""
|
||||
self.groups = dict()
|
||||
self.hosts = dict()
|
||||
|
||||
if self.args.debug:
|
||||
print("Updating cache...")
|
||||
|
||||
for host in self._get_hosts():
|
||||
# Ignore VMs that are not powered on
|
||||
if host['power_state'] != 'on':
|
||||
if self.args.debug:
|
||||
print("Skipping %s because power_state = %s" % (host['name'], host['power_state']))
|
||||
continue
|
||||
|
||||
# purge actions
|
||||
if self.cloudforms_purge_actions and 'actions' in host:
|
||||
del host['actions']
|
||||
|
||||
# Create ansible groups for tags
|
||||
if 'tags' in host:
|
||||
|
||||
# Create top-level group
|
||||
if 'tags' not in self.inventory:
|
||||
self.inventory['tags'] = dict(children=[], vars={}, hosts=[])
|
||||
|
||||
if not self.cloudforms_nest_tags:
|
||||
# don't expand tags, just use them in a safe way
|
||||
for group in host['tags']:
|
||||
# Add sub-group, as a child of top-level
|
||||
safe_key = self.to_safe(group['name'])
|
||||
if safe_key:
|
||||
if self.args.debug:
|
||||
print("Adding sub-group '%s' to parent 'tags'" % safe_key)
|
||||
|
||||
if safe_key not in self.inventory['tags']['children']:
|
||||
self.push(self.inventory['tags'], 'children', safe_key)
|
||||
|
||||
self.push(self.inventory, safe_key, host['name'])
|
||||
|
||||
if self.args.debug:
|
||||
print("Found tag [%s] for host which will be mapped to [%s]" % (group['name'], safe_key))
|
||||
else:
|
||||
# expand the tags into nested groups / sub-groups
|
||||
# Create nested groups for tags
|
||||
safe_parent_tag_name = 'tags'
|
||||
for tag in host['tags']:
|
||||
tag_hierarchy = tag['name'][1:].split('/')
|
||||
|
||||
if self.args.debug:
|
||||
print("Working on list %s" % tag_hierarchy)
|
||||
|
||||
for tag_name in tag_hierarchy:
|
||||
if self.args.debug:
|
||||
print("Working on tag_name = %s" % tag_name)
|
||||
|
||||
safe_tag_name = self.to_safe(tag_name)
|
||||
if self.args.debug:
|
||||
print("Using sanitized name %s" % safe_tag_name)
|
||||
|
||||
# Create sub-group
|
||||
if safe_tag_name not in self.inventory:
|
||||
self.inventory[safe_tag_name] = dict(children=[], vars={}, hosts=[])
|
||||
|
||||
# Add sub-group, as a child of top-level
|
||||
if safe_parent_tag_name:
|
||||
if self.args.debug:
|
||||
print("Adding sub-group '%s' to parent '%s'" % (safe_tag_name, safe_parent_tag_name))
|
||||
|
||||
if safe_tag_name not in self.inventory[safe_parent_tag_name]['children']:
|
||||
self.push(self.inventory[safe_parent_tag_name], 'children', safe_tag_name)
|
||||
|
||||
# Make sure the next one uses this one as it's parent
|
||||
safe_parent_tag_name = safe_tag_name
|
||||
|
||||
# Add the host to the last tag
|
||||
self.push(self.inventory[safe_parent_tag_name], 'hosts', host['name'])
|
||||
|
||||
# Set ansible_ssh_host to the first available ip address
|
||||
if 'ipaddresses' in host and host['ipaddresses'] and isinstance(host['ipaddresses'], list):
|
||||
host['ansible_ssh_host'] = host['ipaddresses'][0]
|
||||
|
||||
# Create additional groups
|
||||
for key in ('location', 'type', 'vendor'):
|
||||
safe_key = self.to_safe(host[key])
|
||||
|
||||
# Create top-level group
|
||||
if key not in self.inventory:
|
||||
self.inventory[key] = dict(children=[], vars={}, hosts=[])
|
||||
|
||||
# Create sub-group
|
||||
if safe_key not in self.inventory:
|
||||
self.inventory[safe_key] = dict(children=[], vars={}, hosts=[])
|
||||
|
||||
# Add sub-group, as a child of top-level
|
||||
if safe_key not in self.inventory[key]['children']:
|
||||
self.push(self.inventory[key], 'children', safe_key)
|
||||
|
||||
if key in host:
|
||||
# Add host to sub-group
|
||||
self.push(self.inventory[safe_key], 'hosts', host['name'])
|
||||
|
||||
self.hosts[host['name']] = host
|
||||
self.push(self.inventory, 'all', host['name'])
|
||||
|
||||
if self.args.debug:
|
||||
print("Saving cached data")
|
||||
|
||||
self.write_to_cache(self.hosts, self.cache_path_hosts)
|
||||
self.write_to_cache(self.inventory, self.cache_path_inventory)
|
||||
|
||||
def get_host_info(self, host):
|
||||
"""
|
||||
Get variables about a specific host
|
||||
"""
|
||||
if not self.hosts or len(self.hosts) == 0:
|
||||
# Need to load cache from cache
|
||||
self.load_hosts_from_cache()
|
||||
|
||||
if host not in self.hosts:
|
||||
if self.args.debug:
|
||||
print("[%s] not found in cache." % host)
|
||||
|
||||
# try updating the cache
|
||||
self.update_cache()
|
||||
|
||||
if host not in self.hosts:
|
||||
if self.args.debug:
|
||||
print("[%s] does not exist after cache update." % host)
|
||||
# host might not exist anymore
|
||||
return self.json_format_dict({}, self.args.pretty)
|
||||
|
||||
return self.json_format_dict(self.hosts[host], self.args.pretty)
|
||||
|
||||
def push(self, d, k, v):
|
||||
"""
|
||||
Safely puts a new entry onto an array.
|
||||
"""
|
||||
if k in d:
|
||||
d[k].append(v)
|
||||
else:
|
||||
d[k] = [v]
|
||||
|
||||
def load_inventory_from_cache(self):
|
||||
"""
|
||||
Reads the inventory from the cache file sets self.inventory
|
||||
"""
|
||||
cache = open(self.cache_path_inventory, 'r')
|
||||
json_inventory = cache.read()
|
||||
self.inventory = json.loads(json_inventory)
|
||||
|
||||
def load_hosts_from_cache(self):
|
||||
"""
|
||||
Reads the cache from the cache file sets self.hosts
|
||||
"""
|
||||
cache = open(self.cache_path_hosts, 'r')
|
||||
json_cache = cache.read()
|
||||
self.hosts = json.loads(json_cache)
|
||||
|
||||
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 to_safe(self, word):
|
||||
"""
|
||||
Converts 'bad' characters in a string to underscores so they can be used as Ansible groups
|
||||
"""
|
||||
if self.cloudforms_clean_group_keys:
|
||||
regex = "[^A-Za-z0-9\_]"
|
||||
return re.sub(regex, "_", word.replace(" ", ""))
|
||||
else:
|
||||
return 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)
|
||||
|
||||
CloudFormsInventory()
|
||||
@ -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)
|
||||
print(json.dumps(data, indent=2))
|
||||
|
||||
elif options.list:
|
||||
data = self.get_list(project_id, domain_id)
|
||||
data = self.get_list()
|
||||
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']
|
||||
|
||||
|
||||
@ -5,10 +5,6 @@
|
||||
|
||||
host = http://PATH_TO_COBBLER_SERVER/cobbler_api
|
||||
|
||||
# If API needs authentication add 'username' and 'password' options here.
|
||||
#username = foo
|
||||
#password = bar
|
||||
|
||||
# API calls to Cobbler can be slow. 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:
|
||||
|
||||
@ -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)
|
||||
@ -120,9 +120,6 @@ class CobblerInventory(object):
|
||||
def _connect(self):
|
||||
if not self.conn:
|
||||
self.conn = xmlrpclib.Server(self.cobbler_host, allow_none=True)
|
||||
self.token = None
|
||||
if self.cobbler_username is not None:
|
||||
self.token = self.conn.login(self.cobbler_username, self.cobbler_password)
|
||||
|
||||
def is_cache_valid(self):
|
||||
""" Determines if the cache files have expired, or if it is still valid """
|
||||
@ -143,12 +140,6 @@ class CobblerInventory(object):
|
||||
config.read(os.path.dirname(os.path.realpath(__file__)) + '/cobbler.ini')
|
||||
|
||||
self.cobbler_host = config.get('cobbler', 'host')
|
||||
self.cobbler_username = None
|
||||
self.cobbler_password = None
|
||||
if config.has_option('cobbler', 'username'):
|
||||
self.cobbler_username = config.get('cobbler', 'username')
|
||||
if config.has_option('cobbler', 'password'):
|
||||
self.cobbler_password = config.get('cobbler', 'password')
|
||||
|
||||
# Cache related
|
||||
cache_path = config.get('cobbler', 'cache_path')
|
||||
@ -172,14 +163,12 @@ class CobblerInventory(object):
|
||||
self._connect()
|
||||
self.groups = dict()
|
||||
self.hosts = dict()
|
||||
if self.token is not None:
|
||||
data = self.conn.get_systems(self.token)
|
||||
else:
|
||||
data = self.conn.get_systems()
|
||||
|
||||
data = self.conn.get_systems()
|
||||
|
||||
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 +179,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 +218,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)
|
||||
|
||||
|
||||
@ -3,8 +3,6 @@
|
||||
|
||||
[collins]
|
||||
|
||||
# You should not have a trailing slash or collins
|
||||
# will not properly match the URI
|
||||
host = http://localhost:9000
|
||||
|
||||
username = blake
|
||||
|
||||
@ -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,13 +168,14 @@ class CollinsInventory(object):
|
||||
print(data_to_print)
|
||||
return successful
|
||||
|
||||
def find_assets(self, attributes={}, operation='AND'):
|
||||
def find_assets(self, attributes = {}, operation = 'AND'):
|
||||
""" Obtains Collins assets matching the provided 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],
|
||||
@ -190,18 +190,18 @@ 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)
|
||||
json_response = json.loads(response.read())
|
||||
# Adds any assets found to the array of assets.
|
||||
assets += json_response['data']['Data']
|
||||
@ -211,7 +211,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
|
||||
|
||||
@ -230,15 +231,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')
|
||||
|
||||
@ -262,12 +267,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):
|
||||
@ -280,7 +289,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:
|
||||
@ -304,7 +314,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(
|
||||
@ -372,11 +383,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)
|
||||
|
||||
@ -392,7 +403,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
|
||||
|
||||
@ -406,7 +417,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
|
||||
@ -90,9 +84,9 @@ to retrieve the kv_groups and kv_metadata based on your consul configuration.
|
||||
This is used to lookup groups for a node in the key value store. It specifies a
|
||||
path to which each discovered node's name will be added to create a key to query
|
||||
the key/value store. There it expects to find a comma separated list of group
|
||||
names to which the node should be added e.g. if the inventory contains node
|
||||
'nyc-web-1' in datacenter 'nyc-dc1' and kv_groups = 'ansible/groups' then the key
|
||||
'ansible/groups/nyc-dc1/nyc-web-1' will be queried for a group list. If this query
|
||||
names to which the node should be added e.g. if the inventory contains
|
||||
'nyc-web-1' and kv_groups = 'ansible/groups' then the key
|
||||
'v1/kv/ansible/groups/nyc-web-1' will be queried for a group list. If this query
|
||||
returned 'test,honeypot' then the node address to both groups.
|
||||
|
||||
'kv_metadata':
|
||||
@ -100,9 +94,7 @@ names to which the node should be added e.g. if the inventory contains node
|
||||
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"}'
|
||||
dictionary is added to the metadata for the node.
|
||||
|
||||
'availability':
|
||||
|
||||
@ -132,360 +124,306 @@ be used to access the machine.
|
||||
import os
|
||||
import re
|
||||
import argparse
|
||||
from time import time
|
||||
import sys
|
||||
import ConfigParser
|
||||
import urllib, urllib2, base64
|
||||
|
||||
try:
|
||||
import configparser
|
||||
import json
|
||||
except ImportError:
|
||||
import ConfigParser as configparser
|
||||
|
||||
|
||||
def get_log_filename():
|
||||
tty_filename = '/dev/tty'
|
||||
stdout_filename = '/dev/stdout'
|
||||
|
||||
if not os.path.exists(tty_filename):
|
||||
return stdout_filename
|
||||
if not os.access(tty_filename, os.W_OK):
|
||||
return stdout_filename
|
||||
if os.getenv('TEAMCITY_VERSION'):
|
||||
return stdout_filename
|
||||
|
||||
return tty_filename
|
||||
|
||||
|
||||
def setup_logging():
|
||||
filename = get_log_filename()
|
||||
|
||||
import logging.config
|
||||
logging.config.dictConfig({
|
||||
'version': 1,
|
||||
'formatters': {
|
||||
'simple': {
|
||||
'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
},
|
||||
},
|
||||
'root': {
|
||||
'level': os.getenv('ANSIBLE_INVENTORY_CONSUL_IO_LOG_LEVEL', 'WARN'),
|
||||
'handlers': ['console'],
|
||||
},
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.FileHandler',
|
||||
'filename': filename,
|
||||
'formatter': 'simple',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'iso8601': {
|
||||
'qualname': 'iso8601',
|
||||
'level': 'INFO',
|
||||
},
|
||||
},
|
||||
})
|
||||
logger = logging.getLogger('consul_io.py')
|
||||
logger.debug('Invoked with %r', sys.argv)
|
||||
|
||||
|
||||
if os.getenv('ANSIBLE_INVENTORY_CONSUL_IO_LOG_ENABLED'):
|
||||
setup_logging()
|
||||
|
||||
import simplejson as json
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
try:
|
||||
import consul
|
||||
import consul
|
||||
except ImportError as e:
|
||||
sys.exit("""failed=True msg='python-consul required for this module.
|
||||
See http://python-consul.readthedocs.org/en/latest/#installation'""")
|
||||
print("""failed=True msg='python-consul required for this module. see
|
||||
http://python-consul.readthedocs.org/en/latest/#installation'""")
|
||||
sys.exit(1)
|
||||
|
||||
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'):
|
||||
from ansible.module_utils.six.moves.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()
|
||||
|
||||
@ -26,9 +26,3 @@ cache_max_age = 300
|
||||
# Use the private network IP address instead of the public when available.
|
||||
#
|
||||
use_private_network = False
|
||||
|
||||
# Pass variables to every group, e.g.:
|
||||
#
|
||||
# group_variables = { 'ansible_user': 'root' }
|
||||
#
|
||||
group_variables = {}
|
||||
|
||||
@ -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,12 +136,7 @@ import sys
|
||||
import re
|
||||
import argparse
|
||||
from time import time
|
||||
import ast
|
||||
|
||||
try:
|
||||
import ConfigParser
|
||||
except ImportError:
|
||||
import configparser as ConfigParser
|
||||
import ConfigParser
|
||||
|
||||
try:
|
||||
import json
|
||||
@ -151,9 +144,11 @@ except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
try:
|
||||
from dopy.manager import DoManager
|
||||
from dopy.manager import DoError, DoManager
|
||||
except ImportError as e:
|
||||
sys.exit("failed=True msg={}".format(e.message))
|
||||
print("failed=True msg='`dopy` library required for this script'")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
||||
class DigitalOceanInventory(object):
|
||||
@ -166,14 +161,13 @@ 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 = '.'
|
||||
self.cache_max_age = 0
|
||||
self.use_private_network = False
|
||||
self.group_variables = {}
|
||||
|
||||
# Read settings, environment variables, and CLI arguments
|
||||
self.read_settings()
|
||||
@ -182,9 +176,9 @@ class DigitalOceanInventory(object):
|
||||
|
||||
# Verify credentials were set
|
||||
if not hasattr(self, 'api_token'):
|
||||
sys.stderr.write('''Could not find values for DigitalOcean api_token.
|
||||
print('''Could not find values for DigitalOcean api_token.
|
||||
They must be specified via either ini file, command line argument (--api-token),
|
||||
or environment variables (DO_API_TOKEN)\n''')
|
||||
or environment variables (DO_API_TOKEN)''')
|
||||
sys.exit(-1)
|
||||
|
||||
# env command, show DigitalOcean credentials
|
||||
@ -196,11 +190,11 @@ 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:
|
||||
sys.stderr.write('''Cache is empty and --force-cache was specified\n''')
|
||||
print('''Cache is empty and --force-cache was specified''')
|
||||
sys.exit(-1)
|
||||
|
||||
self.manager = DoManager(None, self.api_token, api_version=2)
|
||||
@ -243,6 +237,7 @@ or environment variables (DO_API_TOKEN)\n''')
|
||||
print(json.dumps(json_data))
|
||||
# That's all she wrote...
|
||||
|
||||
|
||||
###########################################################################
|
||||
# Script configuration
|
||||
###########################################################################
|
||||
@ -264,11 +259,7 @@ or environment variables (DO_API_TOKEN)\n''')
|
||||
|
||||
# Private IP Address
|
||||
if config.has_option('digital_ocean', 'use_private_network'):
|
||||
self.use_private_network = config.getboolean('digital_ocean', 'use_private_network')
|
||||
|
||||
# Group variables
|
||||
if config.has_option('digital_ocean', 'group_variables'):
|
||||
self.group_variables = ast.literal_eval(config.get('digital_ocean', 'group_variables'))
|
||||
self.use_private_network = config.get('digital_ocean', 'use_private_network')
|
||||
|
||||
def read_environment(self):
|
||||
''' Reads the settings from environment variables '''
|
||||
@ -278,6 +269,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 +278,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 +308,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,69 +342,56 @@ 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': {}}
|
||||
}
|
||||
self.inventory = {}
|
||||
|
||||
# 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:
|
||||
dest = droplet['ip_address']
|
||||
|
||||
self.inventory['all']['hosts'].append(dest)
|
||||
|
||||
self.inventory[droplet['id']] = [dest]
|
||||
self.inventory[droplet['name']] = [dest]
|
||||
self.push(self.inventory, droplet['name'], dest)
|
||||
self.push(self.inventory, 'region_' + droplet['region']['slug'], dest)
|
||||
self.push(self.inventory, 'image_' + str(droplet['image']['id']), dest)
|
||||
self.push(self.inventory, 'size_' + droplet['size']['slug'], 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']):
|
||||
if group not in self.inventory:
|
||||
self.inventory[group] = {'hosts': [], 'vars': {}}
|
||||
self.inventory[group]['hosts'].append(dest)
|
||||
image_slug = droplet['image']['slug']
|
||||
if image_slug:
|
||||
self.push(self.inventory, 'image_' + self.to_safe(image_slug), dest)
|
||||
else:
|
||||
image_name = droplet['image']['name']
|
||||
if image_name:
|
||||
self.push(self.inventory, 'image_' + self.to_safe(image_name), dest)
|
||||
|
||||
# groups that are not always present
|
||||
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'].append(dest)
|
||||
self.push(self.inventory, 'distro_' + self.to_safe(droplet['image']['distribution']), dest)
|
||||
self.push(self.inventory, 'status_' + droplet['status'], 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 +405,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 +419,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 +441,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
|
||||
|
||||
|
||||
###########################################################################
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,74 +1,134 @@
|
||||
# This is the configuration file for the Docker inventory script: docker_inventory.py.
|
||||
# This is the configuration file for the Ansible plugin for Docker inventory.
|
||||
#
|
||||
# You can define the following in this file:
|
||||
# Author: Paul Durivage <paul.durivage@gmail.com>
|
||||
#
|
||||
# defaults
|
||||
# Defines a default connection. Defaults will be taken from this and applied to any values not provided
|
||||
# for a host defined in the hosts list.
|
||||
# Description:
|
||||
# This module queries local or remote Docker daemons and generates
|
||||
# inventory information.
|
||||
#
|
||||
# hosts
|
||||
# If you wish to get inventory from more than one Docker host, define a hosts list.
|
||||
# This plugin does not support targeting of specific hosts using the --host
|
||||
# flag. Instead, it it queries the Docker API for each container, running
|
||||
# or not, and returns this data all once.
|
||||
#
|
||||
# For the default host and each host in the hosts list define the following attributes:
|
||||
# The plugin returns the following custom attributes on Docker containers:
|
||||
# docker_args
|
||||
# docker_config
|
||||
# docker_created
|
||||
# docker_driver
|
||||
# docker_exec_driver
|
||||
# docker_host_config
|
||||
# docker_hostname_path
|
||||
# docker_hosts_path
|
||||
# docker_id
|
||||
# docker_image
|
||||
# docker_name
|
||||
# docker_network_settings
|
||||
# docker_path
|
||||
# docker_resolv_conf_path
|
||||
# docker_state
|
||||
# docker_volumes
|
||||
# docker_volumes_rw
|
||||
#
|
||||
# host:
|
||||
# description: The URL or Unix socket path used to connect to the Docker API.
|
||||
# required: yes
|
||||
# Requirements:
|
||||
# The docker-py module: https://github.com/dotcloud/docker-py
|
||||
#
|
||||
# tls:
|
||||
# description: Connect using TLS without verifying the authenticity of the Docker host server.
|
||||
# default: false
|
||||
# Notes:
|
||||
# A config file can be used to configure this inventory module, and there
|
||||
# are several environment variables that can be set to modify the behavior
|
||||
# of the plugin at runtime:
|
||||
# DOCKER_CONFIG_FILE
|
||||
# DOCKER_HOST
|
||||
# DOCKER_VERSION
|
||||
# DOCKER_TIMEOUT
|
||||
# DOCKER_PRIVATE_SSH_PORT
|
||||
# DOCKER_DEFAULT_IP
|
||||
#
|
||||
# Environment Variables:
|
||||
# environment variable: DOCKER_CONFIG_FILE
|
||||
# description:
|
||||
# - A path to a Docker inventory hosts/defaults file in YAML format
|
||||
# - A sample file has been provided, colocated with the inventory
|
||||
# file called 'docker.yml'
|
||||
# required: false
|
||||
#
|
||||
# tls_verify:
|
||||
# description: Connect using TLS without verifying the authenticity of the Docker host server.
|
||||
# default: false
|
||||
# default: Uses docker.docker.Client constructor defaults
|
||||
# environment variable: DOCKER_HOST
|
||||
# description:
|
||||
# - The socket on which to connect to a Docker daemon API
|
||||
# required: false
|
||||
#
|
||||
# cert_path:
|
||||
# description: Path to the client's TLS certificate file.
|
||||
# default: null
|
||||
# default: Uses docker.docker.Client constructor defaults
|
||||
# environment variable: DOCKER_VERSION
|
||||
# description:
|
||||
# - Version of the Docker API to use
|
||||
# default: Uses docker.docker.Client constructor defaults
|
||||
# required: false
|
||||
#
|
||||
# cacert_path:
|
||||
# description: Use a CA certificate when performing server verification by providing the path to a CA certificate file.
|
||||
# default: null
|
||||
# required: false
|
||||
#
|
||||
# key_path:
|
||||
# description: Path to the client's TLS key file.
|
||||
# default: null
|
||||
# required: false
|
||||
#
|
||||
# version:
|
||||
# description: The Docker API version.
|
||||
# required: false
|
||||
# default: will be supplied by the docker-py module.
|
||||
#
|
||||
# timeout:
|
||||
# description: The amount of time in seconds to wait on an API response.
|
||||
# required: false
|
||||
# default: 60
|
||||
#
|
||||
# default_ip:
|
||||
# description: The IP address to assign to ansible_host when the container's SSH port is mapped to interface
|
||||
# '0.0.0.0'.
|
||||
# required: false
|
||||
# default: 127.0.0.1
|
||||
#
|
||||
# private_ssh_port:
|
||||
# description: The port containers use for SSH
|
||||
# environment variable: DOCKER_TIMEOUT
|
||||
# description:
|
||||
# - Timeout in seconds for connections to Docker daemon API
|
||||
# default: Uses docker.docker.Client constructor defaults
|
||||
# required: false
|
||||
# environment variable: DOCKER_PRIVATE_SSH_PORT
|
||||
# description:
|
||||
# - The private port (container port) on which SSH is listening
|
||||
# for connections
|
||||
# default: 22
|
||||
# required: false
|
||||
# environment variable: DOCKER_DEFAULT_IP
|
||||
# description:
|
||||
# - This environment variable overrides the container SSH connection
|
||||
# IP address (aka, 'ansible_ssh_host')
|
||||
#
|
||||
# This option allows one to override the ansible_ssh_host whenever
|
||||
# Docker has exercised its default behavior of binding private ports
|
||||
# to all interfaces of the Docker host. This behavior, when dealing
|
||||
# with remote Docker hosts, does not allow Ansible to determine
|
||||
# a proper host IP address on which to connect via SSH to containers.
|
||||
# By default, this inventory module assumes all 0.0.0.0-exposed
|
||||
# ports to be bound to localhost:<port>. To override this
|
||||
# behavior, for example, to bind a container's SSH port to the public
|
||||
# interface of its host, one must manually set this IP.
|
||||
#
|
||||
# It is preferable to begin to launch Docker containers with
|
||||
# ports exposed on publicly accessible IP addresses, particularly
|
||||
# if the containers are to be targeted by Ansible for remote
|
||||
# configuration, not accessible via localhost SSH connections.
|
||||
#
|
||||
# Docker containers can be explicitly exposed on IP addresses by
|
||||
# a) starting the daemon with the --ip argument
|
||||
# b) running containers with the -P/--publish ip::containerPort
|
||||
# argument
|
||||
# default: 127.0.0.1 if port exposed on 0.0.0.0 by Docker
|
||||
# required: false
|
||||
#
|
||||
# Examples:
|
||||
# Use the config file:
|
||||
# DOCKER_CONFIG_FILE=./docker.yml docker.py --list
|
||||
#
|
||||
# Connect to docker instance on localhost port 4243
|
||||
# DOCKER_HOST=tcp://localhost:4243 docker.py --list
|
||||
#
|
||||
# Any container's ssh port exposed on 0.0.0.0 will mapped to
|
||||
# another IP address (where Ansible will attempt to connect via SSH)
|
||||
# DOCKER_DEFAULT_IP=1.2.3.4 docker.py --list
|
||||
#
|
||||
#
|
||||
#
|
||||
# The Docker inventory plugin provides several environment variables that
|
||||
# may be overridden here. This configuration file always takes precedence
|
||||
# over environment variables.
|
||||
#
|
||||
# Variable precedence is: hosts > defaults > environment
|
||||
|
||||
#defaults:
|
||||
# host: unix:///var/run/docker.sock
|
||||
# private_ssh_port: 22
|
||||
# default_ip: 127.0.0.1
|
||||
|
||||
#hosts:
|
||||
---
|
||||
defaults:
|
||||
host: unix:///var/run/docker.sock
|
||||
version: 1.9
|
||||
timeout: 60
|
||||
private_ssh_port: 22
|
||||
default_ip: 127.0.0.1
|
||||
hosts:
|
||||
# - host: tcp://10.45.5.16:4243
|
||||
# version: 1.9
|
||||
# timeout: 60
|
||||
# private_ssh_port: 2022
|
||||
# default_ip: 172.16.3.45
|
||||
# - host: tcp://localhost:4243
|
||||
# private_ssh_port: 2029
|
||||
|
||||
@ -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:
|
||||
@ -31,41 +29,21 @@ regions_exclude = us-gov-west-1, cn-north-1
|
||||
# in the event of a collision.
|
||||
destination_variable = public_dns_name
|
||||
|
||||
# This allows you to override the inventory_name with an ec2 variable, instead
|
||||
# of using the destination_variable above. Addressing (aka ansible_ssh_host)
|
||||
# will still use destination_variable. Tags should be written as 'tag_TAGNAME'.
|
||||
#hostname_variable = tag_Name
|
||||
|
||||
# For server inside a VPC, using DNS names may not make sense. When an instance
|
||||
# has 'subnet_id' set, this variable is used. If the subnet is public, setting
|
||||
# this to 'ip_address' will return the public IP address. For instances in a
|
||||
# private subnet, this should be set to 'private_ip_address', and Ansible must
|
||||
# be run from within EC2. The key of an EC2 tag may optionally be used; however
|
||||
# the boto instance variables hold precedence in the event of a collision.
|
||||
# WARNING: - instances that are in the private vpc, _without_ public ip address
|
||||
# WARNING: - instances that are in the private vpc, _without_ public ip address
|
||||
# will not be listed in the inventory until You set:
|
||||
# vpc_destination_variable = private_ip_address
|
||||
# vpc_destination_variable = 'private_ip_address'
|
||||
vpc_destination_variable = ip_address
|
||||
|
||||
# The following two settings allow flexible ansible host naming based on a
|
||||
# python format string and a comma-separated list of ec2 tags. Note that:
|
||||
#
|
||||
# 1) If the tags referenced are not present for some instances, empty strings
|
||||
# will be substituted in the format string.
|
||||
# 2) This overrides both destination_variable and vpc_destination_variable.
|
||||
#
|
||||
#destination_format = {0}.{1}.example.com
|
||||
#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,16 +60,13 @@ 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
|
||||
# 'all_rds_instances' to True return all RDS instances regardless of state.
|
||||
all_rds_instances = False
|
||||
|
||||
# Include RDS cluster information (Aurora etc.)
|
||||
include_rds_clusters = False
|
||||
|
||||
# By default, only ElastiCache clusters and nodes in the 'available' state
|
||||
# are returned. Set 'all_elasticache_clusters' and/or 'all_elastic_nodes'
|
||||
# to True return all ElastiCache clusters and nodes, regardless of state.
|
||||
@ -131,11 +106,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
|
||||
@ -161,12 +133,6 @@ group_by_elasticache_replication_group = True
|
||||
# Filters are key/value pairs separated by '=', to list multiple filters use
|
||||
# a list separated by commas. 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
|
||||
|
||||
# Retrieve only instances with (key=value) env=staging tag
|
||||
# instance_filters = tag:env=staging
|
||||
|
||||
@ -178,33 +144,9 @@ stack_filters = False
|
||||
|
||||
# You can use wildcards in filter values also. Below will list instances which
|
||||
# tag Name value matches webservers1*
|
||||
# (ex. webservers15, webservers1a, webservers123 etc)
|
||||
# (ex. webservers15, webservers1a, webservers123 etc)
|
||||
# instance_filters = tag:Name=webservers1*
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
[credentials]
|
||||
|
||||
# The AWS credentials can optionally be specified here. Credentials specified
|
||||
# here are ignored if the environment variable AWS_ACCESS_KEY_ID or
|
||||
# AWS_PROFILE is set, or if the boto_profile property above is set.
|
||||
#
|
||||
# Supplying AWS credentials here is not recommended, as it introduces
|
||||
# non-trivial security concerns. When going down this route, please make sure
|
||||
# to set access permissions for this file correctly, e.g. handle it the same
|
||||
# way as you would a private SSH key.
|
||||
#
|
||||
# Unlike the boto and AWS configure files, this section does not support
|
||||
# profiles.
|
||||
#
|
||||
# aws_access_key_id = AXXXXXXXXXXXXXX
|
||||
# aws_secret_access_key = XXXXXXXXXXXXXXXXXXX
|
||||
# aws_security_token = XXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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,34 +37,11 @@
|
||||
# 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
|
||||
#instance_states = RUNNING,PROVISIONING
|
||||
|
||||
|
||||
[inventory]
|
||||
# The 'inventory_ip_type' parameter specifies whether 'ansible_ssh_host' should
|
||||
# contain the instance internal or external address. Values may be either
|
||||
# 'internal' or 'external'. If 'external' is specified but no external instance
|
||||
# address exists, the internal address will be used.
|
||||
# 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,10 @@ 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
|
||||
Version: 0.0.1
|
||||
'''
|
||||
|
||||
__requires__ = ['pycrypto>=2.6']
|
||||
try:
|
||||
import pkg_resources
|
||||
except ImportError:
|
||||
@ -83,22 +82,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="v1"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
|
||||
from time import time
|
||||
|
||||
if sys.version_info >= (3, 0):
|
||||
import configparser
|
||||
else:
|
||||
import ConfigParser as configparser
|
||||
|
||||
import logging
|
||||
logging.getLogger('libcloud.common.google').addHandler(logging.NullHandler())
|
||||
import ConfigParser
|
||||
|
||||
try:
|
||||
import json
|
||||
@ -110,103 +100,31 @@ try:
|
||||
from libcloud.compute.providers import get_driver
|
||||
_ = Provider.GCE
|
||||
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
|
||||
print("GCE inventory script requires libcloud >= 0.13")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
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()
|
||||
self.driver = self.get_gce_driver()
|
||||
self.ip_type = self.get_inventory_options()
|
||||
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)
|
||||
|
||||
# Otherwise, assume user wants all instances grouped
|
||||
print(self.json_format_dict(self.group_instances(),
|
||||
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
|
||||
not present, the filename defaults to gce.ini in the current
|
||||
working directory.
|
||||
def get_gce_driver(self):
|
||||
"""Determine the GCE authorization settings and return a
|
||||
libcloud driver.
|
||||
"""
|
||||
gce_ini_default_path = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)), "gce.ini")
|
||||
@ -216,69 +134,24 @@ 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)
|
||||
|
||||
#########
|
||||
# Section added for processing ini settings
|
||||
#########
|
||||
|
||||
# Set the instance_states filter based on config file options
|
||||
self.instance_states = []
|
||||
if config.has_option('gce', 'instance_states'):
|
||||
states = config.get('gce', 'instance_states')
|
||||
# Ignore if instance_states is an empty string.
|
||||
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):
|
||||
"""Determine inventory options. Environment variables always
|
||||
take precedence over configuration files."""
|
||||
ip_type = self.config.get('inventory', 'inventory_ip_type')
|
||||
# If the appropriate environment variables are set, they override
|
||||
# other configuration
|
||||
ip_type = os.environ.get('INVENTORY_IP_TYPE', ip_type)
|
||||
return ip_type
|
||||
|
||||
def get_gce_driver(self):
|
||||
"""Determine the GCE authorization settings and return a
|
||||
libcloud driver.
|
||||
"""
|
||||
# Attempt to get GCE params from a configuration file, if one
|
||||
# exists.
|
||||
secrets_path = self.config.get('gce', 'libcloud_secrets')
|
||||
secrets_path = 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
|
||||
@ -287,7 +160,8 @@ class GceInventory(object):
|
||||
if not secrets_path.endswith('secrets.py'):
|
||||
err = "Must specify libcloud secrets file as "
|
||||
err += "/absolute/path/to/secrets.py"
|
||||
sys.exit(err)
|
||||
print(err)
|
||||
sys.exit(1)
|
||||
sys.path.append(os.path.dirname(secrets_path))
|
||||
try:
|
||||
import secrets
|
||||
@ -296,23 +170,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')
|
||||
config.get('gce','gce_service_account_email_address'),
|
||||
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': 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)
|
||||
@ -321,50 +190,31 @@ class GceInventory(object):
|
||||
)
|
||||
return gce
|
||||
|
||||
def parse_env_zones(self):
|
||||
'''returns a list of comma separated 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)
|
||||
zones = [r for r in reader]
|
||||
return [z for z in zones[0]]
|
||||
|
||||
def parse_cli_args(self):
|
||||
''' 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 = {}
|
||||
|
||||
if inst is None:
|
||||
return {}
|
||||
|
||||
if 'items' in inst.extra['metadata']:
|
||||
if inst.extra['metadata'].has_key('items'):
|
||||
for entry in inst.extra['metadata']['items']:
|
||||
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]
|
||||
else:
|
||||
ssh_host = inst.public_ips[0] if len(inst.public_ips) >= 1 else inst.private_ips[0]
|
||||
|
||||
return {
|
||||
'gce_uuid': inst.uuid,
|
||||
'gce_id': inst.id,
|
||||
@ -379,72 +229,31 @@ 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
|
||||
'ansible_ssh_host': inst.public_ips[0] if len(inst.public_ips) >= 1 else inst.private_ips[0]
|
||||
}
|
||||
|
||||
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
|
||||
return None
|
||||
|
||||
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
|
||||
|
||||
def group_instances(self, zones=None):
|
||||
def group_instances(self):
|
||||
'''Group all instances'''
|
||||
groups = {}
|
||||
meta = {}
|
||||
meta["hostvars"] = {}
|
||||
|
||||
for node in self.list_nodes():
|
||||
|
||||
# This check filters on the desired instance states defined in the
|
||||
# config file with the instance_states config option.
|
||||
#
|
||||
# If the instance_states list is _empty_ then _ALL_ states are returned.
|
||||
#
|
||||
# If the instance_states list is _populated_ then check the current
|
||||
# state against the instance_states list
|
||||
if self.instance_states and not node.extra['status'] in self.instance_states:
|
||||
continue
|
||||
|
||||
for node in self.driver.list_nodes():
|
||||
name = node.name
|
||||
|
||||
meta["hostvars"][name] = self.node_to_dict(node)
|
||||
|
||||
zone = node.extra['zone'].name
|
||||
|
||||
# To avoid making multiple requests per zone
|
||||
# we list all nodes and then filter the results
|
||||
if zones and zone not in zones:
|
||||
continue
|
||||
|
||||
if zone in groups:
|
||||
groups[zone].append(name)
|
||||
else:
|
||||
groups[zone] = [name]
|
||||
if groups.has_key(zone): groups[zone].append(name)
|
||||
else: groups[zone] = [name]
|
||||
|
||||
tags = node.extra['tags']
|
||||
for t in tags:
|
||||
@ -452,43 +261,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 groups.has_key(tag): 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 groups.has_key(net): 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 groups.has_key(machine_type): 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 groups.has_key(image): 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 groups.has_key(stat): groups[stat].append(name)
|
||||
else: groups[stat] = [name]
|
||||
|
||||
groups["_meta"] = meta
|
||||
|
||||
@ -503,6 +295,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
|
||||
|
||||
@ -34,4 +34,4 @@ if len(sys.argv) == 2 and sys.argv[1] == '--list':
|
||||
elif len(sys.argv) == 3 and sys.argv[1] == '--host':
|
||||
print(json.dumps({'ansible_connection': 'jail'}))
|
||||
else:
|
||||
sys.stderr.write("Need an argument, either --list or --host <host>\n")
|
||||
print("Need an argument, either --list or --host <host>")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -34,4 +34,4 @@ if len(sys.argv) == 2 and sys.argv[1] == '--list':
|
||||
elif len(sys.argv) == 3 and sys.argv[1] == '--host':
|
||||
print(json.dumps({'ansible_connection': 'libvirt_lxc'}))
|
||||
else:
|
||||
sys.stderr.write("Need an argument, either --list or --host <host>\n")
|
||||
print("Need an argument, either --list or --host <host>")
|
||||
|
||||
@ -13,6 +13,3 @@ cache_path = /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.
|
||||
cache_max_age = 300
|
||||
|
||||
# If set to true use the hosts public ip in the dictionary instead of the label
|
||||
use_public_ip = false
|
||||
@ -15,13 +15,9 @@ installed and has a valid config at ~/.chube. If not, run:
|
||||
|
||||
For more details, see: https://github.com/exosite/chube
|
||||
|
||||
NOTE: By default, this script also assumes that the Linodes in your account all have
|
||||
NOTE: This script also assumes that the Linodes in your account all have
|
||||
labels that correspond to hostnames that are in your resolver search path.
|
||||
Your resolver search path resides in /etc/hosts.
|
||||
Optionally, if you would like to use the hosts public IP instead of it's label use
|
||||
the following setting in linode.ini:
|
||||
|
||||
use_public_ip = true
|
||||
|
||||
When run against a specific host, this script returns the following variables:
|
||||
|
||||
@ -113,15 +109,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 +134,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)
|
||||
@ -169,17 +161,16 @@ class LinodeInventory(object):
|
||||
self.cache_path_cache = cache_path + "/ansible-linode.cache"
|
||||
self.cache_path_index = cache_path + "/ansible-linode.index"
|
||||
self.cache_max_age = config.getint('linode', 'cache_max_age')
|
||||
self.use_public_ip = config.getboolean('linode', 'use_public_ip')
|
||||
|
||||
def parse_cli_args(self):
|
||||
"""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):
|
||||
@ -194,14 +185,20 @@ class LinodeInventory(object):
|
||||
for node in Linode.search(status=Linode.STATUS_RUNNING):
|
||||
self.add_node(node)
|
||||
except chube_api.linode_api.ApiError as e:
|
||||
sys.exit("Looks like Linode's API is down:\n %s" % e)
|
||||
print("Looks like Linode's API is down:")
|
||||
print("")
|
||||
print(e)
|
||||
sys.exit(1)
|
||||
|
||||
def get_node(self, linode_id):
|
||||
"""Gets details about a specific node."""
|
||||
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)
|
||||
print("Looks like Linode's API is down:")
|
||||
print("")
|
||||
print(e)
|
||||
sys.exit(1)
|
||||
|
||||
def populate_datacenter_cache(self):
|
||||
"""Creates self._datacenter_cache, containing all Datacenters indexed by ID."""
|
||||
@ -221,10 +218,8 @@ class LinodeInventory(object):
|
||||
|
||||
def add_node(self, node):
|
||||
"""Adds an node to the inventory and index."""
|
||||
if self.use_public_ip:
|
||||
dest = self.get_node_public_ip(node)
|
||||
else:
|
||||
dest = node.label
|
||||
|
||||
dest = node.label
|
||||
|
||||
# Add to index
|
||||
self.index[dest] = node.api_id
|
||||
@ -235,19 +230,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]
|
||||
|
||||
def get_host_info(self):
|
||||
"""Get variables about a specific host."""
|
||||
|
||||
@ -255,19 +240,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",
|
||||
@ -296,24 +278,19 @@ class LinodeInventory(object):
|
||||
node_vars[direct_attr] = getattr(node, direct_attr)
|
||||
|
||||
node_vars["datacenter_city"] = self.get_datacenter_city(node)
|
||||
node_vars["public_ip"] = self.get_node_public_ip(node)
|
||||
|
||||
# Set the SSH host information, so these inventory items can be used if
|
||||
# their labels aren't FQDNs
|
||||
node_vars['ansible_ssh_host'] = node_vars["public_ip"]
|
||||
node_vars['ansible_host'] = node_vars["public_ip"]
|
||||
node_vars["public_ip"] = [addr.address for addr in node.ipaddresses if addr.is_public][0]
|
||||
|
||||
private_ips = [addr.address for addr in node.ipaddresses if not addr.is_public]
|
||||
|
||||
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]
|
||||
|
||||
|
||||
@ -1,69 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# (c) 2015-16 Florian Haas, hastexo Professional Services GmbH
|
||||
# <florian@hastexo.com>
|
||||
# Based in part on:
|
||||
# libvirt_lxc.py, (c) 2013, Michael Scherer <misc@zarb.org>
|
||||
#
|
||||
# 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/>.
|
||||
"""
|
||||
Ansible inventory script for LXC containers. Requires Python
|
||||
bindings for LXC API.
|
||||
|
||||
In LXC, containers can be grouped by setting the lxc.group option,
|
||||
which may be found more than once in a container's
|
||||
configuration. So, we enumerate all containers, fetch their list
|
||||
of groups, and then build the dictionary in the way Ansible expects
|
||||
it.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
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
|
||||
"all" group."""
|
||||
# Enumerate all containers, and list the groups they are in. Also,
|
||||
# implicitly add every container to the 'all' group.
|
||||
containers = dict([(c,
|
||||
['all'] +
|
||||
(lxc.Container(c).get_config_item('lxc.group') or []))
|
||||
for c in lxc.list_containers()])
|
||||
|
||||
# Extract the groups, flatten the list, and remove duplicates
|
||||
groups = set(sum([g for g in containers.values()], []))
|
||||
|
||||
# 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])
|
||||
|
||||
|
||||
def main(argv):
|
||||
"""Returns a JSON dictionary as expected by Ansible"""
|
||||
result = build_dict()
|
||||
if len(argv) == 2 and argv[1] == '--list':
|
||||
json.dump(result, sys.stdout)
|
||||
elif len(argv) == 3 and argv[1] == '--host':
|
||||
json.dump({'ansible_connection': 'lxc'}, sys.stdout)
|
||||
else:
|
||||
print("Need an argument, either --list or --host <host>", file=sys.stderr)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
@ -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):
|
||||
|
||||
@ -26,7 +26,7 @@ import re
|
||||
import os
|
||||
import ConfigParser
|
||||
from novaclient import client as nova_client
|
||||
from six import iteritems, itervalues
|
||||
from six import iteritems
|
||||
|
||||
try:
|
||||
import json
|
||||
@ -105,7 +105,7 @@ def get_ips(server, access_ip=True):
|
||||
# Iterate through each servers network(s), get addresses and get type
|
||||
addresses = getattr(server, 'addresses', {})
|
||||
if len(addresses) > 0:
|
||||
for network in itervalues(addresses):
|
||||
for network in addresses.itervalues():
|
||||
for address in network:
|
||||
if address.get('OS-EXT-IPS:type', False) == 'fixed':
|
||||
private.append(address['addr'])
|
||||
@ -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
|
||||
@ -226,4 +226,5 @@ elif len(sys.argv) == 3 and (sys.argv[1] == '--host'):
|
||||
sys.exit(0)
|
||||
|
||||
else:
|
||||
sys.exit("usage: --list ..OR.. --host <hostname>")
|
||||
print("usage: --list ..OR.. --host <hostname>")
|
||||
sys.exit(1)
|
||||
|
||||
14
contrib/inventory/nsot.py
Executable file → Normal file
14
contrib/inventory/nsot.py
Executable file → Normal file
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/bin/env python
|
||||
|
||||
'''
|
||||
nsot
|
||||
@ -148,11 +148,9 @@ from pynsot.client import get_api_client
|
||||
from pynsot.app import HttpServerError
|
||||
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):
|
||||
@ -202,7 +200,7 @@ class NSoTInventory(object):
|
||||
_inventory_group()
|
||||
'''
|
||||
inventory = dict()
|
||||
for group, contents in self.config.items():
|
||||
for group, contents in self.config.iteritems():
|
||||
group_response = self._inventory_group(group, contents)
|
||||
inventory.update(group_response)
|
||||
inventory.update({'_meta': self._meta})
|
||||
@ -217,7 +215,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']})
|
||||
@ -253,7 +251,7 @@ class NSoTInventory(object):
|
||||
obj[group]['hosts'] = []
|
||||
obj[group]['vars'] = hostvars
|
||||
try:
|
||||
assert isinstance(query, string_types)
|
||||
assert isinstance(query, basestring)
|
||||
except:
|
||||
sys.exit('ERR: Group queries must be a single string\n'
|
||||
' Group: %s\n'
|
||||
@ -278,7 +276,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
|
||||
|
||||
@ -62,7 +62,8 @@ def get_config(env_var, config_var):
|
||||
if not result:
|
||||
result = get_from_rhc_config(config_var)
|
||||
if not result:
|
||||
sys.exit("failed=True msg='missing %s'" % env_var)
|
||||
print("failed=True msg='missing %s'" % env_var)
|
||||
sys.exit(1)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
# Copyright (c) 2012, Marco Vito Moscaritolo <marco@agavee.com>
|
||||
# Copyright (c) 2013, Jesse Keating <jesse.keating@rackspace.com>
|
||||
# Copyright (c) 2015, Hewlett-Packard Development Company, L.P.
|
||||
# Copyright (c) 2016, Rackspace Australia
|
||||
#
|
||||
# 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
|
||||
@ -19,7 +18,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 +28,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
|
||||
@ -43,20 +39,12 @@
|
||||
# use_hostnames changes the behavior from registering every host with its UUID
|
||||
# and making a group of its hostname to only doing this if the
|
||||
# hostname in question has more than one server
|
||||
# fail_on_errors causes the inventory to fail and return no hosts if one cloud
|
||||
# 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
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from distutils.version import StrictVersion
|
||||
|
||||
try:
|
||||
import json
|
||||
@ -67,7 +55,7 @@ import os_client_config
|
||||
import shade
|
||||
import shade.inventory
|
||||
|
||||
CONFIG_FILES = ['/etc/ansible/openstack.yaml', '/etc/ansible/openstack.yml']
|
||||
CONFIG_FILES = ['/etc/ansible/openstack.yaml']
|
||||
|
||||
|
||||
def get_groups_from_server(server_vars, namegroup=True):
|
||||
@ -92,7 +80,7 @@ def get_groups_from_server(server_vars, namegroup=True):
|
||||
|
||||
for extra_group in metadata.get('groups', '').split(','):
|
||||
if extra_group:
|
||||
groups.append(extra_group.strip())
|
||||
groups.append(extra_group)
|
||||
|
||||
groups.append('instance-%s' % server_vars['id'])
|
||||
if namegroup:
|
||||
@ -114,8 +102,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)
|
||||
@ -124,20 +112,6 @@ def get_host_groups(inventory, refresh=False, cloud=None):
|
||||
return groups
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def get_host_groups_from_cloud(inventory):
|
||||
groups = collections.defaultdict(list)
|
||||
firstpass = collections.defaultdict(list)
|
||||
@ -146,9 +120,6 @@ def get_host_groups_from_cloud(inventory):
|
||||
if hasattr(inventory, 'extra_config'):
|
||||
use_hostnames = inventory.extra_config['use_hostnames']
|
||||
list_args['expand'] = inventory.extra_config['expand_hostvars']
|
||||
if StrictVersion(shade.__version__) >= StrictVersion("1.6.0"):
|
||||
list_args['fail_on_cloud_config'] = \
|
||||
inventory.extra_config['fail_on_errors']
|
||||
else:
|
||||
use_hostnames = False
|
||||
|
||||
@ -159,19 +130,20 @@ def get_host_groups_from_cloud(inventory):
|
||||
firstpass[server['name']].append(server)
|
||||
for name, servers in firstpass.items():
|
||||
if len(servers) == 1 and use_hostnames:
|
||||
append_hostvars(hostvars, groups, name, servers[0])
|
||||
server = servers[0]
|
||||
hostvars[name] = dict(
|
||||
ansible_ssh_host=server['interface_ip'],
|
||||
openstack=server)
|
||||
for group in get_groups_from_server(server, namegroup=False):
|
||||
groups[group].append(server['name'])
|
||||
else:
|
||||
server_ids = set()
|
||||
# Trap for duplicate results
|
||||
for server in servers:
|
||||
server_ids.add(server['id'])
|
||||
if len(server_ids) == 1 and use_hostnames:
|
||||
append_hostvars(hostvars, groups, name, servers[0])
|
||||
else:
|
||||
for server in servers:
|
||||
append_hostvars(
|
||||
hostvars, groups, server['id'], server,
|
||||
namegroup=True)
|
||||
server_id = server['id']
|
||||
hostvars[server_id] = dict(
|
||||
ansible_ssh_host=server['interface_ip'],
|
||||
openstack=server)
|
||||
for group in get_groups_from_server(server, namegroup=True):
|
||||
groups[group].append(server_id)
|
||||
groups['_meta'] = {'hostvars': hostvars}
|
||||
return groups
|
||||
|
||||
@ -180,7 +152,7 @@ def is_cache_stale(cache_file, cache_expiration_time, refresh=False):
|
||||
''' Determines if cache file has expired, or if it is still valid '''
|
||||
if refresh:
|
||||
return True
|
||||
if os.path.isfile(cache_file) and os.path.getsize(cache_file) > 0:
|
||||
if os.path.isfile(cache_file):
|
||||
mod_time = os.path.getmtime(cache_file)
|
||||
current_time = time.time()
|
||||
if (mod_time + cache_expiration_time) > current_time:
|
||||
@ -188,14 +160,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 +178,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 +202,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(
|
||||
@ -242,14 +209,13 @@ def main():
|
||||
config_defaults={
|
||||
'use_hostnames': False,
|
||||
'expand_hostvars': True,
|
||||
'fail_on_errors': True,
|
||||
}
|
||||
))
|
||||
|
||||
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)
|
||||
|
||||
@ -29,4 +29,3 @@ clouds:
|
||||
ansible:
|
||||
use_hostnames: True
|
||||
expand_hostvars: False
|
||||
fail_on_errors: True
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -172,9 +172,9 @@ class OVirtInventory(object):
|
||||
|
||||
# If the appropriate environment variables are set, they override
|
||||
# other configuration; process those into our args and kwargs.
|
||||
kwargs['url'] = os.environ.get('OVIRT_URL', kwargs['url'])
|
||||
kwargs['username'] = next(val for val in [os.environ.get('OVIRT_EMAIL'), os.environ.get('OVIRT_USERNAME'), kwargs['username']] if val is not None)
|
||||
kwargs['password'] = next(val for val in [os.environ.get('OVIRT_PASS'), os.environ.get('OVIRT_PASSWORD'), kwargs['password']] if val is not None)
|
||||
kwargs['url'] = os.environ.get('OVIRT_URL')
|
||||
kwargs['username'] = os.environ.get('OVIRT_EMAIL')
|
||||
kwargs['password'] = os.environ.get('OVIRT_PASS')
|
||||
|
||||
# Retrieve and return the ovirt driver.
|
||||
return API(insecure=True, **kwargs)
|
||||
@ -211,7 +211,7 @@ class OVirtInventory(object):
|
||||
'ovirt_uuid': inst.get_id(),
|
||||
'ovirt_id': inst.get_id(),
|
||||
'ovirt_image': inst.get_os().get_type(),
|
||||
'ovirt_machine_type': self.get_machine_type(inst),
|
||||
'ovirt_machine_type': inst.get_instance_type(),
|
||||
'ovirt_ips': ips,
|
||||
'ovirt_name': inst.get_name(),
|
||||
'ovirt_description': inst.get_description(),
|
||||
@ -230,11 +230,6 @@ class OVirtInventory(object):
|
||||
"""
|
||||
return [x.get_name() for x in inst.get_tags().list()]
|
||||
|
||||
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
|
||||
|
||||
# noinspection PyBroadException,PyUnusedLocal
|
||||
def get_instance(self, instance_name):
|
||||
"""Gets details about a specific instance """
|
||||
|
||||
@ -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()
|
||||
@ -15,16 +15,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Updated 2016 by Matt Harris <matthaeus.harris@gmail.com>
|
||||
#
|
||||
# Added support for Proxmox VE 4.x
|
||||
# Added support for using the Notes field of a VM to define groups and variables:
|
||||
# A well-formatted JSON object in the Notes field will be added to the _meta
|
||||
# section for that VM. In addition, the "groups" key of this JSON object may be
|
||||
# used to specify group membership:
|
||||
#
|
||||
# { "groups": ["utility", "databases"], "a": false, "b": true }
|
||||
|
||||
import urllib
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
@ -34,54 +25,47 @@ 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):
|
||||
class ProxmoxQemu(dict):
|
||||
def get_variables(self):
|
||||
variables = {}
|
||||
for key, value in iteritems(self):
|
||||
variables['proxmox_' + key] = value
|
||||
return variables
|
||||
|
||||
|
||||
class ProxmoxVMList(list):
|
||||
class ProxmoxQemuList(list):
|
||||
def __init__(self, data=[]):
|
||||
for item in data:
|
||||
self.append(ProxmoxVM(item))
|
||||
self.append(ProxmoxQemu(item))
|
||||
|
||||
def get_names(self):
|
||||
return [vm['name'] for vm in self if vm['template'] != 1]
|
||||
return [qemu['name'] for qemu in self if qemu['template'] != 1]
|
||||
|
||||
def get_by_name(self, name):
|
||||
results = [vm for vm in self if vm['name'] == name]
|
||||
results = [qemu for qemu in self if qemu['name'] == name]
|
||||
return results[0] if len(results) > 0 else None
|
||||
|
||||
def get_variables(self):
|
||||
variables = {}
|
||||
for vm in self:
|
||||
variables[vm['name']] = vm.get_variables()
|
||||
for qemu in self:
|
||||
variables[qemu['name']] = qemu.get_variables()
|
||||
|
||||
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
|
||||
@ -97,7 +81,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,
|
||||
})
|
||||
@ -121,23 +105,8 @@ class ProxmoxAPI(object):
|
||||
def nodes(self):
|
||||
return ProxmoxNodeList(self.get('api2/json/nodes'))
|
||||
|
||||
def vms_by_type(self, node, type):
|
||||
return ProxmoxVMList(self.get('api2/json/nodes/{}/{}'.format(node, type)))
|
||||
|
||||
def vm_description_by_type(self, node, vm, type):
|
||||
return self.get('api2/json/nodes/{}/{}/{}/config'.format(node, type, vm))
|
||||
|
||||
def node_qemu(self, node):
|
||||
return self.vms_by_type(node, 'qemu')
|
||||
|
||||
def node_qemu_description(self, node, vm):
|
||||
return self.vm_description_by_type(node, vm, 'qemu')
|
||||
|
||||
def node_lxc(self, node):
|
||||
return self.vms_by_type(node, 'lxc')
|
||||
|
||||
def node_lxc_description(self, node, vm):
|
||||
return self.vm_description_by_type(node, vm, 'lxc')
|
||||
return ProxmoxQemuList(self.get('api2/json/nodes/{}/qemu'.format(node)))
|
||||
|
||||
def pools(self):
|
||||
return ProxmoxPoolList(self.get('api2/json/pools'))
|
||||
@ -145,7 +114,6 @@ class ProxmoxAPI(object):
|
||||
def pool(self, poolid):
|
||||
return ProxmoxPool(self.get('api2/json/pools/{}'.format(poolid)))
|
||||
|
||||
|
||||
def main_list(options):
|
||||
results = {
|
||||
'all': {
|
||||
@ -163,40 +131,6 @@ def main_list(options):
|
||||
qemu_list = proxmox_api.node_qemu(node)
|
||||
results['all']['hosts'] += qemu_list.get_names()
|
||||
results['_meta']['hostvars'].update(qemu_list.get_variables())
|
||||
lxc_list = proxmox_api.node_lxc(node)
|
||||
results['all']['hosts'] += lxc_list.get_names()
|
||||
results['_meta']['hostvars'].update(lxc_list.get_variables())
|
||||
|
||||
for vm in results['_meta']['hostvars']:
|
||||
vmid = results['_meta']['hostvars'][vm]['proxmox_vmid']
|
||||
try:
|
||||
type = results['_meta']['hostvars'][vm]['proxmox_type']
|
||||
except KeyError:
|
||||
type = 'qemu'
|
||||
try:
|
||||
description = proxmox_api.vm_description_by_type(node, vmid, type)['description']
|
||||
except KeyError:
|
||||
description = None
|
||||
|
||||
try:
|
||||
metadata = json.loads(description)
|
||||
except TypeError:
|
||||
metadata = {}
|
||||
except ValueError:
|
||||
metadata = {
|
||||
'notes': description
|
||||
}
|
||||
|
||||
if 'groups' in metadata:
|
||||
# print metadata
|
||||
for group in metadata['groups']:
|
||||
if group not in results:
|
||||
results[group] = {
|
||||
'hosts': []
|
||||
}
|
||||
results[group]['hosts'] += [vm]
|
||||
|
||||
results['_meta']['hostvars'][vm].update(metadata)
|
||||
|
||||
# pools
|
||||
for pool in proxmox_api.pools().get_names():
|
||||
@ -206,7 +140,6 @@ def main_list(options):
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main_host(options):
|
||||
proxmox_api = ProxmoxAPI(options)
|
||||
proxmox_api.auth()
|
||||
@ -219,7 +152,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")
|
||||
@ -244,6 +176,5 @@ def main():
|
||||
|
||||
print(json.dumps(data, indent=indent))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@ -1,96 +0,0 @@
|
||||
#!/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/>.
|
||||
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
import argparse
|
||||
|
||||
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))
|
||||
print(json.dumps(inventory))
|
||||
|
||||
def _load_inventory_data(self, nodeid):
|
||||
info = {}
|
||||
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)
|
||||
results[key] = r.text
|
||||
self._inventory[nodeid] = results
|
||||
|
||||
def _format_output(self, nodeid, info):
|
||||
try:
|
||||
node_info = json.loads(info['lookup'])
|
||||
ipaddress = ''
|
||||
if len(node_info) > 0:
|
||||
ipaddress = node_info[0]['ipAddress']
|
||||
output = {'hosts': [ipaddress], 'vars': {}}
|
||||
for (key, result) in info.items():
|
||||
output['vars'][key] = json.loads(result)
|
||||
output['vars']['ansible_ssh_user'] = 'monorail'
|
||||
except KeyError:
|
||||
pass
|
||||
return output
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--host')
|
||||
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
|
||||
RACKHD_URL = 'http://' + str(os.environ['RACKHD_URL'])
|
||||
except:
|
||||
# use default values
|
||||
pass
|
||||
|
||||
# Use the nodeid specified in the environment to limit the data returned
|
||||
# or return data for all available nodes
|
||||
nodeids = []
|
||||
|
||||
if (parse_args().host):
|
||||
try:
|
||||
nodeids += parse_args().host.split(',')
|
||||
RackhdInventory(nodeids)
|
||||
except:
|
||||
pass
|
||||
if (parse_args().list):
|
||||
try:
|
||||
url = RACKHD_URL + '/api/common/nodes'
|
||||
r = requests.get(url, verify=False)
|
||||
data = json.loads(r.text)
|
||||
for entry in data:
|
||||
if entry['type'] == 'compute':
|
||||
nodeids.append(entry['id'])
|
||||
RackhdInventory(nodeids)
|
||||
except:
|
||||
pass
|
||||
@ -55,12 +55,3 @@
|
||||
# will be ignored, and 4 will be used. Accepts a comma separated list,
|
||||
# the first found wins.
|
||||
# access_ip_version = 4
|
||||
|
||||
# Environment Variable: RAX_CACHE_MAX_AGE
|
||||
# Default: 600
|
||||
#
|
||||
# A configuration the changes the behavior or the inventory cache.
|
||||
# Inventory listing performed before this value will be returned from
|
||||
# the cache instead of making a full request for all inventory. Setting
|
||||
# this value to 0 will force a full request.
|
||||
# cache_max_age = 600
|
||||
@ -155,6 +155,8 @@ import ConfigParser
|
||||
|
||||
from six import iteritems
|
||||
|
||||
from ansible.constants import get_config, mk_boolean
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
@ -164,15 +166,13 @@ try:
|
||||
import pyrax
|
||||
from pyrax.utils import slugify
|
||||
except ImportError:
|
||||
sys.exit('pyrax is required for this module')
|
||||
print('pyrax is required for this module')
|
||||
sys.exit(1)
|
||||
|
||||
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,21 +227,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
|
||||
networks = get_config(p, 'rax', 'access_network',
|
||||
'RAX_ACCESS_NETWORK', 'public', islist=True)
|
||||
try:
|
||||
try:
|
||||
ip_versions = map(int, get_config(p, 'rax', 'access_ip_version',
|
||||
'RAX_ACCESS_IP_VERSION', 4, value_type='list'))
|
||||
except TypeError:
|
||||
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:
|
||||
@ -289,7 +280,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']
|
||||
@ -364,12 +355,9 @@ def get_cache_file_path(regions):
|
||||
|
||||
|
||||
def _list(regions, refresh_cache=True):
|
||||
cache_max_age = int(get_config(p, 'rax', 'cache_max_age',
|
||||
'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]) > 600):
|
||||
# Cache file doesn't exist or older than 10m or refresh cache requested
|
||||
_list_into_cache(regions)
|
||||
|
||||
@ -411,9 +399,10 @@ def setup():
|
||||
if os.path.isfile(default_creds_file):
|
||||
creds_file = default_creds_file
|
||||
elif not keyring_username:
|
||||
sys.exit('No value in environment variable %s and/or no '
|
||||
'credentials file at %s'
|
||||
% ('RAX_CREDS_FILE', default_creds_file))
|
||||
sys.stderr.write('No value in environment variable %s and/or no '
|
||||
'credentials file at %s\n'
|
||||
% ('RAX_CREDS_FILE', default_creds_file))
|
||||
sys.exit(1)
|
||||
|
||||
identity_type = pyrax.get_setting('identity_type')
|
||||
pyrax.set_setting('identity_type', identity_type or 'rackspace')
|
||||
@ -426,28 +415,23 @@ def setup():
|
||||
else:
|
||||
pyrax.set_credential_file(creds_file, region=region)
|
||||
except Exception as e:
|
||||
sys.exit("%s: %s" % (e, e.message))
|
||||
sys.stderr.write("%s: %s\n" % (e, e.message))
|
||||
sys.exit(1)
|
||||
|
||||
regions = []
|
||||
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
|
||||
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':
|
||||
regions = pyrax.regions
|
||||
break
|
||||
elif region not in pyrax.regions:
|
||||
sys.exit('Unsupported region %s' % region)
|
||||
sys.stderr.write('Unsupported region %s' % region)
|
||||
sys.exit(1)
|
||||
elif region not in regions:
|
||||
regions.append(region)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -56,8 +56,12 @@ import argparse
|
||||
import six
|
||||
import httplib2 as http
|
||||
from time import time
|
||||
from ansible.module_utils.six.moves import configparser
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlparse
|
||||
from six.moves import configparser
|
||||
|
||||
try:
|
||||
from urlparse import urlparse
|
||||
except ImportError:
|
||||
from urllib.parse import urlparse
|
||||
|
||||
try:
|
||||
import json
|
||||
|
||||
@ -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)
|
||||
@ -73,11 +70,10 @@ if not os.path.exists(SW_REPORT):
|
||||
# Pre-startup work
|
||||
if not os.path.exists(CACHE_DIR):
|
||||
os.mkdir(CACHE_DIR)
|
||||
os.chmod(CACHE_DIR, 0o2775)
|
||||
os.chmod(CACHE_DIR, 2775)
|
||||
|
||||
# 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
|
||||
|
||||
@ -27,10 +27,6 @@ guests_only = True
|
||||
# not be returned.
|
||||
# prefix_filter = test_
|
||||
|
||||
# Specify a cluster filter list (colon delimited). Only clusters matching by
|
||||
# name will be scanned for virtualmachines
|
||||
#clusters = cluster1,cluster2
|
||||
|
||||
[auth]
|
||||
|
||||
# Specify hostname or IP address of vCenter/ESXi server. A port may be
|
||||
@ -45,6 +41,3 @@ user = ihasaccess
|
||||
# Specify a password to access the vCenter host. This setting may also be
|
||||
# defined with the VMWARE_PASSWORD environment variable.
|
||||
password = ssshverysecret
|
||||
|
||||
# Force SSL certificate checking by default or ignore self-signed certs.
|
||||
#sslcheck=True
|
||||
|
||||
@ -35,40 +35,37 @@ import json
|
||||
import logging
|
||||
import optparse
|
||||
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
|
||||
|
||||
# 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())
|
||||
|
||||
from psphere.client import Client
|
||||
from psphere.errors import ObjectNotFoundError
|
||||
from psphere.managedobjects import HostSystem, VirtualMachine, ManagedObject, Network, ClusterComputeResource
|
||||
from psphere.managedobjects import HostSystem, VirtualMachine, ManagedObject, Network
|
||||
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)
|
||||
@ -93,28 +90,6 @@ class VMwareInventory(object):
|
||||
auth_password = os.environ.get('VMWARE_PASSWORD')
|
||||
if not auth_password and self.config.has_option('auth', 'password'):
|
||||
auth_password = self.config.get('auth', 'password')
|
||||
sslcheck = os.environ.get('VMWARE_SSLCHECK')
|
||||
if not sslcheck and self.config.has_option('auth', 'sslcheck'):
|
||||
sslcheck = self.config.get('auth', 'sslcheck')
|
||||
if not sslcheck:
|
||||
sslcheck = True
|
||||
else:
|
||||
if sslcheck.lower() in ['no', 'false']:
|
||||
sslcheck = False
|
||||
else:
|
||||
sslcheck = True
|
||||
|
||||
# Limit the clusters being scanned
|
||||
self.filter_clusters = os.environ.get('VMWARE_CLUSTERS')
|
||||
if not self.filter_clusters and self.config.has_option('defaults', 'clusters'):
|
||||
self.filter_clusters = self.config.get('defaults', 'clusters')
|
||||
if self.filter_clusters:
|
||||
self.filter_clusters = [x.strip() for x in self.filter_clusters.split(',') if x.strip()]
|
||||
|
||||
# Override certificate checks
|
||||
if not sslcheck:
|
||||
if hasattr(ssl, '_create_unverified_context'):
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
|
||||
# Create the VMware client connection.
|
||||
self.client = Client(auth_host, auth_user, auth_password)
|
||||
@ -162,7 +137,7 @@ class VMwareInventory(object):
|
||||
if isinstance(v, collections.MutableMapping):
|
||||
items.extend(self._flatten_dict(v, new_key, sep).items())
|
||||
elif isinstance(v, (list, tuple)):
|
||||
if all([isinstance(x, string_types) for x in v]):
|
||||
if all([isinstance(x, basestring) for x in v]):
|
||||
items.append((new_key, v))
|
||||
else:
|
||||
items.append((new_key, v))
|
||||
@ -210,7 +185,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, basestring)):
|
||||
return obj
|
||||
else:
|
||||
return ()
|
||||
@ -339,19 +314,8 @@ class VMwareInventory(object):
|
||||
else:
|
||||
prefix_filter = None
|
||||
|
||||
if self.filter_clusters:
|
||||
# Loop through clusters and find hosts:
|
||||
hosts = []
|
||||
for cluster in ClusterComputeResource.all(self.client):
|
||||
if cluster.name in self.filter_clusters:
|
||||
for host in cluster.host:
|
||||
hosts.append(host)
|
||||
else:
|
||||
# Get list of all physical hosts
|
||||
hosts = HostSystem.all(self.client)
|
||||
|
||||
# Loop through physical hosts:
|
||||
for host in hosts:
|
||||
for host in HostSystem.all(self.client):
|
||||
|
||||
if not self.guests_only:
|
||||
self._add_host(inv, 'all', host.name)
|
||||
@ -364,7 +328,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)
|
||||
|
||||
@ -1,113 +0,0 @@
|
||||
# Ansible VMware external inventory script settings
|
||||
|
||||
[vmware]
|
||||
|
||||
# The resolvable hostname or ip address of the vsphere
|
||||
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.
|
||||
username=administrator@vsphere.local
|
||||
|
||||
# The password for the vsphere API. This setting
|
||||
# may also be defined via the VMWARE_PASSWORD environment variable.
|
||||
password=vmware
|
||||
|
||||
# Verify the server's SSL certificate
|
||||
#validate_certs = True
|
||||
|
||||
# Specify the number of seconds to use the inventory cache before it is
|
||||
# considered stale. If not defined, defaults to 0 seconds.
|
||||
#cache_max_age = 3600
|
||||
|
||||
|
||||
# Specify the directory used for storing the inventory cache. If not defined,
|
||||
# caching will be disabled.
|
||||
#cache_path = ~/.cache/ansible
|
||||
|
||||
|
||||
# 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.
|
||||
# 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.
|
||||
#max_object_level=1
|
||||
|
||||
|
||||
# Lower the keynames for facts to make addressing them easier.
|
||||
#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
|
||||
# expects that the ansible_host will be set by the host_pattern.
|
||||
#alias_pattern={{ config.name + '_' + config.uuid }}
|
||||
|
||||
|
||||
# Host pattern is the value set for ansible_host and ansible_ssh_host, which
|
||||
# needs to be a hostname or ipaddress the ansible controlhost can reach.
|
||||
#host_pattern={{ guest.ipaddress }}
|
||||
|
||||
|
||||
# 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' }}
|
||||
# host_filters={{ config.cpuhotremoveenabled != False }},{{ runtime.maxmemoryusage >= 512 }}
|
||||
# host_filters={{ config.cpuhotremoveenabled != False }},{{ runtime.maxmemoryusage >= 512 }}
|
||||
# The default is only gueststate of 'running'
|
||||
#host_filters={{ guest.gueststate == "running" }}
|
||||
|
||||
|
||||
# Groupby patterns enable the user to create groups via any possible jinja
|
||||
# expression. The resulting value will the groupname and the host will be added
|
||||
# to that group. Be careful to not make expressions that simply return True/False
|
||||
# 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,771 +0,0 @@
|
||||
#!/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
|
||||
|
||||
"""
|
||||
$ jq '._meta.hostvars[].config' data.json | head
|
||||
{
|
||||
"alternateguestname": "",
|
||||
"instanceuuid": "5035a5cd-b8e8-d717-e133-2d383eb0d675",
|
||||
"memoryhotaddenabled": false,
|
||||
"guestfullname": "Red Hat Enterprise Linux 7 (64-bit)",
|
||||
"changeversion": "2016-05-16T18:43:14.977925Z",
|
||||
"uuid": "4235fc97-5ddb-7a17-193b-9a3ac97dc7b4",
|
||||
"cpuhotremoveenabled": false,
|
||||
"vpmcenabled": false,
|
||||
"firmware": "bios",
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import atexit
|
||||
import datetime
|
||||
import getpass
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import ssl
|
||||
import sys
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from time import time
|
||||
|
||||
import six
|
||||
from jinja2 import Environment
|
||||
from six import integer_types, string_types
|
||||
from six.moves import configparser
|
||||
|
||||
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 pyVim.connect import SmartConnect, Disconnect
|
||||
except ImportError:
|
||||
sys.exit("ERROR: This inventory script required 'pyVmomi' Python module, it was not able to load it")
|
||||
|
||||
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
|
||||
|
||||
|
||||
class VMwareMissingHostException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class VMWareInventory(object):
|
||||
__name__ = 'VMWareInventory'
|
||||
|
||||
guest_props = False
|
||||
instances = []
|
||||
debug = False
|
||||
load_dumpfile = None
|
||||
write_dumpfile = None
|
||||
maxlevel = 1
|
||||
lowerkeys = True
|
||||
config = None
|
||||
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']
|
||||
|
||||
vimTableMaxDepth = {
|
||||
"vim.HostSystem": 2,
|
||||
"vim.VirtualMachine": 2,
|
||||
}
|
||||
|
||||
custom_fields = {}
|
||||
|
||||
# use jinja environments to allow for custom filters
|
||||
env = Environment()
|
||||
env.filters['regex_match'] = regex_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()
|
||||
|
||||
if load:
|
||||
# Read settings and parse CLI arguments
|
||||
self.parse_cli_args()
|
||||
self.read_settings()
|
||||
|
||||
# Check the cache
|
||||
cache_valid = self.is_cache_valid()
|
||||
|
||||
# Handle Cache
|
||||
if self.args.refresh_cache or not cache_valid:
|
||||
self.do_api_calls_update_cache()
|
||||
else:
|
||||
self.debugl('loading inventory from cache')
|
||||
self.inventory = self.get_inventory_from_cache()
|
||||
|
||||
def debugl(self, text):
|
||||
if self.args.debug:
|
||||
try:
|
||||
text = str(text)
|
||||
except UnicodeEncodeError:
|
||||
text = text.encode('ascii', 'ignore')
|
||||
print('%s %s' % (datetime.datetime.now(), 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)
|
||||
elif self.args.list:
|
||||
# Display list of instances for inventory
|
||||
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 '''
|
||||
|
||||
valid = False
|
||||
|
||||
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:
|
||||
valid = True
|
||||
|
||||
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)
|
||||
|
||||
def write_to_cache(self, data):
|
||||
|
||||
''' 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 '''
|
||||
|
||||
jdata = None
|
||||
with open(self.cache_path_cache, 'rb') as f:
|
||||
jdata = f.read()
|
||||
return json.loads(jdata)
|
||||
|
||||
def read_settings(self):
|
||||
|
||||
''' Reads the settings from the vmware_inventory.ini file '''
|
||||
|
||||
scriptbasename = __file__
|
||||
scriptbasename = os.path.basename(scriptbasename)
|
||||
scriptbasename = scriptbasename.replace('.py', '')
|
||||
|
||||
defaults = {'vmware': {
|
||||
'server': '',
|
||||
'port': 443,
|
||||
'username': '',
|
||||
'password': '',
|
||||
'validate_certs': True,
|
||||
'ini_path': os.path.join(os.path.dirname(__file__), '%s.ini' % scriptbasename),
|
||||
'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}
|
||||
}
|
||||
|
||||
if six.PY3:
|
||||
config = configparser.ConfigParser()
|
||||
else:
|
||||
config = configparser.SafeConfigParser()
|
||||
|
||||
# where is the config?
|
||||
vmware_ini_path = os.environ.get('VMWARE_INI_PATH', defaults['vmware']['ini_path'])
|
||||
vmware_ini_path = os.path.expanduser(os.path.expandvars(vmware_ini_path))
|
||||
config.read(vmware_ini_path)
|
||||
|
||||
# apply defaults
|
||||
for k, v in defaults['vmware'].items():
|
||||
if not config.has_option('vmware', k):
|
||||
config.set('vmware', k, str(v))
|
||||
|
||||
# where is the cache?
|
||||
self.cache_dir = os.path.expanduser(config.get('vmware', 'cache_path'))
|
||||
if self.cache_dir and not os.path.exists(self.cache_dir):
|
||||
os.makedirs(self.cache_dir)
|
||||
|
||||
# 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)
|
||||
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)
|
||||
|
||||
# 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:
|
||||
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])
|
||||
|
||||
# save the config
|
||||
self.config = config
|
||||
|
||||
def parse_cli_args(self):
|
||||
|
||||
''' Command line argument processing '''
|
||||
|
||||
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')
|
||||
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 VSphere (default: False - use cache files)')
|
||||
parser.add_argument('--max-instances', default=None, type=int,
|
||||
help='maximum number of instances to retrieve')
|
||||
self.args = parser.parse_args()
|
||||
|
||||
def get_instances(self):
|
||||
|
||||
''' Get a list of vm instances with pyvmomi '''
|
||||
kwargs = {'host': self.server,
|
||||
'user': self.username,
|
||||
'pwd': self.password,
|
||||
'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)
|
||||
|
||||
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)
|
||||
|
||||
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')
|
||||
if not si:
|
||||
sys.exit("Could not connect to the specified host using specified "
|
||||
"username and password")
|
||||
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
|
||||
containerView = content.viewManager.CreateContainerView(container, viewType, recursive)
|
||||
children = containerView.view
|
||||
for child in children:
|
||||
# If requested, limit the total number of instances
|
||||
if 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))
|
||||
|
||||
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.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['all'] = {}
|
||||
inventory['all']['hosts'] = []
|
||||
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())
|
||||
idata = instance[1]
|
||||
|
||||
# Put it in the inventory
|
||||
inventory['all']['hosts'].append(thisid)
|
||||
inventory['_meta']['hostvars'][thisid] = idata.copy()
|
||||
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')
|
||||
)
|
||||
|
||||
# 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')
|
||||
)
|
||||
|
||||
# Reset the inventory keys
|
||||
for k, v in name_mapping.items():
|
||||
|
||||
if not host_mapping or k not in host_mapping:
|
||||
continue
|
||||
|
||||
# set ansible_host (2.x)
|
||||
try:
|
||||
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:
|
||||
continue
|
||||
|
||||
if k == v:
|
||||
continue
|
||||
|
||||
# add new key
|
||||
inventory['all']['hosts'].append(v)
|
||||
inventory['_meta']['hostvars'][v] = inventory['_meta']['hostvars'][k]
|
||||
|
||||
# cleanup old key
|
||||
inventory['all']['hosts'].remove(k)
|
||||
inventory['_meta']['hostvars'].pop(k, None)
|
||||
|
||||
self.debugl('pre-filtered hosts:')
|
||||
for i in inventory['all']['hosts']:
|
||||
self.debugl(' * %s' % i)
|
||||
# Apply host filters
|
||||
for hf in self.host_filters:
|
||||
if not hf:
|
||||
continue
|
||||
self.debugl('filter: %s' % hf)
|
||||
filter_map = self.create_template_mapping(inventory, hf, dtype='boolean')
|
||||
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:')
|
||||
for i in inventory['all']['hosts']:
|
||||
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():
|
||||
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)
|
||||
|
||||
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)
|
||||
newkey = None
|
||||
try:
|
||||
newkey = t.render(v)
|
||||
newkey = newkey.strip()
|
||||
except Exception as e:
|
||||
self.debugl(e)
|
||||
if not newkey:
|
||||
continue
|
||||
elif dtype == 'integer':
|
||||
newkey = int(newkey)
|
||||
elif dtype == 'boolean':
|
||||
if newkey.lower() == 'false':
|
||||
newkey = False
|
||||
elif newkey.lower() == 'true':
|
||||
newkey = True
|
||||
elif dtype == 'string':
|
||||
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):
|
||||
|
||||
''' Traverse a VM object and return a json compliant data structure '''
|
||||
|
||||
# pyvmomi objects are not yet serializable, but may be one day ...
|
||||
# https://github.com/vmware/pyvmomi/issues/21
|
||||
|
||||
# WARNING:
|
||||
# Accessing an object attribute will trigger a SOAP call to the remote.
|
||||
# Increasing the attributes collected or the depth of recursion greatly
|
||||
# increases runtime duration and potentially memory+network utilization.
|
||||
|
||||
if level == 0:
|
||||
try:
|
||||
self.debugl("get facts for %s" % vobj.name)
|
||||
except Exception as e:
|
||||
self.debugl(e)
|
||||
|
||||
rdata = {}
|
||||
|
||||
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 = sorted(methods)
|
||||
|
||||
for method in methods:
|
||||
# Attempt to get the method, skip on fail
|
||||
try:
|
||||
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,
|
||||
)
|
||||
|
||||
return rdata
|
||||
|
||||
def _process_object_types(self, vobj, thisvm=None, inkey='', 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
|
||||
else:
|
||||
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):
|
||||
rdata = vobj
|
||||
elif issubclass(type(vobj), float) or isinstance(vobj, float):
|
||||
rdata = vobj
|
||||
elif issubclass(type(vobj), list) or issubclass(type(vobj), tuple):
|
||||
rdata = []
|
||||
try:
|
||||
vobj = sorted(vobj)
|
||||
except Exception:
|
||||
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)
|
||||
)
|
||||
|
||||
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 = sorted(methods)
|
||||
|
||||
for method in methods:
|
||||
# Attempt to get the method, skip on fail
|
||||
try:
|
||||
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)
|
||||
else:
|
||||
pass
|
||||
|
||||
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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run the script
|
||||
print(VMWareInventory().show())
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user