Compare commits
677 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 53c927ff26 | |||
| 08c1f96e4b | |||
| 7e9e29011e | |||
| ce01c3f7e7 | |||
| 227dfc17f3 | |||
| 05df6464ef | |||
| b76efa39be | |||
| bd7de28a64 | |||
| 4d29cd6ee3 | |||
| f924c96370 | |||
| 0394ef776f | |||
| fd492bebc6 | |||
| 98e4034de2 | |||
| 45d2c90169 | |||
| 25b52109b4 | |||
| 85c838c835 | |||
| 9a3ad060dc | |||
| 38ed7ca7df | |||
| aed6ee7f74 | |||
| fa9fe9d246 | |||
| c192d8a2b2 | |||
| a015c2e581 | |||
| fa79d57d13 | |||
| b7c3fd1246 | |||
| fe87a16a3e | |||
| 91d31d8164 | |||
| c91befda5c | |||
| 4fedb17e38 | |||
| e820c282be | |||
| f092a69d77 | |||
| 9e0bf168ca | |||
| 0e26ef9bdd | |||
| d43d81a9a4 | |||
| d0892c19ad | |||
| 52afacf475 | |||
| bf25193ec1 | |||
| 25acfa8170 | |||
| 86ec639176 | |||
| 08c5fe875b | |||
| ed14312ad6 | |||
| 08999a2a2f | |||
| 3ff03d14e3 | |||
| 77029a4227 | |||
| 128a606a59 | |||
| 53c62f2655 | |||
| b1b543c289 | |||
| abe8d8d4d6 | |||
| 42ad1c879f | |||
| 2ac4acbf97 | |||
| 6baaea90ae | |||
| b973910283 | |||
| 5f4bf813b1 | |||
| 418445d3c5 | |||
| 2d9ceafbd4 | |||
| f60517aa33 | |||
| 31c0e523c2 | |||
| 6b774f8cf5 | |||
| ec7c8eb8ca | |||
| cfea9c54bb | |||
| 0a3ebdb6d5 | |||
| db9955d3df | |||
| 6728b6cdda | |||
| 7d2877fbf9 | |||
| 845a7ac5ae | |||
| 8700de964c | |||
| 190d72761f | |||
| dc3e142486 | |||
| b30f4688cd | |||
| 9f7ad62376 | |||
| 9bcc18d17b | |||
| d79900b434 | |||
| 89ef236e9c | |||
| e6d898cd17 | |||
| a8d13fe40d | |||
| 7538110036 | |||
| 24c1d32120 | |||
| 526a0b1f60 | |||
| 7e279f972e | |||
| 3cb51acd78 | |||
| e863ba0cec | |||
| ce5f3dd148 | |||
| 8e60ad986a | |||
| d0f6410326 | |||
| da44fb1e43 | |||
| fe558f680a | |||
| 6b72804cb4 | |||
| d0a5dec686 | |||
| d79ba6f2aa | |||
| 5fd2018117 | |||
| 63459110c4 | |||
| 9a1265b856 | |||
| af3b0e3b82 | |||
| 47cead3603 | |||
| 0188889aea | |||
| d0f0315c9d | |||
| 21a35bde00 | |||
| 3ad07d5965 | |||
| 547395a2be | |||
| c4c53d545a | |||
| 8f9e136ca0 | |||
| 4ad0ff61e5 | |||
| b7da73b73f | |||
| 238524ccc6 | |||
| 45bf88b78c | |||
| 1bcba4f4a2 | |||
| 4d4d0b90c5 | |||
| 90bf67cf56 | |||
| 87b3e1ecdc | |||
| ad9b8e2312 | |||
| 6219fc4932 | |||
| f5e08158ca | |||
| 5a109154f1 | |||
| b3aba405dd | |||
| dcd214a631 | |||
| a9c2e597ac | |||
| ff82f0a168 | |||
| e9c4eb36d1 | |||
| aca860df9c | |||
| 61780e0980 | |||
| 7a67145ef4 | |||
| 487c826700 | |||
| 4d9ecebc20 | |||
| 17be5537d6 | |||
| fcdc281c28 | |||
| aa40c5f3b3 | |||
| 1e464d6c4b | |||
| ee5f12cd40 | |||
| c717934b7e | |||
| 4ecdd17caf | |||
| d55012e570 | |||
| 9253d89280 | |||
| 0c61d049a2 | |||
| ebfd7ec7e3 | |||
| def1fa23f8 | |||
| 7395becf3a | |||
| 888ac86d70 | |||
| ac23c69bcc | |||
| 6afe3fd497 | |||
| daf44331c4 | |||
| b3b12b5e41 | |||
| dd9e09dee6 | |||
| 4c8d949642 | |||
| 1727bd3b7a | |||
| a65b4fef97 | |||
| 19fc8eea80 | |||
| 51739fc47d | |||
| da4562f495 | |||
| f371092845 | |||
| e035ce97ce | |||
| 2030f82bf2 | |||
| 1584eda3f8 | |||
| 12979cf834 | |||
| fcd02e45a2 | |||
| 101c4084f2 | |||
| b63a3bd0c7 | |||
| 40eaf282ea | |||
| b47bed9601 | |||
| 8d7f526df6 | |||
| d3e8e8ff54 | |||
| c16b691883 | |||
| 263432daa1 | |||
| 82cca242e3 | |||
| 8fa4dc3920 | |||
| 416b8d59a9 | |||
| 6a0326b6d0 | |||
| 15062b38e1 | |||
| 772e241d1f | |||
| f787cda7be | |||
| 12ff9b5b9a | |||
| f4a4649086 | |||
| 45354c6be5 | |||
| 5315dd146d | |||
| e3b2521f01 | |||
| 2d1c297fb8 | |||
| f6fe9124b8 | |||
| 8bbcef6f13 | |||
| bbaa2048c8 | |||
| 44a9226ece | |||
| c2a63a5ce3 | |||
| 97cc259d8c | |||
| fef53b1a9e | |||
| bc17553d1c | |||
| 432dae769e | |||
| 483f7fd625 | |||
| 3a2df329af | |||
| dd6399b5ae | |||
| 2cda36f7dc | |||
| e4afa83b82 | |||
| ab8a36686e | |||
| 628b289ab3 | |||
| 153f3932b5 | |||
| 632a83c79e | |||
| 479f8b8c26 | |||
| fdf14ec7ab | |||
| 9b8793e42e | |||
| 183330a9fa | |||
| cbea17e4c5 | |||
| cac7afd700 | |||
| 1f44c07ddf | |||
| 4b9b9a8a5b | |||
| 220ab33da3 | |||
| 738cea9c93 | |||
| fc96b88205 | |||
| ec12cc4154 | |||
| f8520750cb | |||
| 6448deb0b1 | |||
| f068bedfa1 | |||
| e8583833a7 | |||
| ade0233d57 | |||
| 08ece6c54a | |||
| 617f9dc942 | |||
| 57f12ac9e3 | |||
| 84e1d21b9c | |||
| 12e23a1a6c | |||
| 0b891fc8fb | |||
| 46650cfcec | |||
| 396b81b647 | |||
| 492953f299 | |||
| 1a9c54b1e1 | |||
| 636a3367b5 | |||
| efb60776f4 | |||
| 9f149c9f1a | |||
| d0f4358730 | |||
| d76c8c9c85 | |||
| 1682dd06c0 | |||
| 54c9c1e25c | |||
| b8d7b5041b | |||
| 2d06ee4c0d | |||
| 2b6d8a8be5 | |||
| dde11baa56 | |||
| 5a67a556cd | |||
| b15dde6b9f | |||
| dcca08b27b | |||
| b574c70006 | |||
| ba3466af95 | |||
| db1a4d8fac | |||
| 6fb74ae2f9 | |||
| d72c0c8898 | |||
| a33713234c | |||
| 2a0dfba838 | |||
| b0eedfbf92 | |||
| 85b9fa580c | |||
| a4a09fec82 | |||
| 290e1af339 | |||
| 8195375412 | |||
| 34e2584220 | |||
| baf07659f7 | |||
| fd059a3df2 | |||
| 70ef8e9ebb | |||
| f0f8eb3445 | |||
| dba87fce91 | |||
| b0b7484390 | |||
| 6e84374c2a | |||
| 31b6c1c28e | |||
| b114a6075f | |||
| d69e70db01 | |||
| 0efc0bec89 | |||
| 162b78ea5c | |||
| e3dd25b1b2 | |||
| 97c64f3632 | |||
| ae1b2394ac | |||
| 5f068913ee | |||
| a94ec130d2 | |||
| 9006d4557d | |||
| ca21423c71 | |||
| 5e35cd6466 | |||
| f1148fc26d | |||
| ae665c15b3 | |||
| 2a281f69c2 | |||
| 4b66d98332 | |||
| b38f8bfa80 | |||
| 413fb15e0a | |||
| 4e74ab4856 | |||
| 5ad9bc8833 | |||
| bfed0f14ec | |||
| 26dc607c5c | |||
| 0d53b1b097 | |||
| 2377152d4c | |||
| c4303c8c04 | |||
| f1b1611956 | |||
| 8bb8314d10 | |||
| 3c2cbae68e | |||
| e4a546e8bb | |||
| 43b3a0be97 | |||
| 72ca9e58e7 | |||
| 9e62fd42b5 | |||
| 0edd0d4736 | |||
| 01f2c85a3c | |||
| 1a88a3362f | |||
| 994019a5c9 | |||
| f6a09bc4c1 | |||
| 3f17ad62cd | |||
| 5f69c557b9 | |||
| 9ef7168a49 | |||
| c3205595b3 | |||
| b542dd8466 | |||
| 80edea2934 | |||
| 1754de3335 | |||
| 4b73931351 | |||
| 6341e75e33 | |||
| 031ba43d22 | |||
| 1d8bb17785 | |||
| 5a63a48e6b | |||
| 227c500ad7 | |||
| 867b2437cc | |||
| bc71c6ddd7 | |||
| cb55cd6f8d | |||
| 68a9adc1be | |||
| 7b177e8ebb | |||
| 244b923559 | |||
| 61d8e2e7c7 | |||
| 706cfd9ade | |||
| 19a2fc00dc | |||
| 60d64251f8 | |||
| 3466ad5e45 | |||
| 64c51ade1e | |||
| 6c84ec72c6 | |||
| 028f216689 | |||
| 102b22be1f | |||
| 15b2b3a020 | |||
| 279b5965b8 | |||
| 931f9f1a61 | |||
| 2a2017f295 | |||
| 60f295f7a0 | |||
| 86c25fd82d | |||
| f47cc1693d | |||
| 420337b50c | |||
| cb40778b84 | |||
| fd0ed04d19 | |||
| 84fa9dcaee | |||
| b1c0fcd1d3 | |||
| 98eec45525 | |||
| 26aa4e2469 | |||
| e5a6627d10 | |||
| fd29ecc24b | |||
| bcd0001cb1 | |||
| 611e5b0c90 | |||
| cf313cde96 | |||
| a765deccce | |||
| 87e911c405 | |||
| dc4ebab345 | |||
| 969c3feb13 | |||
| 83f23ef861 | |||
| fd7e96d33e | |||
| 626e4cb003 | |||
| ecacb67d31 | |||
| 3f5c4772be | |||
| 949f8b8fc5 | |||
| 36177cc935 | |||
| 7ecdb83800 | |||
| a35a0f885e | |||
| f38b9d1ca7 | |||
| ac3341cc12 | |||
| e261d49acc | |||
| 3a5f2126a6 | |||
| 3235da074e | |||
| 8bb41bc627 | |||
| 25986fa181 | |||
| 09f025b7a0 | |||
| 2f11350665 | |||
| 7af3bdc89e | |||
| 238e4adba2 | |||
| 375a1eaf43 | |||
| 211f1e69c3 | |||
| 55694db7c3 | |||
| f39b99f823 | |||
| c432f9bc74 | |||
| fb51805e1b | |||
| 19b43cd454 | |||
| 921bf3da9d | |||
| 0d28466b22 | |||
| c592fdf838 | |||
| c2b8fabf66 | |||
| 6c08913448 | |||
| 494077591a | |||
| 8220d57690 | |||
| b551eba6d4 | |||
| 749472559b | |||
| fed1e9870c | |||
| fced8cf6b0 | |||
| 1a151a581a | |||
| ecaa794f7f | |||
| 367dce9790 | |||
| 7c0ac3653d | |||
| bd2a5724ec | |||
| a490213d45 | |||
| e3aba7770f | |||
| ea0c82d680 | |||
| 0bbe0a8670 | |||
| 9d85a55434 | |||
| bea2939deb | |||
| 263fcab594 | |||
| ec0522a127 | |||
| 3003aa2d47 | |||
| e0a1c5be42 | |||
| 24b91169d9 | |||
| 9882dd56f6 | |||
| d47de18b6c | |||
| 73c89bced0 | |||
| f72b1f4fd7 | |||
| 9ad622946a | |||
| bbf2939063 | |||
| be9ff7ff46 | |||
| 756df550f8 | |||
| 9714f5c79d | |||
| fc4ae3e270 | |||
| cedbcfcc0a | |||
| a548769792 | |||
| 82832021e8 | |||
| 3d7a51f2ec | |||
| a5dd280149 | |||
| d95914ca87 | |||
| efac68b636 | |||
| 9efea6f7a2 | |||
| d592e15dcc | |||
| afb2e3e5ac | |||
| 37fce9c0f6 | |||
| 91ad42ae75 | |||
| daf858648b | |||
| 8468e2255b | |||
| 879ddddf6f | |||
| c2694986d8 | |||
| c4b489ab1f | |||
| 6c92d2fa88 | |||
| a42167ca73 | |||
| 270266e920 | |||
| 618924a24f | |||
| 81c69d7333 | |||
| d42df3a82e | |||
| 6e60fc20c1 | |||
| 9be2a94045 | |||
| d28bbe14ed | |||
| 54ac3dac34 | |||
| a7dc1bf350 | |||
| d9753a798a | |||
| bdec9e7064 | |||
| 30d4f070a1 | |||
| 25083953fc | |||
| dc13265a70 | |||
| 26816b72ec | |||
| fa887bc90f | |||
| 7884bc02ea | |||
| dca75033fe | |||
| 9662902c31 | |||
| 071978a2e7 | |||
| f5ecd075f7 | |||
| 12825edae1 | |||
| 980c9d2d8c | |||
| bd653c8f3c | |||
| e0f12292d8 | |||
| 7f6359b143 | |||
| d08ba12a42 | |||
| 97cdc7ae82 | |||
| a30f5461da | |||
| b02959d64f | |||
| 1ec518543e | |||
| aa18e66987 | |||
| 031c75e53a | |||
| 9f93839d13 | |||
| ab08fea1aa | |||
| abe33a5881 | |||
| 2cabbd50fc | |||
| 24d73b0fe6 | |||
| 13b814a33c | |||
| dad9695ac7 | |||
| 38b186b159 | |||
| 1b091a7e3e | |||
| 05a7a3725b | |||
| 8df9cc7649 | |||
| 3d5c93bf44 | |||
| 80f402aa9b | |||
| 834f6a216d | |||
| c0a5d91592 | |||
| 9feb26ab28 | |||
| 036e0866b5 | |||
| 4a6a3fd305 | |||
| 2a8d1f07d4 | |||
| 7926ab5b95 | |||
| 27c949ade9 | |||
| 2a49438c37 | |||
| bd893b765b | |||
| 37fd408c69 | |||
| 1d553830f2 | |||
| 9cf182c225 | |||
| 5ea8ad2d94 | |||
| 7942570411 | |||
| eba81ee36f | |||
| 93ac6c5ba0 | |||
| c1b38f62c8 | |||
| a05da2d312 | |||
| 933b5e8631 | |||
| e8e6c98016 | |||
| 01387520e9 | |||
| e265cdfa1d | |||
| b1c2820a32 | |||
| 9fe4a13217 | |||
| d0d036fd3f | |||
| ad7b744764 | |||
| dff8d7be04 | |||
| 1f63c47738 | |||
| b3b22c41e9 | |||
| bee04de26a | |||
| d0c4f4015c | |||
| 51b460e8c6 | |||
| 57a34d914f | |||
| e9f770fa11 | |||
| 9ff59090c2 | |||
| 5a8e61a610 | |||
| 1ee7ff2f7b | |||
| 16f0d7b333 | |||
| 4886b26ea9 | |||
| 99430fa0a3 | |||
| 634cf9aaba | |||
| 5c44ec440f | |||
| 70837469c6 | |||
| a8fd6ee9c3 | |||
| 6034b5b0f3 | |||
| 0972b761de | |||
| 960e7c331c | |||
| 18c59a774e | |||
| ab55b1a412 | |||
| 0eca3030f4 | |||
| c34921fe7b | |||
| 81591009ea | |||
| 3eef7a629b | |||
| 50ef1bb93f | |||
| af9651f015 | |||
| b9fd948c26 | |||
| 7ea4a0e895 | |||
| 32b9ea689a | |||
| c03fec4864 | |||
| 0f7ab7cbf5 | |||
| f96be11a3a | |||
| 74bfd7eb81 | |||
| 4664e354c3 | |||
| ecb944892d | |||
| b9b53d1941 | |||
| cf9ddf3a30 | |||
| 6f114a2e2c | |||
| db1d5b154a | |||
| 6d580aea02 | |||
| da3816d370 | |||
| abbfb20947 | |||
| 28b94cd2f6 | |||
| ac4e55aeff | |||
| 639763c138 | |||
| f8abf67ebe | |||
| 7bbcf5d2d5 | |||
| 1125ca548a | |||
| effd931031 | |||
| 8a687b77f8 | |||
| 8e88667f82 | |||
| 7302b7bbc4 | |||
| 3dfd240b86 | |||
| 0db6d73523 | |||
| 7338c7e838 | |||
| 81866ccac4 | |||
| 3eff44a2b0 | |||
| 2b8bc789c2 | |||
| ecc0b0770c | |||
| 25f808de8f | |||
| 306bab93d8 | |||
| bddf2f9755 | |||
| 153df9885e | |||
| 5da7f2aff4 | |||
| d80fd74b64 | |||
| b42628d858 | |||
| 96ef6482c5 | |||
| 8e2528eb83 | |||
| e4991d1a61 | |||
| f8b17ef8d3 | |||
| 4b0480dd9d | |||
| 60d44e1a01 | |||
| ccab8ac2c0 | |||
| 54a56e478b | |||
| 45100f03b1 | |||
| e6416def6b | |||
| b7bb0ab225 | |||
| bfe5f4d0ed | |||
| e060b06081 | |||
| d4f660edc3 | |||
| f8807da57b | |||
| cd9f926b5f | |||
| dab50574e0 | |||
| 5ebe915da3 | |||
| d9427db14b | |||
| 4cf0c5550e | |||
| e5d3576e0b | |||
| 3cbc229990 | |||
| 4819ce4989 | |||
| 24c171a759 | |||
| c1fd1348b1 | |||
| 74e21e6154 | |||
| eb41bfd3c8 | |||
| 089c75016f | |||
| 87610f2ff0 | |||
| fa9bfbba13 | |||
| 36639186e0 | |||
| 291de4f3b4 | |||
| 43083996fe | |||
| 83b9a43e60 | |||
| fba2bdcf0c | |||
| c37014083a | |||
| dc60f2d844 | |||
| 204bffa21e | |||
| 262b35e7d3 | |||
| 9bb8e8e36d | |||
| cb6f0f4284 | |||
| 2bbc05185e | |||
| 82144fd543 | |||
| 036b779188 | |||
| 98f93fcc07 | |||
| ee34c0c154 | |||
| adfe6b8589 | |||
| 73ffec9710 | |||
| e56023b8bb | |||
| b5b60ab1b4 | |||
| d118cf3e31 | |||
| 0fe8e51cb5 | |||
| d149c1823a | |||
| aea022b002 | |||
| 43b1727f6a | |||
| 39a92f9cd6 | |||
| 7ee6ae3663 | |||
| c9fafcecbe | |||
| 0894206467 | |||
| 8e8cde3a2c | |||
| 07508ad535 | |||
| fc300723da | |||
| e1914a0e62 | |||
| 121d7c4ece | |||
| 1af461f476 | |||
| e09572a8e1 | |||
| d419a13ca7 | |||
| 9b741a1d16 | |||
| be32d3b00f | |||
| b460980d7d | |||
| 9b1add8c40 | |||
| 3a027c2749 | |||
| a5a64f089d | |||
| 710d085def | |||
| 5059588b17 | |||
| 2635195185 | |||
| cd28d82639 | |||
| 279e5e4522 | |||
| d814136ec9 | |||
| fba4d9232d | |||
| dcbe48e2d4 | |||
| eda2333b24 | |||
| 05545e3735 | |||
| 5582e6a4f1 | |||
| 2cd8d7b5e4 | |||
| 8d39ae709c | |||
| 93a20a33e9 | |||
| 36559badd5 | |||
| f718fc99b7 | |||
| 157d21b1c3 | |||
| 1a00e2635e | |||
| a8f0b5fd06 | |||
| 256377166a | |||
| 5730a29814 | |||
| 0669ce858a | |||
| 39e791a1d3 | |||
| 99a7522b03 | |||
| 81418afbbb | |||
| b300aac551 | |||
| adca320490 | |||
| a33320d03d | |||
| e54eeb7629 | |||
| 28cf753756 | |||
| 5b8ee8488d | |||
| 02efcdced6 | |||
| 3de61fb180 | |||
| 06e99ee75e | |||
| 7ff1679007 | |||
| 548b551bfd | |||
| a7e4da92db |
62
CHANGELOG.md
62
CHANGELOG.md
@ -1,7 +1,67 @@
|
||||
Ansible Changes By Release
|
||||
==========================
|
||||
|
||||
0.5 "Amsterdam" ------- July XX, 2012
|
||||
0.6 "Cabo" ------------ pending
|
||||
|
||||
playbooks:
|
||||
|
||||
* support to tag tasks and includes and use --tags in playbook CLI
|
||||
* playbooks can now include other playbooks (example/playbooks/nested_playbooks.yml)
|
||||
* vars_files now usable with with_items, provided file paths don't contain host specific facts
|
||||
* error reporting if with_items value is unbound
|
||||
* with_items no longer creates lots of tasks, creates one task that makes multiple calls
|
||||
* can use host_specific facts inside with_items (see above)
|
||||
* at the top level of a playbook, set 'gather_facts: False' to skip fact gathering
|
||||
* first_available_file and with_items used together will now raise an error
|
||||
* to catch typos, like 'var' for 'vars', playbooks and tasks now yell on invalid parameters
|
||||
* automatically load (directory_of_inventory_file)/group_vars/groupname and /host_vars/hostname in vars_files
|
||||
* playbook is now colorized, set ANSIBLE_NOCOLOR=1 if you do not like this, does not colorize if not a TTY
|
||||
* hostvars now preserved between plays (regression in 0.5 from 0.4), useful for sharing vars in multinode configs
|
||||
* ignore_errors: True on a task can be used to allow a task to fail and not stop the play
|
||||
* with_items with the apt/yum module will install/remove/update everything in a single command
|
||||
|
||||
inventory:
|
||||
|
||||
* groups variable available as a hash to return the hosts in each group name
|
||||
* in YAML inventory, hosts can list their groups in inverted order now also (see tests/yaml_hosts)
|
||||
* YAML inventory is deprecated and will be removed in 0.7
|
||||
* ec2 inventory script
|
||||
* support ranges of hosts in the host file, like www[001-100].example.com (supports leading zeros and also not)
|
||||
|
||||
modules:
|
||||
|
||||
* fetch module now does not fail a system when requesting file paths (ex: logs) that don't exist
|
||||
* apt module now takes an optional install-recommends=yes|no (default yes)
|
||||
* fixes to the return codes of the copy module
|
||||
* copy module takes a remote md5sum to avoid large file transfer
|
||||
* various user and group module fixes (error handling, etc)
|
||||
* apt module now takes an optional force parameter
|
||||
* slightly better psychic service status handling for the service module
|
||||
* fetch module fixes for SSH connection type
|
||||
* modules now consistently all take yes/no for boolean parameters (and DWIM on true/false/1/0/y/n/etc)
|
||||
* setup module no longer saves to disk, template module now only used in playbooks
|
||||
* setup module no longer needs to run twice per playbook
|
||||
* apt module now passes DEBIAN_FRONTEND=noninteractive
|
||||
* mount module (manages active mounts + fstab)
|
||||
* setup module fixes if no ipv6 support
|
||||
* internals: template in common module boilerplate, also causes less SSH operations when used
|
||||
* git module fixes
|
||||
* setup module overhaul, more modular
|
||||
* minor caching logic added to inventory to reduce hammering of inventory scripts.
|
||||
* MySQL and PostgreSQL modules for user and db management
|
||||
* vars_prompt now supports private password entry (see examples/playbooks/prompts.yml)
|
||||
* yum module modified to be more tolerant of plugins spewing random console messages (ex: RHN)
|
||||
|
||||
internals:
|
||||
|
||||
* when sudoing to root, still use /etc/ansible/setup as the metadata path, as if root
|
||||
* paramiko is now only imported if needed when running from source checkout
|
||||
* cowsay support on Ubuntu
|
||||
* various ssh connection fixes for old Ubuntu clients
|
||||
* ./hacking/test-module now supports options like ansible takes and has a debugger mode
|
||||
* sudoing to a user other than root now works more seamlessly (uses /tmp, avoids umask issues)
|
||||
|
||||
0.5 "Amsterdam" ------- July 04, 2012
|
||||
|
||||
* Service module gets more accurate service states when running with upstart
|
||||
* Jinja2 usage in playbooks (not templates), reinstated, supports %include directive
|
||||
|
||||
26
Makefile
26
Makefile
@ -6,7 +6,7 @@
|
||||
# useful targets:
|
||||
# make sdist ---------------- produce a tarball
|
||||
# make rpm ----------------- produce RPMs
|
||||
# make debian --------------- produce a dpkg (FIXME?)
|
||||
# make deb ------------------ produce a DEB
|
||||
# make docs ----------------- rebuild the manpages (results are checked in)
|
||||
# make tests ---------------- run the tests
|
||||
# make pyflakes, make pep8 -- source code checks
|
||||
@ -28,13 +28,21 @@ SITELIB = $(shell python -c "from distutils.sysconfig import get_python_lib; pri
|
||||
# VERSION file provides one place to update the software version
|
||||
VERSION := $(shell cat VERSION)
|
||||
|
||||
### Get the branch information from git
|
||||
ifneq ($(shell which git),)
|
||||
GIT_DATE := $(shell git log -n 1 --format="%ai")
|
||||
endif
|
||||
DATE := $(shell date --date="$(GIT_DATE)" +%Y%m%d%H%M)
|
||||
|
||||
# RPM build parameters
|
||||
RPMSPECDIR= packaging/rpm
|
||||
RPMSPEC = $(RPMSPECDIR)/ansible.spec
|
||||
RPMVERSION := $(shell awk '/Version/{print $$2; exit}' < $(RPMSPEC) | cut -d "%" -f1)
|
||||
RPMRELEASE := $(shell awk '/Release/{print $$2; exit}' < $(RPMSPEC) | cut -d "%" -f1)
|
||||
RPMDIST = $(shell rpm --eval '%dist')
|
||||
RPMNVR = "$(NAME)-$(RPMVERSION)-$(RPMRELEASE)$(RPMDIST)"
|
||||
RPMRELEASE = 1
|
||||
ifeq ($(OFFICIAL),)
|
||||
RPMRELEASE = 0.git$(DATE)
|
||||
endif
|
||||
RPMNVR = "$(NAME)-$(VERSION)-$(RPMRELEASE)$(RPMDIST)"
|
||||
|
||||
########################################################
|
||||
|
||||
@ -102,6 +110,7 @@ sdist: clean
|
||||
rpmcommon: sdist
|
||||
@mkdir -p rpm-build
|
||||
@cp dist/*.gz rpm-build/
|
||||
@sed -e 's#^Version:.*#Version: $(VERSION)#' -e 's#^Release:.*#Release: $(RPMRELEASE)%{?dist}#' $(RPMSPEC) >rpm-build/$(NAME).spec
|
||||
|
||||
srpm: rpmcommon
|
||||
@rpmbuild --define "_topdir %(pwd)/rpm-build" \
|
||||
@ -110,7 +119,8 @@ srpm: rpmcommon
|
||||
--define "_srcrpmdir %{_topdir}" \
|
||||
--define "_specdir $(RPMSPECDIR)" \
|
||||
--define "_sourcedir %{_topdir}" \
|
||||
-bs $(RPMSPEC)
|
||||
-bs rpm-build/$(NAME).spec
|
||||
@rm -f rpm-build/$(NAME).spec
|
||||
@echo "#############################################"
|
||||
@echo "Ansible SRPM is built:"
|
||||
@echo " rpm-build/$(RPMNVR).src.rpm"
|
||||
@ -123,10 +133,12 @@ rpm: rpmcommon
|
||||
--define "_srcrpmdir %{_topdir}" \
|
||||
--define "_specdir $(RPMSPECDIR)" \
|
||||
--define "_sourcedir %{_topdir}" \
|
||||
-ba $(RPMSPEC)
|
||||
--define "_rpmfilename $(RPMNVR).%%{ARCH}.rpm" \
|
||||
-ba rpm-build/$(NAME).spec
|
||||
@rm -f rpm-build/$(NAME).spec
|
||||
@echo "#############################################"
|
||||
@echo "Ansible RPM is built:"
|
||||
@echo " rpm-build/noarch/$(RPMNVR).noarch.rpm"
|
||||
@echo " rpm-build/$(RPMNVR).noarch.rpm"
|
||||
@echo "#############################################"
|
||||
|
||||
debian: sdist
|
||||
|
||||
12
README.md
12
README.md
@ -4,7 +4,7 @@ Ansible
|
||||
Ansible is a radically simple configuration-management, deployment, task-execution, and
|
||||
multinode orchestration framework.
|
||||
|
||||
Read all about at it at (http://ansible.github.com)
|
||||
Read the documentation at http://ansible.github.com
|
||||
|
||||
Design Principles
|
||||
=================
|
||||
@ -27,9 +27,11 @@ Get Involved
|
||||
Branch Info
|
||||
===========
|
||||
|
||||
* The master branch corresponds to release 0.4 "Unchained".
|
||||
* Major bug fixes will be periodically made to the master branch.
|
||||
* The devel branch corresponds to release 0.5 "Amsterdam".
|
||||
* Releases are named after Van Halen songs.
|
||||
* The master branch corresponds to release 0.5 "Amsterdam".
|
||||
* The devel branch corresponds to release 0.6 "Cabo".
|
||||
* All feature work happens on the development branch.
|
||||
* Major bug fixes will be made to the master branch, but not minor ones.
|
||||
* See CHANGELOG.md for release notes to track each release.
|
||||
|
||||
Patch Instructions
|
||||
@ -43,6 +45,7 @@ Contributions to the core and modules are greatly welcome.
|
||||
* Make sure "make tests" passes before submitting any requests.
|
||||
* Bonus points:
|
||||
* Joining the mailing list
|
||||
* Fixing bugs instead of sending bug reports.
|
||||
* Using squash merges
|
||||
* Updating the "rst/*" files in the docs project and "docs/" manpage content
|
||||
* Adding more unit tests
|
||||
@ -51,6 +54,7 @@ Contributions to the core and modules are greatly welcome.
|
||||
* Sending feature pull requests to the 'master' branch instead of the devel branch
|
||||
* Sending pull requests to mpdehaan's personal ansible fork.
|
||||
|
||||
|
||||
Author
|
||||
======
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
|
||||
#######################################################
|
||||
|
||||
import os
|
||||
import sys
|
||||
import getpass
|
||||
|
||||
@ -26,6 +27,25 @@ import ansible.constants as C
|
||||
from ansible import errors
|
||||
from ansible import callbacks
|
||||
from ansible import utils
|
||||
from ansible.color import ANSIBLE_COLOR, stringc
|
||||
|
||||
def colorize(lead, num, color):
|
||||
""" Print 'lead' = 'num' in 'color' """
|
||||
if num != 0 and ANSIBLE_COLOR:
|
||||
return "%s%s%-15s" % (stringc(lead, color), stringc("=", color), stringc(str(num), color))
|
||||
else:
|
||||
return "%s=%-4s" % (lead, str(num))
|
||||
|
||||
def hostcolor(host, stats):
|
||||
if ANSIBLE_COLOR:
|
||||
if stats['failures'] != 0 or stats['unreachable'] != 0:
|
||||
return "%-41s" % stringc(host, 'red')
|
||||
elif stats['changed'] != 0:
|
||||
return "%-41s" % stringc(host, 'yellow')
|
||||
else:
|
||||
return "%-41s" % stringc(host, 'green')
|
||||
return "%-30s" % host
|
||||
|
||||
|
||||
def main(args):
|
||||
''' run ansible-playbook operations '''
|
||||
@ -35,6 +55,8 @@ def main(args):
|
||||
parser = utils.base_parser(constants=C, usage=usage, connect_opts=True, runas_opts=True)
|
||||
parser.add_option('-e', '--extra-vars', dest="extra_vars", default=None,
|
||||
help="set additional key=value variables from the CLI")
|
||||
parser.add_option('-t', '--tags', dest='tags', default='all',
|
||||
help="only run plays and tasks tagged with these values")
|
||||
|
||||
options, args = parser.parse_args(args)
|
||||
|
||||
@ -53,6 +75,7 @@ def main(args):
|
||||
options.sudo = True
|
||||
options.sudo_user = options.sudo_user or C.DEFAULT_SUDO_USER
|
||||
extra_vars = utils.parse_kv(options.extra_vars)
|
||||
only_tags = options.tags.split(",")
|
||||
|
||||
# run all playbooks specified on the command line
|
||||
for playbook in args:
|
||||
@ -78,7 +101,8 @@ def main(args):
|
||||
sudo_user=options.sudo_user,
|
||||
sudo_pass=sudopass,
|
||||
extra_vars=extra_vars,
|
||||
private_key_file=options.private_key_file
|
||||
private_key_file=options.private_key_file,
|
||||
only_tags=only_tags,
|
||||
)
|
||||
try:
|
||||
|
||||
@ -87,9 +111,13 @@ def main(args):
|
||||
print callbacks.banner("PLAY RECAP")
|
||||
for h in hosts:
|
||||
t = pb.stats.summarize(h)
|
||||
print "%-30s : ok=%4s changed=%4s unreachable=%4s failed=%4s " % (h,
|
||||
t['ok'], t['changed'], t['unreachable'], t['failures']
|
||||
)
|
||||
print "%-30s : %s %s %s %s " % (
|
||||
hostcolor(h, t),
|
||||
colorize('ok', t['ok'], 'green'),
|
||||
colorize('changed', t['changed'], 'yellow'),
|
||||
colorize('unreachable', t['unreachable'], 'red'),
|
||||
colorize('failed', t['failures'], 'red'))
|
||||
|
||||
print "\n"
|
||||
|
||||
except errors.AnsibleError, e:
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
.\" Title: ansible-playbook
|
||||
.\" Author: [see the "AUTHOR" section]
|
||||
.\" Generator: DocBook XSL Stylesheets v1.75.2 <http://docbook.sf.net/>
|
||||
.\" Date: 06/19/2012
|
||||
.\" Date: 07/24/2012
|
||||
.\" Manual: System administration commands
|
||||
.\" Source: Ansible 0.5
|
||||
.\" Source: Ansible 0.6
|
||||
.\" Language: English
|
||||
.\"
|
||||
.TH "ANSIBLE\-PLAYBOOK" "1" "06/19/2012" "Ansible 0\&.5" "System administration commands"
|
||||
.TH "ANSIBLE\-PLAYBOOK" "1" "07/24/2012" "Ansible 0\&.6" "System administration commands"
|
||||
.\" -----------------------------------------------------------------
|
||||
.\" * set default formatting
|
||||
.\" -----------------------------------------------------------------
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
.\" Title: ansible
|
||||
.\" Author: [see the "AUTHOR" section]
|
||||
.\" Generator: DocBook XSL Stylesheets v1.75.2 <http://docbook.sf.net/>
|
||||
.\" Date: 06/19/2012
|
||||
.\" Date: 07/24/2012
|
||||
.\" Manual: System administration commands
|
||||
.\" Source: Ansible 0.5
|
||||
.\" Source: Ansible 0.6
|
||||
.\" Language: English
|
||||
.\"
|
||||
.TH "ANSIBLE" "1" "06/19/2012" "Ansible 0\&.5" "System administration commands"
|
||||
.TH "ANSIBLE" "1" "07/24/2012" "Ansible 0\&.6" "System administration commands"
|
||||
.\" -----------------------------------------------------------------
|
||||
.\" * set default formatting
|
||||
.\" -----------------------------------------------------------------
|
||||
@ -140,7 +140,9 @@ Connection type to use\&. Possible options are
|
||||
.RE
|
||||
.SH "INVENTORY"
|
||||
.sp
|
||||
Ansible stores the hosts it can potentially operate on in an inventory file\&. The syntax is one host per line\&. Groups headers are allowed and are included on their own line, enclosed in square brackets\&.
|
||||
Ansible stores the hosts it can potentially operate on in an inventory file\&. The syntax is one host per line\&. Groups headers are allowed and are included on their own line, enclosed in square brackets that start the line\&.
|
||||
.sp
|
||||
Ranges of hosts are also supported\&. For more information and additional options, see the documentation on http://ansible\&.github\&.com/\&.
|
||||
.SH "FILES"
|
||||
.sp
|
||||
/etc/ansible/hosts \(em Default inventory file
|
||||
|
||||
@ -31,7 +31,6 @@ A name of a group in the inventory file, a shell-like glob selecting
|
||||
hosts in inventory file, or any combination of the two separated by
|
||||
semicolons.
|
||||
|
||||
|
||||
OPTIONS
|
||||
-------
|
||||
|
||||
@ -112,7 +111,11 @@ INVENTORY
|
||||
|
||||
Ansible stores the hosts it can potentially operate on in an inventory
|
||||
file. The syntax is one host per line. Groups headers are allowed and
|
||||
are included on their own line, enclosed in square brackets.
|
||||
are included on their own line, enclosed in square brackets that
|
||||
start the line.
|
||||
|
||||
Ranges of hosts are also supported. For more information and
|
||||
additional options, see the documentation on http://ansible.github.com/.
|
||||
|
||||
FILES
|
||||
-----
|
||||
|
||||
@ -7,31 +7,38 @@
|
||||
# - Groups of hosts are delimited by [header] elements
|
||||
# - You can enter hostnames or ip addresses
|
||||
# - A hostname/ip can be a member of multiple groups
|
||||
#
|
||||
|
||||
# Ex 1: Ungrouped hosts, specify before any group headers.
|
||||
green.bikeshed.org
|
||||
blue.bikeshed.org
|
||||
red.bikeshed.org
|
||||
bikeshed.org
|
||||
bastion.secure.bikeshed.org
|
||||
|
||||
green.example.com
|
||||
blue.example.com
|
||||
192.168.100.1
|
||||
192.168.100.10
|
||||
|
||||
# Ex 2: A collection of hosts belonging to the 'webservers' group
|
||||
|
||||
[webservers]
|
||||
www01.bikeshed.org
|
||||
www02.bikeshed.org
|
||||
wheel.colors.com
|
||||
alpha.example.org
|
||||
beta.example.org
|
||||
192.168.1.100
|
||||
192.168.1.110
|
||||
# Your personal website also runs a webserver:
|
||||
myserver.com
|
||||
|
||||
# If you have multiple hosts following a pattern you can specify
|
||||
# them like this:
|
||||
|
||||
www[001:006].example.com
|
||||
|
||||
# Ex 3: A collection of database servers in the 'dbservers' group
|
||||
|
||||
[dbservers]
|
||||
|
||||
db01.intranet.mydomain.net
|
||||
10.25.1.56
|
||||
db02.intranet.mydomain.net
|
||||
10.25.1.56
|
||||
10.25.1.57
|
||||
# Perhaps you serve a db off your personal server too:
|
||||
myserver.com
|
||||
|
||||
# Here's another example of host ranges, this time there are no
|
||||
# leading 0s:
|
||||
|
||||
db-[99:101]-node.example.com
|
||||
|
||||
|
||||
16
examples/playbooks/get_url.yml
Normal file
16
examples/playbooks/get_url.yml
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
- hosts: webservers
|
||||
vars:
|
||||
- jquery_directory: /var/www/html/javascript
|
||||
- person: 'Susie%20Smith'
|
||||
tasks:
|
||||
- name: Create directory for jQuery
|
||||
action: file dest=${jquery_directory} state=directory mode=0755
|
||||
- name: Grab a bunch of jQuery stuff
|
||||
action: get_url url=http://code.jquery.com/$item dest=${jquery_directory} mode=0444
|
||||
with_items:
|
||||
- jquery.min.js
|
||||
- mobile/latest/jquery.mobile.min.js
|
||||
- ui/jquery-ui-git.css
|
||||
#- name: Pass urlencoded name to CGI
|
||||
# action: get_url url=http://example.com/name.cgi?name='${person}' dest=/tmp/test
|
||||
21
examples/playbooks/mysql.yml
Normal file
21
examples/playbooks/mysql.yml
Normal file
@ -0,0 +1,21 @@
|
||||
##
|
||||
# Example Ansible playbook that uses the MySQL module.
|
||||
#
|
||||
|
||||
---
|
||||
- hosts: all
|
||||
user: root
|
||||
|
||||
vars:
|
||||
mysql_root_password: 'password'
|
||||
|
||||
tasks:
|
||||
|
||||
- name: Create database user
|
||||
action: mysql_user loginpass=$mysql_root_password user=bob passwd=12345 priv=*.*:ALL state=present
|
||||
|
||||
- name: Create database
|
||||
action: mysql_db loginpass=$mysql_root_password db=bobdata state=present
|
||||
|
||||
- name: Ensure no user named 'sally' exists
|
||||
action: mysql_user loginpass=$mysql_root_password user=sally state=absent
|
||||
26
examples/playbooks/nested_playbooks.yml
Normal file
26
examples/playbooks/nested_playbooks.yml
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
# it is possible to have top level playbook files import other playbook
|
||||
# files. For example, a playbook called could include three
|
||||
# different playbooks, such as webservers, workers, dbservers, etc.
|
||||
#
|
||||
# Running the site playbook would run all playbooks, while individual
|
||||
# playbooks could still be run directly. This is somewhat like
|
||||
# the tag feature and can be used in conjunction for very fine grained
|
||||
# control over what you want to target when running ansible.
|
||||
|
||||
- name: this is a play at the top level of a file
|
||||
hosts: all
|
||||
user: root
|
||||
tasks:
|
||||
- name: say hi
|
||||
tags: foo
|
||||
action: shell echo "hi..."
|
||||
|
||||
# and this is how we include another playbook, be careful and
|
||||
# don't recurse infinitely or anything. Note you can't use
|
||||
# any variables here.
|
||||
|
||||
- include: intro_example.yml
|
||||
|
||||
# and if we wanted, we can continue with more includes here,
|
||||
# or more plays inline in this file
|
||||
38
examples/playbooks/postgresql.yml
Normal file
38
examples/playbooks/postgresql.yml
Normal file
@ -0,0 +1,38 @@
|
||||
##
|
||||
# Example Ansible playbook that uses the PostgreSQL module.
|
||||
#
|
||||
# This installs PostgreSQL on an Ubuntu system, creates a database called
|
||||
# "myapp" and a user called "django" with password "mysupersecretpassword"
|
||||
# with access to the "myapp" database.
|
||||
#
|
||||
---
|
||||
- hosts: webservers
|
||||
sudo: True
|
||||
gather_facts: False
|
||||
|
||||
tasks:
|
||||
- name: ensure apt cache is up to date
|
||||
action: apt update_cache=yes
|
||||
- name: ensure packages are installed
|
||||
action: apt pkg=$item
|
||||
with_items:
|
||||
- postgresql
|
||||
- libpq-dev
|
||||
- python-psycopg2
|
||||
|
||||
- hosts: webservers
|
||||
sudo: True
|
||||
sudo_user: postgres
|
||||
gather_facts: False
|
||||
|
||||
vars:
|
||||
dbname: myapp
|
||||
dbuser: django
|
||||
dbpassword: mysupersecreetpassword
|
||||
|
||||
tasks:
|
||||
- name: ensure database is created
|
||||
action: postgresql_db db=$dbname
|
||||
|
||||
- name: ensure user has access to database
|
||||
action: postgresql_user db=$dbname user=$dbuser password=$dbpassword
|
||||
@ -12,22 +12,32 @@
|
||||
this_is_a_regular_var: 'moo'
|
||||
so_is_this: 'quack'
|
||||
|
||||
# prompted variables are a list of variable names and a description
|
||||
# that will be presented to the user.
|
||||
#
|
||||
# alternatively, they can ALSO be passed in from the outside:
|
||||
# ansible-playbook foo.yml --extra-vars="foo=100 bar=101"
|
||||
# or through external inventory scripts (see online API docs)
|
||||
|
||||
vars_prompt:
|
||||
release_version: "product release version"
|
||||
# here's basic mode prompting. Specify a hash of variable names and a prompt for
|
||||
# each.
|
||||
#
|
||||
# vars_prompt:
|
||||
# release_version: "product release version"
|
||||
|
||||
tasks:
|
||||
# prompts can also be specified like this, allowing for hiding the prompt as
|
||||
# entered. In the future, this may also be used to support crypted variables
|
||||
|
||||
vars_prompt:
|
||||
- name: "some_password"
|
||||
prompt: "Enter password"
|
||||
private: True
|
||||
- name: "release_version"
|
||||
prompt: "Product release version"
|
||||
private: False
|
||||
|
||||
# this is just a simple example to show that vars_prompt works, but
|
||||
# you might ask for a tag to use with the git module or perhaps
|
||||
# a package version to use with the yum module.
|
||||
|
||||
tasks:
|
||||
- name: imagine this did something interesting with $release_version
|
||||
action: shell echo foo >> /tmp/$release_version-$alpha
|
||||
|
||||
|
||||
44
examples/playbooks/tags.yml
Normal file
44
examples/playbooks/tags.yml
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
# tags allow us to run all of a playbook or part of it.
|
||||
#
|
||||
# assume: ansible-playbook tags.yml --tags foo
|
||||
#
|
||||
# try this with:
|
||||
# --tags foo
|
||||
# --tags bar
|
||||
# --tags extra
|
||||
#
|
||||
# the value of a 'tags:' element can be a string or list
|
||||
# of tag names. Variables are not usable in tag names.
|
||||
|
||||
- name: example play one
|
||||
hosts: all
|
||||
user: root
|
||||
|
||||
# any tags applied to the play are shorthand to applying
|
||||
# the tag to all tasks in it. Here, each task is given
|
||||
# the tag extra
|
||||
|
||||
tags:
|
||||
- extra
|
||||
|
||||
tasks:
|
||||
|
||||
# this task will run if you don't specify any tags,
|
||||
# if you specify 'foo' or if you specify 'extra'
|
||||
|
||||
- name: hi
|
||||
tags: foo
|
||||
action: shell echo "first task ran"
|
||||
|
||||
- name: example play two
|
||||
hosts: all
|
||||
user: root
|
||||
tasks:
|
||||
- name: hi
|
||||
tags:
|
||||
- bar
|
||||
action: shell echo "second task ran"
|
||||
- include: tasks/base.yml tags=base
|
||||
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
# This is a very simple Jinja2 template representing an imaginary configuration file
|
||||
# for an imaginary app.
|
||||
|
||||
# this is an example of loading a fact from the setup module
|
||||
system={{ ansible_system }}
|
||||
|
||||
# here is a variable that could be set in a playbook or inventory file
|
||||
http_port={{ http_port }}
|
||||
|
||||
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
---
|
||||
packager: aptitude
|
||||
packager: apt
|
||||
apache: apache
|
||||
|
||||
@ -69,7 +69,7 @@ conn = xmlrpclib.Server("http://127.0.0.1/cobbler_api", allow_none=True)
|
||||
# executed with no parameters, return the list of
|
||||
# all groups and hosts
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
if len(sys.argv) == 2 and (sys.argv[1] == '--list'):
|
||||
|
||||
systems = conn.get_item_names('system')
|
||||
groups = { 'ungrouped' : [] }
|
||||
@ -103,10 +103,10 @@ if len(sys.argv) == 1:
|
||||
# executed with a hostname as a parameter, return the
|
||||
# variables for that host
|
||||
|
||||
if len(sys.argv) == 2:
|
||||
elif len(sys.argv) == 3 and (sys.argv[1] == '--host'):
|
||||
|
||||
# look up the system record for the given DNS name
|
||||
result = conn.find_system_by_dns_name(sys.argv[1])
|
||||
result = conn.find_system_by_dns_name(sys.argv[2])
|
||||
system = result.get('name', None)
|
||||
data = {}
|
||||
if system is None:
|
||||
@ -125,3 +125,7 @@ if len(sys.argv) == 2:
|
||||
print json.dumps(results)
|
||||
sys.exit(0)
|
||||
|
||||
else:
|
||||
|
||||
print "usage: --list ..OR.. --host <hostname>"
|
||||
sys.exit(1)
|
||||
|
||||
42
examples/scripts/ec2.ini
Normal file
42
examples/scripts/ec2.ini
Normal file
@ -0,0 +1,42 @@
|
||||
# Ansible EC2 external inventory script settings
|
||||
#
|
||||
|
||||
[ec2]
|
||||
|
||||
# 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'
|
||||
regions = all
|
||||
|
||||
# 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:
|
||||
# http://docs.pythonboto.org/en/latest/ref/ec2.html#module-boto.ec2.instance
|
||||
# Below are 2 variables that are used as the address of a server:
|
||||
# - destination_variable
|
||||
# - vpc_destination_variable
|
||||
|
||||
# This is the normal destination variable to use. If you are running Ansible
|
||||
# from outside EC2, then 'public_dns_name' makes the most sense. If you are
|
||||
# running Ansible from within EC2, then perhaps you want to use the internal
|
||||
# address, and should set this to 'private_dns_name'.
|
||||
destination_variable = public_dns_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 with EC2.
|
||||
vpc_destination_variable = ip_address
|
||||
|
||||
# API calls to EC2 are 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:
|
||||
# - ansible-ec2.cache
|
||||
# - ansible-ec2.index
|
||||
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
|
||||
|
||||
|
||||
377
examples/scripts/ec2_external_inventory.py
Executable file
377
examples/scripts/ec2_external_inventory.py
Executable file
@ -0,0 +1,377 @@
|
||||
#!/usr/bin/python -tt
|
||||
|
||||
'''
|
||||
EC2 external inventory script
|
||||
=================================
|
||||
|
||||
Generates inventory that Ansible can understand by making API request to
|
||||
AWS EC2 using the Boto library.
|
||||
|
||||
NOTE: This script assumes Ansible is being executed where the environment
|
||||
variables needed for Boto have already been set:
|
||||
export AWS_ACCESS_KEY_ID='AK123'
|
||||
export AWS_SECRET_ACCESS_KEY='abc123'
|
||||
|
||||
For more details, see: http://docs.pythonboto.org/en/latest/boto_config_tut.html
|
||||
|
||||
When run against a specific host, this script returns the following variables:
|
||||
- ec2_ami_launch_index
|
||||
- ec2_architecture
|
||||
- ec2_association
|
||||
- ec2_attachTime
|
||||
- ec2_attachment
|
||||
- ec2_attachmentId
|
||||
- ec2_client_token
|
||||
- ec2_deleteOnTermination
|
||||
- ec2_description
|
||||
- ec2_deviceIndex
|
||||
- ec2_dns_name
|
||||
- ec2_eventsSet
|
||||
- ec2_group_name
|
||||
- ec2_hypervisor
|
||||
- ec2_id
|
||||
- ec2_image_id
|
||||
- ec2_instanceState
|
||||
- ec2_instance_type
|
||||
- ec2_ipOwnerId
|
||||
- ec2_ip_address
|
||||
- ec2_item
|
||||
- ec2_kernel
|
||||
- ec2_key_name
|
||||
- ec2_launch_time
|
||||
- ec2_monitored
|
||||
- ec2_monitoring
|
||||
- ec2_networkInterfaceId
|
||||
- ec2_ownerId
|
||||
- ec2_persistent
|
||||
- ec2_placement
|
||||
- ec2_platform
|
||||
- ec2_previous_state
|
||||
- ec2_private_dns_name
|
||||
- ec2_private_ip_address
|
||||
- ec2_publicIp
|
||||
- ec2_public_dns_name
|
||||
- ec2_ramdisk
|
||||
- ec2_reason
|
||||
- ec2_region
|
||||
- ec2_requester_id
|
||||
- ec2_root_device_name
|
||||
- ec2_root_device_type
|
||||
- ec2_security_group_ids
|
||||
- ec2_security_group_names
|
||||
- ec2_shutdown_state
|
||||
- ec2_sourceDestCheck
|
||||
- ec2_spot_instance_request_id
|
||||
- ec2_state
|
||||
- ec2_state_code
|
||||
- ec2_state_reason
|
||||
- ec2_status
|
||||
- ec2_subnet_id
|
||||
- ec2_tenancy
|
||||
- ec2_virtualization_type
|
||||
- ec2_vpc_id
|
||||
|
||||
These variables are pulled out of a boto.ec2.instance object. There is a lack of
|
||||
consistency with variable spellings (camelCase and underscores) since this
|
||||
just loops through all variables the object exposes. It is preferred to use the
|
||||
ones with underscores when multiple exist.
|
||||
|
||||
In addition, if an instance has AWS Tags associated with it, each tag is a new
|
||||
variable named:
|
||||
- ec2_tag_[Key] = [Value]
|
||||
|
||||
Security groups are comma-separated in 'ec2_security_group_ids' and
|
||||
'ec2_security_group_names'.
|
||||
'''
|
||||
|
||||
# (c) 2012, Peter Sankauskas
|
||||
#
|
||||
# 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 os
|
||||
import argparse
|
||||
import re
|
||||
from time import time
|
||||
from boto import ec2
|
||||
import ConfigParser
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
|
||||
class Ec2Inventory(object):
|
||||
def __init__(self):
|
||||
''' Main execution path '''
|
||||
|
||||
# Inventory grouped by instance IDs, tags, security groups, regions,
|
||||
# and availability zones
|
||||
self.inventory = {}
|
||||
|
||||
# Index of hostname (address) to instance ID
|
||||
self.index = {}
|
||||
|
||||
# Read settings and parse CLI arguments
|
||||
self.read_settings()
|
||||
self.parse_cli_args()
|
||||
|
||||
# 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 instances for inventory
|
||||
if len(self.inventory) == 0:
|
||||
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 ec2.ini file '''
|
||||
|
||||
config = ConfigParser.SafeConfigParser()
|
||||
config.read(os.path.dirname(os.path.realpath(__file__)) + '/ec2.ini')
|
||||
|
||||
# Regions
|
||||
self.regions = []
|
||||
configRegions = config.get('ec2', 'regions')
|
||||
if (configRegions == 'all'):
|
||||
for regionInfo in ec2.regions():
|
||||
self.regions.append(regionInfo.name)
|
||||
else:
|
||||
self.regions = configRegions.split(",")
|
||||
|
||||
# Destination addresses
|
||||
self.destination_variable = config.get('ec2', 'destination_variable')
|
||||
self.vpc_destination_variable = config.get('ec2', 'vpc_destination_variable')
|
||||
|
||||
# Cache related
|
||||
cache_path = config.get('ec2', 'cache_path')
|
||||
self.cache_path_cache = cache_path + "/ansible-ec2.cache"
|
||||
self.cache_path_index = cache_path + "/ansible-ec2.index"
|
||||
self.cache_max_age = config.getint('ec2', 'cache_max_age')
|
||||
|
||||
|
||||
def parse_cli_args(self):
|
||||
''' Command line argument processing '''
|
||||
|
||||
parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on EC2')
|
||||
parser.add_argument('--list', action='store_true', default=True,
|
||||
help='List instances (default: True)')
|
||||
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 EC2 (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 region in self.regions:
|
||||
self.get_instances_by_region(region)
|
||||
|
||||
self.write_to_cache(self.inventory, self.cache_path_cache)
|
||||
self.write_to_cache(self.index, self.cache_path_index)
|
||||
|
||||
|
||||
def get_instances_by_region(self, region):
|
||||
''' Makes an AWS EC2 API call to the list of instances in a particular
|
||||
region '''
|
||||
|
||||
conn = ec2.connect_to_region(region)
|
||||
reservations = conn.get_all_instances()
|
||||
for reservation in reservations:
|
||||
for instance in reservation.instances:
|
||||
self.add_instance(instance, region)
|
||||
|
||||
|
||||
def get_instance(self, region, instance_id):
|
||||
''' Gets details about a specific instance '''
|
||||
conn = ec2.connect_to_region(region)
|
||||
reservations = conn.get_all_instances([instance_id])
|
||||
for reservation in reservations:
|
||||
for instance in reservation.instances:
|
||||
return instance
|
||||
|
||||
|
||||
def add_instance(self, instance, region):
|
||||
''' Adds an instance to the inventory and index, as long as it is
|
||||
addressable '''
|
||||
|
||||
# Only want running instances
|
||||
if instance.state == 'terminated':
|
||||
return
|
||||
|
||||
# Select the best destination address
|
||||
if instance.subnet_id:
|
||||
dest = getattr(instance, self.vpc_destination_variable)
|
||||
else:
|
||||
dest = getattr(instance, self.destination_variable)
|
||||
|
||||
if dest == None:
|
||||
# Skip instances we cannot address (e.g. private VPC subnet)
|
||||
return
|
||||
|
||||
# Add to index
|
||||
self.index[dest] = [region, instance.id]
|
||||
|
||||
# Inventory: Group by instance ID (always a group of 1)
|
||||
self.inventory[instance.id] = [dest]
|
||||
|
||||
# Inventory: Group by region
|
||||
self.push(self.inventory, region, dest)
|
||||
|
||||
# Inventory: Group by availability zone
|
||||
self.push(self.inventory, instance.placement, dest)
|
||||
|
||||
# Inventory: Group by security group
|
||||
for group in instance.groups:
|
||||
key = self.to_safe("security_group_" + group.name)
|
||||
self.push(self.inventory, key, dest)
|
||||
|
||||
# Inventory: Group by tag keys
|
||||
for k, v in instance.tags.iteritems():
|
||||
key = self.to_safe("tag_" + k + "=" + v)
|
||||
self.push(self.inventory, key, dest)
|
||||
|
||||
|
||||
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()
|
||||
|
||||
(region, instance_id) = self.index[self.args.host]
|
||||
instance = self.get_instance(region, instance_id)
|
||||
instance_vars = {}
|
||||
for key in vars(instance):
|
||||
value = getattr(instance, key)
|
||||
key = self.to_safe('ec2_' + key)
|
||||
|
||||
# Handle complex types
|
||||
if type(value) in [int, bool]:
|
||||
instance_vars[key] = value
|
||||
elif type(value) in [str, unicode]:
|
||||
instance_vars[key] = value.strip()
|
||||
elif type(value) == type(None):
|
||||
instance_vars[key] = ''
|
||||
elif key == 'ec2_region':
|
||||
instance_vars[key] = value.name
|
||||
elif key == 'ec2_tags':
|
||||
for k, v in value.iteritems():
|
||||
key = self.to_safe('ec2_tag_' + k)
|
||||
instance_vars[key] = v
|
||||
elif key == 'ec2_groups':
|
||||
group_ids = []
|
||||
group_names = []
|
||||
for group in value:
|
||||
group_ids.append(group.id)
|
||||
group_names.append(group.name)
|
||||
instance_vars["ec2_security_group_ids"] = ','.join(group_ids)
|
||||
instance_vars["ec2_security_group_names"] = ','.join(group_names)
|
||||
else:
|
||||
pass
|
||||
# TODO Product codes if someone finds them useful
|
||||
#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
|
||||
the dict '''
|
||||
|
||||
if key in my_dict:
|
||||
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
|
||||
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 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 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
|
||||
Ec2Inventory()
|
||||
|
||||
206
examples/scripts/yaml_to_ini.py
Normal file
206
examples/scripts/yaml_to_ini.py
Normal file
@ -0,0 +1,206 @@
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@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/>.
|
||||
|
||||
import ansible.constants as C
|
||||
from ansible.inventory.host import Host
|
||||
from ansible.inventory.group import Group
|
||||
from ansible import errors
|
||||
from ansible import utils
|
||||
import os
|
||||
import yaml
|
||||
import sys
|
||||
|
||||
class InventoryParserYaml(object):
|
||||
''' Host inventory parser for ansible '''
|
||||
|
||||
def __init__(self, filename=C.DEFAULT_HOST_LIST):
|
||||
|
||||
sys.stderr.write("WARNING: YAML inventory files are deprecated in 0.6 and will be removed in 0.7, to migrate" +
|
||||
" download and run https://github.com/ansible/ansible/blob/devel/examples/scripts/yaml_to_ini.py\n")
|
||||
|
||||
fh = open(filename)
|
||||
data = fh.read()
|
||||
fh.close()
|
||||
self._hosts = {}
|
||||
self._parse(data)
|
||||
|
||||
def _make_host(self, hostname):
|
||||
|
||||
if hostname in self._hosts:
|
||||
return self._hosts[hostname]
|
||||
else:
|
||||
host = Host(hostname)
|
||||
self._hosts[hostname] = host
|
||||
return host
|
||||
|
||||
# see file 'test/yaml_hosts' for syntax
|
||||
|
||||
def _parse(self, data):
|
||||
# FIXME: refactor into subfunctions
|
||||
|
||||
all = Group('all')
|
||||
|
||||
ungrouped = Group('ungrouped')
|
||||
all.add_child_group(ungrouped)
|
||||
|
||||
self.groups = dict(all=all, ungrouped=ungrouped)
|
||||
grouped_hosts = []
|
||||
|
||||
yaml = utils.parse_yaml(data)
|
||||
|
||||
# first add all groups
|
||||
for item in yaml:
|
||||
if type(item) == dict and 'group' in item:
|
||||
group = Group(item['group'])
|
||||
|
||||
for subresult in item.get('hosts',[]):
|
||||
|
||||
if type(subresult) in [ str, unicode ]:
|
||||
host = self._make_host(subresult)
|
||||
group.add_host(host)
|
||||
grouped_hosts.append(host)
|
||||
elif type(subresult) == dict:
|
||||
host = self._make_host(subresult['host'])
|
||||
vars = subresult.get('vars',{})
|
||||
if type(vars) == list:
|
||||
for subitem in vars:
|
||||
for (k,v) in subitem.items():
|
||||
host.set_variable(k,v)
|
||||
elif type(vars) == dict:
|
||||
for (k,v) in subresult.get('vars',{}).items():
|
||||
host.set_variable(k,v)
|
||||
else:
|
||||
raise errors.AnsibleError("unexpected type for variable")
|
||||
group.add_host(host)
|
||||
grouped_hosts.append(host)
|
||||
|
||||
vars = item.get('vars',{})
|
||||
if type(vars) == dict:
|
||||
for (k,v) in item.get('vars',{}).items():
|
||||
group.set_variable(k,v)
|
||||
elif type(vars) == list:
|
||||
for subitem in vars:
|
||||
if type(subitem) != dict:
|
||||
raise errors.AnsibleError("expected a dictionary")
|
||||
for (k,v) in subitem.items():
|
||||
group.set_variable(k,v)
|
||||
|
||||
self.groups[group.name] = group
|
||||
all.add_child_group(group)
|
||||
|
||||
# add host definitions
|
||||
for item in yaml:
|
||||
if type(item) in [ str, unicode ]:
|
||||
host = self._make_host(item)
|
||||
if host not in grouped_hosts:
|
||||
ungrouped.add_host(host)
|
||||
|
||||
elif type(item) == dict and 'host' in item:
|
||||
host = self._make_host(item['host'])
|
||||
|
||||
vars = item.get('vars', {})
|
||||
if type(vars)==list:
|
||||
varlist, vars = vars, {}
|
||||
for subitem in varlist:
|
||||
vars.update(subitem)
|
||||
for (k,v) in vars.items():
|
||||
host.set_variable(k,v)
|
||||
|
||||
groups = item.get('groups', {})
|
||||
if type(groups) in [ str, unicode ]:
|
||||
groups = [ groups ]
|
||||
if type(groups)==list:
|
||||
for subitem in groups:
|
||||
if subitem in self.groups:
|
||||
group = self.groups[subitem]
|
||||
else:
|
||||
group = Group(subitem)
|
||||
self.groups[group.name] = group
|
||||
all.add_child_group(group)
|
||||
group.add_host(host)
|
||||
grouped_hosts.append(host)
|
||||
|
||||
if host not in grouped_hosts:
|
||||
ungrouped.add_host(host)
|
||||
|
||||
# make sure ungrouped.hosts is the complement of grouped_hosts
|
||||
ungrouped_hosts = [host for host in ungrouped.hosts if host not in grouped_hosts]
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print "usage: yaml_to_ini.py /path/to/ansible/hosts"
|
||||
sys.exit(1)
|
||||
|
||||
result = ""
|
||||
|
||||
original = sys.argv[1]
|
||||
yamlp = InventoryParserYaml(filename=sys.argv[1])
|
||||
dirname = os.path.dirname(original)
|
||||
|
||||
group_names = [ g.name for g in yamlp.groups.values() ]
|
||||
|
||||
for group_name in sorted(group_names):
|
||||
|
||||
record = yamlp.groups[group_name]
|
||||
|
||||
if group_name == 'all':
|
||||
continue
|
||||
|
||||
hosts = record.hosts
|
||||
result = result + "[%s]\n" % record.name
|
||||
for h in hosts:
|
||||
result = result + "%s\n" % h.name
|
||||
result = result + "\n"
|
||||
|
||||
groupfiledir = os.path.join(dirname, "group_vars")
|
||||
if not os.path.exists(groupfiledir):
|
||||
print "* creating: %s" % groupfiledir
|
||||
os.makedirs(groupfiledir)
|
||||
groupfile = os.path.join(groupfiledir, group_name)
|
||||
print "* writing group variables for %s into %s" % (group_name, groupfile)
|
||||
groupfh = open(groupfile, 'w')
|
||||
groupfh.write(yaml.dump(record.get_variables()))
|
||||
groupfh.close()
|
||||
|
||||
for (host_name, host_record) in yamlp._hosts.iteritems():
|
||||
hostfiledir = os.path.join(dirname, "host_vars")
|
||||
if not os.path.exists(hostfiledir):
|
||||
print "* creating: %s" % hostfiledir
|
||||
os.makedirs(hostfiledir)
|
||||
hostfile = os.path.join(hostfiledir, host_record.name)
|
||||
print "* writing host variables for %s into %s" % (host_record.name, hostfile)
|
||||
hostfh = open(hostfile, 'w')
|
||||
hostfh.write(yaml.dump(host_record.get_variables()))
|
||||
hostfh.close()
|
||||
|
||||
|
||||
# also need to keep a hash of variables per each host
|
||||
# and variables per each group
|
||||
# and write those to disk
|
||||
|
||||
newfilepath = os.path.join(dirname, "hosts.new")
|
||||
fdh = open(newfilepath, 'w')
|
||||
fdh.write(result)
|
||||
fdh.close()
|
||||
|
||||
print "* COMPLETE: review your new inventory file and replace your original when ready"
|
||||
print "* new inventory file saved as %s" % newfilepath
|
||||
print "* edit group specific variables in %s/group_vars/" % dirname
|
||||
print "* edit host specific variables in %s/host_vars/" % dirname
|
||||
|
||||
# now need to write this to disk as (oldname).new
|
||||
# and inform the user
|
||||
@ -1,20 +1,35 @@
|
||||
#!/bin/bash
|
||||
# usage: source ./hacking/env-setup
|
||||
# usage: source ./hacking/env-setup [-q]
|
||||
# modifies environment for running Ansible from checkout
|
||||
|
||||
PREFIX_PYTHONPATH="$PWD/lib"
|
||||
PREFIX_PATH="$PWD/bin"
|
||||
PREFIX_MANPATH="$PWD/docs/man"
|
||||
# When run using source as directed, $0 gets set to bash, so we must use $BASH_SOURCE
|
||||
if [ -n "$BASH_SOURCE" ] ; then
|
||||
HACKING_DIR=`dirname $BASH_SOURCE`
|
||||
else
|
||||
HACKING_DIR="$PWD/hacking"
|
||||
fi
|
||||
# The below is an alternative to readlink -fn which doesn't exist on OS X
|
||||
# Source: http://stackoverflow.com/a/1678636
|
||||
FULL_PATH=`python -c "import os, sys; print os.path.realpath('$HACKING_DIR')"`
|
||||
ANSIBLE_HOME=`dirname $FULL_PATH`
|
||||
|
||||
PREFIX_PYTHONPATH="$ANSIBLE_HOME/lib"
|
||||
PREFIX_PATH="$ANSIBLE_HOME/bin"
|
||||
PREFIX_MANPATH="$ANSIBLE_HOME/docs/man"
|
||||
|
||||
export PYTHONPATH=$PREFIX_PYTHONPATH:$PYTHONPATH
|
||||
export PATH=$PREFIX_PATH:$PATH
|
||||
export ANSIBLE_LIBRARY="$PWD/library"
|
||||
export ANSIBLE_LIBRARY="$ANSIBLE_HOME/library"
|
||||
export MANPATH=$PREFIX_MANPATH:$MANPATH
|
||||
|
||||
echo "PATH=$PATH"
|
||||
echo "PYTHONPATH=$PYTHONPATH"
|
||||
echo "ANSIBLE_LIBRARY=$ANSIBLE_LIBRARY"
|
||||
echo "MANPATH=$MANPATH"
|
||||
# Print out values unless -q is set
|
||||
|
||||
echo "Reminder: specify your host file with -i"
|
||||
echo "Done."
|
||||
if [ $# -eq 0 -o "$1" != "-q" ] ; then
|
||||
echo "PATH=$PATH"
|
||||
echo "PYTHONPATH=$PYTHONPATH"
|
||||
echo "ANSIBLE_LIBRARY=$ANSIBLE_LIBRARY"
|
||||
echo "MANPATH=$MANPATH"
|
||||
|
||||
echo "Reminder: specify your host file with -i"
|
||||
echo "Done."
|
||||
fi
|
||||
|
||||
@ -23,69 +23,129 @@
|
||||
# modules
|
||||
#
|
||||
# example:
|
||||
# test-module ../library/command /bin/sleep 3
|
||||
# test-module ../library/service name=httpd ensure=restarted
|
||||
# test-module -m ../library/command -a "/bin/sleep 3"
|
||||
# test-module -m ../library/service -a "name=httpd ensure=restarted"
|
||||
# test-module -m ../library/service -a "name=httpd ensure=restarted" --debugger /usr/bin/pdb
|
||||
|
||||
import sys
|
||||
import base64
|
||||
import os
|
||||
import subprocess
|
||||
import traceback
|
||||
from ansible import utils
|
||||
import optparse
|
||||
import ansible.utils as utils
|
||||
import ansible.module_common as module_common
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
modfile = None
|
||||
def parse():
|
||||
"""parse command line
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
print >>sys.stderr, "usage: test-module ./library/command [key=value ...]"
|
||||
sys.exit(1)
|
||||
:return : (options, args)"""
|
||||
parser = optparse.OptionParser()
|
||||
|
||||
modfile = sys.argv[1]
|
||||
if len(sys.argv) > 1:
|
||||
args = " ".join(sys.argv[2:])
|
||||
else:
|
||||
args = ""
|
||||
parser.usage = "%prog -[options] (-h for help)"
|
||||
|
||||
argspath = os.path.expanduser("~/.ansible_test_module_arguments")
|
||||
argsfile = open(argspath, 'w')
|
||||
argsfile.write(args)
|
||||
argsfile.close()
|
||||
parser.add_option('-m', '--module-path', dest='module_path',
|
||||
help="REQUIRED: full path of module source to execute")
|
||||
parser.add_option('-a', '--args', dest='module_args', default="",
|
||||
help="module argument string")
|
||||
parser.add_option('-D', '--debugger', dest='debugger',
|
||||
help="path to python debugger (e.g. /usr/bin/pdb)")
|
||||
options, args = parser.parse_args()
|
||||
if not options.module_path:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
else:
|
||||
return options, args
|
||||
|
||||
os.system("chmod +x %s" % modfile)
|
||||
cmd = subprocess.Popen("%s %s" % (modfile, argspath),
|
||||
shell=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
(out, err) = cmd.communicate()
|
||||
def write_argsfile(argstring):
|
||||
""" Write args to a file for old-style module's use. """
|
||||
argspath = os.path.expanduser("~/.ansible_test_module_arguments")
|
||||
argsfile = open(argspath, 'w')
|
||||
argsfile.write(argstring)
|
||||
argsfile.close()
|
||||
return argspath
|
||||
|
||||
def boilerplate_module(modfile, args):
|
||||
""" simulate what ansible does with new style modules """
|
||||
|
||||
module_fh = open(modfile)
|
||||
module_data = module_fh.read()
|
||||
included_boilerplate = module_data.find(module_common.REPLACER) != -1
|
||||
module_fh.close()
|
||||
|
||||
if included_boilerplate:
|
||||
|
||||
module_data = module_data.replace(module_common.REPLACER, module_common.MODULE_COMMON)
|
||||
encoded_args = "\"\"\"%s\"\"\"" % args.replace("\"","\\\"")
|
||||
module_data = module_data.replace(module_common.REPLACER_ARGS, encoded_args)
|
||||
|
||||
modfile2_path = os.path.expanduser("~/.ansible_module_generated")
|
||||
print "* including generated source, if any, saving to: %s" % modfile2_path
|
||||
print "* this will offset any line numbers in tracebacks/debuggers!"
|
||||
modfile2 = open(modfile2_path, 'w')
|
||||
modfile2.write(module_data)
|
||||
modfile2.close()
|
||||
modfile = modfile2_path
|
||||
return (modfile2_path, included_boilerplate)
|
||||
else:
|
||||
print "* module boilerplate substitution not requested in module, line numbers will be unaltered"
|
||||
return (modfile, included_boilerplate)
|
||||
|
||||
def runtest( modfile, argspath):
|
||||
"""Test run a module, piping it's output for reporting."""
|
||||
|
||||
os.system("chmod +x %s" % modfile)
|
||||
|
||||
invoke = "%s" % (modfile)
|
||||
if argspath is not None:
|
||||
invoke = "%s %s" % (modfile, argspath)
|
||||
|
||||
cmd = subprocess.Popen(invoke, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(out, err) = cmd.communicate()
|
||||
|
||||
try:
|
||||
print "***********************************"
|
||||
print "RAW OUTPUT"
|
||||
print out
|
||||
print err
|
||||
results = utils.parse_json(out)
|
||||
except:
|
||||
print "***********************************"
|
||||
print "INVALID OUTPUT FORMAT"
|
||||
print out
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
if err and err != '':
|
||||
print "***********************************"
|
||||
print "RECIEVED DATA ON STDERR, THIS WILL CRASH YOUR MODULE"
|
||||
print err
|
||||
sys.exit(1)
|
||||
print "PARSED OUTPUT"
|
||||
print utils.jsonify(results,format=True)
|
||||
|
||||
try:
|
||||
print "***********************************"
|
||||
print "RAW OUTPUT"
|
||||
print out
|
||||
results = utils.parse_json(out)
|
||||
|
||||
except:
|
||||
print "***********************************"
|
||||
print "INVALID OUTPUT FORMAT"
|
||||
print out
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
def rundebug(debugger, modfile, argspath):
|
||||
"""Run interactively with console debugger."""
|
||||
|
||||
print "***********************************"
|
||||
print "PARSED OUTPUT"
|
||||
if argspath is not None:
|
||||
subprocess.call("%s %s %s" % (debugger, modfile, argspath), shell=True)
|
||||
else:
|
||||
subprocess.call("%s %s" % (debugger, modfile), shell=True)
|
||||
|
||||
print utils.bigjson(results)
|
||||
|
||||
sys.exit(0)
|
||||
def main():
|
||||
|
||||
options, args = parse()
|
||||
(modfile, is_new_style) = boilerplate_module(options.module_path, options.module_args)
|
||||
argspath=None
|
||||
if not is_new_style:
|
||||
argspath = write_argsfile(options.module_args)
|
||||
if options.debugger:
|
||||
rundebug(options.debugger, modfile, argspath)
|
||||
else:
|
||||
runtest(modfile, argspath)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
|
||||
@ -14,5 +14,5 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
__version__ = '0.5'
|
||||
__version__ = '0.6'
|
||||
__author__ = 'Michael DeHaan'
|
||||
|
||||
@ -15,20 +15,24 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#######################################################
|
||||
|
||||
import utils
|
||||
import sys
|
||||
import getpass
|
||||
import os
|
||||
import subprocess
|
||||
from ansible.color import stringc
|
||||
|
||||
#######################################################
|
||||
cowsay = None
|
||||
if os.path.exists("/usr/bin/cowsay"):
|
||||
cowsay = "/usr/bin/cowsay"
|
||||
elif os.path.exists("/usr/games/cowsay"):
|
||||
cowsay = "/usr/games/cowsay"
|
||||
|
||||
class AggregateStats(object):
|
||||
''' holds stats about per-host activity during playbook runs '''
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.processed = {}
|
||||
self.failures = {}
|
||||
self.ok = {}
|
||||
@ -76,17 +80,66 @@ class AggregateStats(object):
|
||||
|
||||
########################################################################
|
||||
|
||||
def regular_generic_msg(hostname, result, oneline, caption):
|
||||
''' output on the result of a module run that is not command '''
|
||||
|
||||
if not oneline:
|
||||
return "%s | %s >> %s\n" % (hostname, caption, utils.jsonify(result,format=True))
|
||||
else:
|
||||
return "%s | %s >> %s\n" % (hostname, caption, utils.jsonify(result))
|
||||
|
||||
|
||||
def banner(msg):
|
||||
res = ""
|
||||
if os.path.exists("/usr/bin/cowsay"):
|
||||
cmd = subprocess.Popen("/usr/bin/cowsay -W 60 \"%s\"" % msg,
|
||||
|
||||
if cowsay != None:
|
||||
cmd = subprocess.Popen("%s -W 60 \"%s\"" % (cowsay, msg),
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
|
||||
(out, err) = cmd.communicate()
|
||||
res = "%s\n" % out
|
||||
return "%s\n" % out
|
||||
else:
|
||||
res = "%s ********************* \n" % msg
|
||||
return res
|
||||
|
||||
return "\n%s ********************* " % msg
|
||||
|
||||
def command_generic_msg(hostname, result, oneline, caption):
|
||||
''' output the result of a command run '''
|
||||
|
||||
rc = result.get('rc', '0')
|
||||
stdout = result.get('stdout','')
|
||||
stderr = result.get('stderr', '')
|
||||
msg = result.get('msg', '')
|
||||
|
||||
if not oneline:
|
||||
buf = "%s | %s | rc=%s >>\n" % (hostname, caption, result.get('rc',0))
|
||||
if stdout:
|
||||
buf += stdout
|
||||
if stderr:
|
||||
buf += stderr
|
||||
if msg:
|
||||
buf += msg
|
||||
return buf + "\n"
|
||||
else:
|
||||
if stderr:
|
||||
return "%s | %s | rc=%s | (stdout) %s (stderr) %s\n" % (hostname, caption, rc, stdout, stderr)
|
||||
else:
|
||||
return "%s | %s | rc=%s | (stdout) %s\n" % (hostname, caption, rc, stdout)
|
||||
|
||||
def host_report_msg(hostname, module_name, result, oneline):
|
||||
''' summarize the JSON results for a particular host '''
|
||||
|
||||
failed = utils.is_failed(result)
|
||||
msg = ''
|
||||
if module_name in [ 'command', 'shell', 'raw' ] and 'ansible_job_id' not in result:
|
||||
if not failed:
|
||||
msg = command_generic_msg(hostname, result, oneline, 'success')
|
||||
else:
|
||||
msg = command_generic_msg(hostname, result, oneline, 'FAILED')
|
||||
else:
|
||||
if not failed:
|
||||
msg = regular_generic_msg(hostname, result, oneline, 'success')
|
||||
else:
|
||||
msg = regular_generic_msg(hostname, result, oneline, 'FAILED')
|
||||
return msg
|
||||
|
||||
###############################################
|
||||
|
||||
class DefaultRunnerCallbacks(object):
|
||||
''' no-op callbacks for API usage of Runner() if no callbacks are specified '''
|
||||
@ -94,7 +147,7 @@ class DefaultRunnerCallbacks(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def on_failed(self, host, res):
|
||||
def on_failed(self, host, res, ignore_errors=False):
|
||||
pass
|
||||
|
||||
def on_ok(self, host, res):
|
||||
@ -127,33 +180,43 @@ class CliRunnerCallbacks(DefaultRunnerCallbacks):
|
||||
''' callbacks for use by /usr/bin/ansible '''
|
||||
|
||||
def __init__(self):
|
||||
|
||||
# set by /usr/bin/ansible later
|
||||
self.options = None
|
||||
self._async_notified = {}
|
||||
|
||||
def on_failed(self, host, res):
|
||||
def on_failed(self, host, res, ignore_errors=False):
|
||||
|
||||
self._on_any(host,res)
|
||||
|
||||
def on_ok(self, host, res):
|
||||
|
||||
self._on_any(host,res)
|
||||
|
||||
def on_unreachable(self, host, res):
|
||||
|
||||
if type(res) == dict:
|
||||
res = res.get('msg','')
|
||||
print "%s | FAILED => %s" % (host, res)
|
||||
if self.options.tree:
|
||||
utils.write_tree_file(self.options.tree, host, utils.bigjson(dict(failed=True, msg=res)))
|
||||
utils.write_tree_file(
|
||||
self.options.tree, host,
|
||||
utils.jsonify(dict(failed=True, msg=res),format=True)
|
||||
)
|
||||
|
||||
def on_skipped(self, host):
|
||||
pass
|
||||
|
||||
def on_error(self, host, err):
|
||||
|
||||
print >>sys.stderr, "err: [%s] => %s\n" % (host, err)
|
||||
|
||||
def on_no_hosts(self):
|
||||
|
||||
print >>sys.stderr, "no hosts matched\n"
|
||||
|
||||
def on_async_poll(self, host, res, jid, clock):
|
||||
|
||||
if jid not in self._async_notified:
|
||||
self._async_notified[jid] = clock + 1
|
||||
if self._async_notified[jid] > clock:
|
||||
@ -161,15 +224,18 @@ class CliRunnerCallbacks(DefaultRunnerCallbacks):
|
||||
print "<job %s> polling, %ss remaining"%(jid, clock)
|
||||
|
||||
def on_async_ok(self, host, res, jid):
|
||||
print "<job %s> finished on %s => %s"%(jid, host, utils.bigjson(res))
|
||||
|
||||
print "<job %s> finished on %s => %s"%(jid, host, utils.jsonify(res,format=True))
|
||||
|
||||
def on_async_failed(self, host, res, jid):
|
||||
print "<job %s> FAILED on %s => %s"%(jid, host, utils.bigjson(res))
|
||||
|
||||
print "<job %s> FAILED on %s => %s"%(jid, host, utils.jsonify(res,format=True))
|
||||
|
||||
def _on_any(self, host, result):
|
||||
print utils.host_report_msg(host, self.options.module_name, result, self.options.one_line)
|
||||
|
||||
print host_report_msg(host, self.options.module_name, result, self.options.one_line)
|
||||
if self.options.tree:
|
||||
utils.write_tree_file(self.options.tree, host, utils.bigjson(result))
|
||||
utils.write_tree_file(self.options.tree, host, utils.jsonify(result,format=True))
|
||||
|
||||
########################################################################
|
||||
|
||||
@ -177,44 +243,105 @@ class PlaybookRunnerCallbacks(DefaultRunnerCallbacks):
|
||||
''' callbacks used for Runner() from /usr/bin/ansible-playbook '''
|
||||
|
||||
def __init__(self, stats, verbose=False):
|
||||
|
||||
self.stats = stats
|
||||
self._async_notified = {}
|
||||
self.verbose = verbose
|
||||
|
||||
def on_unreachable(self, host, msg):
|
||||
print "fatal: [%s] => %s" % (host, msg)
|
||||
|
||||
def on_failed(self, host, results):
|
||||
print "failed: [%s] => %s\n" % (host, utils.smjson(results))
|
||||
item = None
|
||||
if type(msg) == dict:
|
||||
item = msg.get('item', None)
|
||||
|
||||
if item:
|
||||
print "fatal: [%s] => (item=%s) => %s" % (host, item, msg)
|
||||
else:
|
||||
print "fatal: [%s] => %s" % (host, msg)
|
||||
|
||||
def on_failed(self, host, results, ignore_errors=False):
|
||||
|
||||
item = results.get('item', None)
|
||||
|
||||
if item:
|
||||
msg = "failed: [%s] => (item=%s) => %s" % (host, item, utils.jsonify(results))
|
||||
else:
|
||||
msg = "failed: [%s] => %s" % (host, utils.jsonify(results))
|
||||
|
||||
print stringc(msg, 'red')
|
||||
if ignore_errors:
|
||||
print stringc("...ignoring", 'yellow')
|
||||
|
||||
def on_ok(self, host, host_result):
|
||||
|
||||
item = host_result.get('item', None)
|
||||
|
||||
# show verbose output for non-setup module results if --verbose is used
|
||||
msg = ''
|
||||
if not self.verbose or host_result.get("verbose_override",None) is not None:
|
||||
print "ok: [%s]\n" % (host)
|
||||
if item:
|
||||
msg = "ok: [%s] => (item=%s)" % (host,item)
|
||||
else:
|
||||
if 'ansible_job_id' not in host_result or 'finished' in host_result:
|
||||
msg = "ok: [%s]" % (host)
|
||||
else:
|
||||
print "ok: [%s] => %s" % (host, utils.smjson(host_result))
|
||||
# verbose ...
|
||||
if item:
|
||||
msg = "ok: [%s] => (item=%s) => %s" % (host, item, utils.jsonify(host_result))
|
||||
else:
|
||||
if 'ansible_job_id' not in host_result or 'finished' in host_result:
|
||||
msg = "ok: [%s] => %s" % (host, utils.jsonify(host_result))
|
||||
|
||||
if msg != '':
|
||||
if not 'changed' in host_result or not host_result['changed']:
|
||||
print stringc(msg, 'green')
|
||||
else:
|
||||
print stringc(msg, 'yellow')
|
||||
|
||||
def on_error(self, host, err):
|
||||
print >>sys.stderr, "err: [%s] => %s\n" % (host, err)
|
||||
|
||||
def on_skipped(self, host):
|
||||
print "skipping: [%s]\n" % host
|
||||
item = err.get('item', None)
|
||||
msg = ''
|
||||
if item:
|
||||
msg = "err: [%s] => (item=%s) => %s" % (host, item, err)
|
||||
else:
|
||||
msg = "err: [%s] => %s" % (host, err)
|
||||
|
||||
msg = stringc(msg, 'red')
|
||||
print >>sys.stderr, msg
|
||||
|
||||
def on_skipped(self, host, item=None):
|
||||
|
||||
msg = ''
|
||||
if item:
|
||||
msg = "skipping: [%s] => (item=%s)" % (host, item)
|
||||
else:
|
||||
msg = "skipping: [%s]" % host
|
||||
print stringc(msg, 'yellow')
|
||||
|
||||
def on_no_hosts(self):
|
||||
print "no hosts matched or remaining\n"
|
||||
|
||||
print stringc("no hosts matched or remaining\n", 'red')
|
||||
|
||||
def on_async_poll(self, host, res, jid, clock):
|
||||
|
||||
if jid not in self._async_notified:
|
||||
self._async_notified[jid] = clock + 1
|
||||
if self._async_notified[jid] > clock:
|
||||
self._async_notified[jid] = clock
|
||||
print "<job %s> polling, %ss remaining"%(jid, clock)
|
||||
msg = "<job %s> polling, %ss remaining"%(jid, clock)
|
||||
print stringc(msg, 'cyan')
|
||||
|
||||
def on_async_ok(self, host, res, jid):
|
||||
print "<job %s> finished on %s"%(jid, host)
|
||||
|
||||
msg = "<job %s> finished on %s"%(jid, host)
|
||||
print stringc(msg, 'cyan')
|
||||
|
||||
def on_async_failed(self, host, res, jid):
|
||||
print "<job %s> FAILED on %s"%(jid, host)
|
||||
|
||||
msg = "<job %s> FAILED on %s"%(jid, host)
|
||||
print stringc(msg, 'red')
|
||||
|
||||
|
||||
########################################################################
|
||||
|
||||
@ -222,34 +349,50 @@ class PlaybookCallbacks(object):
|
||||
''' playbook.py callbacks used by /usr/bin/ansible-playbook '''
|
||||
|
||||
def __init__(self, verbose=False):
|
||||
|
||||
self.verbose = verbose
|
||||
|
||||
def on_start(self):
|
||||
print "\n"
|
||||
|
||||
pass
|
||||
|
||||
def on_notify(self, host, handler):
|
||||
|
||||
pass
|
||||
|
||||
def on_task_start(self, name, is_conditional):
|
||||
print banner(utils.task_start_msg(name, is_conditional))
|
||||
|
||||
def on_vars_prompt(self, varname, private=True):
|
||||
msg = 'input for %s: ' % varname
|
||||
msg = "TASK: [%s]" % name
|
||||
if is_conditional:
|
||||
msg = "NOTIFIED: [%s]" % name
|
||||
print banner(msg)
|
||||
|
||||
def on_vars_prompt(self, varname, private=True, prompt=None):
|
||||
|
||||
if prompt:
|
||||
msg = prompt
|
||||
else:
|
||||
msg = 'input for %s: ' % varname
|
||||
if private:
|
||||
return getpass.getpass(msg)
|
||||
return raw_input(msg)
|
||||
|
||||
def on_setup_primary(self):
|
||||
print banner("SETUP PHASE")
|
||||
|
||||
def on_setup_secondary(self):
|
||||
print banner("VARIABLE IMPORT PHASE")
|
||||
def on_setup(self):
|
||||
|
||||
print banner("GATHERING FACTS")
|
||||
|
||||
def on_import_for_host(self, host, imported_file):
|
||||
print "%s: importing %s" % (host, imported_file)
|
||||
|
||||
msg = "%s: importing %s" % (host, imported_file)
|
||||
print stringc(msg, 'cyan')
|
||||
|
||||
def on_not_import_for_host(self, host, missing_file):
|
||||
print "%s: not importing file: %s" % (host, missing_file)
|
||||
|
||||
msg = "%s: not importing file: %s" % (host, missing_file)
|
||||
print stringc(msg, 'cyan')
|
||||
|
||||
def on_play_start(self, pattern):
|
||||
|
||||
print banner("PLAY [%s]" % pattern)
|
||||
|
||||
|
||||
|
||||
59
lib/ansible/color.py
Normal file
59
lib/ansible/color.py
Normal file
@ -0,0 +1,59 @@
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@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/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
ANSIBLE_COLOR=True
|
||||
if os.getenv("ANSIBLE_NOCOLOR") is not None:
|
||||
ANSIBLE_COLOR=False
|
||||
if not sys.stdout.isatty():
|
||||
ANSIBLE_COLOR=False
|
||||
|
||||
# --- begin "pretty"
|
||||
#
|
||||
# pretty - A miniature library that provides a Python print and stdout
|
||||
# wrapper that makes colored terminal text easier to use (eg. without
|
||||
# having to mess around with ANSI escape sequences). This code is public
|
||||
# domain - there is no license except that you must leave this header.
|
||||
#
|
||||
# Copyright (C) 2008 Brian Nez <thedude at bri1 dot com>
|
||||
#
|
||||
# http://nezzen.net/2008/06/23/colored-text-in-python-using-ansi-escape-sequences/
|
||||
|
||||
codeCodes = {
|
||||
'black': '0;30', 'bright gray': '0;37',
|
||||
'blue': '0;34', 'white': '1;37',
|
||||
'green': '0;32', 'bright blue': '1;34',
|
||||
'cyan': '0;36', 'bright green': '1;32',
|
||||
'red': '0;31', 'bright cyan': '1;36',
|
||||
'purple': '0;35', 'bright red': '1;31',
|
||||
'yellow': '0;33', 'bright purple': '1;35',
|
||||
'dark gray': '1;30', 'bright yellow': '1;33',
|
||||
'normal': '0'
|
||||
}
|
||||
|
||||
def stringc(text, color):
|
||||
"""String in color."""
|
||||
|
||||
if ANSIBLE_COLOR:
|
||||
return "\033["+codeCodes[color]+"m"+text+"\033[0m"
|
||||
else:
|
||||
return text
|
||||
|
||||
# --- end "pretty"
|
||||
|
||||
@ -14,17 +14,12 @@
|
||||
#
|
||||
# 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
|
||||
|
||||
|
||||
DEFAULT_HOST_LIST = os.environ.get('ANSIBLE_HOSTS',
|
||||
'/etc/ansible/hosts')
|
||||
DEFAULT_MODULE_PATH = os.environ.get('ANSIBLE_LIBRARY',
|
||||
'/usr/share/ansible')
|
||||
DEFAULT_REMOTE_TMP = os.environ.get('ANSIBLE_REMOTE_TMP',
|
||||
'$HOME/.ansible/tmp')
|
||||
DEFAULT_HOST_LIST = os.environ.get('ANSIBLE_HOSTS', '/etc/ansible/hosts')
|
||||
DEFAULT_MODULE_PATH = os.environ.get('ANSIBLE_LIBRARY', '/usr/share/ansible')
|
||||
DEFAULT_REMOTE_TMP = os.environ.get('ANSIBLE_REMOTE_TMP', '$HOME/.ansible/tmp')
|
||||
|
||||
DEFAULT_MODULE_NAME = 'command'
|
||||
DEFAULT_PATTERN = '*'
|
||||
@ -34,7 +29,7 @@ DEFAULT_TIMEOUT = os.environ.get('ANSIBLE_TIMEOUT',10)
|
||||
DEFAULT_POLL_INTERVAL = os.environ.get('ANSIBLE_POLL_INTERVAL',15)
|
||||
DEFAULT_REMOTE_USER = os.environ.get('ANSIBLE_REMOTE_USER','root')
|
||||
DEFAULT_REMOTE_PASS = None
|
||||
DEFAULT_PRIVATE_KEY_FILE = os.environ.get('ANSIBLE_REMOTE_USER',None)
|
||||
DEFAULT_PRIVATE_KEY_FILE = os.environ.get('ANSIBLE_PRIVATE_KEY_FILE',None)
|
||||
DEFAULT_SUDO_PASS = None
|
||||
DEFAULT_SUDO_USER = os.environ.get('ANSIBLE_SUDO_USER','root')
|
||||
DEFAULT_REMOTE_PORT = 22
|
||||
|
||||
@ -15,11 +15,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
class AnsibleError(Exception):
|
||||
"""
|
||||
The base Ansible exception from which all others should subclass.
|
||||
"""
|
||||
''' The base Ansible exception from which all others should subclass '''
|
||||
|
||||
def __init__(self, msg):
|
||||
self.msg = msg
|
||||
@ -27,11 +24,9 @@ class AnsibleError(Exception):
|
||||
def __str__(self):
|
||||
return self.msg
|
||||
|
||||
|
||||
class AnsibleFileNotFound(AnsibleError):
|
||||
pass
|
||||
|
||||
class AnsibleConnectionFailed(AnsibleError):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@ -35,12 +35,22 @@ class Inventory(object):
|
||||
Host inventory for ansible.
|
||||
"""
|
||||
|
||||
__slots__ = [ 'host_list', 'groups', '_restriction', '_is_script',
|
||||
'parser', '_vars_per_host', '_vars_per_group', '_hosts_cache' ]
|
||||
|
||||
def __init__(self, host_list=C.DEFAULT_HOST_LIST):
|
||||
|
||||
# the host file file, or script path, or list of hosts
|
||||
# if a list, inventory data will NOT be loaded
|
||||
self.host_list = host_list
|
||||
|
||||
# caching to avoid repeated calculations, particularly with
|
||||
# external inventory scripts.
|
||||
|
||||
self._vars_per_host = {}
|
||||
self._vars_per_group = {}
|
||||
self._hosts_cache = {}
|
||||
|
||||
# the inventory object holds a list of groups
|
||||
self.groups = []
|
||||
|
||||
@ -52,7 +62,8 @@ class Inventory(object):
|
||||
|
||||
if type(host_list) in [ str, unicode ]:
|
||||
if host_list.find(",") != -1:
|
||||
host_list = host_list.split(",")
|
||||
host_list = host_list.split(",")
|
||||
host_list = [ h for h in host_list if h and h.strip() ]
|
||||
|
||||
if type(host_list) == list:
|
||||
all = Group('all')
|
||||
@ -79,6 +90,7 @@ class Inventory(object):
|
||||
def _match(self, str, pattern_str):
|
||||
return fnmatch.fnmatch(str, pattern_str)
|
||||
|
||||
# TODO: cache this logic so if called a second time the result is not recalculated
|
||||
def get_hosts(self, pattern="all"):
|
||||
""" Get all host objects matching the pattern """
|
||||
hosts = {}
|
||||
@ -93,8 +105,8 @@ class Inventory(object):
|
||||
inverted = False
|
||||
for group in groups:
|
||||
for host in group.get_hosts():
|
||||
if group.name == pat or pat == 'all' or self._match(host.name, pat):
|
||||
#must test explicitly for None because [] means no hosts allowed
|
||||
if self._match(group.name, pat) or pat == 'all' or self._match(host.name, pat):
|
||||
# must test explicitly for None because [] means no hosts allowed
|
||||
if self._restriction==None or host.name in self._restriction:
|
||||
if inverted:
|
||||
if host.name in hosts:
|
||||
@ -107,9 +119,14 @@ class Inventory(object):
|
||||
return self.groups
|
||||
|
||||
def get_host(self, hostname):
|
||||
if hostname not in self._hosts_cache:
|
||||
self._hosts_cache[hostname] = self._get_host(hostname)
|
||||
return self._hosts_cache[hostname]
|
||||
|
||||
def _get_host(self, hostname):
|
||||
for group in self.groups:
|
||||
for host in group.get_hosts():
|
||||
if hostname == host.name:
|
||||
for host in group.get_hosts():
|
||||
if hostname == host.name:
|
||||
return host
|
||||
return None
|
||||
|
||||
@ -118,17 +135,26 @@ class Inventory(object):
|
||||
if group.name == groupname:
|
||||
return group
|
||||
return None
|
||||
|
||||
|
||||
def get_group_variables(self, groupname):
|
||||
if groupname not in self._vars_per_group:
|
||||
self._vars_per_group[groupname] = self._get_group_variables(groupname)
|
||||
return self._vars_per_group[groupname]
|
||||
|
||||
def _get_group_variables(self, groupname):
|
||||
group = self.get_group(groupname)
|
||||
if group is None:
|
||||
raise Exception("group not found: %s" % groupname)
|
||||
return group.get_variables()
|
||||
|
||||
def get_variables(self, hostname):
|
||||
if hostname not in self._vars_per_host:
|
||||
self._vars_per_host[hostname] = self._get_variables(hostname)
|
||||
return self._vars_per_host[hostname]
|
||||
|
||||
def _get_variables(self, hostname):
|
||||
|
||||
if self._is_script:
|
||||
# TODO: move this to inventory_script.py
|
||||
host = self.get_host(hostname)
|
||||
cmd = subprocess.Popen(
|
||||
[self.host_list,"--host",hostname],
|
||||
@ -137,9 +163,13 @@ class Inventory(object):
|
||||
)
|
||||
(out, err) = cmd.communicate()
|
||||
results = utils.parse_json(out)
|
||||
|
||||
# FIXME: this is a bit redundant with host.py and should share code
|
||||
results['inventory_hostname'] = hostname
|
||||
results['inventory_hostname_short'] = hostname.split('.')[0]
|
||||
groups = [ g.name for g in host.get_groups() if g.name != 'all' ]
|
||||
results['group_names'] = sorted(groups)
|
||||
|
||||
return results
|
||||
|
||||
host = self.get_host(hostname)
|
||||
@ -161,12 +191,23 @@ class Inventory(object):
|
||||
|
||||
def restrict_to(self, restriction, append_missing=False):
|
||||
""" Restrict list operations to the hosts given in restriction """
|
||||
|
||||
if type(restriction) != list:
|
||||
restriction = [ restriction ]
|
||||
self._restriction = restriction
|
||||
if type(restriction) != list:
|
||||
restriction = [ restriction ]
|
||||
self._restriction = restriction
|
||||
|
||||
def lift_restriction(self):
|
||||
""" Do not restrict list operations """
|
||||
|
||||
self._restriction = None
|
||||
|
||||
def is_file(self):
|
||||
""" did inventory come from a file? """
|
||||
if not isinstance(self.host_list, basestring):
|
||||
return False
|
||||
return os.path.exists(self.host_list)
|
||||
|
||||
def basedir(self):
|
||||
""" if inventory came from a file, what's the directory? """
|
||||
if not self.is_file():
|
||||
return None
|
||||
return os.path.dirname(self.host_list)
|
||||
|
||||
97
lib/ansible/inventory/expand_hosts.py
Normal file
97
lib/ansible/inventory/expand_hosts.py
Normal file
@ -0,0 +1,97 @@
|
||||
# (c) 2012, Zettar Inc.
|
||||
# Written by Chin Fang <fangchin@zettar.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
'''
|
||||
This module is for enhancing ansible's inventory parsing capability such
|
||||
that it can deal with hostnames specified using a simple pattern in the
|
||||
form of [beg:end], example: [1:5] where if beg is not specified, it
|
||||
defaults to 0.
|
||||
|
||||
If beg is given and is left-zero-padded, e.g. '001', it is taken as a
|
||||
formatting hint when the range is expanded. e.g. [001:010] is to be
|
||||
expanded into 001, 002 ...009, 010.
|
||||
|
||||
Note that when beg is specified with left zero padding, then the length of
|
||||
end must be the same as that of beg, else a exception is raised.
|
||||
'''
|
||||
|
||||
from ansible import errors
|
||||
|
||||
def detect_range(line = None):
|
||||
'''
|
||||
A helper function that checks a given host line to see if it contains
|
||||
a range pattern descibed in the docstring above.
|
||||
|
||||
Returnes True if the given line contains a pattern, else False.
|
||||
'''
|
||||
if (not line.startswith("[") and
|
||||
line.find("[") != -1 and
|
||||
line.find(":") != -1 and
|
||||
line.find("]") != -1 and
|
||||
line.index("[") < line.index(":") < line.index("]")):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def expand_hostname_range(line = None):
|
||||
'''
|
||||
A helper function that expands a given line that contains a pattern
|
||||
specified in top docstring, and returns a list that consists of the
|
||||
expanded version.
|
||||
|
||||
The '[' and ']' characters are used to maintain the pseudo-code
|
||||
appearance. They are replaced in this function with '|' to ease
|
||||
string splitting.
|
||||
|
||||
References: http://ansible.github.com/patterns.html#hosts-and-groups
|
||||
'''
|
||||
all_hosts = []
|
||||
if line:
|
||||
# A hostname such as db[1:6]-node is considered to consists
|
||||
# three parts:
|
||||
# head: 'db'
|
||||
# nrange: [1:6]; range() is a built-in. Can't use the name
|
||||
# tail: '-node'
|
||||
|
||||
(head, nrange, tail) = line.replace('[','|').replace(']','|').split('|')
|
||||
bounds = nrange.split(":")
|
||||
if len(bounds) != 2:
|
||||
raise errors.AnsibleError("host range incorrectly specified")
|
||||
beg = bounds[0]
|
||||
end = bounds[1]
|
||||
if not beg:
|
||||
beg = "0"
|
||||
if not end:
|
||||
raise errors.AnsibleError("host range end value missing")
|
||||
if beg[0] == '0' and len(beg) > 1:
|
||||
rlen = len(beg) # range length formatting hint
|
||||
else:
|
||||
rlen = None
|
||||
if rlen > 1 and rlen != len(end):
|
||||
raise errors.AnsibleError("host range format incorrectly specified!")
|
||||
|
||||
for _ in range(int(beg), int(end)+1):
|
||||
if rlen:
|
||||
rseq = str(_).zfill(rlen) # range sequence
|
||||
else:
|
||||
rseq = str(_)
|
||||
hname = ''.join((head, rseq, tail))
|
||||
all_hosts.append(hname)
|
||||
|
||||
return all_hosts
|
||||
@ -15,38 +15,39 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#############################################
|
||||
|
||||
# from ansible import errors
|
||||
|
||||
class Group(object):
|
||||
"""
|
||||
Group of ansible hosts
|
||||
"""
|
||||
''' a group of ansible hosts '''
|
||||
|
||||
__slots__ = [ 'name', 'hosts', 'vars', 'child_groups', 'parent_groups' ]
|
||||
|
||||
def __init__(self, name=None):
|
||||
|
||||
self.name = name
|
||||
self.hosts = []
|
||||
self.vars = {}
|
||||
self.child_groups = []
|
||||
self.parent_groups = []
|
||||
if self.name is None:
|
||||
raise Exception("group name is required")
|
||||
raise Exception("group name is required")
|
||||
|
||||
def add_child_group(self, group):
|
||||
|
||||
if self == group:
|
||||
raise Exception("can't add group to itself")
|
||||
self.child_groups.append(group)
|
||||
group.parent_groups.append(self)
|
||||
|
||||
def add_host(self, host):
|
||||
|
||||
self.hosts.append(host)
|
||||
host.add_group(self)
|
||||
|
||||
def set_variable(self, key, value):
|
||||
|
||||
self.vars[key] = value
|
||||
|
||||
def get_hosts(self):
|
||||
|
||||
hosts = []
|
||||
for kid in self.child_groups:
|
||||
hosts.extend(kid.get_hosts())
|
||||
@ -54,14 +55,16 @@ class Group(object):
|
||||
return hosts
|
||||
|
||||
def get_variables(self):
|
||||
|
||||
vars = {}
|
||||
# FIXME: verify this variable override order is what we want
|
||||
for ancestor in self.get_ancestors():
|
||||
vars.update(ancestor.get_variables())
|
||||
vars.update(ancestor.get_variables())
|
||||
vars.update(self.vars)
|
||||
return vars
|
||||
|
||||
def _get_ancestors(self):
|
||||
|
||||
results = {}
|
||||
for g in self.parent_groups:
|
||||
results[g.name] = g
|
||||
@ -69,8 +72,6 @@ class Group(object):
|
||||
return results
|
||||
|
||||
def get_ancestors(self):
|
||||
|
||||
return self._get_ancestors().values()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -15,17 +15,16 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#############################################
|
||||
|
||||
from ansible import errors
|
||||
import ansible.constants as C
|
||||
|
||||
class Host(object):
|
||||
"""
|
||||
Group of ansible hosts
|
||||
"""
|
||||
''' a single ansible host '''
|
||||
|
||||
__slots__ = [ 'name', 'vars', 'groups' ]
|
||||
|
||||
def __init__(self, name=None, port=None):
|
||||
|
||||
self.name = name
|
||||
self.vars = {}
|
||||
self.groups = []
|
||||
@ -36,12 +35,15 @@ class Host(object):
|
||||
raise Exception("host name is required")
|
||||
|
||||
def add_group(self, group):
|
||||
|
||||
self.groups.append(group)
|
||||
|
||||
def set_variable(self, key, value):
|
||||
self.vars[key]=value;
|
||||
|
||||
self.vars[key]=value
|
||||
|
||||
def get_groups(self):
|
||||
|
||||
groups = {}
|
||||
for g in self.groups:
|
||||
groups[g.name] = g
|
||||
@ -51,11 +53,13 @@ class Host(object):
|
||||
return groups.values()
|
||||
|
||||
def get_variables(self):
|
||||
|
||||
results = {}
|
||||
for group in self.groups:
|
||||
results.update(group.get_variables())
|
||||
results.update(self.vars)
|
||||
results['inventory_hostname'] = self.name
|
||||
results['inventory_hostname_short'] = self.name.split('.')[0]
|
||||
groups = self.get_groups()
|
||||
results['group_names'] = sorted([ g.name for g in groups if g.name != 'all'])
|
||||
return results
|
||||
|
||||
@ -24,6 +24,8 @@ import subprocess
|
||||
import ansible.constants as C
|
||||
from ansible.inventory.host import Host
|
||||
from ansible.inventory.group import Group
|
||||
from ansible.inventory.expand_hosts import detect_range
|
||||
from ansible.inventory.expand_hosts import expand_hostname_range
|
||||
from ansible import errors
|
||||
from ansible import utils
|
||||
|
||||
@ -46,6 +48,7 @@ class InventoryParser(object):
|
||||
self._parse_group_children()
|
||||
self._parse_group_variables()
|
||||
return self.groups
|
||||
|
||||
|
||||
# [webservers]
|
||||
# alpha
|
||||
@ -54,6 +57,7 @@ class InventoryParser(object):
|
||||
# delta asdf=jkl favcolor=red
|
||||
|
||||
def _parse_base_groups(self):
|
||||
# FIXME: refactor
|
||||
|
||||
ungrouped = Group(name='ungrouped')
|
||||
all = Group(name='all')
|
||||
@ -78,21 +82,40 @@ class InventoryParser(object):
|
||||
continue
|
||||
hostname = tokens[0]
|
||||
port = C.DEFAULT_REMOTE_PORT
|
||||
if hostname.find(":") != -1:
|
||||
tokens2 = hostname.split(":")
|
||||
hostname = tokens2[0]
|
||||
port = tokens2[1]
|
||||
# Two cases to check:
|
||||
# 0. A hostname that contains a range pesudo-code and a port
|
||||
# 1. A hostname that contains just a port
|
||||
if (hostname.find("[") != -1 and
|
||||
hostname.find("]") != -1 and
|
||||
hostname.find(":") != -1 and
|
||||
(hostname.rindex("]") < hostname.rindex(":")) or
|
||||
(hostname.find("]") == -1 and hostname.find(":") != -1)):
|
||||
tokens2 = hostname.rsplit(":", 1)
|
||||
hostname = tokens2[0]
|
||||
port = tokens2[1]
|
||||
|
||||
host = None
|
||||
_all_hosts = []
|
||||
if hostname in self.hosts:
|
||||
host = self.hosts[hostname]
|
||||
_all_hosts.append(host)
|
||||
else:
|
||||
host = Host(name=hostname, port=port)
|
||||
self.hosts[hostname] = host
|
||||
if detect_range(hostname):
|
||||
_hosts = expand_hostname_range(hostname)
|
||||
for _ in _hosts:
|
||||
host = Host(name=_, port=port)
|
||||
self.hosts[_] = host
|
||||
_all_hosts.append(host)
|
||||
else:
|
||||
host = Host(name=hostname, port=port)
|
||||
self.hosts[hostname] = host
|
||||
_all_hosts.append(host)
|
||||
if len(tokens) > 1:
|
||||
for t in tokens[1:]:
|
||||
(k,v) = t.split("=")
|
||||
host.set_variable(k,v)
|
||||
self.groups[active_group_name].add_host(host)
|
||||
for t in tokens[1:]:
|
||||
(k,v) = t.split("=")
|
||||
host.set_variable(k,v)
|
||||
for _ in _all_hosts:
|
||||
self.groups[active_group_name].add_host(_)
|
||||
|
||||
# [southeast:children]
|
||||
# atlanta
|
||||
@ -134,7 +157,7 @@ class InventoryParser(object):
|
||||
line = line.replace("[","").replace(":vars]","")
|
||||
group = self.groups.get(line, None)
|
||||
if group is None:
|
||||
raise errors.AnsibleError("can't add vars to undefined group: %s" % line)
|
||||
raise errors.AnsibleError("can't add vars to undefined group: %s" % line)
|
||||
elif line.startswith("#"):
|
||||
pass
|
||||
elif line.startswith("["):
|
||||
@ -145,7 +168,7 @@ class InventoryParser(object):
|
||||
if line.find("=") == -1:
|
||||
raise errors.AnsibleError("variables assigned to group must be in key=value form")
|
||||
else:
|
||||
(k,v) = line.split("=")
|
||||
(k,v) = line.split("=",1)
|
||||
group.set_variable(k,v)
|
||||
|
||||
|
||||
|
||||
@ -26,9 +26,7 @@ from ansible import errors
|
||||
from ansible import utils
|
||||
|
||||
class InventoryScript(object):
|
||||
"""
|
||||
Host inventory parser for ansible using external inventory scripts.
|
||||
"""
|
||||
''' Host inventory parser for ansible using external inventory scripts. '''
|
||||
|
||||
def __init__(self, filename=C.DEFAULT_HOST_LIST):
|
||||
|
||||
@ -39,6 +37,7 @@ class InventoryScript(object):
|
||||
self.groups = self._parse()
|
||||
|
||||
def _parse(self):
|
||||
|
||||
groups = {}
|
||||
self.raw = utils.parse_json(self.data)
|
||||
all=Group('all')
|
||||
@ -55,4 +54,3 @@ class InventoryScript(object):
|
||||
all.add_child_group(group)
|
||||
return groups
|
||||
|
||||
|
||||
|
||||
@ -15,21 +15,23 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#############################################
|
||||
|
||||
import ansible.constants as C
|
||||
from ansible.inventory.host import Host
|
||||
from ansible.inventory.group import Group
|
||||
from ansible import errors
|
||||
from ansible import utils
|
||||
import sys
|
||||
|
||||
class InventoryParserYaml(object):
|
||||
"""
|
||||
Host inventory for ansible.
|
||||
"""
|
||||
''' Host inventory parser for ansible '''
|
||||
|
||||
__slots__ = [ '_hosts', 'groups' ]
|
||||
|
||||
def __init__(self, filename=C.DEFAULT_HOST_LIST):
|
||||
|
||||
sys.stderr.write("WARNING: YAML inventory files are deprecated in 0.6 and will be removed in 0.7, to migrate" +
|
||||
" download and run https://github.com/ansible/ansible/blob/devel/examples/scripts/yaml_to_ini.py\n")
|
||||
|
||||
fh = open(filename)
|
||||
data = fh.read()
|
||||
fh.close()
|
||||
@ -37,6 +39,7 @@ class InventoryParserYaml(object):
|
||||
self._parse(data)
|
||||
|
||||
def _make_host(self, hostname):
|
||||
|
||||
if hostname in self._hosts:
|
||||
return self._hosts[hostname]
|
||||
else:
|
||||
@ -47,6 +50,7 @@ class InventoryParserYaml(object):
|
||||
# see file 'test/yaml_hosts' for syntax
|
||||
|
||||
def _parse(self, data):
|
||||
# FIXME: refactor into subfunctions
|
||||
|
||||
all = Group('all')
|
||||
ungrouped = Group('ungrouped')
|
||||
@ -73,8 +77,8 @@ class InventoryParserYaml(object):
|
||||
vars = subresult.get('vars',{})
|
||||
if type(vars) == list:
|
||||
for subitem in vars:
|
||||
for (k,v) in subitem.items():
|
||||
host.set_variable(k,v)
|
||||
for (k,v) in subitem.items():
|
||||
host.set_variable(k,v)
|
||||
elif type(vars) == dict:
|
||||
for (k,v) in subresult.get('vars',{}).items():
|
||||
host.set_variable(k,v)
|
||||
@ -106,12 +110,33 @@ class InventoryParserYaml(object):
|
||||
|
||||
elif type(item) == dict and 'host' in item:
|
||||
host = self._make_host(item['host'])
|
||||
|
||||
vars = item.get('vars', {})
|
||||
if type(vars)==list:
|
||||
varlist, vars = vars, {}
|
||||
for subitem in varlist:
|
||||
vars.update(subitem)
|
||||
for (k,v) in vars.items():
|
||||
host.set_variable(k,v)
|
||||
host.set_variable(k,v)
|
||||
|
||||
groups = item.get('groups', {})
|
||||
if type(groups) in [ str, unicode ]:
|
||||
groups = [ groups ]
|
||||
if type(groups)==list:
|
||||
for subitem in groups:
|
||||
if subitem in self.groups:
|
||||
group = self.groups[subitem]
|
||||
else:
|
||||
group = Group(subitem)
|
||||
self.groups[group.name] = group
|
||||
all.add_child_group(group)
|
||||
group.add_host(host)
|
||||
grouped_hosts.append(host)
|
||||
|
||||
if host not in grouped_hosts:
|
||||
ungrouped.add_host(host)
|
||||
|
||||
# make sure ungrouped.hosts is the complement of grouped_hosts
|
||||
ungrouped_hosts = [host for host in ungrouped.hosts if host not in grouped_hosts]
|
||||
|
||||
|
||||
|
||||
211
lib/ansible/module_common.py
Normal file
211
lib/ansible/module_common.py
Normal file
@ -0,0 +1,211 @@
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@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/>.
|
||||
|
||||
REPLACER = "#<<INCLUDE_ANSIBLE_MODULE_COMMON>>"
|
||||
REPLACER_ARGS = "<<INCLUDE_ANSIBLE_MODULE_ARGS>>"
|
||||
|
||||
MODULE_COMMON = """
|
||||
|
||||
# == BEGIN DYNAMICALLY INSERTED CODE ==
|
||||
|
||||
MODULE_ARGS = <<INCLUDE_ANSIBLE_MODULE_ARGS>>
|
||||
|
||||
BOOLEANS_TRUE = ['yes', 'on', '1', 'true', 1]
|
||||
BOOLEANS_FALSE = ['no', 'off', '0', 'false', 0]
|
||||
BOOLEANS = BOOLEANS_TRUE + BOOLEANS_FALSE
|
||||
|
||||
# ansible modules can be written in any language. To simplify
|
||||
# development of Python modules, the functions available here
|
||||
# can be inserted in any module source automatically by including
|
||||
# #<<INCLUDE_ANSIBLE_MODULE_COMMON>> on a blank line by itself inside
|
||||
# of an ansible module. The source of this common code lives
|
||||
# in lib/ansible/module_common.py
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
import base64
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import syslog
|
||||
import types
|
||||
|
||||
try:
|
||||
from hashlib import md5 as _md5
|
||||
except ImportError:
|
||||
from md5 import md5 as _md5
|
||||
|
||||
class AnsibleModule(object):
|
||||
|
||||
def __init__(self, argument_spec, bypass_checks=False, no_log=False, check_invalid_arguments=True):
|
||||
'''
|
||||
common code for quickly building an ansible module in Python
|
||||
(although you can write modules in anything that can return JSON)
|
||||
see library/slurp and others for examples
|
||||
'''
|
||||
|
||||
self.argument_spec = argument_spec
|
||||
(self.params, self.args) = self._load_params()
|
||||
|
||||
self._legal_inputs = []
|
||||
self._handle_aliases()
|
||||
|
||||
# this may be disabled where modules are going to daisy chain into others
|
||||
if check_invalid_arguments:
|
||||
self._check_invalid_arguments()
|
||||
|
||||
self._set_defaults(pre=True)
|
||||
|
||||
if not bypass_checks:
|
||||
self._check_required_arguments()
|
||||
self._check_argument_types()
|
||||
|
||||
self._set_defaults(pre=False)
|
||||
if not no_log:
|
||||
self._log_invocation()
|
||||
|
||||
|
||||
def _handle_aliases(self):
|
||||
for (k,v) in self.argument_spec.iteritems():
|
||||
self._legal_inputs.append(k)
|
||||
aliases = v.get('aliases', None)
|
||||
default = v.get('default', None)
|
||||
required = v.get('required', False)
|
||||
if default is not None and required:
|
||||
# not alias specific but this is a good place to check this
|
||||
self.fail_json(msg="internal error: required and default are mutally exclusive for %s" % k)
|
||||
if aliases is None:
|
||||
continue
|
||||
if type(aliases) != list:
|
||||
self.fail_json(msg='internal error: aliases must be a list')
|
||||
for alias in aliases:
|
||||
self._legal_inputs.append(alias)
|
||||
if alias in self.params:
|
||||
self.params[k] = self.params[alias]
|
||||
|
||||
def _check_invalid_arguments(self):
|
||||
for (k,v) in self.params.iteritems():
|
||||
if k not in self._legal_inputs:
|
||||
self.fail_json(msg="unsupported parameter for module: %s" % k)
|
||||
|
||||
def _check_required_arguments(self):
|
||||
''' ensure all required arguments are present '''
|
||||
missing = []
|
||||
for (k,v) in self.argument_spec.iteritems():
|
||||
required = v.get('required', False)
|
||||
if required and k not in self.params:
|
||||
missing.append(k)
|
||||
if len(missing) > 0:
|
||||
self.fail_json(msg="missing required arguments: %s" % ",".join(missing))
|
||||
|
||||
def _check_argument_types(self):
|
||||
''' ensure all arguments have the requested values, and there are no stray arguments '''
|
||||
for (k,v) in self.argument_spec.iteritems():
|
||||
choices = v.get('choices',None)
|
||||
if choices is None:
|
||||
continue
|
||||
if type(choices) == list:
|
||||
if k in self.params:
|
||||
if self.params[k] not in choices:
|
||||
choices_str=",".join(choices)
|
||||
msg="value of %s must be one of: %s, got: %s" % (k, choices_str, self.params[k])
|
||||
self.fail_json(msg=msg)
|
||||
else:
|
||||
self.fail_json(msg="internal error: do not know how to interpret argument_spec")
|
||||
|
||||
def _set_defaults(self, pre=True):
|
||||
for (k,v) in self.argument_spec.iteritems():
|
||||
default = v.get('default', None)
|
||||
if pre == True:
|
||||
# this prevents setting defaults on required items
|
||||
if default and k not in self.params:
|
||||
self.params[k] = default
|
||||
else:
|
||||
# make sure things without a default still get set None
|
||||
if k not in self.params:
|
||||
self.params[k] = default
|
||||
|
||||
def _load_params(self):
|
||||
''' read the input and return a dictionary and the arguments string '''
|
||||
args = MODULE_ARGS
|
||||
items = shlex.split(args)
|
||||
params = {}
|
||||
for x in items:
|
||||
(k, v) = x.split("=",1)
|
||||
params[k] = v
|
||||
return (params, args)
|
||||
|
||||
def _log_invocation(self):
|
||||
''' log that ansible ran the module '''
|
||||
syslog.openlog('ansible-%s' % os.path.basename(__file__))
|
||||
# Sanitize possible password argument when logging
|
||||
log_args = re.sub(r'password=.+ (.*)', r"password=NOT_LOGGING_PASSWORD \1", self.args)
|
||||
log_args = re.sub(r'login_password=.+ (.*)', r"login_password=NOT_LOGGING_PASSWORD \1", log_args)
|
||||
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % log_args)
|
||||
|
||||
def boolean(self, arg):
|
||||
''' return a bool for the arg '''
|
||||
if arg is None or type(arg) == bool:
|
||||
return arg
|
||||
if type(arg) in types.StringTypes:
|
||||
arg = arg.lower()
|
||||
if arg in BOOLEANS_TRUE:
|
||||
return True
|
||||
elif arg in BOOLEANS_FALSE:
|
||||
return False
|
||||
else:
|
||||
self.fail_json(msg='Boolean %s not in either boolean list' % arg)
|
||||
|
||||
def jsonify(self, data):
|
||||
return json.dumps(data)
|
||||
|
||||
def exit_json(self, **kwargs):
|
||||
''' return from the module, without error '''
|
||||
print self.jsonify(kwargs)
|
||||
sys.exit(0)
|
||||
|
||||
def fail_json(self, **kwargs):
|
||||
''' return from the module, with an error message '''
|
||||
assert 'msg' in kwargs, "implementation error -- msg to explain the error is required"
|
||||
kwargs['failed'] = True
|
||||
print self.jsonify(kwargs)
|
||||
sys.exit(1)
|
||||
|
||||
def md5(self, filename):
|
||||
''' Return MD5 hex digest of local file, or None if file is not present. '''
|
||||
if not os.path.exists(filename):
|
||||
return None
|
||||
if os.path.isdir(filename):
|
||||
self.fail_json(msg="attempted to take md5sum of directory: %s" % filename)
|
||||
digest = _md5()
|
||||
blocksize = 64 * 1024
|
||||
infile = open(filename, 'rb')
|
||||
block = infile.read(blocksize)
|
||||
while block:
|
||||
digest.update(block)
|
||||
block = infile.read(blocksize)
|
||||
infile.close()
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
# == END DYNAMICALLY INSERTED CODE ===
|
||||
|
||||
"""
|
||||
@ -15,30 +15,26 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#############################################
|
||||
|
||||
import ansible.inventory
|
||||
import ansible.runner
|
||||
import ansible.constants as C
|
||||
from ansible import utils
|
||||
from ansible import errors
|
||||
import os
|
||||
import collections
|
||||
from play import Play
|
||||
|
||||
#############################################
|
||||
SETUP_CACHE = collections.defaultdict(dict)
|
||||
|
||||
class PlayBook(object):
|
||||
|
||||
'''
|
||||
runs an ansible playbook, given as a datastructure
|
||||
or YAML filename. a playbook is a deployment, config
|
||||
management, or automation based set of commands to
|
||||
run in series.
|
||||
runs an ansible playbook, given as a datastructure or YAML filename.
|
||||
A playbook is a deployment, config management, or automation based
|
||||
set of commands to run in series.
|
||||
|
||||
multiple plays/tasks do not execute simultaneously,
|
||||
but tasks in each pattern do execute in parallel
|
||||
(according to the number of forks requested) among
|
||||
the hosts they address
|
||||
multiple plays/tasks do not execute simultaneously, but tasks in each
|
||||
pattern do execute in parallel (according to the number of forks
|
||||
requested) among the hosts they address
|
||||
'''
|
||||
|
||||
# *****************************************************
|
||||
@ -61,7 +57,8 @@ class PlayBook(object):
|
||||
stats = None,
|
||||
sudo = False,
|
||||
sudo_user = C.DEFAULT_SUDO_USER,
|
||||
extra_vars = None):
|
||||
extra_vars = None,
|
||||
only_tags = None):
|
||||
|
||||
"""
|
||||
playbook: path to a playbook file
|
||||
@ -80,14 +77,16 @@ class PlayBook(object):
|
||||
sudo: if not specified per play, requests all plays use sudo mode
|
||||
"""
|
||||
|
||||
self.SETUP_CACHE = {}
|
||||
self.SETUP_CACHE = SETUP_CACHE
|
||||
|
||||
if playbook is None or callbacks is None or runner_callbacks is None or stats is None:
|
||||
raise Exception('missing required arguments')
|
||||
|
||||
if extra_vars is None:
|
||||
extra_vars = {}
|
||||
|
||||
if only_tags is None:
|
||||
only_tags = [ 'all' ]
|
||||
|
||||
self.module_path = module_path
|
||||
self.forks = forks
|
||||
self.timeout = timeout
|
||||
@ -105,18 +104,45 @@ class PlayBook(object):
|
||||
self.extra_vars = extra_vars
|
||||
self.global_vars = {}
|
||||
self.private_key_file = private_key_file
|
||||
self.only_tags = only_tags
|
||||
|
||||
self.inventory = ansible.inventory.Inventory(host_list)
|
||||
self.inventory = ansible.inventory.Inventory(host_list)
|
||||
|
||||
if not self.inventory._is_script:
|
||||
self.global_vars.update(self.inventory.get_group_variables('all'))
|
||||
|
||||
self.basedir = os.path.dirname(playbook)
|
||||
self.playbook = utils.parse_yaml_from_file(playbook)
|
||||
|
||||
self.basedir = os.path.dirname(playbook)
|
||||
self.playbook = self._load_playbook_from_file(playbook)
|
||||
self.module_path = self.module_path + os.pathsep + os.path.join(self.basedir, "library")
|
||||
|
||||
# *****************************************************
|
||||
|
||||
def _load_playbook_from_file(self, path):
|
||||
'''
|
||||
run top level error checking on playbooks and allow them to include other playbooks.
|
||||
'''
|
||||
|
||||
playbook_data = utils.parse_yaml_from_file(path)
|
||||
accumulated_plays = []
|
||||
|
||||
if type(playbook_data) != list:
|
||||
raise errors.AnsibleError("parse error: playbooks must be formatted as a YAML list")
|
||||
|
||||
for play in playbook_data:
|
||||
if type(play) != dict:
|
||||
raise errors.AnsibleError("parse error: each play in a playbook must a YAML dictionary (hash), recieved: %s" % play)
|
||||
if 'include' in play:
|
||||
if len(play.keys()) == 1:
|
||||
included_path = utils.path_dwim(self.basedir, play['include'])
|
||||
accumulated_plays.extend(self._load_playbook_from_file(included_path))
|
||||
else:
|
||||
raise errors.AnsibleError("parse error: top level includes cannot be used with other directives: %s" % play)
|
||||
else:
|
||||
accumulated_plays.append(play)
|
||||
|
||||
return accumulated_plays
|
||||
|
||||
# *****************************************************
|
||||
|
||||
def run(self):
|
||||
''' run all patterns in the playbook '''
|
||||
@ -124,7 +150,6 @@ class PlayBook(object):
|
||||
# loop through all patterns and run them
|
||||
self.callbacks.on_start()
|
||||
for play_ds in self.playbook:
|
||||
self.SETUP_CACHE = {}
|
||||
self._run_play(Play(self,play_ds))
|
||||
|
||||
# summarize the results
|
||||
@ -191,18 +216,16 @@ class PlayBook(object):
|
||||
|
||||
# load up an appropriate ansible runner to run the task in parallel
|
||||
results = self._run_task_internal(task)
|
||||
|
||||
# add facts to the global setup cache
|
||||
for host, result in results['contacted'].iteritems():
|
||||
if "ansible_facts" in result:
|
||||
for k,v in result['ansible_facts'].iteritems():
|
||||
self.SETUP_CACHE[host][k]=v
|
||||
|
||||
self.stats.compute(results)
|
||||
|
||||
# if no hosts are matched, carry on
|
||||
if results is None:
|
||||
results = {}
|
||||
|
||||
self.stats.compute(results)
|
||||
|
||||
# add facts to the global setup cache
|
||||
for host, result in results['contacted'].iteritems():
|
||||
facts = result.get('ansible_facts', {})
|
||||
self.SETUP_CACHE[host].update(facts)
|
||||
|
||||
# flag which notify handlers need to be run
|
||||
if len(task.notify) > 0:
|
||||
@ -231,28 +254,21 @@ class PlayBook(object):
|
||||
|
||||
# *****************************************************
|
||||
|
||||
def _do_setup_step(self, play, vars_files=None):
|
||||
|
||||
''' push variables down to the systems and get variables+facts back up '''
|
||||
|
||||
# this enables conditional includes like $facter_os.yml and is only done
|
||||
# after the original pass when we have that data.
|
||||
#
|
||||
|
||||
if vars_files is not None:
|
||||
self.callbacks.on_setup_secondary()
|
||||
play.update_vars_files(self.inventory.list_hosts(play.hosts))
|
||||
else:
|
||||
self.callbacks.on_setup_primary()
|
||||
|
||||
def _do_setup_step(self, play):
|
||||
''' get facts from the remote system '''
|
||||
|
||||
host_list = [ h for h in self.inventory.list_hosts(play.hosts)
|
||||
if not (h in self.stats.failures or h in self.stats.dark) ]
|
||||
|
||||
if not play.gather_facts:
|
||||
return {}
|
||||
|
||||
self.callbacks.on_setup()
|
||||
self.inventory.restrict_to(host_list)
|
||||
|
||||
# push any variables down to the system
|
||||
setup_results = ansible.runner.Runner(
|
||||
pattern=play.hosts, module_name='setup', module_args=play.vars, inventory=self.inventory,
|
||||
pattern=play.hosts, module_name='setup', module_args={}, inventory=self.inventory,
|
||||
forks=self.forks, module_path=self.module_path, timeout=self.timeout, remote_user=play.remote_user,
|
||||
remote_pass=self.remote_pass, remote_port=play.remote_port, private_key_file=self.private_key_file,
|
||||
setup_cache=self.SETUP_CACHE, callbacks=self.runner_callbacks, sudo=play.sudo, sudo_user=play.sudo_user,
|
||||
@ -265,11 +281,9 @@ class PlayBook(object):
|
||||
# now for each result, load into the setup cache so we can
|
||||
# let runner template out future commands
|
||||
setup_ok = setup_results.get('contacted', {})
|
||||
if vars_files is None:
|
||||
# first pass only or we'll erase good work
|
||||
for (host, result) in setup_ok.iteritems():
|
||||
if 'ansible_facts' in result:
|
||||
self.SETUP_CACHE[host] = result['ansible_facts']
|
||||
for (host, result) in setup_ok.iteritems():
|
||||
facts = result.get('ansible_facts', {})
|
||||
self.SETUP_CACHE[host] = result.get('ansible_facts', {})
|
||||
return setup_results
|
||||
|
||||
# *****************************************************
|
||||
@ -277,18 +291,29 @@ class PlayBook(object):
|
||||
def _run_play(self, play):
|
||||
''' run a list of tasks for a given pattern, in order '''
|
||||
|
||||
if not play.should_run(self.only_tags):
|
||||
return
|
||||
|
||||
self.callbacks.on_play_start(play.name)
|
||||
|
||||
# push any variables down to the system # and get facts/ohai/other data back up
|
||||
rc = self._do_setup_step(play) # pattern, vars, user, port, sudo, sudo_user, transport, None)
|
||||
# get facts from system
|
||||
rc = self._do_setup_step(play)
|
||||
|
||||
# now with that data, handle contentional variable file imports!
|
||||
if play.vars_files and len(play.vars_files) > 0:
|
||||
rc = self._do_setup_step(play, play.vars_files)
|
||||
play.update_vars_files(self.inventory.list_hosts(play.hosts))
|
||||
|
||||
# run all the top level tasks, these get run on every node
|
||||
for task in play.tasks():
|
||||
self._run_task(play, task, False)
|
||||
|
||||
# only run the task if the requested tags match
|
||||
should_run = False
|
||||
for x in self.only_tags:
|
||||
for y in task.tags:
|
||||
if (x==y):
|
||||
should_run = True
|
||||
break
|
||||
if should_run:
|
||||
self._run_task(play, task, False)
|
||||
|
||||
# run notify actions
|
||||
for handler in play.handlers():
|
||||
|
||||
@ -26,8 +26,18 @@ import os
|
||||
class Play(object):
|
||||
|
||||
__slots__ = [
|
||||
'hosts', 'name', 'vars', 'vars_prompt', 'vars_files', 'handlers', 'remote_user', 'remote_port',
|
||||
'sudo', 'sudo_user', 'transport', 'playbook', '_ds', '_handlers', '_tasks'
|
||||
'hosts', 'name', 'vars', 'vars_prompt', 'vars_files',
|
||||
'handlers', 'remote_user', 'remote_port',
|
||||
'sudo', 'sudo_user', 'transport', 'playbook',
|
||||
'tags', 'gather_facts', '_ds', '_handlers', '_tasks'
|
||||
]
|
||||
|
||||
# to catch typos and so forth -- these are userland names
|
||||
# and don't line up 1:1 with how they are stored
|
||||
VALID_KEYS = [
|
||||
'hosts', 'name', 'vars', 'vars_prompt', 'vars_files',
|
||||
'tasks', 'handlers', 'user', 'port', 'include',
|
||||
'sudo', 'sudo_user', 'connection', 'tags', 'gather_facts'
|
||||
]
|
||||
|
||||
# *************************************************
|
||||
@ -35,6 +45,10 @@ class Play(object):
|
||||
def __init__(self, playbook, ds):
|
||||
''' constructor loads from a play datastructure '''
|
||||
|
||||
for x in ds.keys():
|
||||
if not x in Play.VALID_KEYS:
|
||||
raise errors.AnsibleError("%s is not a legal parameter in an Ansible Playbook" % x)
|
||||
|
||||
# TODO: more error handling
|
||||
|
||||
hosts = ds.get('hosts')
|
||||
@ -42,26 +56,38 @@ class Play(object):
|
||||
raise errors.AnsibleError('hosts declaration is required')
|
||||
elif isinstance(hosts, list):
|
||||
hosts = ';'.join(hosts)
|
||||
hosts = utils.template(hosts, playbook.extra_vars, {})
|
||||
hosts = utils.template(hosts, playbook.extra_vars)
|
||||
|
||||
self._ds = ds
|
||||
self.playbook = playbook
|
||||
self.hosts = hosts
|
||||
self.name = ds.get('name', self.hosts)
|
||||
self.vars = ds.get('vars', {})
|
||||
self.vars_files = ds.get('vars_files', [])
|
||||
self.vars_prompt = ds.get('vars_prompt', {})
|
||||
self.vars = self._get_vars(self.playbook.basedir)
|
||||
self._tasks = ds.get('tasks', [])
|
||||
self._handlers = ds.get('handlers', [])
|
||||
self.remote_user = ds.get('user', self.playbook.remote_user)
|
||||
self.remote_port = ds.get('port', self.playbook.remote_port)
|
||||
self.sudo = ds.get('sudo', self.playbook.sudo)
|
||||
self.sudo_user = ds.get('sudo_user', self.playbook.sudo_user)
|
||||
self.transport = ds.get('connection', self.playbook.transport)
|
||||
self.tags = ds.get('tags', None)
|
||||
self.gather_facts = ds.get('gather_facts', True)
|
||||
|
||||
self._update_vars_files_for_host(None)
|
||||
|
||||
self._ds = ds
|
||||
self.playbook = playbook
|
||||
self.hosts = hosts
|
||||
self.name = ds.get('name', self.hosts)
|
||||
self.vars = ds.get('vars', {})
|
||||
self.vars_files = ds.get('vars_files', [])
|
||||
self.vars_prompt = ds.get('vars_prompt', {})
|
||||
self.vars = self._get_vars(self.playbook.basedir)
|
||||
self._tasks = ds.get('tasks', [])
|
||||
self._handlers = ds.get('handlers', [])
|
||||
self.remote_user = ds.get('user', self.playbook.remote_user)
|
||||
self.remote_port = ds.get('port', self.playbook.remote_port)
|
||||
self.sudo = ds.get('sudo', self.playbook.sudo)
|
||||
self.sudo_user = ds.get('sudo_user', self.playbook.sudo_user)
|
||||
self.transport = ds.get('connection', self.playbook.transport)
|
||||
self._tasks = self._load_tasks(self._ds, 'tasks')
|
||||
self._handlers = self._load_tasks(self._ds, 'handlers')
|
||||
|
||||
if self.tags is None:
|
||||
self.tags = []
|
||||
elif type(self.tags) in [ str, unicode ]:
|
||||
self.tags = [ self.tags ]
|
||||
elif type(self.tags) != list:
|
||||
self.tags = []
|
||||
|
||||
if self.sudo_user != 'root':
|
||||
self.sudo = True
|
||||
|
||||
@ -76,25 +102,24 @@ class Play(object):
|
||||
task_vars = self.vars.copy()
|
||||
if 'include' in x:
|
||||
tokens = shlex.split(x['include'])
|
||||
|
||||
for t in tokens[1:]:
|
||||
(k,v) = t.split("=", 1)
|
||||
task_vars[k]=v
|
||||
include_file = tokens[0]
|
||||
include_file = utils.template(tokens[0], task_vars)
|
||||
data = utils.parse_yaml_from_file(utils.path_dwim(self.playbook.basedir, include_file))
|
||||
elif type(x) == dict:
|
||||
data = [x]
|
||||
else:
|
||||
raise Exception("unexpected task type")
|
||||
for y in data:
|
||||
items = y.get('with_items',None)
|
||||
if items is None:
|
||||
items = [ '' ]
|
||||
elif isinstance(items, basestring):
|
||||
items = utils.varLookup(items, task_vars)
|
||||
for item in items:
|
||||
mv = task_vars.copy()
|
||||
mv['item'] = item
|
||||
results.append(Task(self,y,module_vars=mv))
|
||||
mv = task_vars.copy()
|
||||
results.append(Task(self,y,module_vars=mv))
|
||||
|
||||
for x in results:
|
||||
if self.tags is not None:
|
||||
x.tags.extend(self.tags)
|
||||
|
||||
return results
|
||||
|
||||
# *************************************************
|
||||
@ -130,10 +155,20 @@ class Play(object):
|
||||
else:
|
||||
vars.update(self.vars)
|
||||
|
||||
if type(self.vars_prompt) != dict:
|
||||
raise errors.AnsibleError("'vars_prompt' section must contain only key/value pairs")
|
||||
for vname in self.vars_prompt:
|
||||
vars[vname] = self.playbook.callbacks.on_vars_prompt(vname)
|
||||
if type(self.vars_prompt) == list:
|
||||
for var in self.vars_prompt:
|
||||
if not 'name' in var:
|
||||
raise errors.AnsibleError("'vars_prompt' item is missing 'name:'")
|
||||
vname = var['name']
|
||||
prompt = "%s: " % var.get("prompt", vname)
|
||||
private = var.get("private", True)
|
||||
vars[vname] = self.playbook.callbacks.on_vars_prompt(vname, private, prompt)
|
||||
elif type(self.vars_prompt) == dict:
|
||||
for (vname, prompt) in self.vars_prompt.iteritems():
|
||||
prompt = "%s: " % prompt
|
||||
vars[vname] = self.playbook.callbacks.on_vars_prompt(vname, False, prompt)
|
||||
else:
|
||||
raise errors.AnsibleError("'vars_prompt' section is malformed, see docs")
|
||||
|
||||
results = self.playbook.extra_vars.copy()
|
||||
results.update(vars)
|
||||
@ -143,16 +178,61 @@ class Play(object):
|
||||
|
||||
def update_vars_files(self, hosts):
|
||||
''' calculate vars_files, which requires that setup runs first so ansible facts can be mixed in '''
|
||||
|
||||
# now loop through all the hosts...
|
||||
for h in hosts:
|
||||
self._update_vars_files_for_host(h)
|
||||
|
||||
# *************************************************
|
||||
|
||||
def should_run(self, tags):
|
||||
''' does the play match any of the tags? '''
|
||||
|
||||
tags_counted = 0
|
||||
for task in self._tasks:
|
||||
for task_tag in task.tags:
|
||||
tags_counted = tags_counted + 1
|
||||
if task_tag in tags:
|
||||
return True
|
||||
|
||||
if tags_counted > 0:
|
||||
return False
|
||||
|
||||
# didn't tag the play, and the play contains no steps
|
||||
# so assume we just want to gather facts
|
||||
return True
|
||||
|
||||
# *************************************************
|
||||
|
||||
def _has_vars_in(self, msg):
|
||||
return ((msg.find("$") != -1) or (msg.find("{{") != -1))
|
||||
|
||||
# *************************************************
|
||||
|
||||
def _update_vars_files_for_host(self, host):
|
||||
|
||||
if not host in self.playbook.SETUP_CACHE:
|
||||
# no need to process failed hosts or hosts not in this play
|
||||
return
|
||||
if type(self.vars_files) != list:
|
||||
self.vars_files = [ self.vars_files ]
|
||||
|
||||
if (host is not None):
|
||||
inventory = self.playbook.inventory
|
||||
hostrec = inventory.get_host(host)
|
||||
groups = [ g.name for g in hostrec.groups ]
|
||||
basedir = inventory.basedir()
|
||||
if basedir is not None:
|
||||
for x in groups:
|
||||
path = os.path.join(basedir, "group_vars/%s" % x)
|
||||
if os.path.exists(path):
|
||||
data = utils.parse_yaml_from_file(path)
|
||||
if type(data) != dict:
|
||||
raise errors.AnsibleError("%s must be stored as a dictionary/hash" % path)
|
||||
self.playbook.SETUP_CACHE[host].update(data)
|
||||
path = os.path.join(basedir, "host_vars/%s" % hostrec.name)
|
||||
if os.path.exists(path):
|
||||
data = utils.parse_yaml_from_file(path)
|
||||
if type(data) != dict:
|
||||
raise errors.AnsibleError("%s must be stored as a dictionary/hash" % path)
|
||||
self.playbook.SETUP_CACHE[host].update(data)
|
||||
|
||||
for filename in self.vars_files:
|
||||
|
||||
@ -162,29 +242,54 @@ class Play(object):
|
||||
found = False
|
||||
sequence = []
|
||||
for real_filename in filename:
|
||||
filename2 = utils.template(real_filename, self.playbook.SETUP_CACHE[host])
|
||||
filename2 = utils.template(filename2, self.vars)
|
||||
filename2 = utils.path_dwim(self.playbook.basedir, filename2)
|
||||
sequence.append(filename2)
|
||||
if os.path.exists(filename2):
|
||||
filename2 = utils.template(real_filename, self.vars)
|
||||
filename3 = filename2
|
||||
if host is not None:
|
||||
filename3 = utils.template(filename2, self.playbook.SETUP_CACHE[host])
|
||||
filename4 = utils.path_dwim(self.playbook.basedir, filename3)
|
||||
sequence.append(filename4)
|
||||
if os.path.exists(filename4):
|
||||
found = True
|
||||
data = utils.parse_yaml_from_file(filename2)
|
||||
self.playbook.SETUP_CACHE[host].update(data)
|
||||
self.playbook.callbacks.on_import_for_host(host, filename2)
|
||||
data = utils.parse_yaml_from_file(filename4)
|
||||
if type(data) != dict:
|
||||
raise errors.AnsibleError("%s must be stored as a dictionary/hash" % filename4)
|
||||
if host is not None:
|
||||
if self._has_vars_in(filename2) and not self._has_vars_in(filename3):
|
||||
# this filename has variables in it that were fact specific
|
||||
# so it needs to be loaded into the per host SETUP_CACHE
|
||||
self.playbook.SETUP_CACHE[host].update(data)
|
||||
self.playbook.callbacks.on_import_for_host(host, filename4)
|
||||
elif not self._has_vars_in(filename4):
|
||||
# found a non-host specific variable, load into vars and NOT
|
||||
# the setup cache
|
||||
self.vars.update(data)
|
||||
elif host is not None:
|
||||
self.playbook.callbacks.on_not_import_for_host(host, filename4)
|
||||
if found:
|
||||
break
|
||||
else:
|
||||
self.playbook.callbacks.on_not_import_for_host(host, filename2)
|
||||
if not found:
|
||||
raise errors.AnsibleError(
|
||||
"%s: FATAL, no files matched for vars_files import sequence: %s" % (host, sequence)
|
||||
)
|
||||
|
||||
else:
|
||||
# just one filename supplied, load it!
|
||||
|
||||
filename2 = utils.template(filename, self.playbook.SETUP_CACHE[host])
|
||||
filename2 = utils.template(filename2, self.vars)
|
||||
fpath = utils.path_dwim(self.playbook.basedir, filename2)
|
||||
new_vars = utils.parse_yaml_from_file(fpath)
|
||||
filename2 = utils.template(filename, self.vars)
|
||||
filename3 = filename2
|
||||
if host is not None:
|
||||
filename3 = utils.template(filename2, self.playbook.SETUP_CACHE[host])
|
||||
filename4 = utils.path_dwim(self.playbook.basedir, filename3)
|
||||
if self._has_vars_in(filename4):
|
||||
return
|
||||
new_vars = utils.parse_yaml_from_file(filename4)
|
||||
if new_vars:
|
||||
self.playbook.SETUP_CACHE[host].update(new_vars)
|
||||
#else: could warn if vars file contains no vars.
|
||||
if type(new_vars) != dict:
|
||||
raise errors.AnsibleError("%s must be stored as dictonary/hash: %s" % filename4)
|
||||
if host is not None and self._has_vars_in(filename2) and not self._has_vars_in(filename3):
|
||||
# running a host specific pass and has host specific variables
|
||||
# load into setup cache
|
||||
self.playbook.SETUP_CACHE[host].update(new_vars)
|
||||
elif host is None:
|
||||
# running a non-host specific pass and we can update the global vars instead
|
||||
self.vars.update(new_vars)
|
||||
|
||||
@ -15,8 +15,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
#############################################
|
||||
|
||||
from ansible import errors
|
||||
from ansible import utils
|
||||
|
||||
@ -24,47 +22,91 @@ class Task(object):
|
||||
|
||||
__slots__ = [
|
||||
'name', 'action', 'only_if', 'async_seconds', 'async_poll_interval',
|
||||
'notify', 'module_name', 'module_args', 'module_vars', 'play', 'notified_by',
|
||||
'notify', 'module_name', 'module_args', 'module_vars',
|
||||
'play', 'notified_by', 'tags', 'with_items', 'first_available_file', 'ignore_errors'
|
||||
]
|
||||
|
||||
# to prevent typos and such
|
||||
VALID_KEYS = [
|
||||
'name', 'action', 'only_if', 'async', 'poll', 'notify', 'with_items', 'first_available_file',
|
||||
'include', 'tags', 'ignore_errors'
|
||||
]
|
||||
|
||||
def __init__(self, play, ds, module_vars=None):
|
||||
''' constructor loads from a task or handler datastructure '''
|
||||
|
||||
# TODO: more error handling
|
||||
# include task specific vars
|
||||
for x in ds.keys():
|
||||
if not x in Task.VALID_KEYS:
|
||||
raise errors.AnsibleError("%s is not a legal parameter in an Ansible task or handler" % x)
|
||||
|
||||
self.module_vars = module_vars
|
||||
|
||||
self.play = play
|
||||
|
||||
# load various attributes
|
||||
self.name = ds.get('name', None)
|
||||
self.action = ds.get('action', '')
|
||||
self.tags = [ 'all' ]
|
||||
|
||||
# notified by is used by Playbook code to flag which hosts
|
||||
# need to run a notifier
|
||||
self.notified_by = []
|
||||
|
||||
# if no name is specified, use the action line as the name
|
||||
if self.name is None:
|
||||
self.name = self.action
|
||||
|
||||
# load various attributes
|
||||
self.only_if = ds.get('only_if', 'True')
|
||||
self.async_seconds = int(ds.get('async', 0)) # not async by default
|
||||
self.async_poll_interval = int(ds.get('poll', 10)) # default poll = 10 seconds
|
||||
self.notify = ds.get('notify', [])
|
||||
self.first_available_file = ds.get('first_available_file', None)
|
||||
self.with_items = ds.get('with_items', None)
|
||||
self.ignore_errors = ds.get('ignore_errors', False)
|
||||
|
||||
# notify can be a string or a list, store as a list
|
||||
if isinstance(self.notify, basestring):
|
||||
self.notify = [ self.notify ]
|
||||
|
||||
# split the action line into a module name + arguments
|
||||
tokens = self.action.split(None, 1)
|
||||
if len(tokens) < 1:
|
||||
raise errors.AnsibleError("invalid/missing action in task")
|
||||
|
||||
raise errors.AnsibleError("invalid/missing action in task. name: %s" % self.name)
|
||||
self.module_name = tokens[0]
|
||||
self.module_args = ''
|
||||
if len(tokens) > 1:
|
||||
self.module_args = tokens[1]
|
||||
|
||||
import_tags = self.module_vars.get('tags',[])
|
||||
if type(import_tags) in [str,unicode]:
|
||||
# allow the user to list comma delimited tags
|
||||
import_tags = import_tags.split(",")
|
||||
|
||||
self.name = utils.template(self.name, self.module_vars)
|
||||
self.action = utils.template(self.name, self.module_vars)
|
||||
self.action = utils.template(self.action, self.module_vars)
|
||||
|
||||
# handle mutually incompatible options
|
||||
if self.with_items is not None and self.first_available_file is not None:
|
||||
raise errors.AnsibleError("with_items and first_available_file are mutually incompatible in a single task")
|
||||
|
||||
if 'first_available_file' in ds:
|
||||
self.module_vars['first_available_file'] = ds.get('first_available_file')
|
||||
# make first_available_file accessable to Runner code
|
||||
if self.first_available_file:
|
||||
self.module_vars['first_available_file'] = self.first_available_file
|
||||
|
||||
# process with_items so it can be used by Runner code
|
||||
if self.with_items is None:
|
||||
self.with_items = [ ]
|
||||
self.module_vars['items'] = self.with_items
|
||||
|
||||
# make ignore_errors accessable to Runner code
|
||||
self.module_vars['ignore_errors'] = self.ignore_errors
|
||||
|
||||
# tags allow certain parts of a playbook to be run without running the whole playbook
|
||||
apply_tags = ds.get('tags', None)
|
||||
if apply_tags is not None:
|
||||
if type(apply_tags) in [ str, unicode ]:
|
||||
self.tags.append(apply_tags)
|
||||
elif type(apply_tags) == list:
|
||||
self.tags.extend(apply_tags)
|
||||
self.tags.extend(import_tags)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -18,17 +18,6 @@
|
||||
|
||||
################################################
|
||||
|
||||
import warnings
|
||||
import traceback
|
||||
import os
|
||||
import time
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import pipes
|
||||
import socket
|
||||
import random
|
||||
|
||||
import local
|
||||
import paramiko_ssh
|
||||
import ssh
|
||||
@ -36,17 +25,17 @@ import ssh
|
||||
class Connection(object):
|
||||
''' Handles abstract connections to remote hosts '''
|
||||
|
||||
def __init__(self, runner, transport,sudo_user):
|
||||
def __init__(self, runner):
|
||||
self.runner = runner
|
||||
self.transport = transport
|
||||
self.sudo_user = sudo_user
|
||||
|
||||
def connect(self, host, port=None):
|
||||
conn = None
|
||||
if self.transport == 'local':
|
||||
transport = self.runner.transport
|
||||
if transport == 'local':
|
||||
conn = local.LocalConnection(self.runner, host)
|
||||
elif self.transport == 'paramiko':
|
||||
elif transport == 'paramiko':
|
||||
conn = paramiko_ssh.ParamikoConnection(self.runner, host, port)
|
||||
elif self.transport == 'ssh':
|
||||
elif transport == 'ssh':
|
||||
conn = ssh.SSHConnection(self.runner, host, port)
|
||||
if conn is None:
|
||||
raise Exception("unsupported connection type")
|
||||
|
||||
@ -14,21 +14,11 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
################################################
|
||||
|
||||
import warnings
|
||||
import traceback
|
||||
import os
|
||||
import time
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import pipes
|
||||
import socket
|
||||
import random
|
||||
|
||||
from ansible import errors
|
||||
|
||||
class LocalConnection(object):
|
||||
@ -43,15 +33,16 @@ class LocalConnection(object):
|
||||
|
||||
return self
|
||||
|
||||
def exec_command(self, cmd, tmp_path,sudo_user,sudoable=False):
|
||||
def exec_command(self, cmd, tmp_path, sudo_user, sudoable=False):
|
||||
''' run a command on the local host '''
|
||||
|
||||
if self.runner.sudo and sudoable:
|
||||
cmd = "sudo -s %s" % cmd
|
||||
if self.runner.sudo_pass:
|
||||
# NOTE: if someone wants to add sudo w/ password to the local connection type, they are welcome
|
||||
# to do so. The primary usage of the local connection is for crontab and kickstart usage however
|
||||
# so this doesn't seem to be a huge priority
|
||||
raise errors.AnsibleError("sudo with password is presently only supported on the paramiko (SSH) connection type")
|
||||
if self.runner.sudo_pass:
|
||||
# NOTE: if someone wants to add sudo w/ password to the local connection type, they are welcome
|
||||
# to do so. The primary usage of the local connection is for crontab and kickstart usage however
|
||||
# so this doesn't seem to be a huge priority
|
||||
raise errors.AnsibleError("sudo with password is presently only supported on the paramiko (SSH) connection type")
|
||||
cmd = "sudo -u {0} -s {1}".format(sudo_user, cmd)
|
||||
|
||||
p = subprocess.Popen(cmd, shell=True, stdin=None,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
@ -60,6 +51,7 @@ class LocalConnection(object):
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
''' transfer a file from local to local '''
|
||||
|
||||
if not os.path.exists(in_path):
|
||||
raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
||||
try:
|
||||
@ -77,5 +69,4 @@ class LocalConnection(object):
|
||||
|
||||
def close(self):
|
||||
''' terminate the connection; nothing to do here '''
|
||||
|
||||
pass
|
||||
|
||||
@ -14,27 +14,27 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
################################################
|
||||
|
||||
import warnings
|
||||
import traceback
|
||||
import os
|
||||
import time
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import pipes
|
||||
import socket
|
||||
import random
|
||||
|
||||
from ansible import errors
|
||||
# prevent paramiko warning noise
|
||||
# see http://stackoverflow.com/questions/3920502/
|
||||
|
||||
# prevent paramiko warning noise -- see http://stackoverflow.com/questions/3920502/
|
||||
HAVE_PARAMIKO=False
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
import paramiko
|
||||
try:
|
||||
import paramiko
|
||||
HAVE_PARAMIKO=True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
class ParamikoConnection(object):
|
||||
''' SSH based connections with Paramiko '''
|
||||
@ -47,23 +47,20 @@ class ParamikoConnection(object):
|
||||
if port is None:
|
||||
self.port = self.runner.remote_port
|
||||
|
||||
def _get_conn(self):
|
||||
user = self.runner.remote_user
|
||||
def connect(self):
|
||||
''' activates the connection object '''
|
||||
|
||||
if not HAVE_PARAMIKO:
|
||||
raise errors.AnsibleError("paramiko is not installed")
|
||||
|
||||
user = self.runner.remote_user
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
try:
|
||||
ssh.connect(
|
||||
self.host,
|
||||
username=user,
|
||||
allow_agent=True,
|
||||
look_for_keys=True,
|
||||
key_filename=self.runner.private_key_file,
|
||||
password=self.runner.remote_pass,
|
||||
timeout=self.runner.timeout,
|
||||
port=self.port
|
||||
)
|
||||
ssh.connect(self.host, username=user, allow_agent=True, look_for_keys=True,
|
||||
key_filename=self.runner.private_key_file, password=self.runner.remote_pass,
|
||||
timeout=self.runner.timeout, port=self.port)
|
||||
except Exception, e:
|
||||
msg = str(e)
|
||||
if "PID check failed" in msg:
|
||||
@ -75,17 +72,12 @@ class ParamikoConnection(object):
|
||||
else:
|
||||
raise errors.AnsibleConnectionFailed(msg)
|
||||
|
||||
return ssh
|
||||
|
||||
def connect(self):
|
||||
''' connect to the remote host '''
|
||||
|
||||
self.ssh = self._get_conn()
|
||||
self.ssh = ssh
|
||||
return self
|
||||
|
||||
def exec_command(self, cmd, tmp_path,sudo_user,sudoable=False):
|
||||
|
||||
def exec_command(self, cmd, tmp_path, sudo_user, sudoable=False):
|
||||
''' run a command on the remote host '''
|
||||
|
||||
bufsize = 4096
|
||||
chan = self.ssh.get_transport().open_session()
|
||||
chan.get_pty()
|
||||
@ -113,16 +105,18 @@ class ParamikoConnection(object):
|
||||
while not sudo_output.endswith(prompt):
|
||||
chunk = chan.recv(bufsize)
|
||||
if not chunk:
|
||||
raise errors.AnsibleError('ssh connection closed waiting for sudo password prompt')
|
||||
if 'unknown user' in sudo_output:
|
||||
raise errors.AnsibleError(
|
||||
'user %s does not exist' % sudo_user)
|
||||
else:
|
||||
raise errors.AnsibleError('ssh connection ' +
|
||||
'closed waiting for password prompt')
|
||||
sudo_output += chunk
|
||||
chan.sendall(self.runner.sudo_pass + '\n')
|
||||
except socket.timeout:
|
||||
raise errors.AnsibleError('ssh timed out waiting for sudo.\n' + sudo_output)
|
||||
|
||||
stdin = chan.makefile('wb', bufsize)
|
||||
stdout = chan.makefile('rb', bufsize)
|
||||
stderr = '' # stderr goes to stdout when using a pty, so this will never output anything.
|
||||
return stdin, stdout, stderr
|
||||
return (chan.makefile('wb', bufsize), chan.makefile('rb', bufsize), '')
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
''' transfer a file from local to remote '''
|
||||
@ -132,21 +126,19 @@ class ParamikoConnection(object):
|
||||
try:
|
||||
sftp.put(in_path, out_path)
|
||||
except IOError:
|
||||
traceback.print_exc()
|
||||
raise errors.AnsibleError("failed to transfer file to %s" % out_path)
|
||||
sftp.close()
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
''' save a remote file to the specified path '''
|
||||
sftp = self.ssh.open_sftp()
|
||||
try:
|
||||
sftp.get(in_path, out_path)
|
||||
except IOError:
|
||||
traceback.print_exc()
|
||||
raise errors.AnsibleError("failed to transfer file from %s" % in_path)
|
||||
sftp.close()
|
||||
|
||||
def close(self):
|
||||
''' terminate the connection '''
|
||||
|
||||
self.ssh.close()
|
||||
|
||||
|
||||
@ -16,17 +16,13 @@
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
################################################
|
||||
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
import shlex
|
||||
import pipes
|
||||
import random
|
||||
import select
|
||||
import fcntl
|
||||
|
||||
from ansible import errors
|
||||
|
||||
class SSHConnection(object):
|
||||
@ -39,6 +35,7 @@ class SSHConnection(object):
|
||||
|
||||
def connect(self):
|
||||
''' connect to the remote host '''
|
||||
|
||||
self.common_args = []
|
||||
extra_args = os.getenv("ANSIBLE_SSH_ARGS", None)
|
||||
if extra_args is not None:
|
||||
@ -56,7 +53,7 @@ class SSHConnection(object):
|
||||
|
||||
return self
|
||||
|
||||
def exec_command(self, cmd, tmp_path,sudo_user,sudoable=False):
|
||||
def exec_command(self, cmd, tmp_path, sudo_user,sudoable=False):
|
||||
''' run a command on the remote host '''
|
||||
|
||||
ssh_cmd = ["ssh", "-tt", "-q"] + self.common_args + [self.host]
|
||||
@ -107,6 +104,8 @@ class SSHConnection(object):
|
||||
stdout += os.read(p.stdout.fileno(), 1024)
|
||||
# older versions of ssh generate this error which we ignore
|
||||
stdout=stdout.replace("tcgetattr: Invalid argument\n", "")
|
||||
# suppress Ubuntu 10.04/12.04 error on -tt option
|
||||
stdout=stdout.replace("tcgetattr: Inappropriate ioctl for device\n","")
|
||||
|
||||
if p.returncode != 0 and stdout.find('Bad configuration option: ControlPersist') != -1:
|
||||
raise errors.AnsibleError('using -c ssh on certain older ssh versions may not support ControlPersist, set ANSIBLE_SSH_ARGS="" before running again')
|
||||
@ -126,8 +125,6 @@ class SSHConnection(object):
|
||||
|
||||
def fetch_file(self, in_path, out_path):
|
||||
''' fetch a file from remote to local '''
|
||||
if not os.path.exists(in_path):
|
||||
raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
||||
sftp_cmd = ["sftp"] + self.common_args + [self.host]
|
||||
p = subprocess.Popen(sftp_cmd, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
@ -136,5 +133,6 @@ class SSHConnection(object):
|
||||
raise errors.AnsibleError("failed to transfer file from %s:\n%s\n%s" % (in_path, stdout, stderr))
|
||||
|
||||
def close(self):
|
||||
''' terminate the connection '''
|
||||
''' not applicable since we're executing openssh binaries '''
|
||||
pass
|
||||
|
||||
|
||||
@ -15,8 +15,6 @@
|
||||
# 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 shlex
|
||||
@ -25,14 +23,21 @@ import codecs
|
||||
import jinja2
|
||||
import yaml
|
||||
import optparse
|
||||
from operator import methodcaller
|
||||
import operator
|
||||
from ansible import errors
|
||||
from ansible import color
|
||||
from ansible import __version__
|
||||
import ansible.constants as C
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
from ansible import errors
|
||||
import ansible.constants as C
|
||||
try:
|
||||
from hashlib import md5 as _md5
|
||||
except ImportError:
|
||||
from md5 import md5 as _md5
|
||||
|
||||
###############################################################
|
||||
# UTILITY FUNCTIONS FOR COMMAND LINE TOOLS
|
||||
@ -40,76 +45,27 @@ import ansible.constants as C
|
||||
|
||||
def err(msg):
|
||||
''' print an error message to stderr '''
|
||||
|
||||
print >> sys.stderr, msg
|
||||
|
||||
def exit(msg, rc=1):
|
||||
''' quit with an error to stdout and a failure code '''
|
||||
|
||||
err(msg)
|
||||
sys.exit(rc)
|
||||
|
||||
def bigjson(result):
|
||||
''' format JSON output (uncompressed) '''
|
||||
def jsonify(result, format=False):
|
||||
''' format JSON output (uncompressed or uncompressed) '''
|
||||
|
||||
result2 = result.copy()
|
||||
return json.dumps(result2, sort_keys=True, indent=4)
|
||||
|
||||
def smjson(result):
|
||||
''' format JSON output (compressed) '''
|
||||
result2 = result.copy()
|
||||
return json.dumps(result2, sort_keys=True)
|
||||
|
||||
def task_start_msg(name, conditional):
|
||||
if conditional:
|
||||
return "NOTIFIED: [%s]" % name
|
||||
if format:
|
||||
return json.dumps(result2, sort_keys=True, indent=4)
|
||||
else:
|
||||
return "TASK: [%s]" % name
|
||||
|
||||
def regular_generic_msg(hostname, result, oneline, caption):
|
||||
''' output on the result of a module run that is not command '''
|
||||
if not oneline:
|
||||
return "%s | %s >> %s\n" % (hostname, caption, bigjson(result))
|
||||
else:
|
||||
return "%s | %s >> %s\n" % (hostname, caption, smjson(result))
|
||||
|
||||
def regular_success_msg(hostname, result, oneline):
|
||||
''' output the result of a successful module run '''
|
||||
return regular_generic_msg(hostname, result, oneline, 'success')
|
||||
|
||||
def regular_failure_msg(hostname, result, oneline):
|
||||
''' output the result of a failed module run '''
|
||||
return regular_generic_msg(hostname, result, oneline, 'FAILED')
|
||||
|
||||
def command_generic_msg(hostname, result, oneline, caption):
|
||||
''' output the result of a command run '''
|
||||
rc = result.get('rc', '0')
|
||||
stdout = result.get('stdout','')
|
||||
stderr = result.get('stderr', '')
|
||||
msg = result.get('msg', '')
|
||||
if not oneline:
|
||||
buf = "%s | %s | rc=%s >>\n" % (hostname, caption, result.get('rc',0))
|
||||
if stdout:
|
||||
buf += stdout
|
||||
if stderr:
|
||||
buf += stderr
|
||||
if msg:
|
||||
buf += msg
|
||||
buf += "\n"
|
||||
return buf
|
||||
else:
|
||||
if stderr:
|
||||
return "%s | %s | rc=%s | (stdout) %s (stderr) %s\n" % (hostname, caption, rc, stdout, stderr)
|
||||
else:
|
||||
return "%s | %s | rc=%s | (stdout) %s\n" % (hostname, caption, rc, stdout)
|
||||
|
||||
def command_success_msg(hostname, result, oneline):
|
||||
''' output from a successful command run '''
|
||||
return command_generic_msg(hostname, result, oneline, 'success')
|
||||
|
||||
def command_failure_msg(hostname, result, oneline):
|
||||
''' output from a failed command run '''
|
||||
return command_generic_msg(hostname, result, oneline, 'FAILED')
|
||||
return json.dumps(result2, sort_keys=True)
|
||||
|
||||
def write_tree_file(tree, hostname, buf):
|
||||
''' write something into treedir/hostname '''
|
||||
|
||||
# TODO: might be nice to append playbook runs per host in a similar way
|
||||
# in which case, we'd want append mode.
|
||||
path = os.path.join(tree, hostname)
|
||||
@ -119,33 +75,12 @@ def write_tree_file(tree, hostname, buf):
|
||||
|
||||
def is_failed(result):
|
||||
''' is a given JSON result a failed result? '''
|
||||
failed = False
|
||||
rc = 0
|
||||
if type(result) == dict:
|
||||
failed = result.get('failed', 0)
|
||||
rc = result.get('rc', 0)
|
||||
if rc != 0:
|
||||
return True
|
||||
return failed
|
||||
|
||||
def host_report_msg(hostname, module_name, result, oneline):
|
||||
''' summarize the JSON results for a particular host '''
|
||||
buf = ''
|
||||
failed = is_failed(result)
|
||||
if module_name in [ 'command', 'shell', 'raw' ] and 'ansible_job_id' not in result:
|
||||
if not failed:
|
||||
buf = command_success_msg(hostname, result, oneline)
|
||||
else:
|
||||
buf = command_failure_msg(hostname, result, oneline)
|
||||
else:
|
||||
if not failed:
|
||||
buf = regular_success_msg(hostname, result, oneline)
|
||||
else:
|
||||
buf = regular_failure_msg(hostname, result, oneline)
|
||||
return buf
|
||||
return ((result.get('rc', 0) != 0) or (result.get('failed', False) in [ True, 'True', 'true']))
|
||||
|
||||
def prepare_writeable_dir(tree):
|
||||
''' make sure a directory exists and is writeable '''
|
||||
|
||||
if tree != '/':
|
||||
tree = os.path.realpath(os.path.expanduser(tree))
|
||||
if not os.path.exists(tree):
|
||||
@ -158,6 +93,7 @@ def prepare_writeable_dir(tree):
|
||||
|
||||
def path_dwim(basedir, given):
|
||||
''' make relative paths work like folks expect '''
|
||||
|
||||
if given.startswith("/"):
|
||||
return given
|
||||
elif given.startswith("~/"):
|
||||
@ -166,21 +102,24 @@ def path_dwim(basedir, given):
|
||||
return os.path.join(basedir, given)
|
||||
|
||||
def json_loads(data):
|
||||
''' parse a JSON string and return a data structure '''
|
||||
|
||||
return json.loads(data)
|
||||
|
||||
def parse_json(data):
|
||||
''' this version for module return data only '''
|
||||
|
||||
try:
|
||||
return json.loads(data)
|
||||
except:
|
||||
# not JSON, but try "Baby JSON" which allows many of our modules to not
|
||||
# require JSON and makes writing modules in bash much simpler
|
||||
results = {}
|
||||
try :
|
||||
try:
|
||||
tokens = shlex.split(data)
|
||||
except:
|
||||
print "failed to parse json: "+ data
|
||||
raise;
|
||||
raise
|
||||
|
||||
for t in tokens:
|
||||
if t.find("=") == -1:
|
||||
@ -202,6 +141,7 @@ _LISTRE = re.compile(r"(\w+)\[(\d+)\]")
|
||||
|
||||
def _varLookup(name, vars):
|
||||
''' find the contents of a possibly complex variable in vars. '''
|
||||
|
||||
path = name.split('.')
|
||||
space = vars
|
||||
for part in path:
|
||||
@ -220,25 +160,20 @@ def _varLookup(name, vars):
|
||||
return space
|
||||
|
||||
_KEYCRE = re.compile(r"\$(?P<complex>\{){0,1}((?(complex)[\w\.\[\]]+|\w+))(?(complex)\})")
|
||||
# if { -> complex if complex, allow . and need trailing }
|
||||
|
||||
def varLookup(varname, vars):
|
||||
''' helper function used by varReplace '''
|
||||
|
||||
m = _KEYCRE.search(varname)
|
||||
if not m:
|
||||
return None
|
||||
return _varLookup(m.group(2), vars)
|
||||
|
||||
def varReplace(raw, vars):
|
||||
'''Perform variable replacement of $vars
|
||||
|
||||
@param raw: String to perform substitution on.
|
||||
@param vars: Dictionary of variables to replace. Key is variable name
|
||||
(without $ prefix). Value is replacement string.
|
||||
@return: Input raw string with substituted values.
|
||||
'''
|
||||
''' Perform variable replacement of $variables in string raw using vars dictionary '''
|
||||
# this code originally from yum
|
||||
|
||||
done = [] # Completed chunks to return
|
||||
done = [] # Completed chunks to return
|
||||
|
||||
while raw:
|
||||
m = _KEYCRE.search(raw)
|
||||
@ -259,38 +194,39 @@ def varReplace(raw, vars):
|
||||
|
||||
return ''.join(done)
|
||||
|
||||
def _template(text, vars, setup_cache=None):
|
||||
''' run a text buffer through the templating engine '''
|
||||
vars = vars.copy()
|
||||
vars['hostvars'] = setup_cache
|
||||
text = varReplace(unicode(text), vars)
|
||||
return text
|
||||
def template(text, vars):
|
||||
''' run a text buffer through the templating engine until it no longer changes '''
|
||||
|
||||
def template(text, vars, setup_cache=None):
|
||||
''' run a text buffer through the templating engine
|
||||
until it no longer changes '''
|
||||
prev_text = ''
|
||||
depth = 0
|
||||
while prev_text != text:
|
||||
depth = depth + 1
|
||||
if (depth > 20):
|
||||
raise errors.AnsibleError("template recursion depth exceeded")
|
||||
prev_text = text
|
||||
text = _template(text, vars, setup_cache)
|
||||
text = varReplace(unicode(text), vars)
|
||||
return text
|
||||
|
||||
def template_from_file(basedir, path, vars, setup_cache):
|
||||
def template_from_file(basedir, path, vars):
|
||||
''' run a file through the templating engine '''
|
||||
|
||||
environment = jinja2.Environment(loader=jinja2.FileSystemLoader(basedir), trim_blocks=False)
|
||||
data = codecs.open(path_dwim(basedir, path), encoding="utf8").read()
|
||||
t = environment.from_string(data)
|
||||
vars = vars.copy()
|
||||
vars['hostvars'] = setup_cache
|
||||
res = t.render(vars)
|
||||
if data.endswith('\n') and not res.endswith('\n'):
|
||||
res = res + '\n'
|
||||
return template(res, vars, setup_cache)
|
||||
return template(res, vars)
|
||||
|
||||
def parse_yaml(data):
|
||||
''' convert a yaml string to a data structure '''
|
||||
|
||||
return yaml.load(data)
|
||||
|
||||
def parse_yaml_from_file(path):
|
||||
''' convert a yaml file to a data structure '''
|
||||
|
||||
try:
|
||||
data = file(path).read()
|
||||
except IOError:
|
||||
@ -299,25 +235,53 @@ def parse_yaml_from_file(path):
|
||||
|
||||
def parse_kv(args):
|
||||
''' convert a string of key/value items to a dict '''
|
||||
|
||||
options = {}
|
||||
if args is not None:
|
||||
vargs = shlex.split(args, posix=True)
|
||||
# attempting to split a unicode here does bad things
|
||||
vargs = shlex.split(str(args), posix=True)
|
||||
for x in vargs:
|
||||
if x.find("=") != -1:
|
||||
k, v = x.split("=", 1)
|
||||
k, v = x.split("=",1)
|
||||
options[k]=v
|
||||
return options
|
||||
|
||||
def md5(filename):
|
||||
''' Return MD5 hex digest of local file, or None if file is not present. '''
|
||||
|
||||
if not os.path.exists(filename):
|
||||
return None
|
||||
digest = _md5()
|
||||
blocksize = 64 * 1024
|
||||
infile = open(filename, 'rb')
|
||||
block = infile.read(blocksize)
|
||||
while block:
|
||||
digest.update(block)
|
||||
block = infile.read(blocksize)
|
||||
infile.close()
|
||||
return digest.hexdigest()
|
||||
|
||||
def default(value, function):
|
||||
''' syntactic sugar around lazy evaluation of defaults '''
|
||||
if value is None:
|
||||
return function()
|
||||
return value
|
||||
|
||||
####################################################################
|
||||
# option handling code for /usr/bin/ansible and ansible-playbook
|
||||
# below this line
|
||||
|
||||
class SortedOptParser(optparse.OptionParser):
|
||||
'''Optparser which sorts the options by opt before outputting --help'''
|
||||
|
||||
def format_help(self, formatter=None):
|
||||
self.option_list.sort(key=methodcaller('get_opt_string'))
|
||||
self.option_list.sort(key=operator.methodcaller('get_opt_string'))
|
||||
return optparse.OptionParser.format_help(self, formatter=None)
|
||||
|
||||
def base_parser(constants=C, usage="", output_opts=False, runas_opts=False, async_opts=False, connect_opts=False):
|
||||
''' create an options parser for any ansible script '''
|
||||
|
||||
parser = SortedOptParser(usage)
|
||||
parser = SortedOptParser(usage, version="%prog " + __version__)
|
||||
parser.add_option('-v','--verbose', default=False, action="store_true",
|
||||
help='verbose mode')
|
||||
parser.add_option('-f','--forks', dest='forks', default=constants.DEFAULT_FORKS, type='int',
|
||||
@ -369,3 +333,4 @@ def base_parser(constants=C, usage="", output_opts=False, runas_opts=False, asyn
|
||||
return parser
|
||||
|
||||
|
||||
|
||||
|
||||
216
library/apt
216
library/apt
@ -1,4 +1,6 @@
|
||||
#!/usr/bin/python -tt
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Flowroute LLC
|
||||
# Written by Matthew Williams <matthew@flowroute.com>
|
||||
# Based on yum module written by Seth Vidal <skvidal at fedoraproject.org>
|
||||
@ -17,36 +19,18 @@
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
import os
|
||||
import sys
|
||||
import shlex
|
||||
import subprocess
|
||||
import syslog
|
||||
import traceback
|
||||
# added to stave off future warnings about apt api
|
||||
import warnings;
|
||||
warnings.filterwarnings('ignore', "apt API not stable yet", FutureWarning)
|
||||
|
||||
# APT related constants
|
||||
APT_PATH = "/usr/bin/apt-get"
|
||||
APT = "DEBIAN_PRIORITY=critical %s" % APT_PATH
|
||||
|
||||
def exit_json(rc=0, **kwargs):
|
||||
print json.dumps(kwargs)
|
||||
sys.exit(rc)
|
||||
|
||||
def fail_json(**kwargs):
|
||||
kwargs['failed'] = True
|
||||
exit_json(rc=1, **kwargs)
|
||||
|
||||
try:
|
||||
import apt, apt_pkg
|
||||
except ImportError:
|
||||
fail_json(msg="could not import apt, please install the python-apt package on this host")
|
||||
APT = "DEBIAN_FRONTEND=noninteractive DEBIAN_PRIORITY=critical %s" % APT_PATH
|
||||
|
||||
def run_apt(command):
|
||||
try:
|
||||
cmd = subprocess.Popen(command, shell=True,
|
||||
cmd = subprocess.Popen(command, shell=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = cmd.communicate()
|
||||
except (OSError, IOError), e:
|
||||
@ -68,115 +52,133 @@ def package_split(pkgspec):
|
||||
else:
|
||||
return parts[0], None
|
||||
|
||||
def package_status(pkgname, version, cache):
|
||||
def package_status(m, pkgname, version, cache):
|
||||
try:
|
||||
pkg = cache[pkgname]
|
||||
except KeyError:
|
||||
fail_json(msg="No package matching '%s' is available" % pkgname)
|
||||
m.fail_json(msg="No package matching '%s' is available" % pkgname)
|
||||
if version:
|
||||
try :
|
||||
return pkg.is_installed and pkg.installed.version == version, False
|
||||
except AttributeError:
|
||||
except AttributeError:
|
||||
#assume older version of python-apt is installed
|
||||
return pkg.isInstalled and pkg.installedVersion == version, False
|
||||
else:
|
||||
try :
|
||||
return pkg.is_installed, pkg.is_upgradable
|
||||
except AttributeError:
|
||||
except AttributeError:
|
||||
#assume older version of python-apt is installed
|
||||
return pkg.isInstalled, pkg.isUpgradable
|
||||
|
||||
def install(pkgspec, cache, upgrade=False, default_release=None):
|
||||
name, version = package_split(pkgspec)
|
||||
installed, upgradable = package_status(name, version, cache)
|
||||
if not installed or (upgrade and upgradable):
|
||||
cmd = "%s --option Dpkg::Options::=--force-confold -q -y install '%s'" % (APT, pkgspec)
|
||||
def install(m, pkgspec, cache, upgrade=False, default_release=None, install_recommends=True, force=False):
|
||||
packages = ""
|
||||
for package in pkgspec:
|
||||
name, version = package_split(package)
|
||||
installed, upgradable = package_status(m, name, version, cache)
|
||||
if not installed or (upgrade and upgradable):
|
||||
packages += "'%s' " % package
|
||||
|
||||
if len(packages) != 0:
|
||||
if force:
|
||||
force_yes = '--force-yes'
|
||||
else:
|
||||
force_yes = ''
|
||||
|
||||
cmd = "%s --option Dpkg::Options::=--force-confold -q -y %s install %s" % (APT, force_yes,packages)
|
||||
if default_release:
|
||||
cmd += " -t '%s'" % (default_release,)
|
||||
if not install_recommends:
|
||||
cmd += " --no-install-recommends"
|
||||
|
||||
rc, out, err = run_apt(cmd)
|
||||
if rc:
|
||||
fail_json(msg="'apt-get install %s' failed: %s" % (pkgspec, err))
|
||||
return True
|
||||
m.fail_json(msg="'apt-get install %s' failed: %s" % (packages, err))
|
||||
else:
|
||||
m.exit_json(changed=True)
|
||||
else:
|
||||
return False
|
||||
m.exit_json(changed=False)
|
||||
|
||||
def remove(pkgspec, cache, purge=False):
|
||||
name, version = package_split(pkgspec)
|
||||
installed, upgradable = package_status(name, version, cache)
|
||||
if not installed:
|
||||
return False
|
||||
def remove(m, pkgspec, cache, purge=False):
|
||||
packages = ""
|
||||
for package in pkgspec:
|
||||
name, version = package_split(package)
|
||||
installed, upgradable = package_status(m, name, version, cache)
|
||||
if installed:
|
||||
packages += "'%s' " % package
|
||||
|
||||
if len(packages) == 0:
|
||||
m.exit_json(changed=False)
|
||||
else:
|
||||
purge = '--purge' if purge else ''
|
||||
cmd = "%s -q -y %s remove '%s'" % (APT, purge, name)
|
||||
cmd = "%s -q -y %s remove %s" % (APT, purge,packages)
|
||||
rc, out, err = run_apt(cmd)
|
||||
if rc:
|
||||
fail_json(msg="'apt-get remove %s' failed: %s" % (name, err))
|
||||
return True
|
||||
m.fail_json(msg="'apt-get remove %s' failed: %s" % (packages, err))
|
||||
m.exit_json(changed=True)
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
state = dict(default='installed', choices=['installed', 'latest', 'removed']),
|
||||
update_cache = dict(default='no', choices=['yes', 'no'], aliases=['update-cache']),
|
||||
purge = dict(default='no', choices=['yes', 'no']),
|
||||
package = dict(default=None, aliases=['pkg', 'name']),
|
||||
default_release = dict(default=None, aliases=['default-release']),
|
||||
install_recommends = dict(default='yes', aliases=['install-recommends'], choices=['yes', 'no']),
|
||||
force = dict(default='no', choices=['yes', 'no'])
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
import apt, apt_pkg
|
||||
except:
|
||||
module.fail_json("Could not import python modules: apt, apt_pkg. Please install python-apt package.")
|
||||
|
||||
if not os.path.exists(APT_PATH):
|
||||
module.fail_json(msg="Cannot find apt-get")
|
||||
|
||||
p = module.params
|
||||
if p['package'] is None and p['update_cache'] != 'yes':
|
||||
module.fail_json(msg='pkg=name and/or update_cache=yes is required')
|
||||
|
||||
install_recommends = module.boolean(p['install_recommends'])
|
||||
|
||||
cache = apt.Cache()
|
||||
if p['default_release']:
|
||||
apt_pkg.config['APT::Default-Release'] = p['default_release']
|
||||
# reopen cache w/ modified config
|
||||
cache.open(progress=None)
|
||||
|
||||
if module.boolean(p['update_cache']):
|
||||
cache.update()
|
||||
cache.open(progress=None)
|
||||
if p['package'] == None:
|
||||
module.exit_json(changed=False)
|
||||
|
||||
force_yes = module.boolean(p['force'])
|
||||
|
||||
packages = p['package'].split(',')
|
||||
latest = p['state'] == 'latest'
|
||||
for package in packages:
|
||||
if package.count('=') > 1:
|
||||
module.fail_json(msg="invalid package spec: %s" % package)
|
||||
if latest and '=' in package:
|
||||
module.fail_json(msg='version number inconsistent with state=latest: %s' % package)
|
||||
|
||||
# ===========================================
|
||||
if p['state'] == 'latest':
|
||||
install(module, packages, cache, upgrade=True,
|
||||
default_release=p['default_release'],
|
||||
install_recommends=install_recommends,
|
||||
force=force_yes)
|
||||
elif p['state'] == 'installed':
|
||||
install(module, packages, cache, default_release=p['default_release'],
|
||||
install_recommends=install_recommends,force=force_yes)
|
||||
elif p['state'] == 'removed':
|
||||
remove(module, packages, cache, purge = module.boolean(p['purge']))
|
||||
|
||||
if not os.path.exists(APT_PATH):
|
||||
fail_json(msg="Cannot find apt-get")
|
||||
|
||||
argfile = sys.argv[1]
|
||||
args = open(argfile, 'r').read()
|
||||
items = shlex.split(args)
|
||||
syslog.openlog('ansible-%s' % os.path.basename(__file__))
|
||||
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
|
||||
|
||||
if not len(items):
|
||||
fail_json(msg='the module requires arguments -a')
|
||||
sys.exit(1)
|
||||
|
||||
params = {}
|
||||
for x in items:
|
||||
(k, v) = x.split("=", 1)
|
||||
params[k] = v
|
||||
|
||||
state = params.get('state', 'installed')
|
||||
package = params.get('pkg', params.get('package', params.get('name', None)))
|
||||
update_cache = params.get('update-cache', 'no')
|
||||
purge = params.get('purge', 'no')
|
||||
default_release = params.get('default-release', None)
|
||||
|
||||
if state not in ['installed', 'latest', 'removed']:
|
||||
fail_json(msg='invalid state')
|
||||
|
||||
if update_cache not in ['yes', 'no']:
|
||||
fail_json(msg='invalid value for update_cache (requires yes or no -- default is no')
|
||||
|
||||
if purge not in ['yes', 'no']:
|
||||
fail_json(msg='invalid value for purge (requires yes or no -- default is no)')
|
||||
|
||||
if package is None and update_cache != 'yes':
|
||||
fail_json(msg='pkg=name and/or update-cache=yes is required')
|
||||
|
||||
cache = apt.Cache()
|
||||
if default_release:
|
||||
apt_pkg.config['APT::Default-Release'] = default_release
|
||||
# reopen cache w/ modified config
|
||||
cache.open()
|
||||
|
||||
if update_cache == 'yes':
|
||||
cache.update()
|
||||
cache.open()
|
||||
if package == None:
|
||||
exit_json(changed=False)
|
||||
|
||||
if package.count('=') > 1:
|
||||
fail_json(msg='invalid package spec')
|
||||
|
||||
if state == 'latest':
|
||||
if '=' in package:
|
||||
fail_json(msg='version number inconsistent with state=latest')
|
||||
changed = install(package, cache, upgrade=True,
|
||||
default_release=default_release)
|
||||
elif state == 'installed':
|
||||
changed = install(package, cache, default_release=default_release)
|
||||
elif state == 'removed':
|
||||
changed = remove(package, cache, purge == 'yes')
|
||||
|
||||
exit_json(changed=changed)
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
|
||||
main()
|
||||
|
||||
|
||||
118
library/assemble
118
library/assemble
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Stephen Fromm <sfromm@gmail.com>
|
||||
#
|
||||
@ -17,38 +18,14 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
import shlex
|
||||
import shutil
|
||||
import syslog
|
||||
import tempfile
|
||||
try:
|
||||
import hashlib
|
||||
HAVE_HASHLIB=True
|
||||
except ImportError:
|
||||
import md5
|
||||
HAVE_HASHLIB=False
|
||||
|
||||
# Since hashlib is only available in 2.5 and onwards, this module
|
||||
# uses md5 which is available in 2.4.
|
||||
|
||||
# ===========================================
|
||||
# Support methods
|
||||
|
||||
def exit_json(rc=0, **kwargs):
|
||||
print json.dumps(kwargs)
|
||||
sys.exit(rc)
|
||||
|
||||
def fail_json(**kwargs):
|
||||
kwargs['failed'] = True
|
||||
exit_json(rc=1, **kwargs)
|
||||
|
||||
def assemble_from_fragments(path):
|
||||
''' assemble a file from a directory of fragments '''
|
||||
assembled = []
|
||||
@ -64,60 +41,47 @@ def write_temp_file(data):
|
||||
os.close(fd)
|
||||
return path
|
||||
|
||||
def file_digest(path):
|
||||
if HAVE_HASHLIB:
|
||||
digest = hashlib.md5(file(path).read()).hexdigest()
|
||||
else:
|
||||
digest = md5.new(file(path).read()).hexdigest()
|
||||
return digest
|
||||
# ==============================================================
|
||||
# main
|
||||
|
||||
# ===========================================
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
src = dict(required=True),
|
||||
dest = dict(required=True),
|
||||
)
|
||||
)
|
||||
|
||||
changed=False
|
||||
pathmd5 = None
|
||||
destmd5 = None
|
||||
src = os.path.expanduser(module.params['src'])
|
||||
dest = os.path.expanduser(module.params['dest'])
|
||||
|
||||
if not os.path.exists(src):
|
||||
module.fail_json(msg="Source (%s) does not exist" % src)
|
||||
|
||||
if not os.path.isdir(src):
|
||||
module.fail_json(msg="Source (%s) is not a directory" % src)
|
||||
|
||||
path = write_temp_file(assemble_from_fragments(src))
|
||||
pathmd5 = module.md5(path)
|
||||
|
||||
if os.path.exists(dest):
|
||||
destmd5 = module.md5(dest)
|
||||
|
||||
if pathmd5 != destmd5:
|
||||
shutil.copy(path, dest)
|
||||
changed = True
|
||||
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
fail_json(msg="the assemble module requires arguments (-a)")
|
||||
# Mission complete
|
||||
module.exit_json(src=src, dest=dest, md5sum=destmd5,
|
||||
changed=changed, msg="OK",
|
||||
daisychain="file", daisychain_args=module.params)
|
||||
|
||||
argfile = sys.argv[1]
|
||||
if not os.path.exists(argfile):
|
||||
fail_json(msg="Argument file not found")
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
|
||||
args = open(argfile, 'r').read()
|
||||
items = shlex.split(args)
|
||||
syslog.openlog('ansible-%s' % os.path.basename(__file__))
|
||||
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
|
||||
|
||||
if not len(items):
|
||||
fail_json(msg="the assemble module requires arguments (-a)")
|
||||
|
||||
params = {}
|
||||
for x in items:
|
||||
(k, v) = x.split("=")
|
||||
params[k] = v
|
||||
|
||||
changed = False
|
||||
pathmd5 = None
|
||||
destmd5 = None
|
||||
src = params.get('src', None)
|
||||
dest = params.get('dest', None)
|
||||
|
||||
if src:
|
||||
src = os.path.expanduser(src)
|
||||
if dest:
|
||||
dest = os.path.expanduser(dest)
|
||||
|
||||
if not os.path.exists(src):
|
||||
fail_json(msg="Source (%s) does not exist" % src)
|
||||
|
||||
if not os.path.isdir(src):
|
||||
fail_json(msg="Source (%s) is not a directory" % src)
|
||||
|
||||
path = write_temp_file(assemble_from_fragments(src))
|
||||
pathmd5 = file_digest(path)
|
||||
|
||||
if os.path.exists(dest):
|
||||
destmd5 = file_digest(dest)
|
||||
|
||||
if pathmd5 != destmd5:
|
||||
shutil.copy(path, dest)
|
||||
changed = True
|
||||
|
||||
exit_json(md5sum=pathmd5, changed=changed)
|
||||
main()
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>, and others
|
||||
#
|
||||
@ -18,96 +19,50 @@
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
import shlex
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import datetime
|
||||
import traceback
|
||||
import syslog
|
||||
|
||||
# ===========================================
|
||||
def main():
|
||||
|
||||
# FIXME: better error handling
|
||||
module = AnsibleModule(argument_spec=dict(
|
||||
jid=dict(required=True),
|
||||
mode=dict(default='status', choices=['status','cleanup']),
|
||||
))
|
||||
|
||||
argsfile = sys.argv[1]
|
||||
args = open(argsfile, 'r').read()
|
||||
items = shlex.split(args)
|
||||
mode = module.params['mode']
|
||||
jid = module.params['jid']
|
||||
|
||||
syslog.openlog('ansible-%s' % os.path.basename(__file__))
|
||||
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
|
||||
# setup logging directory
|
||||
logdir = os.path.expanduser("~/.ansible_async")
|
||||
log_path = os.path.join(logdir, jid)
|
||||
|
||||
params = {}
|
||||
for x in items:
|
||||
(k, v) = x.split("=")
|
||||
params[k] = v
|
||||
if not os.path.exists(log_path):
|
||||
module.fail_json(msg="could not find job", ansible_job_id=jid)
|
||||
|
||||
mode = params.get('mode', 'status')
|
||||
jid = params.get('jid', None)
|
||||
if mode == 'cleanup':
|
||||
os.unlink(log_path)
|
||||
module.exit_json(ansible_job_id=jid, erased=log_path)
|
||||
|
||||
# ===========================================
|
||||
# NOT in cleanup mode, assume regular status mode
|
||||
# no remote kill mode currently exists, but probably should
|
||||
# consider log_path + ".pid" file and also unlink that above
|
||||
|
||||
if jid is None:
|
||||
print json.dumps({
|
||||
"failed" : True,
|
||||
"msg" : "jid=INTEGER is required"
|
||||
})
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# setup logging directory
|
||||
logdir = os.path.expanduser("~/.ansible_async")
|
||||
log_path = os.path.join(logdir, jid)
|
||||
|
||||
if not os.path.exists(log_path):
|
||||
print json.dumps({
|
||||
"failed" : 1,
|
||||
"msg" : "could not find job",
|
||||
"ansible_job_id" : jid
|
||||
})
|
||||
sys.exit(1)
|
||||
|
||||
if mode == 'cleanup':
|
||||
os.unlink(log_path)
|
||||
print json.dumps({
|
||||
"ansible_job_id" : jid,
|
||||
"erased" : log_path
|
||||
})
|
||||
sys.exit(0)
|
||||
|
||||
# NOT in cleanup mode, assume regular status mode
|
||||
# no remote kill mode currently exists, but probably should
|
||||
# consider log_path + ".pid" file and also unlink that above
|
||||
|
||||
data = file(log_path).read()
|
||||
try:
|
||||
data = json.loads(data)
|
||||
except Exception, e:
|
||||
if data == '':
|
||||
# file not written yet? That means it is running
|
||||
print json.dumps({
|
||||
"results_file" : log_path,
|
||||
"ansible_job_id" : jid,
|
||||
"started" : 1,
|
||||
})
|
||||
else:
|
||||
print json.dumps({
|
||||
"failed" : True,
|
||||
"ansible_job_id" : jid,
|
||||
"results_file" : log_path,
|
||||
"msg" : "Could not parse job output: %s" % data,
|
||||
})
|
||||
sys.exit(0)
|
||||
|
||||
if not data.has_key("started"):
|
||||
data['finished'] = 1
|
||||
data['ansible_job_id'] = jid
|
||||
|
||||
print json.dumps(data)
|
||||
sys.exit(0)
|
||||
data = file(log_path).read()
|
||||
try:
|
||||
data = json.loads(data)
|
||||
except Exception, e:
|
||||
if data == '':
|
||||
# file not written yet? That means it is running
|
||||
module.exit_json(results_file=log_path, ansible_job_id=jid, started=1)
|
||||
else:
|
||||
module_fail_json(ansible_job_id=jid, results_file=log_path,
|
||||
msg="Could not parse job output: %s" % data)
|
||||
|
||||
if not data.has_key("started"):
|
||||
data['finished'] = 1
|
||||
data['ansible_job_id'] = jid
|
||||
module.exit_json(**data)
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
main()
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>, and others
|
||||
#
|
||||
|
||||
@ -1,35 +1,10 @@
|
||||
#!/usr/bin/env python
|
||||
"""Ansible module to add authorized_keys for ssh logins.
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Ansible module to add authorized_keys for ssh logins.
|
||||
(c) 2012, Brad Olson <brado@movedbylight.com>
|
||||
|
||||
Results: Makes sure the public key line is present or absent in the user's .ssh/authorized_keys.
|
||||
|
||||
Arguments
|
||||
=========
|
||||
user = username
|
||||
key = line to add to authorized_keys for user
|
||||
state = absent|present (default: present)
|
||||
|
||||
Command Line Example
|
||||
====================
|
||||
|
||||
ansible somehost -m authorized_key -a user=charlie key="ssh-dss AAAABUfOL+8BTwaRYr/rycsBF1D8e5pTxEsXHQs4iq+mZdyWqlW++L6pMiam1A8yweP+rKtgjK2httVS6GigVsuWWfOd7/sdWippefq74nppVUELHPKkaIOjJNN1zUHFoL/YMwAAAEBALnAsQN10TNGsRDe5arBsW8cTOjqLyYBcIqgPYTZW8zENErFxt7ij3fW3Jh/sCpnmy8rkS7FyK8ULX0PEy/2yDx8/5rXgMIICbRH/XaBy9Ud5bRBFVkEDu/r+rXP33wFPHjWjwvHAtfci1NRBAudQI/98DbcGQw5HmE89CjgZRo5ktkC5yu/8agEPocVjdHyZr7PaHfxZGUDGKtGRL2QzRYukCmWo1cZbMBHcI5FzImvTHS9/8B3SATjXMPgbfBuEeBwuBK5EjL+CtHY5bWs9kmYjmeo0KfUMH8hY4MAXDoKhQ7DhBPIrcjS5jPtoGxIREZjba67r6/P2XKXaCZH6Fc= charlie@somemail.org 2011-01-17"
|
||||
|
||||
Playbook Example
|
||||
================
|
||||
|
||||
---
|
||||
# include like this:
|
||||
# - include: tasks/logins.yaml users=charlie,sue
|
||||
- name: create user charlie
|
||||
action: user name=charlie shell=/bin/bash createhome=yes groups=www-data
|
||||
only_if: "'charlie' in '$users'"
|
||||
- name: add public key for charlie
|
||||
action: authorized_key user=charlie key="ssh-dss AAAABUfOL+8BTwaRYr/rycsBF1D8e5pTxEsXHQs4iq+mZdyWqlW++L6pMiam1A8yweP+rKtgjK2httVS6GigVsuWWfOd7/sdWippefq74nppVUELHPKkaIOjJNN1zUHFoL/YMwAAAEBALnAsQN10TNGsRDe5arBsW8cTOjqLyYBcIqgPYTZW8zENErFxt7ij3fW3Jh/sCpnmy8rkS7FyK8ULX0PEy/2yDx8/5rXgMIICbRH/XaBy9Ud5bRBFVkEDu/r+rXP33wFPHjWjwvHAtfci1NRBAudQI/98DbcGQw5HmE89CjgZRo5ktkC5yu/8agEPocVjdHyZr7PaHfxZGUDGKtGRL2QzRYukCmWo1cZbMBHcI5FzImvTHS9/8B3SATjXMPgbfBuEeBwuBK5EjL+CtHY5bWs9kmYjmeo0KfUMH8hY4MAXDoKhQ7DhBPIrcjS5jPtoGxIREZjba67r6/P2XKXaCZH6Fc= charlie@somemail.org 2011-01-17"
|
||||
only_if: "'charlie' in '$users'"
|
||||
|
||||
|
||||
This file is part of Ansible
|
||||
|
||||
Ansible is free software: you can redistribute it and/or modify
|
||||
@ -46,57 +21,24 @@ You should have received a copy of the GNU General Public License
|
||||
along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
# Makes sure the public key line is present or absent in the user's .ssh/authorized_keys.
|
||||
#
|
||||
# Arguments
|
||||
# =========
|
||||
# user = username
|
||||
# key = line to add to authorized_keys for user
|
||||
# state = absent|present (default: present)
|
||||
#
|
||||
# see example in examples/playbooks
|
||||
|
||||
import sys, os, shlex, pwd, syslog
|
||||
from os.path import expanduser, exists, isfile, join
|
||||
|
||||
params = {}
|
||||
msg=""
|
||||
|
||||
def exit_json(rc=0, **kwargs):
|
||||
if 'name' in kwargs:
|
||||
add_user_info(kwargs)
|
||||
print json.dumps(kwargs)
|
||||
sys.exit(rc)
|
||||
|
||||
def fail_json(**kwargs):
|
||||
kwargs['failed'] = True
|
||||
exit_json(rc=1, **kwargs)
|
||||
|
||||
def get_params():
|
||||
"""Startup tasks and read params.
|
||||
|
||||
:return: parameters as dictionary.
|
||||
"""
|
||||
global msg
|
||||
|
||||
msg = "reading params"
|
||||
argfile = sys.argv[1]
|
||||
try:
|
||||
f = open(argfile,"r")
|
||||
args = f.read()
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
msg = "writing syslog."
|
||||
syslog.openlog('ansible-%s' % os.path.basename(__file__))
|
||||
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
|
||||
|
||||
msg = "parsing params"
|
||||
params = dict( # make a dictionary of...
|
||||
[ arg.split("=", 1) # assignment pairs
|
||||
for arg in shlex.split(args) # using shell lexing
|
||||
if "=" in arg # ignoring tokens without assignment
|
||||
])
|
||||
|
||||
return params
|
||||
import sys
|
||||
import os
|
||||
import pwd
|
||||
import os.path
|
||||
|
||||
def keyfile(user, write=False):
|
||||
"""Calculate name of authorized keys file, optionally creating the
|
||||
"""
|
||||
Calculate name of authorized keys file, optionally creating the
|
||||
directories and file, properly setting permissions.
|
||||
|
||||
:param str user: name of user in passwd file
|
||||
@ -104,98 +46,91 @@ def keyfile(user, write=False):
|
||||
:return: full path string to authorized_keys for user
|
||||
"""
|
||||
|
||||
global msg
|
||||
msg = "Reading system user entry."
|
||||
user_entry = pwd.getpwnam(user)
|
||||
msg = "Calculating special directories"
|
||||
homedir = user_entry.pw_dir
|
||||
sshdir = join(homedir, ".ssh")
|
||||
keysfile = join(sshdir, "authorized_keys")
|
||||
if not write: return keysfile
|
||||
homedir = user_entry.pw_dir
|
||||
sshdir = os.path.join(homedir, ".ssh")
|
||||
keysfile = os.path.join(sshdir, "authorized_keys")
|
||||
|
||||
if not write:
|
||||
return keysfile
|
||||
|
||||
#create directories and files for authorized keys
|
||||
msg = "Reading user and group info."
|
||||
uid = user_entry.pw_uid
|
||||
gid = user_entry.pw_gid
|
||||
msg = "Making ~/.ssh."
|
||||
if not exists(sshdir): os.mkdir(sshdir, 0700)
|
||||
|
||||
if not os.path.exists(sshdir):
|
||||
os.mkdir(sshdir, 0700)
|
||||
os.chown(sshdir, uid, gid)
|
||||
os.chmod(sshdir, 0700)
|
||||
msg = "Touching authorized keys file."
|
||||
if not exists( keysfile):
|
||||
|
||||
if not os.path.exists( keysfile):
|
||||
try:
|
||||
f = open(keysfile, "w") #touches file so we can set ownership and perms
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
os.chown(keysfile, uid, gid)
|
||||
os.chmod(keysfile, 0600)
|
||||
return keysfile
|
||||
|
||||
def readkeys( filename):
|
||||
global msg
|
||||
msg = "Reading authorized_keys."
|
||||
if not isfile(filename): return []
|
||||
try:
|
||||
f = open(filename)
|
||||
keys = [line.rstrip() for line in f.readlines()]
|
||||
finally:
|
||||
f.close()
|
||||
def readkeys(filename):
|
||||
|
||||
if not os.path.isfile(filename):
|
||||
return []
|
||||
f = open(filename)
|
||||
keys = [line.rstrip() for line in f.readlines()]
|
||||
f.close()
|
||||
return keys
|
||||
|
||||
def writekeys( filename, keys):
|
||||
global msg
|
||||
msg = "Writing authorized_keys."
|
||||
try:
|
||||
f = open(filename,"w")
|
||||
f.writelines( (key + "\n" for key in keys) )
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
def enforce_state( params):
|
||||
"""Add or remove key.
|
||||
f = open(filename,"w")
|
||||
f.writelines( (key + "\n" for key in keys) )
|
||||
f.close()
|
||||
|
||||
:return: True=changed, False=unchanged
|
||||
def enforce_state(module, params):
|
||||
"""
|
||||
Add or remove key.
|
||||
"""
|
||||
global msg
|
||||
|
||||
#== scrub params
|
||||
msg = "Invalid or missing param: user."
|
||||
user = params["user"]
|
||||
msg = "Invalid or missing param: key."
|
||||
key = params["key"]
|
||||
user = params["user"]
|
||||
key = params["key"]
|
||||
state = params.get("state", "present")
|
||||
|
||||
#== check current state
|
||||
params["keyfile"] = keyfile(user, write=False) #just get the filename, don't create file
|
||||
keys = readkeys( params["keyfile"])
|
||||
# check current state -- just get the filename, don't create file
|
||||
params["keyfile"] = keyfile(user, write=False)
|
||||
keys = readkeys(params["keyfile"])
|
||||
present = key in keys
|
||||
|
||||
#== handle idempotent state=present
|
||||
# handle idempotent state=present
|
||||
if state=="present":
|
||||
if present: return False #nothing to do
|
||||
if present:
|
||||
module.exit_json(changed=False)
|
||||
keys.append(key)
|
||||
writekeys(keyfile(user,write=True), keys)
|
||||
|
||||
elif state=="absent":
|
||||
if not present: return False #nothing to do
|
||||
if not present:
|
||||
module.exit_json(changed=False)
|
||||
keys.remove(key)
|
||||
writekeys(keyfile(user,write=True), keys)
|
||||
else:
|
||||
msg = "Invalid param: state."
|
||||
raise StandardError(msg)
|
||||
return True
|
||||
|
||||
#===== MAIN SCRIPT ===================================================
|
||||
params['changed'] = True
|
||||
return params
|
||||
|
||||
try:
|
||||
params = get_params()
|
||||
changed = enforce_state( params)
|
||||
msg = ""
|
||||
except:
|
||||
msg = "Error %s" % msg
|
||||
def main():
|
||||
|
||||
# Don't do sys.exit() within try...except
|
||||
if msg:
|
||||
fail_json(msg=msg)
|
||||
else:
|
||||
exit_json( user=params["user"], changed=changed)
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
user = dict(required=True),
|
||||
key = dict(required=True),
|
||||
state = dict(default='present', choices=['absent','present'])
|
||||
)
|
||||
)
|
||||
|
||||
params = module.params
|
||||
results = enforce_state(module, module.params)
|
||||
module.exit_json(**results)
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
main()
|
||||
|
||||
160
library/command
160
library/command
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>, and others
|
||||
#
|
||||
@ -16,12 +17,6 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
@ -29,79 +24,102 @@ import datetime
|
||||
import traceback
|
||||
import shlex
|
||||
import os
|
||||
import syslog
|
||||
|
||||
argfile = sys.argv[1]
|
||||
args = open(argfile, 'r').read()
|
||||
syslog.openlog('ansible-%s' % os.path.basename(__file__))
|
||||
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
|
||||
def main():
|
||||
|
||||
shell = False
|
||||
# the command module is the one ansible module that does not take key=value args
|
||||
# hence don't copy this one if you are looking to build others!
|
||||
module = CommandModule(argument_spec=dict())
|
||||
|
||||
if args.find("#USE_SHELL") != -1:
|
||||
args = args.replace("#USE_SHELL", "")
|
||||
shell = True
|
||||
shell = module.params['shell']
|
||||
chdir = module.params['chdir']
|
||||
args = module.params['args']
|
||||
|
||||
check_args = shlex.split(args)
|
||||
for x in check_args:
|
||||
if x.startswith("creates="):
|
||||
# do not run the command if the line contains creates=filename
|
||||
# and the filename already exists. This allows idempotence
|
||||
# of command executions.
|
||||
(k,v) = x.split("=",1)
|
||||
if os.path.exists(v):
|
||||
print json.dumps({
|
||||
"cmd" : args,
|
||||
"stdout" : "skipped, since %s exists" % v,
|
||||
"skipped" : True,
|
||||
"changed" : False,
|
||||
"stderr" : "",
|
||||
"rc" : 0,
|
||||
})
|
||||
sys.exit(0)
|
||||
args = args.replace(x,'')
|
||||
|
||||
if chdir:
|
||||
os.chdir(chdir)
|
||||
|
||||
if not shell:
|
||||
args = shlex.split(args)
|
||||
if not shell:
|
||||
args = shlex.split(args)
|
||||
startd = datetime.datetime.now()
|
||||
|
||||
startd = datetime.datetime.now()
|
||||
try:
|
||||
cmd = subprocess.Popen(args, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = cmd.communicate()
|
||||
except (OSError, IOError), e:
|
||||
module.fail_json(cmd=args, msg=str(e))
|
||||
except:
|
||||
module.fail_json(msg=traceback.format_exc())
|
||||
|
||||
try:
|
||||
cmd = subprocess.Popen(args, shell=shell,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = cmd.communicate()
|
||||
except (OSError, IOError), e:
|
||||
print json.dumps({
|
||||
"cmd" : args,
|
||||
"failed" : 1,
|
||||
"msg" : str(e),
|
||||
})
|
||||
sys.exit(1)
|
||||
except:
|
||||
print json.dumps({
|
||||
"failed" : 1,
|
||||
"msg" : traceback.format_exc()
|
||||
})
|
||||
sys.exit(1)
|
||||
endd = datetime.datetime.now()
|
||||
delta = endd - startd
|
||||
|
||||
endd = datetime.datetime.now()
|
||||
delta = endd - startd
|
||||
if out is None:
|
||||
out = ''
|
||||
if err is None:
|
||||
err = ''
|
||||
|
||||
if out is None:
|
||||
out = ''
|
||||
if err is None:
|
||||
err = ''
|
||||
module.exit_json(
|
||||
cmd = args,
|
||||
stdout = out.strip(),
|
||||
stderr = err.strip(),
|
||||
rc = cmd.returncode,
|
||||
start = str(startd),
|
||||
end = str(endd),
|
||||
delta = str(delta),
|
||||
changed = True
|
||||
)
|
||||
|
||||
result = {
|
||||
"cmd" : args,
|
||||
"stdout" : out.strip(),
|
||||
"stderr" : err.strip(),
|
||||
"rc" : cmd.returncode,
|
||||
"start" : str(startd),
|
||||
"end" : str(endd),
|
||||
"delta" : str(delta),
|
||||
"changed" : True
|
||||
}
|
||||
# include magic from lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
|
||||
print json.dumps(result)
|
||||
# only the command module should ever need to do this
|
||||
# everything else should be simple key=value
|
||||
|
||||
class CommandModule(AnsibleModule):
|
||||
|
||||
def _handle_aliases(self):
|
||||
pass
|
||||
|
||||
def _check_invalid_arguments(self):
|
||||
pass
|
||||
|
||||
def _load_params(self):
|
||||
''' read the input and return a dictionary and the arguments string '''
|
||||
args = MODULE_ARGS
|
||||
items = shlex.split(args)
|
||||
params = {}
|
||||
params['chdir'] = None
|
||||
params['shell'] = False
|
||||
if args.find("#USE_SHELL") != -1:
|
||||
args = args.replace("#USE_SHELL", "")
|
||||
params['shell'] = True
|
||||
|
||||
check_args = shlex.split(args)
|
||||
for x in check_args:
|
||||
if x.startswith("creates="):
|
||||
# do not run the command if the line contains creates=filename
|
||||
# and the filename already exists. This allows idempotence
|
||||
# of command executions.
|
||||
(k,v) = x.split("=",1)
|
||||
if os.path.exists(v):
|
||||
self.exit_json(
|
||||
cmd=args,
|
||||
stdout="skipped, since %s exists" % v,
|
||||
skipped=True,
|
||||
changed=False,
|
||||
stderr=False,
|
||||
rc=0
|
||||
)
|
||||
args = args.replace(x,'')
|
||||
elif x.startswith("chdir="):
|
||||
(k,v) = x.split("=", 1)
|
||||
if not (os.path.exists(v) and os.path.isdir(v)):
|
||||
self.fail_json(msg="cannot change to directory '%s': path does not exist" % v)
|
||||
elif v[0] != '/':
|
||||
self.fail_json(msg="the path for 'chdir' argument must be fully qualified")
|
||||
params['chdir'] = v
|
||||
args = args.replace(x, '')
|
||||
params['args'] = args
|
||||
return (params, args)
|
||||
|
||||
main()
|
||||
|
||||
115
library/copy
115
library/copy
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
@ -16,84 +17,58 @@
|
||||
#
|
||||
# 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 shlex
|
||||
import syslog
|
||||
import shutil
|
||||
|
||||
# ===========================================
|
||||
# convert arguments of form a=b c=d
|
||||
# to a dictionary
|
||||
# FIXME: make more idiomatic
|
||||
def main():
|
||||
|
||||
def dump_kv(vars):
|
||||
return " ".join("%s='%s'" % (k,v) for (k,v) in vars.items())
|
||||
module = AnsibleModule(
|
||||
# not checking because of daisy chain to file module
|
||||
check_invalid_arguments = False,
|
||||
argument_spec = dict(
|
||||
src=dict(required=True),
|
||||
dest=dict(required=True)
|
||||
)
|
||||
)
|
||||
|
||||
def exit_kv(rc=0, **kwargs):
|
||||
if 'path' in kwargs:
|
||||
add_path_info(kwargs)
|
||||
print dump_kv(kwargs)
|
||||
sys.exit(rc)
|
||||
src = os.path.expanduser(module.params['src'])
|
||||
dest = os.path.expanduser(module.params['dest'])
|
||||
|
||||
def md5_sum(f):
|
||||
md5sum = os.popen("/usr/bin/md5sum %(file)s 2>/dev/null || /sbin/md5 -q %(file)s 2>/dev/null || /usr/bin/digest -a md5 -v %(file)s 2>/dev/null" % {"file": f}).read().split()[0]
|
||||
return md5sum
|
||||
if not os.path.exists(src):
|
||||
module.fail_json(msg="Source %s failed to transfer" % (src))
|
||||
if not os.access(src, os.R_OK):
|
||||
module.fail_json(msg="Source %s not readable" % (src))
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
exit_kv(rc=1, failed=1, msg="incorrect number of arguments given")
|
||||
md5sum_src = module.md5(src)
|
||||
md5sum_dest = None
|
||||
|
||||
argfile = sys.argv[1]
|
||||
if not os.path.exists(argfile):
|
||||
exit_kv(rc=1, failed=1, msg="file %s does not exist" % (argfile))
|
||||
if os.path.exists(dest):
|
||||
if not os.access(dest, os.W_OK):
|
||||
module.fail_json(msg="Destination %s not writable" % (dest))
|
||||
if not os.access(dest, os.R_OK):
|
||||
module.fail_json(msg="Destination %s not readable" % (dest))
|
||||
# Allow dest to be directory without compromising md5 check
|
||||
if (os.path.isdir(dest)):
|
||||
module.fail_json(msg="Destination %s cannot be a directory" % (dest))
|
||||
md5sum_dest = module.md5(dest)
|
||||
else:
|
||||
if not os.access(os.path.dirname(dest), os.W_OK):
|
||||
module.fail_json(msg="Destination %s not writable" % (os.path.dirname(dest)))
|
||||
|
||||
args = open(argfile, 'r').read()
|
||||
items = shlex.split(args)
|
||||
syslog.openlog('ansible-%s' % os.path.basename(__file__))
|
||||
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
|
||||
if md5sum_src != md5sum_dest:
|
||||
try:
|
||||
shutil.copyfile(src, dest)
|
||||
except shutil.Error:
|
||||
module.fail_json(msg="failed to copy: %s and %s are the same" % (src, dest))
|
||||
except IOError:
|
||||
module.fail_json(msg="failed to copy: %s to %s" % (src, dest))
|
||||
changed = True
|
||||
else:
|
||||
changed = False
|
||||
|
||||
module.exit_json(dest=dest, src=src, md5sum=md5sum_src, changed=changed)
|
||||
|
||||
params = {}
|
||||
for x in items:
|
||||
(k, v) = x.split("=")
|
||||
params[k] = v
|
||||
|
||||
src = params['src']
|
||||
dest = params['dest']
|
||||
if src:
|
||||
src = os.path.expanduser(src)
|
||||
if dest:
|
||||
dest = os.path.expanduser(dest)
|
||||
|
||||
md5sum_src = None
|
||||
# raise an error if there is no src file
|
||||
if not os.path.exists(src):
|
||||
exit_kv(rc=1, failed=1, msg="Source %s failed to transfer" % (src))
|
||||
if not os.access(src, os.R_OK):
|
||||
exit_kv(rc=1, failed=1, msg="Source %s not readable" % (src))
|
||||
md5sum_src = md5_sum(src)
|
||||
|
||||
md5sum_dest = None
|
||||
# check if there is no dest file
|
||||
if os.path.exists(dest):
|
||||
# raise an error if copy has no permission on dest
|
||||
if not os.access(dest, os.W_OK):
|
||||
exit_kv(rc=1, failed=1, msg="Destination %s not writable" % (dest))
|
||||
if not os.access(dest, os.R_OK):
|
||||
exit_kv(rc=1, failed=1, msg="Destination %s not readable" % (dest))
|
||||
md5sum_dest = md5_sum(dest)
|
||||
else:
|
||||
if not os.access(os.path.dirname(dest), os.W_OK):
|
||||
exit_kv(rc=1, failed=1, msg="Destination %s not writable" % (dest))
|
||||
|
||||
if md5sum_src != md5sum_dest:
|
||||
os.system("cp %s %s" % (src, dest))
|
||||
changed = True
|
||||
else:
|
||||
changed = False
|
||||
|
||||
# mission accomplished
|
||||
#print "md5sum=%s changed=%s" % (md5sum_src, changed)
|
||||
exit_kv(dest=dest, src=src, changed="md5sum=%s changed=%s" % (md5sum_src, changed))
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
main()
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
@ -22,5 +23,28 @@
|
||||
# facter
|
||||
# ruby-json
|
||||
|
||||
/usr/bin/logger -t ansible-facter Invoked as-is
|
||||
/usr/bin/facter --json 2>/dev/null
|
||||
import subprocess
|
||||
|
||||
def get_facter_data():
|
||||
p = subprocess.Popen(["/usr/bin/env", "facter", "--json"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(out, err) = p.communicate()
|
||||
rc = p.returncode
|
||||
return rc, out, err
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict()
|
||||
)
|
||||
|
||||
rc, out, err = get_facter_data()
|
||||
if rc != 0:
|
||||
module.fail_json(msg=err)
|
||||
else:
|
||||
module.exit_json(**json.loads(out))
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
|
||||
main()
|
||||
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@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/>.
|
||||
|
||||
import sys
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
print >>sys.stderr, "THIS IS A TEST FAILURE"
|
||||
|
||||
print json.dumps({
|
||||
"failed" : True,
|
||||
"msg" : "this module always fails"
|
||||
})
|
||||
|
||||
|
||||
@ -1,24 +1 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@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/>.
|
||||
#
|
||||
|
||||
### THIS FILE IS FOR REFERENCE OR FUTURE USE ###
|
||||
|
||||
# See lib/ansible/runner.py for implementation of the fetch functionality #
|
||||
|
||||
# this is a virtual module that is entirely implemented server side
|
||||
|
||||
295
library/file
295
library/file
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
@ -17,39 +18,21 @@
|
||||
# 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
|
||||
import sys
|
||||
import shlex
|
||||
import subprocess
|
||||
import shutil
|
||||
import stat
|
||||
import grp
|
||||
import pwd
|
||||
import syslog
|
||||
try:
|
||||
import selinux
|
||||
HAVE_SELINUX=True
|
||||
except ImportError:
|
||||
HAVE_SELINUX=False
|
||||
|
||||
def dump_kv(vars):
|
||||
return " ".join("%s='%s'" % (k,v) for (k,v) in vars.items())
|
||||
|
||||
def exit_kv(rc=0, **kwargs):
|
||||
if 'path' in kwargs:
|
||||
add_path_info(kwargs)
|
||||
print dump_kv(kwargs)
|
||||
sys.exit(rc)
|
||||
|
||||
def fail_kv(**kwargs):
|
||||
kwargs['failed'] = True
|
||||
exit_kv(rc=1, **kwargs)
|
||||
|
||||
def add_path_info(kwargs):
|
||||
path = kwargs['path']
|
||||
if os.path.exists(path):
|
||||
(user, group) = user_and_group(path)
|
||||
kwargs['user'] = user
|
||||
kwargs['owner'] = user
|
||||
kwargs['group'] = group
|
||||
st = os.stat(path)
|
||||
kwargs['mode'] = oct(stat.S_IMODE(st[stat.ST_MODE]))
|
||||
@ -66,6 +49,14 @@ def add_path_info(kwargs):
|
||||
kwargs['state'] = 'absent'
|
||||
return kwargs
|
||||
|
||||
def module_exit_json(**kwargs):
|
||||
add_path_info(kwargs)
|
||||
module.exit_json(**kwargs)
|
||||
|
||||
def module_fail_json(**kwargs):
|
||||
add_path_info(kwargs)
|
||||
module.fail_json(**kwargs)
|
||||
|
||||
# Detect whether using selinux that is MLS-aware.
|
||||
# While this means you can set the level/range with
|
||||
# selinux.lsetfilecon(), it may or may not mean that you
|
||||
@ -115,7 +106,7 @@ def selinux_context(path):
|
||||
try:
|
||||
ret = selinux.lgetfilecon(path)
|
||||
except:
|
||||
fail_kv(path=path, msg='failed to retrieve selinux context')
|
||||
module_fail_json(path=path, msg='failed to retrieve selinux context')
|
||||
if ret[0] == -1:
|
||||
return context
|
||||
context = ret[1].split(':')
|
||||
@ -123,59 +114,10 @@ def selinux_context(path):
|
||||
|
||||
# ===========================================
|
||||
|
||||
argfile = sys.argv[1]
|
||||
args = open(argfile, 'r').read()
|
||||
items = shlex.split(args)
|
||||
syslog.openlog('ansible-%s' % os.path.basename(__file__))
|
||||
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
|
||||
|
||||
if not len(items):
|
||||
fail_kv(msg='the module requires arguments -a')
|
||||
sys.exit(1)
|
||||
|
||||
params = {}
|
||||
for x in items:
|
||||
(k, v) = x.split("=")
|
||||
params[k] = v
|
||||
|
||||
state = params.get('state','file')
|
||||
path = params.get('path', params.get('dest', params.get('name', None)))
|
||||
if path:
|
||||
path = os.path.expanduser(path)
|
||||
src = params.get('src', None)
|
||||
if src:
|
||||
src = os.path.expanduser(src)
|
||||
dest = params.get('dest', None)
|
||||
mode = params.get('mode', None)
|
||||
owner = params.get('owner', None)
|
||||
group = params.get('group', None)
|
||||
|
||||
# presently unused, we always use -R (FIXME?)
|
||||
recurse = params.get('recurse', 'false')
|
||||
|
||||
# selinux related options
|
||||
seuser = params.get('seuser', None)
|
||||
serole = params.get('serole', None)
|
||||
setype = params.get('setype', None)
|
||||
selevel = params.get('serange', 's0')
|
||||
secontext = [seuser, serole, setype]
|
||||
if selinux_mls_enabled():
|
||||
secontext.append(selevel)
|
||||
|
||||
default_secontext = selinux_default_context(path)
|
||||
for i in range(len(default_secontext)):
|
||||
if i is not None and secontext[i] == '_default':
|
||||
secontext[i] = default_secontext[i]
|
||||
|
||||
if state not in [ 'file', 'directory', 'link', 'absent']:
|
||||
fail_kv(msg='invalid state: %s' % state)
|
||||
|
||||
if state == 'link' and (src is None or dest is None):
|
||||
fail_kv(msg='src and dest are required for "link" state')
|
||||
elif path is None:
|
||||
fail_kv(msg='path is required')
|
||||
|
||||
changed = False
|
||||
|
||||
# ===========================================
|
||||
# support functions
|
||||
@ -208,9 +150,9 @@ def set_context_if_different(path, context, changed):
|
||||
try:
|
||||
rc = selinux.lsetfilecon(path, ':'.join(new_context))
|
||||
except OSError:
|
||||
fail_kv(path=path, msg='invalid selinux context')
|
||||
module_fail_json(path=path, msg='invalid selinux context')
|
||||
if rc != 0:
|
||||
fail_kv(path=path, msg='set selinux context failed')
|
||||
module_fail_json(path=path, msg='set selinux context failed')
|
||||
changed = True
|
||||
return changed
|
||||
|
||||
@ -221,7 +163,7 @@ def set_owner_if_different(path, owner, changed):
|
||||
if owner != user:
|
||||
rc = os.system("/bin/chown -R %s %s 2>/dev/null" % (owner, path))
|
||||
if rc != 0:
|
||||
fail_kv(path=path, msg='chown failed')
|
||||
module_fail_json(path=path, msg='chown failed')
|
||||
return True
|
||||
|
||||
return changed
|
||||
@ -233,7 +175,7 @@ def set_group_if_different(path, group, changed):
|
||||
if old_group != group:
|
||||
rc = os.system("/bin/chgrp -R %s %s" % (group, path))
|
||||
if rc != 0:
|
||||
fail_kv(path=path, msg='chgrp failed')
|
||||
module_fail_json(path=path, msg='chgrp failed')
|
||||
return True
|
||||
return changed
|
||||
|
||||
@ -244,7 +186,7 @@ def set_mode_if_different(path, mode, changed):
|
||||
# FIXME: support English modes
|
||||
mode = int(mode, 8)
|
||||
except Exception, e:
|
||||
fail_kv(path=path, msg='mode needs to be something octalish', details=str(e))
|
||||
module_fail_json(path=path, msg='mode needs to be something octalish', details=str(e))
|
||||
|
||||
st = os.stat(path)
|
||||
prev_mode = stat.S_IMODE(st[stat.ST_MODE])
|
||||
@ -255,7 +197,7 @@ def set_mode_if_different(path, mode, changed):
|
||||
try:
|
||||
os.chmod(path, mode)
|
||||
except Exception, e:
|
||||
fail_kv(path=path, msg='chmod failed', details=str(e))
|
||||
module_fail_json(path=path, msg='chmod failed', details=str(e))
|
||||
|
||||
st = os.stat(path)
|
||||
new_mode = stat.S_IMODE(st[stat.ST_MODE])
|
||||
@ -266,98 +208,147 @@ def set_mode_if_different(path, mode, changed):
|
||||
|
||||
|
||||
def rmtree_error(func, path, exc_info):
|
||||
fail_kv(path=path, msg='failed to remove directory')
|
||||
module_fail_json(path=path, msg='failed to remove directory')
|
||||
|
||||
# ===========================================
|
||||
# go...
|
||||
def main():
|
||||
|
||||
prev_state = 'absent'
|
||||
if os.path.lexists(path):
|
||||
if os.path.islink(path):
|
||||
prev_state = 'link'
|
||||
elif os.path.isfile(path):
|
||||
prev_state = 'file'
|
||||
else:
|
||||
prev_state = 'directory'
|
||||
global module
|
||||
module = AnsibleModule(
|
||||
check_invalid_arguments = False,
|
||||
argument_spec = dict(
|
||||
state = dict(choices=['file','directory','link','absent'], default='file'),
|
||||
path = dict(aliases=['dest', 'name'], required=True),
|
||||
src = dict(),
|
||||
mode = dict(),
|
||||
owner = dict(),
|
||||
group = dict(),
|
||||
seuser = dict(),
|
||||
serole = dict(),
|
||||
selevel = dict(),
|
||||
setype = dict(),
|
||||
)
|
||||
)
|
||||
|
||||
if prev_state != 'absent' and state == 'absent':
|
||||
try:
|
||||
if prev_state == 'directory':
|
||||
if os.path.islink(path):
|
||||
os.unlink(path)
|
||||
else:
|
||||
shutil.rmtree(path, ignore_errors=False, onerror=rmtree_error)
|
||||
params = module.params
|
||||
state = params['state']
|
||||
path = os.path.expanduser(params['path'])
|
||||
src = params.get('src', None)
|
||||
if src:
|
||||
src = os.path.expanduser(src)
|
||||
|
||||
mode = params.get('mode', None)
|
||||
owner = params.get('owner', None)
|
||||
group = params.get('group', None)
|
||||
|
||||
# selinux related options
|
||||
seuser = params.get('seuser', None)
|
||||
serole = params.get('serole', None)
|
||||
setype = params.get('setype', None)
|
||||
selevel = params.get('serange', 's0')
|
||||
secontext = [seuser, serole, setype]
|
||||
if selinux_mls_enabled():
|
||||
secontext.append(selevel)
|
||||
|
||||
default_secontext = selinux_default_context(path)
|
||||
for i in range(len(default_secontext)):
|
||||
if i is not None and secontext[i] == '_default':
|
||||
secontext[i] = default_secontext[i]
|
||||
|
||||
if state == 'link' and (src is None or path is None):
|
||||
module_fail_json(msg='src and dest are required for "link" state')
|
||||
elif path is None:
|
||||
module_fail_json(msg='path is required')
|
||||
|
||||
changed = False
|
||||
|
||||
prev_state = 'absent'
|
||||
if os.path.lexists(path):
|
||||
if os.path.islink(path):
|
||||
prev_state = 'link'
|
||||
elif os.path.isfile(path):
|
||||
prev_state = 'file'
|
||||
else:
|
||||
os.unlink(path)
|
||||
except Exception, e:
|
||||
fail_kv(path=path, msg=str(e))
|
||||
exit_kv(path=path, changed=True)
|
||||
sys.exit(0)
|
||||
prev_state = 'directory'
|
||||
|
||||
if prev_state != 'absent' and prev_state != state:
|
||||
fail_kv(path=path, msg='refusing to convert between %s and %s' % (prev_state, state))
|
||||
if prev_state != 'absent' and state == 'absent':
|
||||
try:
|
||||
if prev_state == 'directory':
|
||||
if os.path.islink(path):
|
||||
os.unlink(path)
|
||||
else:
|
||||
shutil.rmtree(path, ignore_errors=False, onerror=rmtree_error)
|
||||
else:
|
||||
os.unlink(path)
|
||||
except Exception, e:
|
||||
module_fail_json(path=path, msg=str(e))
|
||||
module_exit_json(path=path, changed=True)
|
||||
|
||||
if prev_state == 'absent' and state == 'absent':
|
||||
exit_kv(path=path, changed=False)
|
||||
if prev_state != 'absent' and prev_state != state:
|
||||
module_fail_json(path=path, msg='refusing to convert between %s and %s' % (prev_state, state))
|
||||
|
||||
if state == 'file':
|
||||
if prev_state == 'absent' and state == 'absent':
|
||||
module_exit_json(path=path, changed=False)
|
||||
|
||||
if prev_state == 'absent':
|
||||
fail_kv(path=path, msg='file does not exist, use copy or template module to create')
|
||||
|
||||
# set modes owners and context as needed
|
||||
changed = set_context_if_different(path, secontext, changed)
|
||||
changed = set_owner_if_different(path, owner, changed)
|
||||
changed = set_group_if_different(path, group, changed)
|
||||
changed = set_mode_if_different(path, mode, changed)
|
||||
|
||||
exit_kv(path=path, changed=changed)
|
||||
|
||||
elif state == 'directory':
|
||||
|
||||
if prev_state == 'absent':
|
||||
os.makedirs(path)
|
||||
changed = True
|
||||
if state == 'file':
|
||||
|
||||
# set modes owners and context as needed
|
||||
changed = set_context_if_different(path, secontext, changed)
|
||||
changed = set_owner_if_different(path, owner, changed)
|
||||
changed = set_group_if_different(path, group, changed)
|
||||
changed = set_mode_if_different(path, mode, changed)
|
||||
if prev_state == 'absent':
|
||||
module_fail_json(path=path, msg='file does not exist, use copy or template module to create')
|
||||
|
||||
exit_kv(path=path, changed=changed)
|
||||
# set modes owners and context as needed
|
||||
changed = set_context_if_different(path, secontext, changed)
|
||||
changed = set_owner_if_different(path, owner, changed)
|
||||
changed = set_group_if_different(path, group, changed)
|
||||
changed = set_mode_if_different(path, mode, changed)
|
||||
|
||||
elif state == 'link':
|
||||
module_exit_json(path=path, changed=changed)
|
||||
|
||||
elif state == 'directory':
|
||||
|
||||
if prev_state == 'absent':
|
||||
os.makedirs(path)
|
||||
changed = True
|
||||
|
||||
# set modes owners and context as needed
|
||||
changed = set_context_if_different(path, secontext, changed)
|
||||
changed = set_owner_if_different(path, owner, changed)
|
||||
changed = set_group_if_different(path, group, changed)
|
||||
changed = set_mode_if_different(path, mode, changed)
|
||||
|
||||
module_exit_json(path=path, changed=changed)
|
||||
|
||||
elif state == 'link':
|
||||
|
||||
if os.path.isabs(src):
|
||||
abs_src = src
|
||||
else:
|
||||
abs_src = os.path.join(os.path.dirname(dest), src)
|
||||
if not os.path.exists(abs_src):
|
||||
fail_kv(dest=dest, src=src, msg='src file does not exist')
|
||||
if os.path.isabs(src):
|
||||
abs_src = src
|
||||
else:
|
||||
abs_src = os.path.join(os.path.dirname(dest), src)
|
||||
if not os.path.exists(abs_src):
|
||||
module_fail_json(path=path, src=src, msg='src file does not exist')
|
||||
|
||||
if prev_state == 'absent':
|
||||
os.symlink(src, dest)
|
||||
changed = True
|
||||
elif prev_state == 'link':
|
||||
old_src = os.readlink(dest)
|
||||
if not os.path.isabs(old_src):
|
||||
old_src = os.path.join(os.path.dirname(dest), old_src)
|
||||
if old_src != src:
|
||||
os.unlink(dest)
|
||||
os.symlink(src, dest)
|
||||
else:
|
||||
fail_kv(dest=dest, src=src, msg='unexpected position reached')
|
||||
if prev_state == 'absent':
|
||||
os.symlink(src, path)
|
||||
changed = True
|
||||
elif prev_state == 'link':
|
||||
old_src = os.readlink(path)
|
||||
if not os.path.isabs(old_src):
|
||||
old_src = os.path.join(os.path.dirname(path), old_src)
|
||||
if old_src != src:
|
||||
os.unlink(path)
|
||||
os.symlink(src, path)
|
||||
else:
|
||||
module_fail_json(dest=path, src=src, msg='unexpected position reached')
|
||||
|
||||
# set modes owners and context as needed
|
||||
changed = set_context_if_different(dest, secontext, changed)
|
||||
changed = set_owner_if_different(dest, owner, changed)
|
||||
changed = set_group_if_different(dest, group, changed)
|
||||
changed = set_mode_if_different(dest, mode, changed)
|
||||
# set modes owners and context as needed
|
||||
changed = set_context_if_different(path, secontext, changed)
|
||||
changed = set_owner_if_different(path, owner, changed)
|
||||
changed = set_group_if_different(path, group, changed)
|
||||
changed = set_mode_if_different(path, mode, changed)
|
||||
|
||||
exit_kv(dest=dest, src=src, changed=changed)
|
||||
module.exit_json(dest=path, src=src, changed=changed)
|
||||
|
||||
module_fail_json(path=path, msg='unexpected position reached')
|
||||
|
||||
fail_kv(path=path, msg='unexpected position reached')
|
||||
sys.exit(0)
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
main()
|
||||
|
||||
|
||||
194
library/get_url
Executable file
194
library/get_url
Executable file
@ -0,0 +1,194 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Jan-Piet Mens <jpmens () 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/>.
|
||||
#
|
||||
# see examples/playbooks/get_url.yml
|
||||
|
||||
import shutil
|
||||
import datetime
|
||||
import tempfile
|
||||
|
||||
HAS_URLLIB2=True
|
||||
try:
|
||||
import urllib2
|
||||
except ImportError:
|
||||
HAS_URLLIB2=False
|
||||
HAS_URLPARSE=True
|
||||
|
||||
try:
|
||||
import urlparse
|
||||
import socket
|
||||
except ImportError:
|
||||
HAS_URLPARSE=False
|
||||
|
||||
# ==============================================================
|
||||
# url handling
|
||||
|
||||
def url_filename(url):
|
||||
fn = os.path.basename(urlparse.urlsplit(url)[2])
|
||||
if fn == '':
|
||||
return 'index.html'
|
||||
return fn
|
||||
|
||||
def url_do_get(module, url, dest):
|
||||
"""
|
||||
Get url and return request and info
|
||||
Credits: http://stackoverflow.com/questions/7006574/how-to-download-file-from-ftp
|
||||
"""
|
||||
|
||||
USERAGENT = 'ansible-httpget'
|
||||
info = dict(url=url)
|
||||
r = None
|
||||
actualdest = None
|
||||
|
||||
if os.path.isdir(dest):
|
||||
urlfilename = url_filename(url)
|
||||
actualdest = "%s/%s" % (dest, urlfilename)
|
||||
module.params['path'] = actualdest
|
||||
else:
|
||||
actualdest = dest
|
||||
info['daisychain_args'] = module.params
|
||||
info['daisychain_args']['state'] = 'file'
|
||||
info['daisychain_args']['dest'] = actualdest
|
||||
info['actualdest'] = actualdest
|
||||
|
||||
request = urllib2.Request(url)
|
||||
request.add_header('User-agent', USERAGENT)
|
||||
|
||||
if os.path.exists(actualdest):
|
||||
t = datetime.datetime.utcfromtimestamp(os.path.getmtime(actualdest))
|
||||
tstamp = t.strftime('%a, %d %b %Y %H:%M:%S +0000')
|
||||
request.add_header('If-Modified-Since', tstamp)
|
||||
|
||||
try:
|
||||
r = urllib2.urlopen(request)
|
||||
info.update(r.info())
|
||||
info.update(dict(msg="OK (%s bytes)" % r.headers.get('Content-Length', 'unknown'), status=200))
|
||||
except urllib2.HTTPError, e:
|
||||
# Must not fail_json() here so caller can handle HTTP 304 unmodified
|
||||
info.update(dict(msg=str(e), status=e.code))
|
||||
return r, info
|
||||
except urllib2.URLError, e:
|
||||
code = getattr(e, 'code', -1)
|
||||
module.fail_json(msg="Request failed: %s" % str(e), status_code=code)
|
||||
|
||||
return r, info
|
||||
|
||||
def url_get(module, url, dest):
|
||||
"""
|
||||
Download url and store at dest.
|
||||
If dest is a directory, determine filename from url.
|
||||
Return (tempfile, info about the request)
|
||||
"""
|
||||
|
||||
req, info = url_do_get(module, url, dest)
|
||||
|
||||
# TODO: should really handle 304, but how? src file could exist (and be newer) but empty
|
||||
if info['status'] == 304:
|
||||
module.exit_json(url=url, dest=info.get('actualdest', dest), changed=False, msg=info.get('msg', ''))
|
||||
|
||||
# create a temporary file and copy content to do md5-based replacement
|
||||
if info['status'] != 200:
|
||||
module.fail_json(msg="Request failed", status_code=info['status'], response=info['msg'], url=url)
|
||||
actualdest = info['actualdest']
|
||||
|
||||
fd, tempname = tempfile.mkstemp()
|
||||
f = os.fdopen(fd, 'wb')
|
||||
try:
|
||||
shutil.copyfileobj(req, f)
|
||||
except Exception, err:
|
||||
os.remove(tempname)
|
||||
module.fail_json(msg="failed to create temporary content file: %s" % str(err))
|
||||
f.close()
|
||||
req.close()
|
||||
return tempname, info
|
||||
|
||||
# ==============================================================
|
||||
# main
|
||||
|
||||
def main():
|
||||
|
||||
# does this really happen on non-ancient python?
|
||||
if not HAS_URLLIB2:
|
||||
module.fail_json(msg="urllib2 is not installed")
|
||||
if not HAS_URLPARSE:
|
||||
module.fail_json(msg="urlparse is not installed")
|
||||
|
||||
module = AnsibleModule(
|
||||
# not checking because of daisy chain to file module
|
||||
check_invalid_arguments = False,
|
||||
argument_spec = dict(
|
||||
url = dict(required=True),
|
||||
dest = dict(required=True),
|
||||
)
|
||||
)
|
||||
|
||||
url = module.params['url']
|
||||
dest = os.path.expanduser(module.params['dest'])
|
||||
|
||||
# download to tmpsrc
|
||||
tmpsrc, info = url_get(module, url, dest)
|
||||
md5sum_src = None
|
||||
md5sum_dest = None
|
||||
dest = info['actualdest']
|
||||
|
||||
# raise an error if there is no tmpsrc file
|
||||
if not os.path.exists(tmpsrc):
|
||||
os.remove(tmpsrc)
|
||||
module.fail_json(msg="Request failed", status_code=info['status'], response=info['msg'])
|
||||
if not os.access(tmpsrc, os.R_OK):
|
||||
os.remove(tmpsrc)
|
||||
module.fail_json( msg="Source %s not readable" % (tmpsrc))
|
||||
md5sum_src = module.md5(tmpsrc)
|
||||
|
||||
# check if there is no dest file
|
||||
if os.path.exists(dest):
|
||||
# raise an error if copy has no permission on dest
|
||||
if not os.access(dest, os.W_OK):
|
||||
os.remove(tmpsrc)
|
||||
module.fail_json( msg="Destination %s not writable" % (dest))
|
||||
if not os.access(dest, os.R_OK):
|
||||
os.remove(tmpsrc)
|
||||
module.fail_json( msg="Destination %s not readable" % (dest))
|
||||
md5sum_dest = module.md5(dest)
|
||||
else:
|
||||
if not os.access(os.path.dirname(dest), os.W_OK):
|
||||
os.remove(tmpsrc)
|
||||
module.fail_json( msg="Destination %s not writable" % (os.path.dirname(dest)))
|
||||
|
||||
if md5sum_src != md5sum_dest:
|
||||
try:
|
||||
shutil.copyfile(tmpsrc, dest)
|
||||
except Exception, err:
|
||||
os.remove(tmpsrc)
|
||||
module.fail_json(msg="failed to copy %s to %s: %s" % (tmpsrc, dest, str(err)))
|
||||
changed = True
|
||||
else:
|
||||
changed = False
|
||||
|
||||
os.remove(tmpsrc)
|
||||
|
||||
# Mission complete
|
||||
module.exit_json(url=url, dest=dest, src=tmpsrc, md5sum=md5sum_src,
|
||||
changed=changed, msg=info.get('msg',''),
|
||||
daisychain="file", daisychain_args=info.get('daisychain_args',''))
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
main()
|
||||
306
library/git
306
library/git
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
@ -22,190 +23,175 @@
|
||||
# tag. Latest is not supported, you should not be doing
|
||||
# that. Contribs welcome! -- MPD
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import shlex
|
||||
import subprocess
|
||||
import syslog
|
||||
|
||||
# ===========================================
|
||||
# Basic support methods
|
||||
|
||||
def exit_json(rc=0, **kwargs):
|
||||
print json.dumps(kwargs)
|
||||
sys.exit(rc)
|
||||
|
||||
def fail_json(**kwargs):
|
||||
kwargs['failed'] = True
|
||||
exit_json(rc=1, **kwargs)
|
||||
|
||||
# ===========================================
|
||||
# convert arguments of form a=b c=d
|
||||
# to a dictionary
|
||||
# FIXME: make more idiomatic
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
fail_json(msg="the command module requires arguments (-a)")
|
||||
|
||||
argfile = sys.argv[1]
|
||||
if not os.path.exists(argfile):
|
||||
fail_json(msg="Argument file not found")
|
||||
|
||||
args = open(argfile, 'r').read()
|
||||
items = shlex.split(args)
|
||||
syslog.openlog('ansible-%s' % os.path.basename(__file__))
|
||||
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
|
||||
|
||||
if not len(items):
|
||||
fail_json(msg="the command module requires arguments (-a)")
|
||||
|
||||
params = {}
|
||||
for x in items:
|
||||
(k, v) = x.split("=")
|
||||
params[k] = v
|
||||
|
||||
dest = params['dest']
|
||||
repo = params['repo']
|
||||
branch = params.get('branch', 'master')
|
||||
version = params.get('version', 'HEAD')
|
||||
|
||||
# ===========================================
|
||||
|
||||
def get_version(dest):
|
||||
''' samples the version of the git repo '''
|
||||
os.chdir(dest)
|
||||
cmd = "git show --abbrev-commit"
|
||||
sha = os.popen(cmd).read().split("\n")
|
||||
sha = sha[0].split()[1]
|
||||
return sha
|
||||
''' samples the version of the git repo '''
|
||||
os.chdir(dest)
|
||||
cmd = "git show --abbrev-commit"
|
||||
sha = os.popen(cmd).read().split("\n")
|
||||
sha = sha[0].split()[1]
|
||||
return sha
|
||||
|
||||
def clone(repo, dest, branch):
|
||||
''' makes a new git repo if it does not already exist '''
|
||||
try:
|
||||
os.makedirs(dest)
|
||||
except:
|
||||
pass
|
||||
cmd = "git clone %s %s" % (repo, dest)
|
||||
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(out, err) = cmd.communicate()
|
||||
rc = cmd.returncode
|
||||
|
||||
if branch is None or rc != 0:
|
||||
return (out, err)
|
||||
|
||||
os.chdir(dest)
|
||||
cmd = "git checkout -b %s origin/%s" % (branch, branch)
|
||||
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
return cmd.communicate()
|
||||
def clone(repo, dest):
|
||||
''' makes a new git repo if it does not already exist '''
|
||||
try:
|
||||
os.makedirs(os.path.dirname(dest))
|
||||
except:
|
||||
pass
|
||||
cmd = "git clone %s %s" % (repo, dest)
|
||||
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(out, err) = cmd.communicate()
|
||||
rc = cmd.returncode
|
||||
return (rc, out, err)
|
||||
|
||||
def reset(dest):
|
||||
'''
|
||||
Resets the index and working tree to HEAD.
|
||||
Discards any changes to tracked files in working
|
||||
tree since that commit.
|
||||
'''
|
||||
os.chdir(dest)
|
||||
cmd = "git reset --hard HEAD"
|
||||
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(out, err) = cmd.communicate()
|
||||
rc = cmd.returncode
|
||||
return (rc, out, err)
|
||||
'''
|
||||
Resets the index and working tree to HEAD.
|
||||
Discards any changes to tracked files in working
|
||||
tree since that commit.
|
||||
'''
|
||||
os.chdir(dest)
|
||||
cmd = "git reset --hard HEAD"
|
||||
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(out, err) = cmd.communicate()
|
||||
rc = cmd.returncode
|
||||
return (rc, out, err)
|
||||
|
||||
def switchLocalBranch( branch ):
|
||||
cmd = "git checkout %s" % branch
|
||||
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
return cmd.communicate()
|
||||
cmd = "git checkout %s" % branch
|
||||
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
return cmd.communicate()
|
||||
|
||||
def pull(repo, dest, branch):
|
||||
''' updates repo from remote sources '''
|
||||
os.chdir(dest)
|
||||
cmd = "git branch -a"
|
||||
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(gbranch_out, gbranch_err) = cmd.communicate()
|
||||
def get_branches(module, dest):
|
||||
os.chdir(dest)
|
||||
branches = []
|
||||
cmd = "git branch -a"
|
||||
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = cmd.communicate()
|
||||
if cmd.returncode != 0:
|
||||
module.fail_json(msg="Could not determine branch data - received %s" % out)
|
||||
for line in out.split('\n'):
|
||||
branches.append(line.strip())
|
||||
return branches
|
||||
|
||||
try:
|
||||
m = re.search( '^\* (\S+|\(no branch\))$', gbranch_out, flags=re.M )
|
||||
cur_branch = m.group(1)
|
||||
m = re.search( '\s+remotes/origin/HEAD -> origin/(\S+)', gbranch_out, flags=re.M )
|
||||
default_branch = m.group(1)
|
||||
except:
|
||||
fail_json(msg="could not determine branch data - received: %s" % gbranch_out)
|
||||
def is_remote_branch(module, dest, remote, branch):
|
||||
branches = get_branches(module, dest)
|
||||
rbranch = 'remotes/%s/%s' % (remote, branch)
|
||||
if rbranch in branches:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
if branch is None:
|
||||
if cur_branch != default_branch:
|
||||
(out, err) = switchLocalBranch( default_branch )
|
||||
def is_local_branch(module, dest, branch):
|
||||
branches = get_branches(module, dest)
|
||||
lbranch = '%s' % branch
|
||||
if lbranch in branches:
|
||||
return True
|
||||
elif '* %s' % branch in branches:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
cmd = "git pull -u origin"
|
||||
def is_current_branch(module, dest, branch):
|
||||
branches = get_branches(module, dest)
|
||||
for b in branches:
|
||||
if b.startswith('* '):
|
||||
cur_branch = b
|
||||
if branch == cur_branch or '* %s' % branch == cur_branch:
|
||||
return True
|
||||
else:
|
||||
return True
|
||||
|
||||
elif branch == cur_branch:
|
||||
cmd = "git pull -u origin"
|
||||
def pull(module, repo, dest, version):
|
||||
''' updates repo from remote sources '''
|
||||
os.chdir(dest)
|
||||
branches = get_branches(module, dest)
|
||||
cur_branch = ''
|
||||
for b in branches:
|
||||
if b.startswith('* '):
|
||||
cur_branch = b
|
||||
if is_local_branch(module, dest, version) and not is_current_branch(module, dest, version):
|
||||
(out, err) = switch_version(module, dest, remote, version)
|
||||
|
||||
else:
|
||||
m = re.search( '^\s+%s$' % branch, gbranch_out, flags=re.M ) #see if we've already checked it out
|
||||
if m is None:
|
||||
cmd = "git checkout -b %s origin/%s" % (branch, branch)
|
||||
cmd = "git pull -u origin"
|
||||
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = cmd.communicate()
|
||||
rc = cmd.returncode
|
||||
return (rc, out, err)
|
||||
|
||||
else:
|
||||
(out, err) = switchLocalBranch( branch )
|
||||
cmd = "git pull -u origin"
|
||||
def switch_version(module, dest, remote, version):
|
||||
''' once pulled, switch to a particular SHA, tag, or branch '''
|
||||
os.chdir(dest)
|
||||
cmd = ''
|
||||
if version != 'HEAD':
|
||||
if not is_local_branch(module, dest, version) and is_remote_branch(module, dest, remote, version):
|
||||
cmd = "git checkout --track -b %s %s/%s" % (version, remote, version)
|
||||
else:
|
||||
cmd = "git checkout --force %s" % version
|
||||
else:
|
||||
# is there a better way to do this?
|
||||
cmd = "git rebase origin"
|
||||
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(out, err) = cmd.communicate()
|
||||
rc = cmd.returncode
|
||||
return (rc, out, err)
|
||||
|
||||
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
return cmd.communicate()
|
||||
# ===========================================
|
||||
|
||||
def switchver(version, dest):
|
||||
''' once pulled, switch to a particular SHA or tag '''
|
||||
os.chdir(dest)
|
||||
if version != 'HEAD':
|
||||
cmd = "git checkout %s --force" % version
|
||||
else:
|
||||
# is there a better way to do this?
|
||||
cmd = "git rebase origin"
|
||||
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(out, err) = cmd.communicate()
|
||||
return (out, err)
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
dest=dict(required=True),
|
||||
repo=dict(required=True, aliases=['name']),
|
||||
version=dict(default='HEAD'),
|
||||
remote=dict(default='origin')
|
||||
)
|
||||
)
|
||||
|
||||
gitconfig = os.path.join(dest, '.git', 'config')
|
||||
dest = module.params['dest']
|
||||
repo = module.params['repo']
|
||||
version = module.params['version']
|
||||
remote = module.params['remote']
|
||||
|
||||
out, err, status = (None, None, None)
|
||||
gitconfig = os.path.join(dest, '.git', 'config')
|
||||
|
||||
# if there is no git configuration, do a clone operation
|
||||
# else pull and switch the version
|
||||
out, err, status = (None, None, None)
|
||||
|
||||
before = None
|
||||
if not os.path.exists(gitconfig):
|
||||
(out, err) = clone(repo, dest, branch)
|
||||
else:
|
||||
# else do a pull
|
||||
before = get_version(dest)
|
||||
(rc, out, err) = reset(dest)
|
||||
if rc != 0:
|
||||
fail_json(out=out, err=err)
|
||||
(out, err) = pull(repo, dest, branch)
|
||||
# if there is no git configuration, do a clone operation
|
||||
# else pull and switch the version
|
||||
before = None
|
||||
if not os.path.exists(gitconfig):
|
||||
(rc, out, err) = clone(repo, dest)
|
||||
if rc != 0:
|
||||
module.fail_json(msg=err)
|
||||
else:
|
||||
# else do a pull
|
||||
before = get_version(dest)
|
||||
(rc, out, err) = reset(dest)
|
||||
if rc != 0:
|
||||
module.fail_json(msg=err)
|
||||
(rc, out, err) = pull(module, repo, dest, version)
|
||||
|
||||
# handle errors from clone or pull
|
||||
# handle errors from clone or pull
|
||||
if out.find('error') != -1 or err.find('ERROR') != -1:
|
||||
module.fail_json(msg=err)
|
||||
|
||||
if out.find('error') != -1:
|
||||
fail_json(out=out, err=err)
|
||||
# switch to version specified regardless of whether
|
||||
# we cloned or pulled
|
||||
(rc, out, err) = switch_version(module, dest, remote, version)
|
||||
if err.find('error') != -1:
|
||||
module.fail_json(msg=err)
|
||||
|
||||
# switch to version specified regardless of whether
|
||||
# we cloned or pulled
|
||||
# determine if we changed anything
|
||||
after = get_version(dest)
|
||||
changed = False
|
||||
|
||||
(out, err) = switchver(version, dest)
|
||||
if err.find('error') != -1:
|
||||
fail_json(out=out, err=err)
|
||||
if before != after:
|
||||
changed = True
|
||||
|
||||
# determine if we changed anything
|
||||
module.exit_json(changed=changed, before=before, after=after)
|
||||
|
||||
after = get_version(dest)
|
||||
changed = False
|
||||
|
||||
if before != after:
|
||||
changed = True
|
||||
|
||||
exit_json(changed=changed, before=before, after=after)
|
||||
# include magic from lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
main()
|
||||
|
||||
183
library/group
183
library/group
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Stephen Fromm <sfromm@gmail.com>
|
||||
#
|
||||
@ -17,51 +18,25 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
import os
|
||||
import grp
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import syslog
|
||||
|
||||
GROUPADD = "/usr/sbin/groupadd"
|
||||
GROUPDEL = "/usr/sbin/groupdel"
|
||||
GROUPMOD = "/usr/sbin/groupmod"
|
||||
|
||||
def exit_json(rc=0, **kwargs):
|
||||
if 'name' in kwargs:
|
||||
add_group_info(kwargs)
|
||||
print json.dumps(kwargs)
|
||||
sys.exit(rc)
|
||||
|
||||
def fail_json(**kwargs):
|
||||
kwargs['failed'] = True
|
||||
exit_json(rc=1, **kwargs)
|
||||
|
||||
def add_group_info(kwargs):
|
||||
name = kwargs['name']
|
||||
if group_exists(name):
|
||||
kwargs['state'] = 'present'
|
||||
info = group_info(name)
|
||||
kwargs['gid'] = info[2]
|
||||
def get_bin_path(module, arg):
|
||||
if os.path.exists('/usr/sbin/%s' % arg):
|
||||
return '/usr/sbin/%s' % arg
|
||||
elif os.path.exists('/sbin/%s' % arg):
|
||||
return '/sbin/%s' % arg
|
||||
else:
|
||||
kwargs['state'] = 'absent'
|
||||
return kwargs
|
||||
module.fail_json(msg="Cannot find %s" % arg)
|
||||
|
||||
def group_del(group):
|
||||
cmd = [GROUPDEL, group]
|
||||
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if rc == 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
def group_del(module, group):
|
||||
cmd = [get_bin_path(module, 'groupdel'), group]
|
||||
p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(out, err) = p.communicate()
|
||||
rc = p.returncode
|
||||
return (rc, out, err)
|
||||
|
||||
def group_add(group, **kwargs):
|
||||
cmd = [GROUPADD]
|
||||
def group_add(module, group, **kwargs):
|
||||
cmd = [get_bin_path(module, 'groupadd')]
|
||||
for key in kwargs:
|
||||
if key == 'gid' and kwargs[key] is not None:
|
||||
cmd.append('-g')
|
||||
@ -69,14 +44,13 @@ def group_add(group, **kwargs):
|
||||
elif key == 'system' and kwargs[key] == 'yes':
|
||||
cmd.append('-r')
|
||||
cmd.append(group)
|
||||
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if rc == 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(out, err) = p.communicate()
|
||||
rc = p.returncode
|
||||
return (rc, out, err)
|
||||
|
||||
def group_mod(group, **kwargs):
|
||||
cmd = [GROUPMOD]
|
||||
def group_mod(module, group, **kwargs):
|
||||
cmd = [get_bin_path(module, 'groupmod')]
|
||||
info = group_info(group)
|
||||
for key in kwargs:
|
||||
if key == 'gid':
|
||||
@ -84,13 +58,12 @@ def group_mod(group, **kwargs):
|
||||
cmd.append('-g')
|
||||
cmd.append(kwargs[key])
|
||||
if len(cmd) == 1:
|
||||
return False
|
||||
return (None, '', '')
|
||||
cmd.append(group)
|
||||
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if rc == 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(out, err) = p.communicate()
|
||||
rc = p.returncode
|
||||
return (rc, out, err)
|
||||
|
||||
def group_exists(group):
|
||||
try:
|
||||
@ -110,64 +83,56 @@ def group_info(group):
|
||||
|
||||
# ===========================================
|
||||
|
||||
if not os.path.exists(GROUPADD):
|
||||
if os.path.exists("/sbin/groupadd"):
|
||||
GROUPADD = "/sbin/groupadd"
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
name=dict(required=True),
|
||||
gid=dict(default=None),
|
||||
system=dict(default='no', choices=['yes', 'no']),
|
||||
)
|
||||
)
|
||||
|
||||
state = module.params['state']
|
||||
name = module.params['name']
|
||||
gid = module.params['gid']
|
||||
system = module.params['system']
|
||||
rc = None
|
||||
out = ''
|
||||
err = ''
|
||||
result = {}
|
||||
result['name'] = name
|
||||
result['state'] = state
|
||||
if state == 'absent':
|
||||
if group_exists(name):
|
||||
(rc, out, err) = group_del(module, name)
|
||||
if rc != 0:
|
||||
module.fail_json(name=name, msg=err)
|
||||
elif state == 'present':
|
||||
if not group_exists(name):
|
||||
(rc, out, err) = group_add(module, name, gid=gid, system=system)
|
||||
else:
|
||||
(rc, out, err) = group_mod(module, name, gid=gid)
|
||||
|
||||
if rc is not None and rc != 0:
|
||||
module.fail_json(name=name, msg=err)
|
||||
|
||||
if rc is None:
|
||||
result['changed'] = False
|
||||
else:
|
||||
fail_json(msg="Cannot find groupadd")
|
||||
if not os.path.exists(GROUPDEL):
|
||||
if os.path.exists("/sbin/groupdel"):
|
||||
GROUPDEL = "/sbin/groupdel"
|
||||
else:
|
||||
fail_json(msg="Cannot find groupdel")
|
||||
if not os.path.exists(GROUPMOD):
|
||||
if os.path.exists("/sbin/groupmod"):
|
||||
GROUPDEL = "/sbin/groupmod"
|
||||
else:
|
||||
fail_json(msg="Cannot find groupmod")
|
||||
result['changed'] = True
|
||||
if out:
|
||||
result['stdout'] = out
|
||||
if err:
|
||||
result['stderr'] = err
|
||||
|
||||
if len(sys.argv) == 2 and os.path.exists(sys.argv[1]):
|
||||
argfile = sys.argv[1]
|
||||
args = open(argfile, 'r').read()
|
||||
else:
|
||||
args = ' '.join(sys.argv[1:])
|
||||
items = shlex.split(args)
|
||||
syslog.openlog('ansible-%s' % os.path.basename(__file__))
|
||||
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
|
||||
|
||||
if not len(items):
|
||||
fail_json(msg='the module requires arguments -a')
|
||||
sys.exit(1)
|
||||
|
||||
params = {}
|
||||
for x in items:
|
||||
(k, v) = x.split("=")
|
||||
params[k] = v
|
||||
|
||||
state = params.get('state','present')
|
||||
name = params.get('name', None)
|
||||
gid = params.get('gid', None)
|
||||
system = params.get('system', 'no')
|
||||
|
||||
if state not in [ 'present', 'absent' ]:
|
||||
fail_json(msg='invalid state')
|
||||
if system not in ['yes', 'no']:
|
||||
fail_json(msg='invalid system')
|
||||
if name is None:
|
||||
fail_json(msg='name is required')
|
||||
|
||||
changed = False
|
||||
rc = 0
|
||||
if state == 'absent':
|
||||
if group_exists(name):
|
||||
changed = group_del(name)
|
||||
exit_json(name=name, changed=changed)
|
||||
elif state == 'present':
|
||||
if not group_exists(name):
|
||||
changed = group_add(name, gid=gid, system=system)
|
||||
else:
|
||||
changed = group_mod(name, gid=gid)
|
||||
info = group_info(name)
|
||||
result['system'] = system
|
||||
result['gid'] = info[2]
|
||||
|
||||
exit_json(name=name, changed=changed)
|
||||
module.exit_json(**result)
|
||||
|
||||
fail_json(name=name, msg='Unexpected position reached')
|
||||
# include magic from lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
main()
|
||||
|
||||
265
library/mount
Executable file
265
library/mount
Executable file
@ -0,0 +1,265 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Red Hat, inc
|
||||
# Written by Seth Vidal
|
||||
# based on the mount modules from salt and puppet
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
# mount module - mount fs and define in fstab
|
||||
# usage:
|
||||
#
|
||||
# mount name=mountpoint, src=device_to_be_mounted fstype=fstype
|
||||
# opts=mount_opts, dump=0 passno=0 state=[present|absent|mounted|unmounted]
|
||||
#
|
||||
# absent == remove from fstab and unmounted
|
||||
# present == add to fstab, do not change mount state
|
||||
# mounted == add to fstab if not there and make sure it is mounted
|
||||
# unmounted == do not change fstab state, but unmount
|
||||
|
||||
def write_fstab(lines, dest):
|
||||
|
||||
fs_w = open(dest, 'w')
|
||||
for l in lines:
|
||||
fs_w.write(l)
|
||||
|
||||
fs_w.flush()
|
||||
fs_w.close()
|
||||
|
||||
def set_mount(**kwargs):
|
||||
""" set/change a mount point location in fstab """
|
||||
|
||||
# kwargs: name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab
|
||||
args = dict(
|
||||
opts = 'defaults',
|
||||
dump = '0',
|
||||
passno = '0',
|
||||
fstab = '/etc/fstab'
|
||||
)
|
||||
args.update(kwargs)
|
||||
|
||||
new_line = '%(src)s %(name)s %(fstype)s %(opts)s %(dump)s %(passno)s\n'
|
||||
|
||||
to_write = []
|
||||
exists = False
|
||||
changed = False
|
||||
for line in open(args['fstab'], 'r').readlines():
|
||||
if not line.strip():
|
||||
to_write.append(line)
|
||||
continue
|
||||
if line.strip().startswith('#'):
|
||||
to_write.append(line)
|
||||
continue
|
||||
if len(line.split()) != 6:
|
||||
# not sure what this is or why it is here
|
||||
# but it is not our fault so leave it be
|
||||
to_write.append(line)
|
||||
continue
|
||||
|
||||
ld = {}
|
||||
ld['src'], ld['name'], ld['fstype'], ld['opts'], ld['dump'], ld['passno'] = line.split()
|
||||
|
||||
if ld['name'] != args['name']:
|
||||
to_write.append(line)
|
||||
continue
|
||||
|
||||
# it exists - now see if what we have is different
|
||||
exists = True
|
||||
for t in ('src', 'fstype','opts', 'dump', 'passno'):
|
||||
if ld[t] != args[t]:
|
||||
changed = True
|
||||
ld[t] = args[t]
|
||||
|
||||
if changed:
|
||||
to_write.append(new_line % ld)
|
||||
else:
|
||||
to_write.append(line)
|
||||
|
||||
if not exists:
|
||||
to_write.append(new_line % args)
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
write_fstab(to_write, args['fstab'])
|
||||
|
||||
return (args['name'], changed)
|
||||
|
||||
|
||||
def unset_mount(**kwargs):
|
||||
""" remove a mount point from fstab """
|
||||
|
||||
# kwargs: name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab
|
||||
args = dict(
|
||||
opts = 'default',
|
||||
dump = '0',
|
||||
passno = '0',
|
||||
fstab = '/etc/fstab'
|
||||
)
|
||||
args.update(kwargs)
|
||||
|
||||
to_write = []
|
||||
changed = False
|
||||
for line in open(args['fstab'], 'r').readlines():
|
||||
if not line.strip():
|
||||
to_write.append(line)
|
||||
continue
|
||||
if line.strip().startswith('#'):
|
||||
to_write.append(line)
|
||||
continue
|
||||
if len(line.split()) != 6:
|
||||
# not sure what this is or why it is here
|
||||
# but it is not our fault so leave it be
|
||||
to_write.append(line)
|
||||
continue
|
||||
|
||||
ld = {}
|
||||
ld['src'], ld['name'], ld['fstype'], ld['opts'], ld['dump'], ld['passno'] = line.split()
|
||||
|
||||
if ld['name'] != args['name']:
|
||||
to_write.append(line)
|
||||
continue
|
||||
|
||||
# if we got here we found a match - continue and mark changed
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
write_fstab(to_write, args['fstab'])
|
||||
|
||||
return (args['name'], changed)
|
||||
|
||||
|
||||
def mount(**kwargs):
|
||||
""" mount up a path or remount if needed """
|
||||
|
||||
name = kwargs['name']
|
||||
if os.path.ismount(name):
|
||||
cmd = ['/bin/mount', '-o', 'remount', name]
|
||||
else:
|
||||
cmd = ['/bin/mount', name ]
|
||||
|
||||
call = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = call.communicate()
|
||||
if call.returncode == 0:
|
||||
return 0, ''
|
||||
else:
|
||||
return call.returncode, out+err
|
||||
|
||||
def umount(**kwargs):
|
||||
""" unmount a path """
|
||||
|
||||
name = kwargs['name']
|
||||
cmd = ['/bin/umount', name]
|
||||
|
||||
call = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = call.communicate()
|
||||
if call.returncode == 0:
|
||||
return 0, ''
|
||||
else:
|
||||
return call.returncode, out+err
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
state = dict(required=True, choices=['present', 'absent', 'mounted', 'unmounted']),
|
||||
name = dict(required=True),
|
||||
opts = dict(default=None),
|
||||
passno = dict(default=None),
|
||||
dump = dict(default=None),
|
||||
src = dict(required=True),
|
||||
fstype = dict(required=True),
|
||||
fstab = dict(default=None)
|
||||
)
|
||||
)
|
||||
|
||||
changed = False
|
||||
rc = 0
|
||||
args = {
|
||||
'name': module.params['name'],
|
||||
'src': module.params['src'],
|
||||
'fstype': module.params['fstype']
|
||||
}
|
||||
if module.params['passno'] is not None:
|
||||
args['passno'] = module.params['passno']
|
||||
if module.params['opts'] is not None:
|
||||
args['opts'] = module.params['opts']
|
||||
if module.params['dump'] is not None:
|
||||
args['dump'] = module.params['dump']
|
||||
if module.params['fstab'] is not None:
|
||||
args['fstab'] = module.params['fstab']
|
||||
|
||||
# absent == remove from fstab and unmounted
|
||||
# unmounted == do not change fstab state, but unmount
|
||||
# present == add to fstab, do not change mount state
|
||||
# mounted == add to fstab if not there and make sure it is mounted, if it has changed in fstab then remount it
|
||||
|
||||
state = module.params['state']
|
||||
name = module.params['name']
|
||||
if state == 'absent':
|
||||
name, changed = unset_mount(**args)
|
||||
if changed:
|
||||
if os.path.ismount(name):
|
||||
res,msg = umount(**args)
|
||||
if res:
|
||||
fail_json(msg="Error unmounting %s: %s" % (name, msg))
|
||||
|
||||
if os.path.exists(name):
|
||||
try:
|
||||
os.rmdir(name)
|
||||
except (OSError, IOError), e:
|
||||
fail_json(msg="Error rmdir %s: %s" % (name, str(e)))
|
||||
|
||||
module.exit_json(changed=changed, **args)
|
||||
|
||||
if state == 'unmounted':
|
||||
if os.path.ismount(name):
|
||||
res,msg = umount(**args)
|
||||
if res:
|
||||
fail_json(msg="Error unmounting %s: %s" % (name, msg))
|
||||
changed = True
|
||||
|
||||
module.exit_json(changed=changed, **args)
|
||||
|
||||
if state in ['mounted', 'present']:
|
||||
name, changed = set_mount(**args)
|
||||
if state == 'mounted':
|
||||
if not os.path.exists(name):
|
||||
try:
|
||||
os.makedirs(name)
|
||||
except (OSError, IOError), e:
|
||||
fail_json(msg="Error making dir %s: %s" % (name, str(e)))
|
||||
|
||||
res = 0
|
||||
if os.path.ismount(name):
|
||||
if changed:
|
||||
res,msg = mount(**args)
|
||||
else:
|
||||
changed = True
|
||||
res,msg = mount(**args)
|
||||
|
||||
if res:
|
||||
fail_json(msg="Error mounting %s: %s" % (name, msg))
|
||||
|
||||
|
||||
module.exit_json(changed=changed, **args)
|
||||
|
||||
module.fail_json(msg='Unexpected position reached')
|
||||
sys.exit(0)
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
main()
|
||||
123
library/mysql_db
Executable file
123
library/mysql_db
Executable file
@ -0,0 +1,123 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Mark Theunissen <mark.theunissen@gmail.com>
|
||||
# Sponsored by Four Kitchens http://fourkitchens.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/>.
|
||||
|
||||
import ConfigParser
|
||||
try:
|
||||
import MySQLdb
|
||||
except ImportError:
|
||||
mysqldb_found = False
|
||||
else:
|
||||
mysqldb_found = True
|
||||
|
||||
# ===========================================
|
||||
# MySQL module specific support methods.
|
||||
#
|
||||
|
||||
def db_exists(cursor, db):
|
||||
res = cursor.execute("SHOW DATABASES LIKE %s", (db,))
|
||||
return bool(res)
|
||||
|
||||
def db_delete(cursor, db):
|
||||
query = "DROP DATABASE %s" % db
|
||||
cursor.execute(query)
|
||||
return True
|
||||
|
||||
def db_create(cursor, db, encoding, collation):
|
||||
if encoding:
|
||||
encoding = " CHARACTER SET %s" % encoding
|
||||
if collation:
|
||||
collation = " COLLATE %s" % collation
|
||||
query = "CREATE DATABASE %s%s%s" % (db, encoding, collation)
|
||||
res = cursor.execute(query)
|
||||
return True
|
||||
|
||||
def load_mycnf():
|
||||
config = ConfigParser.RawConfigParser()
|
||||
mycnf = os.path.expanduser('~/.my.cnf')
|
||||
if not os.path.exists(mycnf):
|
||||
return False
|
||||
try:
|
||||
config.readfp(open(mycnf))
|
||||
creds = dict(user=config.get('client', 'user'),passwd=config.get('client', 'pass'))
|
||||
except (ConfigParser.NoOptionError, IOError):
|
||||
return False
|
||||
return creds
|
||||
|
||||
# ===========================================
|
||||
# Module execution.
|
||||
#
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
login_user=dict(default=None),
|
||||
login_password=dict(default=None),
|
||||
login_host=dict(default="localhost"),
|
||||
db=dict(required=True, aliases=['name']),
|
||||
encoding=dict(default=""),
|
||||
collation=dict(default=""),
|
||||
state=dict(default="present", choices=["absent", "present"]),
|
||||
)
|
||||
)
|
||||
|
||||
if not mysqldb_found:
|
||||
module.fail_json(msg="the python mysqldb module is required")
|
||||
|
||||
db = module.params["db"]
|
||||
encoding = module.params["encoding"]
|
||||
collation = module.params["collation"]
|
||||
state = module.params["state"]
|
||||
|
||||
# Either the caller passes both a username and password with which to connect to
|
||||
# mysql, or they pass neither and allow this module to read the credentials from
|
||||
# ~/.my.cnf.
|
||||
login_password = module.params["login_password"]
|
||||
login_user = module.params["login_user"]
|
||||
if login_user is None and login_password is None:
|
||||
mycnf_creds = load_mycnf()
|
||||
if mycnf_creds is False:
|
||||
login_user = "root"
|
||||
login_password = ""
|
||||
else:
|
||||
login_user = mycnf_creds["user"]
|
||||
login_password = mycnf_creds["passwd"]
|
||||
elif login_password is None or login_user is None:
|
||||
module.fail_json(msg="when supplying login arguments, both login_user and login_password must be provided")
|
||||
|
||||
try:
|
||||
db_connection = MySQLdb.connect(host=module.params["login_host"], user=login_user, passwd=login_password, db="mysql")
|
||||
cursor = db_connection.cursor()
|
||||
except Exception as e:
|
||||
module.fail_json(msg="unable to connect, check login_user and login_password are correct, or alternatively check ~/.my.cnf contains credentials")
|
||||
|
||||
changed = False
|
||||
if db_exists(cursor, db):
|
||||
if state == "absent":
|
||||
changed = db_delete(cursor, db)
|
||||
else:
|
||||
if state == "present":
|
||||
changed = db_create(cursor, db, encoding, collation)
|
||||
|
||||
module.exit_json(changed=changed, db=db)
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
main()
|
||||
227
library/mysql_user
Executable file
227
library/mysql_user
Executable file
@ -0,0 +1,227 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# (c) 2012, Mark Theunissen <mark.theunissen@gmail.com>
|
||||
# Sponsored by Four Kitchens http://fourkitchens.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/>.
|
||||
|
||||
import ConfigParser
|
||||
try:
|
||||
import MySQLdb
|
||||
except ImportError:
|
||||
mysqldb_found = False
|
||||
else:
|
||||
mysqldb_found = True
|
||||
|
||||
# ===========================================
|
||||
# MySQL module specific support methods.
|
||||
#
|
||||
|
||||
def user_exists(cursor, user, host):
|
||||
cursor.execute("SELECT count(*) FROM user WHERE user = %s AND host = %s", (user,host))
|
||||
count = cursor.fetchone()
|
||||
return count[0] > 0
|
||||
|
||||
def user_add(cursor, user, host, password, new_priv):
|
||||
cursor.execute("CREATE USER %s@%s IDENTIFIED BY %s", (user,host,password))
|
||||
if new_priv is not None:
|
||||
for db_table, priv in new_priv.iteritems():
|
||||
privileges_grant(cursor, user,host,db_table,priv)
|
||||
return True
|
||||
|
||||
def user_mod(cursor, user, host, password, new_priv):
|
||||
changed = False
|
||||
|
||||
# Handle passwords.
|
||||
if password is not None:
|
||||
cursor.execute("SELECT password FROM user WHERE user = %s AND host = %s", (user,host))
|
||||
current_pass_hash = cursor.fetchone()
|
||||
cursor.execute("SELECT PASSWORD(%s)", (password,))
|
||||
new_pass_hash = cursor.fetchone()
|
||||
if current_pass_hash[0] != new_pass_hash[0]:
|
||||
cursor.execute("SET PASSWORD FOR %s@%s = PASSWORD(%s)", (user,host,password))
|
||||
changed = True
|
||||
|
||||
# Handle privileges.
|
||||
if new_priv is not None:
|
||||
curr_priv = privileges_get(cursor, user,host)
|
||||
|
||||
# If the user has privileges on a db.table that doesn't appear at all in
|
||||
# the new specification, then revoke all privileges on it.
|
||||
for db_table, priv in curr_priv.iteritems():
|
||||
if db_table not in new_priv:
|
||||
privileges_revoke(cursor, user,host,db_table)
|
||||
changed = True
|
||||
|
||||
# If the user doesn't currently have any privileges on a db.table, then
|
||||
# we can perform a straight grant operation.
|
||||
for db_table, priv in new_priv.iteritems():
|
||||
if db_table not in curr_priv:
|
||||
privileges_grant(cursor, user,host,db_table,priv)
|
||||
changed = True
|
||||
|
||||
# If the db.table specification exists in both the user's current privileges
|
||||
# and in the new privileges, then we need to see if there's a difference.
|
||||
db_table_intersect = set(new_priv.keys()) & set(curr_priv.keys())
|
||||
for db_table in db_table_intersect:
|
||||
priv_diff = set(new_priv[db_table]) ^ set(curr_priv[db_table])
|
||||
if (len(priv_diff) > 0):
|
||||
privileges_revoke(cursor, user,host,db_table)
|
||||
privileges_grant(cursor, user,host,db_table,new_priv[db_table])
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
def user_delete(cursor, user, host):
|
||||
cursor.execute("DROP USER %s@%s", (user,host))
|
||||
return True
|
||||
|
||||
def privileges_get(cursor, user,host):
|
||||
""" MySQL doesn't have a better method of getting privileges aside from the
|
||||
SHOW GRANTS query syntax, which requires us to then parse the returned string.
|
||||
Here's an example of the string that is returned from MySQL:
|
||||
|
||||
GRANT USAGE ON *.* TO 'user'@'localhost' IDENTIFIED BY 'pass';
|
||||
|
||||
This function makes the query and returns a dictionary containing the results.
|
||||
The dictionary format is the same as that returned by privileges_unpack() below.
|
||||
"""
|
||||
output = {}
|
||||
cursor.execute("SHOW GRANTS FOR %s@%s", (user,host))
|
||||
grants = cursor.fetchall()
|
||||
for grant in grants:
|
||||
res = re.match("GRANT\ (.+)\ ON\ (.+)\ TO", grant[0])
|
||||
if res is None:
|
||||
module.fail_json(msg="unable to parse the MySQL grant string")
|
||||
privileges = res.group(1).split(", ")
|
||||
privileges = ['ALL' if x=='ALL PRIVILEGES' else x for x in privileges]
|
||||
db = res.group(2).replace('`', '')
|
||||
output[db] = privileges
|
||||
return output
|
||||
|
||||
def privileges_unpack(priv):
|
||||
""" Take a privileges string, typically passed as a parameter, and unserialize
|
||||
it into a dictionary, the same format as privileges_get() above. We have this
|
||||
custom format to avoid using YAML/JSON strings inside YAML playbooks. Example
|
||||
of a privileges string:
|
||||
|
||||
mydb.*:INSERT,UPDATE/anotherdb.*:SELECT/yetanother.*:ALL
|
||||
|
||||
The privilege USAGE stands for no privileges, so we add that in on *.* if it's
|
||||
not specified in the string, as MySQL will always provide this by default.
|
||||
"""
|
||||
output = {}
|
||||
for item in priv.split('/'):
|
||||
pieces = item.split(':')
|
||||
output[pieces[0]] = pieces[1].upper().split(',')
|
||||
|
||||
if '*.*' not in output:
|
||||
output['*.*'] = ['USAGE']
|
||||
|
||||
return output
|
||||
|
||||
def privileges_revoke(cursor, user,host,db_table):
|
||||
query = "REVOKE ALL PRIVILEGES ON %s FROM '%s'@'%s'" % (db_table,user,host)
|
||||
cursor.execute(query)
|
||||
|
||||
def privileges_grant(cursor, user,host,db_table,priv):
|
||||
priv_string = ",".join(priv)
|
||||
query = "GRANT %s ON %s TO '%s'@'%s'" % (priv_string,db_table,user,host)
|
||||
cursor.execute(query)
|
||||
|
||||
def load_mycnf():
|
||||
config = ConfigParser.RawConfigParser()
|
||||
mycnf = os.path.expanduser('~/.my.cnf')
|
||||
if not os.path.exists(mycnf):
|
||||
return False
|
||||
try:
|
||||
config.readfp(open(mycnf))
|
||||
creds = dict(user=config.get('client', 'user'),password=config.get('client', 'pass'))
|
||||
except (ConfigParser.NoOptionError, IOError):
|
||||
return False
|
||||
return creds
|
||||
|
||||
# ===========================================
|
||||
# Module execution.
|
||||
#
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
login_user=dict(default=None),
|
||||
login_password=dict(default=None),
|
||||
login_host=dict(default="localhost"),
|
||||
user=dict(required=True, aliases=['name']),
|
||||
password=dict(default=None),
|
||||
host=dict(default="localhost"),
|
||||
state=dict(default="present", choices=["absent", "present"]),
|
||||
priv=dict(default=None),
|
||||
)
|
||||
)
|
||||
user = module.params["user"]
|
||||
password = module.params["password"]
|
||||
host = module.params["host"]
|
||||
state = module.params["state"]
|
||||
priv = module.params["priv"]
|
||||
|
||||
if not mysqldb_found:
|
||||
module.fail_json(msg="the python mysqldb module is required")
|
||||
|
||||
if priv is not None:
|
||||
try:
|
||||
priv = privileges_unpack(priv)
|
||||
except:
|
||||
module.fail_json(msg="invalid privileges string")
|
||||
|
||||
# Either the caller passes both a username and password with which to connect to
|
||||
# mysql, or they pass neither and allow this module to read the credentials from
|
||||
# ~/.my.cnf.
|
||||
login_password = module.params["login_password"]
|
||||
login_user = module.params["login_user"]
|
||||
if login_user is None and login_password is None:
|
||||
mycnf_creds = load_mycnf()
|
||||
if mycnf_creds is False:
|
||||
login_user = "root"
|
||||
login_password = ""
|
||||
else:
|
||||
login_user = mycnf_creds["user"]
|
||||
login_password = mycnf_creds["password"]
|
||||
elif login_password is None or login_user is None:
|
||||
module.fail_json(msg="when supplying login arguments, both login_user and login_password must be provided")
|
||||
|
||||
try:
|
||||
db_connection = MySQLdb.connect(host=module.params["login_host"], user=login_user, passwd=login_password, db="mysql")
|
||||
cursor = db_connection.cursor()
|
||||
except Exception as e:
|
||||
module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or ~/.my.cnf has the credentials")
|
||||
|
||||
if state == "present":
|
||||
if user_exists(cursor, user, host):
|
||||
changed = user_mod(cursor, user, host, password, priv)
|
||||
else:
|
||||
if password is None:
|
||||
module.fail_json(msg="password parameter required when adding a user")
|
||||
changed = user_add(cursor, user, host, password, priv)
|
||||
elif state == "absent":
|
||||
if user_exists(cursor, user, host):
|
||||
changed = user_delete(cursor, user, host)
|
||||
else:
|
||||
changed = False
|
||||
module.exit_json(changed=changed, user=user)
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
main()
|
||||
27
library/ohai
27
library/ohai
@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
@ -18,5 +19,25 @@
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
/usr/bin/logger -t ansible-ohai Invoked as-is
|
||||
/usr/bin/ohai
|
||||
def get_ohai_data():
|
||||
p = subprocess.Popen(["/usr/bin/env", "ohai"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(out, err) = p.communicate()
|
||||
rc = p.returncode
|
||||
return rc, out, err
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict()
|
||||
)
|
||||
rc, out, err = get_ohai_data()
|
||||
if rc != 0:
|
||||
module.fail_json(msg=err)
|
||||
else:
|
||||
module.exit_json(**json.loads(out))
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
|
||||
main()
|
||||
|
||||
|
||||
|
||||
19
library/ping
19
library/ping
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
@ -17,15 +18,13 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict()
|
||||
)
|
||||
module.exit_json(ping='pong')
|
||||
|
||||
import os
|
||||
import syslog
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
main()
|
||||
|
||||
syslog.openlog('ansible-%s' % os.path.basename(__file__))
|
||||
syslog.syslog(syslog.LOG_NOTICE, 'Invoked as-is')
|
||||
|
||||
print json.dumps({ "ping" : "pong" })
|
||||
|
||||
103
library/postgresql_db
Executable file
103
library/postgresql_db
Executable file
@ -0,0 +1,103 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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/>.
|
||||
|
||||
try:
|
||||
import psycopg2
|
||||
except ImportError:
|
||||
postgresqldb_found = False
|
||||
else:
|
||||
postgresqldb_found = True
|
||||
|
||||
# ===========================================
|
||||
# PostgreSQL module specific support methods.
|
||||
#
|
||||
|
||||
def db_exists(cursor, db):
|
||||
query = "SELECT * FROM pg_database WHERE datname=%(db)s"
|
||||
cursor.execute(query, {'db': db})
|
||||
return cursor.rowcount == 1
|
||||
|
||||
def db_delete(cursor, db):
|
||||
query = "DROP DATABASE %s" % db
|
||||
cursor.execute(query)
|
||||
return True
|
||||
|
||||
def db_create(cursor, db, owner, template, encoding):
|
||||
if owner:
|
||||
owner = " OWNER %s" % owner
|
||||
if template:
|
||||
template = " TEMPLATE %s" % template
|
||||
if encoding:
|
||||
encoding = " ENCODING '%s'" % encoding
|
||||
query = "CREATE DATABASE %s%s%s%s" % (db, owner, template, encoding)
|
||||
cursor.execute(query)
|
||||
return True
|
||||
|
||||
# ===========================================
|
||||
# Module execution.
|
||||
#
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
login_user=dict(default="postgres"),
|
||||
login_password=dict(default=""),
|
||||
login_host=dict(default=""),
|
||||
db=dict(required=True, aliases=['name']),
|
||||
owner=dict(default=""),
|
||||
template=dict(default=""),
|
||||
encoding=dict(default=""),
|
||||
state=dict(default="present", choices=["absent", "present"]),
|
||||
)
|
||||
)
|
||||
|
||||
if not postgresqldb_found:
|
||||
module.fail_json(msg="the python psycopg2 module is required")
|
||||
|
||||
db = module.params["db"]
|
||||
owner = module.params["owner"]
|
||||
template = module.params["template"]
|
||||
encoding = module.params["encoding"]
|
||||
state = module.params["state"]
|
||||
changed = False
|
||||
try:
|
||||
db_connection = psycopg2.connect(host=module.params["login_host"],
|
||||
user=module.params["login_user"],
|
||||
password=module.params["login_password"],
|
||||
database="template1")
|
||||
# Enable autocommit so we can create databases
|
||||
db_connection.autocommit = True
|
||||
cursor = db_connection.cursor()
|
||||
except Exception as e:
|
||||
module.fail_json(msg="unable to connect to database: %s" % e)
|
||||
|
||||
try:
|
||||
if db_exists(cursor, db):
|
||||
if state == "absent":
|
||||
changed = db_delete(cursor, db)
|
||||
else:
|
||||
if state == "present":
|
||||
changed = db_create(cursor, db, owner, template, encoding)
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Database query failed: %s" % e)
|
||||
|
||||
module.exit_json(changed=changed, db=db)
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
main()
|
||||
153
library/postgresql_user
Executable file
153
library/postgresql_user
Executable file
@ -0,0 +1,153 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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/>.
|
||||
|
||||
try:
|
||||
import psycopg2
|
||||
except ImportError:
|
||||
postgresqldb_found = False
|
||||
else:
|
||||
postgresqldb_found = True
|
||||
|
||||
# ===========================================
|
||||
# PostgreSQL module specific support methods.
|
||||
#
|
||||
|
||||
|
||||
def user_exists(cursor, user):
|
||||
query = "SELECT rolname FROM pg_roles WHERE rolname=%(user)s"
|
||||
cursor.execute(query, {'user': user})
|
||||
return cursor.rowcount > 0
|
||||
|
||||
|
||||
def user_add(cursor, user, password, db):
|
||||
"""Create a new user with write access to the database"""
|
||||
query = "CREATE USER %(user)s with PASSWORD '%(password)s'"
|
||||
cursor.execute(query % {"user": user, "password": password})
|
||||
grant_privileges(cursor, user, db)
|
||||
return True
|
||||
|
||||
|
||||
def has_privileges(cursor, user, db):
|
||||
"""Check if the user has create privileges on the database"""
|
||||
query = "SELECT has_database_privilege(%(user)s, %(db)s, 'CREATE')"
|
||||
cursor.execute(query, {'user': user, 'db': db})
|
||||
return cursor.fetchone()[0]
|
||||
|
||||
|
||||
def grant_privileges(cursor, user, db):
|
||||
"""Grant all privileges on the database"""
|
||||
query = "GRANT ALL PRIVILEGES ON DATABASE %(db)s TO %(user)s"
|
||||
cursor.execute(query % {'user': user, 'db': db})
|
||||
|
||||
|
||||
def revoke_privileges(cursor, user, db):
|
||||
"""Revoke all privileges on the database"""
|
||||
query = "REVOKE ALL PRIVILEGES ON DATABASE %(db)s FROM %(user)s"
|
||||
cursor.execute(query % {'user': user, 'db': db})
|
||||
|
||||
|
||||
def user_mod(cursor, user, password, db):
|
||||
"""Update password and permissions"""
|
||||
changed = False
|
||||
|
||||
# Handle passwords.
|
||||
if password is not None:
|
||||
select = "SELECT rolpassword FROM pg_authid where rolname=%(user)s"
|
||||
cursor.execute(select, {"user": user})
|
||||
current_pass_hash = cursor.fetchone()[0]
|
||||
# Not sure how to hash the new password, so we just initiate the
|
||||
# change and check if the hash changed
|
||||
alter = "ALTER USER %(user)s WITH PASSWORD '%(password)s'"
|
||||
cursor.execute(alter % {"user": user, "password": password})
|
||||
cursor.execute(select, {"user": user})
|
||||
new_pass_hash = cursor.fetchone()[0]
|
||||
if current_pass_hash != new_pass_hash:
|
||||
changed = True
|
||||
|
||||
# Handle privileges.
|
||||
# For now, we just check if the user has access to the database
|
||||
if not has_privileges(cursor, user, db):
|
||||
grant_privileges(cursor, user, db)
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
def user_delete(cursor, user, db):
|
||||
"""Delete a user, first revoking privileges"""
|
||||
revoke_privileges(cursor, user, db)
|
||||
cursor.execute("DROP USER %(user)s" % {'user': user})
|
||||
return True
|
||||
|
||||
|
||||
|
||||
# ===========================================
|
||||
# Module execution.
|
||||
#
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
login_user=dict(default="postgres"),
|
||||
login_password=dict(default=""),
|
||||
login_host=dict(default=""),
|
||||
user=dict(required=True, aliases=['name']),
|
||||
password=dict(default=None),
|
||||
state=dict(default="present", choices=["absent", "present"]),
|
||||
db=dict(required=True),
|
||||
)
|
||||
)
|
||||
user = module.params["user"]
|
||||
password = module.params["password"]
|
||||
state = module.params["state"]
|
||||
db = module.params["db"]
|
||||
|
||||
if not postgresqldb_found:
|
||||
module.fail_json(msg="the python psycopg2 module is required")
|
||||
|
||||
try:
|
||||
db_connection = psycopg2.connect(host=module.params["login_host"],
|
||||
user=module.params["login_user"],
|
||||
password=module.params["login_password"],
|
||||
database=db)
|
||||
cursor = db_connection.cursor()
|
||||
except Exception as e:
|
||||
module.fail_json(msg="unable to connect to database: %s" % e)
|
||||
|
||||
if state == "present":
|
||||
if user_exists(cursor, user):
|
||||
changed = user_mod(cursor, user, password, db)
|
||||
else:
|
||||
if password is None:
|
||||
msg = "password parameter required when adding a user"
|
||||
module.fail_json(msg=msg)
|
||||
changed = user_add(cursor, user, password, db)
|
||||
|
||||
elif state == "absent":
|
||||
if user_exists(cursor, user):
|
||||
changed = user_delete(cursor, user, db)
|
||||
else:
|
||||
changed = False
|
||||
# Commit the database changes
|
||||
db_connection.commit()
|
||||
module.exit_json(changed=changed, user=user)
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
main()
|
||||
24
library/raw
24
library/raw
@ -1,23 +1 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@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/>.
|
||||
|
||||
# hey the Ansible raw module isn't really a remote transferred
|
||||
# module. All the magic happens in Runner.py, see the web docs
|
||||
# for more details.
|
||||
|
||||
# this is a virtual module that is entirely implemented server side
|
||||
|
||||
178
library/service
178
library/service
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
@ -17,33 +18,15 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
import sys
|
||||
import shlex
|
||||
import subprocess
|
||||
import os.path
|
||||
import syslog
|
||||
|
||||
# TODO: switch to fail_json and other helper functions
|
||||
# like other modules are using
|
||||
|
||||
# ===========================================
|
||||
|
||||
SERVICE = None
|
||||
CHKCONFIG = None
|
||||
INITCTL = None
|
||||
|
||||
def fail_json(d):
|
||||
print json.dumps(d)
|
||||
sys.exit(1)
|
||||
|
||||
def _find_binaries():
|
||||
def _find_binaries(m):
|
||||
# list of possible paths for service/chkconfig binaries
|
||||
# with the most probable first
|
||||
global CHKCONFIG
|
||||
global SERVICE
|
||||
global CHKCONFIG
|
||||
global INITCTL
|
||||
paths = ['/sbin', '/usr/sbin', '/bin', '/usr/bin']
|
||||
binaries = [ 'service', 'chkconfig', 'update-rc.d', 'initctl']
|
||||
@ -63,11 +46,11 @@ def _find_binaries():
|
||||
elif location.get('update-rc.d', None):
|
||||
CHKCONFIG = location['update-rc.d']
|
||||
else:
|
||||
fail_json(dict(failed=True, msg='unable to find chkconfig or update-rc.d binary'))
|
||||
m.fail_json(msg='unable to find chkconfig or update-rc.d binary')
|
||||
if location.get('service', None):
|
||||
SERVICE = location['service']
|
||||
else:
|
||||
fail_json(dict(failed=True, msg='unable to find service binary'))
|
||||
m.fail_json(msg='unable to find service binary')
|
||||
if location.get('initctl', None):
|
||||
INITCTL = location['initctl']
|
||||
else:
|
||||
@ -75,7 +58,6 @@ def _find_binaries():
|
||||
|
||||
def _get_service_status(name):
|
||||
rc, status_stdout, status_stderr = _run("%s %s status" % (SERVICE, name))
|
||||
status = status_stdout + status_stderr
|
||||
|
||||
# set the running state to None because we don't know it yet
|
||||
running = None
|
||||
@ -99,15 +81,19 @@ def _get_service_status(name):
|
||||
# if the job status is still not known check it by status output keywords
|
||||
if running == None:
|
||||
# first tranform the status output that could irritate keyword matching
|
||||
cleaned_status_stdout = status_stdout.lower().replace(name.lower(),'')
|
||||
if cleaned_status_stdout.find("stop") != -1:
|
||||
cleanout = status_stdout.lower().replace(name.lower(), '')
|
||||
if "stop" in cleanout:
|
||||
running = False
|
||||
elif cleaned_status_stdout.find("run") != -1 and cleaned_status_stdout.find("not") != -1:
|
||||
elif "run" in cleanout and "not" in cleanout:
|
||||
running = False
|
||||
elif cleaned_status_stdout.find("run") != -1 and cleaned_status_stdout.find("not") == -1:
|
||||
elif "run" in cleanout and "not" not in cleanout:
|
||||
running = True
|
||||
elif cleaned_status_stdout.find("start") != -1 and cleaned_status_stdout.find("not") == -1:
|
||||
elif "start" in cleanout and "not" not in cleanout:
|
||||
running = True
|
||||
elif 'could not access pid file' in cleanout:
|
||||
running = False
|
||||
elif 'is dead and pid file exists' in cleanout:
|
||||
running = False
|
||||
|
||||
# if the job status is still not known check it by special conditions
|
||||
if running == None:
|
||||
@ -135,94 +121,67 @@ def _do_enable(name, enable):
|
||||
valid_argument['on'] = "enable"
|
||||
valid_argument['off'] = "disable"
|
||||
|
||||
if enable.lower() in ['on', 'true', 'yes', 'enable']:
|
||||
rc, stdout, stderr = _run("%s %s %s" % (CHKCONFIG, name, valid_argument['on']))
|
||||
elif enable.lower() in ['off', 'false', 'no', 'disable']:
|
||||
rc, stdout, stderr = _run("%s %s %s" % (CHKCONFIG, name, valid_argument['off']))
|
||||
if enable is not None:
|
||||
if enable:
|
||||
rc, stdout, stderr = _run("%s %s %s" % (CHKCONFIG, name, valid_argument['on']))
|
||||
else:
|
||||
rc, stdout, stderr = _run("%s %s %s" % (CHKCONFIG, name, valid_argument['off']))
|
||||
|
||||
return rc, stdout, stderr
|
||||
|
||||
argfile = sys.argv[1]
|
||||
args = open(argfile, 'r').read()
|
||||
items = shlex.split(args)
|
||||
syslog.openlog('ansible-%s' % os.path.basename(__file__))
|
||||
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
name = dict(required=True),
|
||||
state = dict(choices=['running', 'started', 'stopped', 'restarted', 'reloaded']),
|
||||
enabled = dict(choices=BOOLEANS)
|
||||
)
|
||||
)
|
||||
|
||||
if not len(items):
|
||||
fail_json(dict(failed=True, msg='this module requires arguments (-a)'))
|
||||
name = module.params['name']
|
||||
state = module.params['state']
|
||||
enable = module.boolean(module.params.get('enabled', None))
|
||||
|
||||
params = {}
|
||||
for arg in items:
|
||||
if "=" not in arg:
|
||||
fail_json(dict(failed=True, msg='expected key=value format arguments'))
|
||||
|
||||
(name, value) = arg.split("=")
|
||||
params[name] = value
|
||||
|
||||
name = params.get('name', None)
|
||||
|
||||
if name is None:
|
||||
fail_json(dict(failed=True, msg='missing name'))
|
||||
|
||||
state = params.get('state', None)
|
||||
list_items = params.get('list', None)
|
||||
enable = params.get('enabled', params.get('enable', None))
|
||||
|
||||
# running and started are the same
|
||||
if state and state.lower() not in [ 'running', 'started', 'stopped', 'restarted','reloaded' ]:
|
||||
fail_json(dict(failed=True, msg='invalid value for state'))
|
||||
if list_items and list_items.lower() not in [ 'status' ]:
|
||||
fail_json(dict(failed=True, msg='invalid value for list'))
|
||||
if enable and enable.lower() not in [ 'on', 'off', 'true', 'false', 'yes', 'no', 'enable', 'disable' ]:
|
||||
fail_json(dict(failed=True, msg='invalid value for enable'))
|
||||
# ===========================================
|
||||
# find binaries locations on minion
|
||||
_find_binaries(module)
|
||||
|
||||
# ===========================================
|
||||
# get service status
|
||||
running = _get_service_status(name)
|
||||
|
||||
# ===========================================
|
||||
# find binaries locations on minion
|
||||
_find_binaries()
|
||||
|
||||
|
||||
# ===========================================
|
||||
# get service status
|
||||
running = _get_service_status(name)
|
||||
|
||||
|
||||
if state or enable:
|
||||
rc = 0
|
||||
out = ''
|
||||
err = ''
|
||||
# ===========================================
|
||||
# Some common variables
|
||||
changed = False
|
||||
|
||||
if enable:
|
||||
rc = 0
|
||||
err = ''
|
||||
out = ''
|
||||
|
||||
if module.params['enabled']:
|
||||
rc_enable, out_enable, err_enable = _do_enable(name, enable)
|
||||
rc += rc_enable
|
||||
out += out_enable
|
||||
err += err_enable
|
||||
|
||||
if state and running == None:
|
||||
print json.dumps({
|
||||
"failed" : True,
|
||||
"msg" : "failed determining the current service state => state stays unchanged",
|
||||
})
|
||||
print >> sys.stderr, out + err
|
||||
module.fail_json(msg="failed determining the current service state => state stays unchanged", changed=False)
|
||||
|
||||
elif state:
|
||||
# a state change command has been requested
|
||||
|
||||
# ===========================================
|
||||
# determine if we are going to change anything
|
||||
|
||||
if not running and state in ("started", "running"):
|
||||
if not running and state in ["started", "running"]:
|
||||
changed = True
|
||||
elif running and state in ("stopped","reloaded"):
|
||||
elif running and state in ["stopped","reloaded"]:
|
||||
changed = True
|
||||
elif state == "restarted":
|
||||
changed = True
|
||||
|
||||
# ===========================================
|
||||
# run change commands if we need to
|
||||
|
||||
if changed:
|
||||
if state in ('started', 'running'):
|
||||
if state in ['started', 'running']:
|
||||
rc_state, stdout, stderr = _run("%s %s start" % (SERVICE, name))
|
||||
elif state == 'stopped':
|
||||
rc_state, stdout, stderr = _run("%s %s stop" % (SERVICE, name))
|
||||
@ -240,40 +199,19 @@ if state or enable:
|
||||
rc = rc + rc_state
|
||||
|
||||
if rc != 0:
|
||||
|
||||
print json.dumps({
|
||||
"failed" : 1,
|
||||
"rc" : rc,
|
||||
})
|
||||
print >> sys.stderr, out + err
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ===============================================
|
||||
# success
|
||||
module.fail_json(msg=err)
|
||||
|
||||
result = {"changed": changed}
|
||||
|
||||
if module.params['enabled']:
|
||||
result['enabled'] = module.params['enabled']
|
||||
if state:
|
||||
result['state'] = state
|
||||
rc, stdout, stderr = _run("%s %s status" % (SERVICE, name))
|
||||
if list_items and list_items in [ 'status' ]:
|
||||
result['status'] = stdout
|
||||
print json.dumps(result)
|
||||
|
||||
module.exit_json(**result);
|
||||
|
||||
elif list_items is not None:
|
||||
|
||||
# solo list=status mode, don't change anything, just return
|
||||
# suitable for /usr/bin/ansible usage or API, playbooks
|
||||
# not so much
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
|
||||
print json.dumps({
|
||||
"status" : status
|
||||
})
|
||||
|
||||
else:
|
||||
|
||||
print json.dumps(dict(failed=True, msg="expected state or list parameters"))
|
||||
|
||||
|
||||
sys.exit(0)
|
||||
main()
|
||||
|
||||
|
||||
935
library/setup
935
library/setup
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
@ -17,21 +18,13 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
DEFAULT_ANSIBLE_SETUP = "/etc/ansible/setup"
|
||||
|
||||
import array
|
||||
import fcntl
|
||||
import glob
|
||||
import sys
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shlex
|
||||
import socket
|
||||
import struct
|
||||
import subprocess
|
||||
import traceback
|
||||
import syslog
|
||||
|
||||
try:
|
||||
import selinux
|
||||
@ -44,375 +37,625 @@ try:
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
_I386RE = re.compile(r'i[3456]86')
|
||||
SIOCGIFCONF = 0x8912
|
||||
SIOCGIFHWADDR = 0x8927
|
||||
MEMORY_FACTS = ['MemTotal', 'SwapTotal', 'MemFree', 'SwapFree']
|
||||
# DMI bits
|
||||
DMI_DICT = { 'form_factor': '/sys/devices/virtual/dmi/id/chassis_type',
|
||||
'product_name': '/sys/devices/virtual/dmi/id/product_name',
|
||||
'product_serial': '/sys/devices/virtual/dmi/id/product_serial',
|
||||
'product_uuid': '/sys/devices/virtual/dmi/id/product_uuid',
|
||||
'product_version': '/sys/devices/virtual/dmi/id/product_version',
|
||||
'system_vendor': '/sys/devices/virtual/dmi/id/sys_vendor',
|
||||
'bios_date': '/sys/devices/virtual/dmi/id/bios_date',
|
||||
'bios_version': '/sys/devices/virtual/dmi/id/bios_version' }
|
||||
# From smolt and DMI spec
|
||||
FORM_FACTOR = [ "Unknown", "Other", "Unknown", "Desktop",
|
||||
"Low Profile Desktop", "Pizza Box", "Mini Tower", "Tower",
|
||||
"Portable", "Laptop", "Notebook", "Hand Held", "Docking Station",
|
||||
"All In One", "Sub Notebook", "Space-saving", "Lunch Box",
|
||||
"Main Server Chassis", "Expansion Chassis", "Sub Chassis",
|
||||
"Bus Expansion Chassis", "Peripheral Chassis", "RAID Chassis",
|
||||
"Rack Mount Chassis", "Sealed-case PC", "Multi-system",
|
||||
"CompactPCI", "AdvancedTCA" ]
|
||||
# For the most part, we assume that platform.dist() will tell the truth.
|
||||
# This is the fallback to handle unknowns or exceptions
|
||||
OSDIST_DICT = { '/etc/redhat-release': 'RedHat',
|
||||
'/etc/vmware-release': 'VMwareESX' }
|
||||
SELINUX_MODE_DICT = { 1: 'enforcing', 0: 'permissive', -1: 'disabled' }
|
||||
class Facts(object):
|
||||
"""
|
||||
This class should only attempt to populate those facts that
|
||||
are mostly generic to all systems. This includes platform facts,
|
||||
service facts (eg. ssh keys or selinux), and distribution facts.
|
||||
Anything that requires extensive code or may have more than one
|
||||
possible implementation to establish facts for a given topic should
|
||||
subclass Facts.
|
||||
"""
|
||||
|
||||
def get_file_content(path):
|
||||
if os.path.exists(path) and os.access(path, os.R_OK):
|
||||
data = open(path).read().strip()
|
||||
if len(data) == 0:
|
||||
data = None
|
||||
else:
|
||||
data = None
|
||||
return data
|
||||
_I386RE = re.compile(r'i[3456]86')
|
||||
# For the most part, we assume that platform.dist() will tell the truth.
|
||||
# This is the fallback to handle unknowns or exceptions
|
||||
OSDIST_DICT = { '/etc/redhat-release': 'RedHat',
|
||||
'/etc/vmware-release': 'VMwareESX' }
|
||||
SELINUX_MODE_DICT = { 1: 'enforcing', 0: 'permissive', -1: 'disabled' }
|
||||
|
||||
# platform.dist() is deprecated in 2.6
|
||||
# in 2.6 and newer, you should use platform.linux_distribution()
|
||||
def get_distribution_facts(facts):
|
||||
dist = platform.dist()
|
||||
facts['distribution'] = dist[0].capitalize() or 'NA'
|
||||
facts['distribution_version'] = dist[1] or 'NA'
|
||||
facts['distribution_release'] = dist[2] or 'NA'
|
||||
# Try to handle the exceptions now ...
|
||||
for (path, name) in OSDIST_DICT.items():
|
||||
if os.path.exists(path):
|
||||
if facts['distribution'] == 'Fedora':
|
||||
pass
|
||||
elif name == 'RedHat':
|
||||
data = get_file_content(path)
|
||||
if 'Red Hat' in data:
|
||||
facts['distribution'] = name
|
||||
else:
|
||||
facts['distribution'] = data.split()[0]
|
||||
else:
|
||||
facts['distribution'] = name
|
||||
def __init__(self):
|
||||
self.facts = {}
|
||||
self.get_platform_facts()
|
||||
self.get_distribution_facts()
|
||||
self.get_public_ssh_host_keys()
|
||||
self.get_selinux_facts()
|
||||
|
||||
# Platform
|
||||
# patform.system() can be Linux, Darwin, Java, or Windows
|
||||
def get_platform_facts(facts):
|
||||
facts['system'] = platform.system()
|
||||
facts['kernel'] = platform.release()
|
||||
facts['machine'] = platform.machine()
|
||||
facts['python_version'] = platform.python_version()
|
||||
if facts['machine'] == 'x86_64':
|
||||
facts['architecture'] = facts['machine']
|
||||
elif _I386RE.search(facts['machine']):
|
||||
facts['architecture'] = 'i386'
|
||||
else:
|
||||
facts['archtecture'] = facts['machine']
|
||||
if facts['system'] == 'Linux':
|
||||
get_distribution_facts(facts)
|
||||
def populate(self):
|
||||
return self.facts
|
||||
|
||||
def get_memory_facts(facts):
|
||||
if not os.access("/proc/meminfo", os.R_OK):
|
||||
return facts
|
||||
for line in open("/proc/meminfo").readlines():
|
||||
data = line.split(":", 1)
|
||||
key = data[0]
|
||||
if key in MEMORY_FACTS:
|
||||
val = data[1].strip().split(' ')[0]
|
||||
facts["%s_mb" % key.lower()] = long(val) / 1024
|
||||
|
||||
def get_cpu_facts(facts):
|
||||
i = 0
|
||||
physid = 0
|
||||
sockets = {}
|
||||
if not os.access("/proc/cpuinfo", os.R_OK):
|
||||
return facts
|
||||
for line in open("/proc/cpuinfo").readlines():
|
||||
data = line.split(":", 1)
|
||||
key = data[0].strip()
|
||||
if key == 'model name':
|
||||
if 'processor' not in facts:
|
||||
facts['processor'] = []
|
||||
facts['processor'].append(data[1].strip())
|
||||
i += 1
|
||||
elif key == 'physical id':
|
||||
physid = data[1].strip()
|
||||
if physid not in sockets:
|
||||
sockets[physid] = 1
|
||||
elif key == 'cpu cores':
|
||||
sockets[physid] = int(data[1].strip())
|
||||
if len(sockets) > 0:
|
||||
facts['processor_count'] = len(sockets)
|
||||
facts['processor_cores'] = reduce(lambda x, y: x + y, sockets.values())
|
||||
else:
|
||||
facts['processor_count'] = i
|
||||
facts['processor_cores'] = 'NA'
|
||||
|
||||
def get_hardware_facts(facts):
|
||||
get_memory_facts(facts)
|
||||
get_cpu_facts(facts)
|
||||
for (key,path) in DMI_DICT.items():
|
||||
data = get_file_content(path)
|
||||
if data is not None:
|
||||
if key == 'form_factor':
|
||||
facts['form_factor'] = FORM_FACTOR[int(data)]
|
||||
else:
|
||||
facts[key] = data
|
||||
# Platform
|
||||
# patform.system() can be Linux, Darwin, Java, or Windows
|
||||
def get_platform_facts(self):
|
||||
self.facts['system'] = platform.system()
|
||||
self.facts['kernel'] = platform.release()
|
||||
self.facts['machine'] = platform.machine()
|
||||
self.facts['python_version'] = platform.python_version()
|
||||
self.facts['fqdn'] = socket.getfqdn()
|
||||
self.facts['hostname'] = self.facts['fqdn'].split('.')[0]
|
||||
if self.facts['machine'] == 'x86_64':
|
||||
self.facts['architecture'] = self.facts['machine']
|
||||
elif Facts._I386RE.search(self.facts['machine']):
|
||||
self.facts['architecture'] = 'i386'
|
||||
else:
|
||||
facts[key] = 'NA'
|
||||
self.facts['archtecture'] = self.facts['machine']
|
||||
if self.facts['system'] == 'Linux':
|
||||
self.get_distribution_facts()
|
||||
|
||||
def get_linux_virtual_facts(facts):
|
||||
if os.path.exists("/proc/xen"):
|
||||
facts['virtualization_type'] = 'xen'
|
||||
facts['virtualization_role'] = 'guest'
|
||||
if os.path.exists("/proc/xen/capabilities"):
|
||||
facts['virtualization_role'] = 'host'
|
||||
if os.path.exists("/proc/modules"):
|
||||
modules = []
|
||||
for line in open("/proc/modules").readlines():
|
||||
data = line.split(" ", 1)
|
||||
modules.append(data[0])
|
||||
if 'kvm' in modules:
|
||||
facts['virtualization_type'] = 'kvm'
|
||||
facts['virtualization_role'] = 'host'
|
||||
elif 'vboxdrv' in modules:
|
||||
facts['virtualization_type'] = 'virtualbox'
|
||||
facts['virtualization_role'] = 'host'
|
||||
elif 'vboxguest' in modules:
|
||||
facts['virtualization_type'] = 'virtualbox'
|
||||
facts['virtualization_role'] = 'guest'
|
||||
if 'QEMU' in facts['processor'][0]:
|
||||
facts['virtualization_type'] = 'kvm'
|
||||
facts['virtualization_role'] = 'guest'
|
||||
if facts['distribution'] == 'VMwareESX':
|
||||
facts['virtualization_type'] = 'VMware'
|
||||
facts['virtualization_role'] = 'host'
|
||||
# You can spawn a dmidecode process and parse that or infer from devices
|
||||
for dev_model in glob.glob('/sys/block/?da/device/vendor'):
|
||||
info = open(dev_model).read()
|
||||
if 'VMware' in info:
|
||||
facts['virtualization_type'] = 'VMware'
|
||||
facts['virtualization_role'] = 'guest'
|
||||
elif 'Virtual HD' in info or 'Virtual CD' in info:
|
||||
facts['virtualization_type'] = 'VirtualPC'
|
||||
facts['virtualization_role'] = 'guest'
|
||||
# platform.dist() is deprecated in 2.6
|
||||
# in 2.6 and newer, you should use platform.linux_distribution()
|
||||
def get_distribution_facts(self):
|
||||
dist = platform.dist()
|
||||
self.facts['distribution'] = dist[0].capitalize() or 'NA'
|
||||
self.facts['distribution_version'] = dist[1] or 'NA'
|
||||
self.facts['distribution_release'] = dist[2] or 'NA'
|
||||
# Try to handle the exceptions now ...
|
||||
for (path, name) in Facts.OSDIST_DICT.items():
|
||||
if os.path.exists(path):
|
||||
if self.facts['distribution'] == 'Fedora':
|
||||
pass
|
||||
elif name == 'RedHat':
|
||||
data = get_file_content(path)
|
||||
if 'Red Hat' in data:
|
||||
self.facts['distribution'] = name
|
||||
else:
|
||||
self.facts['distribution'] = data.split()[0]
|
||||
else:
|
||||
self.facts['distribution'] = name
|
||||
|
||||
def get_virtual_facts(facts):
|
||||
facts['virtualization_type'] = 'None'
|
||||
facts['virtualization_role'] = 'None'
|
||||
if facts['system'] == 'Linux':
|
||||
facts = get_linux_virtual_facts(facts)
|
||||
def get_public_ssh_host_keys(self):
|
||||
dsa = get_file_content('/etc/ssh/ssh_host_dsa_key.pub')
|
||||
rsa = get_file_content('/etc/ssh/ssh_host_rsa_key.pub')
|
||||
if dsa is None:
|
||||
dsa = 'NA'
|
||||
else:
|
||||
self.facts['ssh_host_key_dsa_public'] = dsa.split()[1]
|
||||
if rsa is None:
|
||||
rsa = 'NA'
|
||||
else:
|
||||
self.facts['ssh_host_key_rsa_public'] = rsa.split()[1]
|
||||
|
||||
# get list of interfaces that are up
|
||||
def get_interfaces(facts):
|
||||
if facts['system'] == 'SunOS':
|
||||
return []
|
||||
length = 4096
|
||||
offset = 32
|
||||
step = 32
|
||||
if platform.architecture()[0] == '64bit':
|
||||
offset = 16
|
||||
step = 40
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
names = array.array('B', '\0' * length)
|
||||
bytelen = struct.unpack('iL', fcntl.ioctl(
|
||||
s.fileno(), SIOCGIFCONF, struct.pack(
|
||||
'iL', length, names.buffer_info()[0])
|
||||
))[0]
|
||||
return [names.tostring()[i:i+offset].split('\0', 1)[0]
|
||||
for i in range(0, bytelen, step)]
|
||||
def get_selinux_facts(self):
|
||||
if not HAVE_SELINUX:
|
||||
self.facts['selinux'] = False
|
||||
return
|
||||
self.facts['selinux'] = {}
|
||||
if not selinux.is_selinux_enabled():
|
||||
self.facts['selinux']['status'] = 'disabled'
|
||||
else:
|
||||
self.facts['selinux']['status'] = 'enabled'
|
||||
self.facts['selinux']['policyvers'] = selinux.security_policyvers()
|
||||
(rc, configmode) = selinux.selinux_getenforcemode()
|
||||
if rc == 0 and Facts.SELINUX_MODE_DICT.has_key(configmode):
|
||||
self.facts['selinux']['config_mode'] = Facts.SELINUX_MODE_DICT[configmode]
|
||||
mode = selinux.security_getenforce()
|
||||
if Facts.SELINUX_MODE_DICT.has_key(mode):
|
||||
self.facts['selinux']['mode'] = Facts.SELINUX_MODE_DICT[mode]
|
||||
(rc, policytype) = selinux.selinux_getpolicytype()
|
||||
if rc == 0:
|
||||
self.facts['selinux']['type'] = policytype
|
||||
|
||||
def get_iface_hwaddr(iface):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
info = fcntl.ioctl(s.fileno(), SIOCGIFHWADDR,
|
||||
struct.pack('256s', iface[:15]))
|
||||
return ''.join(['%02x:' % ord(char) for char in info[18:24]])[:-1]
|
||||
class Hardware(Facts):
|
||||
"""
|
||||
This is a generic Hardware subclass of Facts. This should be further
|
||||
subclassed to implement per platform. If you subclass this, it
|
||||
should define:
|
||||
- memfree_mb
|
||||
- memtotal_mb
|
||||
- swapfree_mb
|
||||
- swaptotal_mb
|
||||
- processor (a list)
|
||||
- processor_cores
|
||||
- processor_count
|
||||
|
||||
def get_network_facts(facts):
|
||||
facts['fqdn'] = socket.getfqdn()
|
||||
facts['hostname'] = facts['fqdn'].split('.')[0]
|
||||
facts['interfaces'] = get_interfaces(facts)
|
||||
for iface in facts['interfaces']:
|
||||
facts[iface] = { 'macaddress': get_iface_hwaddr(iface) }
|
||||
# This is lame, but there doesn't appear to be a good way
|
||||
# to get all addresses for both IPv4 and IPv6.
|
||||
cmd = subprocess.Popen("/sbin/ifconfig %s" % iface, shell=True,
|
||||
All subclasses MUST define platform.
|
||||
"""
|
||||
platform = 'Generic'
|
||||
|
||||
def __new__(cls, *arguments, **keyword):
|
||||
subclass = cls
|
||||
for sc in Hardware.__subclasses__():
|
||||
if sc.platform == platform.system():
|
||||
subclass = sc
|
||||
return super(cls, subclass).__new__(subclass, *arguments, **keyword)
|
||||
|
||||
def __init__(self):
|
||||
Facts.__init__(self)
|
||||
|
||||
def populate(self):
|
||||
return self.facts
|
||||
|
||||
class LinuxHardware(Hardware):
|
||||
"""
|
||||
Linux-specific subclass of Hardware. Defines memory and CPU facts:
|
||||
- memfree_mb
|
||||
- memtotal_mb
|
||||
- swapfree_mb
|
||||
- swaptotal_mb
|
||||
- processor (a list)
|
||||
- processor_cores
|
||||
- processor_count
|
||||
|
||||
In addition, it also defines number of DMI facts.
|
||||
"""
|
||||
platform = 'Linux'
|
||||
MEMORY_FACTS = ['MemTotal', 'SwapTotal', 'MemFree', 'SwapFree']
|
||||
# DMI bits
|
||||
DMI_DICT = { 'form_factor': '/sys/devices/virtual/dmi/id/chassis_type',
|
||||
'product_name': '/sys/devices/virtual/dmi/id/product_name',
|
||||
'product_serial': '/sys/devices/virtual/dmi/id/product_serial',
|
||||
'product_uuid': '/sys/devices/virtual/dmi/id/product_uuid',
|
||||
'product_version': '/sys/devices/virtual/dmi/id/product_version',
|
||||
'system_vendor': '/sys/devices/virtual/dmi/id/sys_vendor',
|
||||
'bios_date': '/sys/devices/virtual/dmi/id/bios_date',
|
||||
'bios_version': '/sys/devices/virtual/dmi/id/bios_version' }
|
||||
# From smolt and DMI spec
|
||||
# See http://www.dmtf.org/sites/default/files/standards/documents/DSP0134_2.7.0.pdf
|
||||
FORM_FACTOR = [ "Unknown", "Other", "Unknown", "Desktop",
|
||||
"Low Profile Desktop", "Pizza Box", "Mini Tower", "Tower",
|
||||
"Portable", "Laptop", "Notebook", "Hand Held", "Docking Station",
|
||||
"All In One", "Sub Notebook", "Space-saving", "Lunch Box",
|
||||
"Main Server Chassis", "Expansion Chassis", "Sub Chassis",
|
||||
"Bus Expansion Chassis", "Peripheral Chassis", "RAID Chassis",
|
||||
"Rack Mount Chassis", "Sealed-case PC", "Multi-system",
|
||||
"CompactPCI", "AdvancedTCA", "Blade" ]
|
||||
|
||||
def __init__(self):
|
||||
Hardware.__init__(self)
|
||||
|
||||
def populate(self):
|
||||
self.get_cpu_facts()
|
||||
self.get_memory_facts()
|
||||
self.get_dmi_facts()
|
||||
return self.facts
|
||||
|
||||
def get_memory_facts(self):
|
||||
if not os.access("/proc/meminfo", os.R_OK):
|
||||
return
|
||||
for line in open("/proc/meminfo").readlines():
|
||||
data = line.split(":", 1)
|
||||
key = data[0]
|
||||
if key in LinuxHardware.MEMORY_FACTS:
|
||||
val = data[1].strip().split(' ')[0]
|
||||
self.facts["%s_mb" % key.lower()] = long(val) / 1024
|
||||
|
||||
def get_cpu_facts(self):
|
||||
i = 0
|
||||
physid = 0
|
||||
sockets = {}
|
||||
if not os.access("/proc/cpuinfo", os.R_OK):
|
||||
return
|
||||
self.facts['processor'] = []
|
||||
for line in open("/proc/cpuinfo").readlines():
|
||||
data = line.split(":", 1)
|
||||
key = data[0].strip()
|
||||
if key == 'model name':
|
||||
if 'processor' not in self.facts:
|
||||
self.facts['processor'] = []
|
||||
self.facts['processor'].append(data[1].strip())
|
||||
i += 1
|
||||
elif key == 'physical id':
|
||||
physid = data[1].strip()
|
||||
if physid not in sockets:
|
||||
sockets[physid] = 1
|
||||
elif key == 'cpu cores':
|
||||
sockets[physid] = int(data[1].strip())
|
||||
if len(sockets) > 0:
|
||||
self.facts['processor_count'] = len(sockets)
|
||||
self.facts['processor_cores'] = reduce(lambda x, y: x + y, sockets.values())
|
||||
else:
|
||||
self.facts['processor_count'] = i
|
||||
self.facts['processor_cores'] = 'NA'
|
||||
|
||||
def get_dmi_facts(self):
|
||||
for (key,path) in LinuxHardware.DMI_DICT.items():
|
||||
data = get_file_content(path)
|
||||
if data is not None:
|
||||
if key == 'form_factor':
|
||||
try:
|
||||
self.facts['form_factor'] = LinuxHardware.FORM_FACTOR[int(data)]
|
||||
except IndexError, e:
|
||||
self.facts['form_factor'] = 'unknown (%s)' % data
|
||||
else:
|
||||
self.facts[key] = data
|
||||
else:
|
||||
self.facts[key] = 'NA'
|
||||
|
||||
class SunOSHardware(Hardware):
|
||||
"""
|
||||
In addition to the generic memory and cpu facts, this also sets
|
||||
swap_reserved_mb and swap_allocated_mb that is available from *swap -s*.
|
||||
"""
|
||||
platform = 'SunOS'
|
||||
|
||||
def __init__(self):
|
||||
Hardware.__init__(self)
|
||||
|
||||
def populate(self):
|
||||
self.get_cpu_facts()
|
||||
self.get_memory_facts()
|
||||
return self.facts
|
||||
|
||||
def get_cpu_facts(self):
|
||||
cmd = subprocess.Popen("/usr/sbin/psrinfo -v", shell=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = cmd.communicate()
|
||||
self.facts['processor'] = []
|
||||
for line in out.split('\n'):
|
||||
if 'processor operates' in line:
|
||||
if 'processor' not in self.facts:
|
||||
self.facts['processor'] = []
|
||||
self.facts['processor'].append(line.strip())
|
||||
self.facts['processor_cores'] = 'NA'
|
||||
self.facts['processor_count'] = len(self.facts['processor'])
|
||||
|
||||
def get_memory_facts(self):
|
||||
cmd = subprocess.Popen("/usr/sbin/prtconf", shell=False,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = cmd.communicate()
|
||||
for line in out.split('\n'):
|
||||
if 'Memory size' in line:
|
||||
self.facts['memtotal_mb'] = line.split()[2]
|
||||
cmd = subprocess.Popen("/usr/sbin/swap -s", shell=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = cmd.communicate()
|
||||
allocated = long(out.split()[1][:-1])
|
||||
reserved = long(out.split()[5][:-1])
|
||||
used = long(out.split()[8][:-1])
|
||||
free = long(out.split()[10][:-1])
|
||||
self.facts['swapfree_mb'] = free / 1024
|
||||
self.facts['swaptotal_mb'] = (free + used) / 1024
|
||||
self.facts['swap_allocated_mb'] = allocated / 1024
|
||||
self.facts['swap_reserved_mb'] = reserved / 1024
|
||||
|
||||
class FreeBSDHardware(Hardware):
|
||||
"""
|
||||
FreeBSD-specific subclass of Hardware. Defines memory and CPU facts:
|
||||
- memfree_mb
|
||||
- memtotal_mb
|
||||
- swapfree_mb
|
||||
- swaptotal_mb
|
||||
- processor (a list)
|
||||
- processor_cores
|
||||
- processor_count
|
||||
"""
|
||||
platform = 'FreeBSD'
|
||||
DMESG_BOOT = '/var/run/dmesg.boot'
|
||||
|
||||
def __init__(self):
|
||||
Hardware.__init__(self)
|
||||
|
||||
def populate(self):
|
||||
self.get_cpu_facts()
|
||||
self.get_memory_facts()
|
||||
return self.facts
|
||||
|
||||
def get_cpu_facts(self):
|
||||
self.facts['processor'] = []
|
||||
cmd = subprocess.Popen("/sbin/sysctl -n hw.ncpu", shell=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = cmd.communicate()
|
||||
self.facts['processor_count'] = out.strip()
|
||||
for line in open(FreeBSDHardware.DMESG_BOOT).readlines():
|
||||
if 'CPU:' in line:
|
||||
cpu = re.sub(r'CPU:\s+', r"", line)
|
||||
self.facts['processor'].append(cpu.strip())
|
||||
if 'Logical CPUs per core' in line:
|
||||
self.facts['processor_cores'] = line.split()[4]
|
||||
|
||||
def get_memory_facts(self):
|
||||
cmd = subprocess.Popen("/sbin/sysctl vm.stats", shell=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = cmd.communicate()
|
||||
for line in out.split('\n'):
|
||||
data = line.split()
|
||||
if 'inet addr' in line:
|
||||
if 'ipv4' not in facts[iface]:
|
||||
facts[iface]['ipv4'] = {}
|
||||
facts[iface]['ipv4'] = { 'address': data[1].split(':')[1],
|
||||
'netmask': data[-1].split(':')[1] }
|
||||
ip = struct.unpack("!L", socket.inet_aton(facts[iface]['ipv4']['address']))[0]
|
||||
mask = struct.unpack("!L", socket.inet_aton(facts[iface]['ipv4']['netmask']))[0]
|
||||
facts[iface]['ipv4']['network'] = socket.inet_ntoa(struct.pack("!L", ip & mask))
|
||||
if 'inet6 addr' in line:
|
||||
(ip, prefix) = data[2].split('/')
|
||||
scope = data[3].split(':')[1].lower()
|
||||
if 'ipv6' not in facts[iface]:
|
||||
facts[iface]['ipv6'] = []
|
||||
facts[iface]['ipv6'].append( { 'address': ip,
|
||||
'prefix': prefix,
|
||||
'scope': scope } )
|
||||
return facts
|
||||
if 'vm.stats.vm.v_page_size' in line:
|
||||
pagesize = long(data[1])
|
||||
if 'vm.stats.vm.v_page_count' in line:
|
||||
pagecount = long(data[1])
|
||||
if 'vm.stats.vm.v_free_count' in line:
|
||||
freecount = long(data[1])
|
||||
self.facts['memtotal_mb'] = pagesize * pagecount / 1024 / 1024
|
||||
self.facts['memfree_mb'] = pagesize * freecount / 1024 / 1024
|
||||
# Get swapinfo. swapinfo output looks like:
|
||||
# Device 1M-blocks Used Avail Capacity
|
||||
# /dev/ada0p3 314368 0 314368 0%
|
||||
#
|
||||
cmd = subprocess.Popen("/usr/sbin/swapinfo -m", shell=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = cmd.communicate()
|
||||
lines = out.split('\n')
|
||||
if len(lines[-1]) == 0:
|
||||
lines.pop()
|
||||
data = lines[-1].split()
|
||||
self.facts['swaptotal_mb'] = data[1]
|
||||
self.facts['swapfree_mb'] = data[3]
|
||||
|
||||
def get_public_ssh_host_keys(facts):
|
||||
dsa = get_file_content('/etc/ssh/ssh_host_dsa_key.pub')
|
||||
rsa = get_file_content('/etc/ssh/ssh_host_rsa_key.pub')
|
||||
if dsa is None:
|
||||
dsa = 'NA'
|
||||
else:
|
||||
facts['ssh_host_key_dsa_public'] = dsa.split()[1]
|
||||
if rsa is None:
|
||||
rsa = 'NA'
|
||||
else:
|
||||
facts['ssh_host_key_rsa_public'] = rsa.split()[1]
|
||||
class Network(Facts):
|
||||
"""
|
||||
This is a generic Network subclass of Facts. This should be further
|
||||
subclassed to implement per platform. If you subclass this,
|
||||
you must define:
|
||||
- interfaces (a list of interface names)
|
||||
- interface_<name> dictionary of ipv4, ipv6, and mac address information.
|
||||
|
||||
def get_selinux_facts(facts):
|
||||
if not HAVE_SELINUX:
|
||||
facts['selinux'] = False
|
||||
return
|
||||
facts['selinux'] = {}
|
||||
if not selinux.is_selinux_enabled():
|
||||
facts['selinux']['status'] = 'disabled'
|
||||
else:
|
||||
facts['selinux']['status'] = 'enabled'
|
||||
facts['selinux']['policyvers'] = selinux.security_policyvers()
|
||||
(rc, configmode) = selinux.selinux_getenforcemode()
|
||||
if rc == 0 and SELINUX_MODE_DICT.has_key(configmode):
|
||||
facts['selinux']['config_mode'] = SELINUX_MODE_DICT[configmode]
|
||||
mode = selinux.security_getenforce()
|
||||
if SELINUX_MODE_DICT.has_key(mode):
|
||||
facts['selinux']['mode'] = SELINUX_MODE_DICT[mode]
|
||||
(rc, policytype) = selinux.selinux_getpolicytype()
|
||||
if rc == 0:
|
||||
facts['selinux']['type'] = policytype
|
||||
All subclasses MUST define platform.
|
||||
"""
|
||||
platform = 'Generic'
|
||||
|
||||
def get_service_facts(facts):
|
||||
get_public_ssh_host_keys(facts)
|
||||
get_selinux_facts(facts)
|
||||
IPV6_SCOPE = { '0' : 'global',
|
||||
'10' : 'host',
|
||||
'20' : 'link',
|
||||
'40' : 'admin',
|
||||
'50' : 'site',
|
||||
'80' : 'organization' }
|
||||
|
||||
def __new__(cls, *arguments, **keyword):
|
||||
subclass = cls
|
||||
for sc in Network.__subclasses__():
|
||||
if sc.platform == platform.system():
|
||||
subclass = sc
|
||||
return super(cls, subclass).__new__(subclass, *arguments, **keyword)
|
||||
|
||||
def __init__(self):
|
||||
Facts.__init__(self)
|
||||
|
||||
def populate(self):
|
||||
return self.facts
|
||||
|
||||
class LinuxNetwork(Network):
|
||||
"""
|
||||
This is a Linux-specific subclass of Network. It defines
|
||||
- interfaces (a list of interface names)
|
||||
- interface_<name> dictionary of ipv4, ipv6, and mac address information.
|
||||
"""
|
||||
platform = 'Linux'
|
||||
|
||||
def __init__(self):
|
||||
Network.__init__(self)
|
||||
|
||||
def populate(self):
|
||||
self.facts['interfaces'] = self.get_interfaces()
|
||||
self.get_interface_facts()
|
||||
self.get_ipv4_facts()
|
||||
self.get_ipv6_facts()
|
||||
return self.facts
|
||||
|
||||
# get list of interfaces
|
||||
def get_interfaces(self):
|
||||
names = []
|
||||
data = get_file_content('/proc/net/dev')
|
||||
# Format of /proc/net/dev is:
|
||||
# Inter-| Receive ...
|
||||
# face |bytes ...
|
||||
# lo: 595059
|
||||
for line in data.split('\n'):
|
||||
if ':' in line:
|
||||
names.append(line.split(':')[0].strip())
|
||||
return names
|
||||
|
||||
def get_iface_hwaddr(self, iface):
|
||||
data = get_file_content('/sys/class/net/%s/address' % iface)
|
||||
if data is None:
|
||||
return 'unknown'
|
||||
else:
|
||||
return data.strip()
|
||||
|
||||
def get_interface_facts(self):
|
||||
for iface in self.facts['interfaces']:
|
||||
if iface not in self.facts:
|
||||
self.facts[iface] = {}
|
||||
self.facts[iface] = { 'macaddress': self.get_iface_hwaddr(iface) }
|
||||
if os.path.exists('/sys/class/net/%s/mtu' % iface):
|
||||
mtu = get_file_content('/sys/class/net/%s/mtu' % iface)
|
||||
self.facts[iface]['mtu'] = mtu.strip()
|
||||
|
||||
def get_ipv4_facts(self):
|
||||
for iface in self.facts['interfaces']:
|
||||
# This is lame, but there doesn't appear to be a good way
|
||||
# to get all addresses for both IPv4 and IPv6.
|
||||
cmd = subprocess.Popen("env LANG=\"\" /sbin/ifconfig %s" % iface, shell=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = cmd.communicate()
|
||||
for line in out.split('\n'):
|
||||
is_ipv4 = False
|
||||
data = line.split()
|
||||
if 'inet addr' in line:
|
||||
if 'ipv4' not in self.facts[iface]:
|
||||
self.facts[iface]['ipv4'] = {}
|
||||
is_ipv4 = True
|
||||
self.facts[iface]['ipv4'] = { 'address': data[1].split(':')[1],
|
||||
'netmask': data[-1].split(':')[1] }
|
||||
# Slightly different output in net-tools-1.60-134.20120127git
|
||||
# Looks like
|
||||
# inet 192.168.1.2 netmask 255.255.255.0 broadcast 192.168.1.255
|
||||
elif 'inet ' in line:
|
||||
is_ipv4 = True
|
||||
if 'ipv4' not in self.facts[iface]:
|
||||
self.facts[iface]['ipv4'] = {}
|
||||
self.facts[iface]['ipv4'] = { 'address': data[1],
|
||||
'netmask': data[3] }
|
||||
if is_ipv4:
|
||||
ip = struct.unpack("!L", socket.inet_aton(self.facts[iface]['ipv4']['address']))[0]
|
||||
mask = struct.unpack("!L", socket.inet_aton(self.facts[iface]['ipv4']['netmask']))[0]
|
||||
self.facts[iface]['ipv4']['network'] = socket.inet_ntoa(struct.pack("!L", ip & mask))
|
||||
|
||||
def get_ipv6_facts(self):
|
||||
if not socket.has_ipv6:
|
||||
return
|
||||
data = get_file_content('/proc/net/if_inet6')
|
||||
if data is None:
|
||||
return
|
||||
for line in data.split('\n'):
|
||||
l = line.split()
|
||||
iface = l[5]
|
||||
if 'ipv6' not in self.facts[iface]:
|
||||
self.facts[iface]['ipv6'] = []
|
||||
scope = l[3]
|
||||
if Network.IPV6_SCOPE.has_key(l[3]):
|
||||
scope = Network.IPV6_SCOPE[l[3]]
|
||||
prefix = int(l[2], 16)
|
||||
str_addr = ':'.join( [ l[0][i:i+4] for i in range(0, len(l[0]), 4) ] )
|
||||
# Normalize ipv6 address from format in /proc/net/if_inet6
|
||||
addr = socket.inet_ntop(socket.AF_INET6,
|
||||
socket.inet_pton(socket.AF_INET6, str_addr))
|
||||
self.facts[iface]['ipv6'].append( { 'address': addr,
|
||||
'prefix': prefix,
|
||||
'scope': scope } )
|
||||
|
||||
class Virtual(Facts):
|
||||
"""
|
||||
This is a generic Virtual subclass of Facts. This should be further
|
||||
subclassed to implement per platform. If you subclass this,
|
||||
you should define:
|
||||
- virtualization_type
|
||||
- virtualization_role
|
||||
|
||||
All subclasses MUST define platform.
|
||||
"""
|
||||
|
||||
def __new__(cls, *arguments, **keyword):
|
||||
subclass = cls
|
||||
for sc in Virtual.__subclasses__():
|
||||
if sc.platform == platform.system():
|
||||
subclass = sc
|
||||
return super(cls, subclass).__new__(subclass, *arguments, **keyword)
|
||||
|
||||
def __init__(self):
|
||||
Facts.__init__(self)
|
||||
|
||||
def populate(self):
|
||||
return self.facts
|
||||
|
||||
class LinuxVirtual(Virtual):
|
||||
"""
|
||||
This is a Linux-specific subclass of Virtual. It defines
|
||||
- virtualization_type
|
||||
- virtualization_role
|
||||
"""
|
||||
platform = 'Linux'
|
||||
|
||||
def __init__(self):
|
||||
Virtual.__init__(self)
|
||||
|
||||
def populate(self):
|
||||
self.get_virtual_facts()
|
||||
return self.facts
|
||||
|
||||
def get_virtual_facts(self):
|
||||
if os.path.exists("/proc/xen"):
|
||||
self.facts['virtualization_type'] = 'xen'
|
||||
self.facts['virtualization_role'] = 'guest'
|
||||
if os.path.exists("/proc/xen/capabilities"):
|
||||
self.facts['virtualization_role'] = 'host'
|
||||
if os.path.exists("/proc/modules"):
|
||||
modules = []
|
||||
for line in open("/proc/modules").readlines():
|
||||
data = line.split(" ", 1)
|
||||
modules.append(data[0])
|
||||
if 'kvm' in modules:
|
||||
self.facts['virtualization_type'] = 'kvm'
|
||||
self.facts['virtualization_role'] = 'host'
|
||||
elif 'vboxdrv' in modules:
|
||||
self.facts['virtualization_type'] = 'virtualbox'
|
||||
self.facts['virtualization_role'] = 'host'
|
||||
elif 'vboxguest' in modules:
|
||||
self.facts['virtualization_type'] = 'virtualbox'
|
||||
self.facts['virtualization_role'] = 'guest'
|
||||
data = get_file_content('/proc/cpuinfo')
|
||||
if 'QEMU' in data:
|
||||
self.facts['virtualization_type'] = 'kvm'
|
||||
self.facts['virtualization_role'] = 'guest'
|
||||
if 'distribution' in self.facts and self.facts['distribution'] == 'VMwareESX':
|
||||
self.facts['virtualization_type'] = 'VMware'
|
||||
self.facts['virtualization_role'] = 'host'
|
||||
# You can spawn a dmidecode process and parse that or infer from devices
|
||||
for dev_model in glob.glob('/sys/block/?da/device/vendor'):
|
||||
info = open(dev_model).read()
|
||||
if 'VMware' in info:
|
||||
self.facts['virtualization_type'] = 'VMware'
|
||||
self.facts['virtualization_role'] = 'guest'
|
||||
elif 'Virtual HD' in info or 'Virtual CD' in info:
|
||||
self.facts['virtualization_type'] = 'VirtualPC'
|
||||
self.facts['virtualization_role'] = 'guest'
|
||||
|
||||
def get_file_content(path):
|
||||
data = None
|
||||
if os.path.exists(path) and os.access(path, os.R_OK):
|
||||
data = open(path).read().strip()
|
||||
if len(data) == 0:
|
||||
data = None
|
||||
return data
|
||||
|
||||
def ansible_facts():
|
||||
facts = {}
|
||||
get_platform_facts(facts)
|
||||
get_hardware_facts(facts)
|
||||
get_virtual_facts(facts)
|
||||
get_network_facts(facts)
|
||||
get_service_facts(facts)
|
||||
facts.update(Facts().populate())
|
||||
facts.update(Hardware().populate())
|
||||
facts.update(Network().populate())
|
||||
facts.update(Virtual().populate())
|
||||
return facts
|
||||
|
||||
def md5_sum(f):
|
||||
md5sum = os.popen("/usr/bin/md5sum %(file)s 2>/dev/null || /sbin/md5 -q %(file)s 2>/dev/null || /usr/bin/digest -a md5 -v %(file)s 2>/dev/null" % {"file": f}).read().split()[0]
|
||||
return md5sum
|
||||
# ===========================================
|
||||
|
||||
# load config & template variables
|
||||
def run_setup(module):
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
sys.exit(1)
|
||||
setup_options = {}
|
||||
facts = ansible_facts()
|
||||
|
||||
for (k, v) in facts.items():
|
||||
setup_options["ansible_%s" % k] = v
|
||||
|
||||
# if facter is installed, and we can use --json because
|
||||
# ruby-json is ALSO installed, include facter data in the JSON
|
||||
|
||||
argfile = sys.argv[1]
|
||||
if not os.path.exists(argfile):
|
||||
sys.exit(1)
|
||||
if os.path.exists("/usr/bin/facter"):
|
||||
cmd = subprocess.Popen("/usr/bin/facter --json", shell=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = cmd.communicate()
|
||||
facter = True
|
||||
try:
|
||||
facter_ds = json.loads(out)
|
||||
except:
|
||||
facter = False
|
||||
if facter:
|
||||
for (k,v) in facter_ds.items():
|
||||
setup_options["facter_%s" % k] = v
|
||||
|
||||
setup_options = open(argfile).read().strip()
|
||||
try:
|
||||
setup_options = json.loads(setup_options)
|
||||
except:
|
||||
list_options = shlex.split(setup_options)
|
||||
setup_options = {}
|
||||
for opt in list_options:
|
||||
(k,v) = opt.split("=")
|
||||
setup_options[k]=v
|
||||
# ditto for ohai, but just top level string keys
|
||||
# because it contains a lot of nested stuff we can't use for
|
||||
# templating w/o making a nicer key for it (TODO)
|
||||
|
||||
syslog.openlog('ansible-%s' % os.path.basename(__file__))
|
||||
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % setup_options)
|
||||
if os.path.exists("/usr/bin/ohai"):
|
||||
cmd = subprocess.Popen("/usr/bin/ohai", shell=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = cmd.communicate()
|
||||
ohai = True
|
||||
try:
|
||||
ohai_ds = json.loads(out)
|
||||
except:
|
||||
ohai = False
|
||||
if ohai:
|
||||
for (k,v) in ohai_ds.items():
|
||||
if type(v) == str or type(v) == unicode:
|
||||
k2 = "ohai_%s" % k
|
||||
setup_options[k2] = v
|
||||
|
||||
ansible_file = os.path.expandvars(setup_options.get('metadata', DEFAULT_ANSIBLE_SETUP))
|
||||
ansible_dir = os.path.dirname(ansible_file)
|
||||
setup_result = {}
|
||||
setup_result['ansible_facts'] = setup_options
|
||||
|
||||
# create the config dir if it doesn't exist
|
||||
# hack to keep --verbose from showing all the setup module results
|
||||
setup_result['verbose_override'] = True
|
||||
|
||||
if not os.path.exists(ansible_dir):
|
||||
os.makedirs(ansible_dir)
|
||||
return setup_result
|
||||
|
||||
changed = False
|
||||
md5sum = None
|
||||
if not os.path.exists(ansible_file):
|
||||
changed = True
|
||||
else:
|
||||
md5sum = md5_sum(ansible_file)
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict()
|
||||
)
|
||||
data = run_setup(module)
|
||||
module.exit_json(**data)
|
||||
|
||||
# Get some basic facts in case facter or ohai are not installed
|
||||
for (k, v) in ansible_facts().items():
|
||||
setup_options["ansible_%s" % k] = v
|
||||
|
||||
# if facter is installed, and we can use --json because
|
||||
# ruby-json is ALSO installed, include facter data in the JSON
|
||||
|
||||
if os.path.exists("/usr/bin/facter"):
|
||||
cmd = subprocess.Popen("/usr/bin/facter --json", shell=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = cmd.communicate()
|
||||
facter = True
|
||||
try:
|
||||
facter_ds = json.loads(out)
|
||||
except:
|
||||
facter = False
|
||||
if facter:
|
||||
for (k,v) in facter_ds.items():
|
||||
setup_options["facter_%s" % k] = v
|
||||
|
||||
# ditto for ohai, but just top level string keys
|
||||
# because it contains a lot of nested stuff we can't use for
|
||||
# templating w/o making a nicer key for it (TODO)
|
||||
|
||||
if os.path.exists("/usr/bin/ohai"):
|
||||
cmd = subprocess.Popen("/usr/bin/ohai", shell=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = cmd.communicate()
|
||||
ohai = True
|
||||
try:
|
||||
ohai_ds = json.loads(out)
|
||||
except:
|
||||
ohai = False
|
||||
if ohai:
|
||||
for (k,v) in ohai_ds.items():
|
||||
if type(v) == str or type(v) == unicode:
|
||||
k2 = "ohai_%s" % k
|
||||
setup_options[k2] = v
|
||||
|
||||
# write the template/settings file using
|
||||
# instructions from server
|
||||
|
||||
f = open(ansible_file, "w+")
|
||||
reformat = json.dumps(setup_options, sort_keys=True, indent=4)
|
||||
f.write(reformat)
|
||||
f.close()
|
||||
|
||||
md5sum2 = md5_sum(ansible_file)
|
||||
|
||||
if md5sum != md5sum2:
|
||||
changed = True
|
||||
|
||||
setup_result = {}
|
||||
setup_result['written'] = ansible_file
|
||||
setup_result['changed'] = changed
|
||||
setup_result['md5sum'] = md5sum2
|
||||
setup_result['ansible_facts'] = setup_options
|
||||
|
||||
# hack to keep --verbose from showing all the setup module results
|
||||
setup_result['verbose_override'] = True
|
||||
|
||||
print json.dumps(setup_result)
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
main()
|
||||
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
# VIRTUAL
|
||||
|
||||
There is actually no actual shell module source, when you use 'shell' in ansible,
|
||||
it runs the 'command' module with special arguments and it behaves differently.
|
||||
See the command source and the comment "#USE_SHELL".
|
||||
|
||||
# There is actually no actual shell module source, when you use 'shell' in ansible,
|
||||
# it runs the 'command' module with special arguments and it behaves differently.
|
||||
# See the command source and the comment "#USE_SHELL".
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
@ -17,60 +18,25 @@
|
||||
# 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 shlex
|
||||
import base64
|
||||
import syslog
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
src = dict(required=True),
|
||||
)
|
||||
)
|
||||
source = module.params['src']
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
if not os.path.exists(source):
|
||||
module.fail_json(msg="file not found: %s" % source)
|
||||
if not os.access(source, os.R_OK):
|
||||
module.fail_json(msg="file is not readable: %s" % source)
|
||||
|
||||
# ===========================================
|
||||
# convert arguments of form a=b c=d
|
||||
# to a dictionary
|
||||
data = base64.b64encode(file(source).read())
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
sys.exit(1)
|
||||
argfile = sys.argv[1]
|
||||
if not os.path.exists(argfile):
|
||||
sys.exit(1)
|
||||
module.exit_json(content=data, encoding='base64')
|
||||
|
||||
args = open(argfile, 'r').read()
|
||||
items = shlex.split(args)
|
||||
syslog.openlog('ansible-%s' % os.path.basename(__file__))
|
||||
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
|
||||
params = {}
|
||||
for x in items:
|
||||
(k, v) = x.split("=")
|
||||
params[k] = v
|
||||
source = os.path.expanduser(params['src'])
|
||||
|
||||
# ==========================================
|
||||
|
||||
# raise an error if there is no template metadata
|
||||
if not os.path.exists(source):
|
||||
print json.dumps(dict(
|
||||
failed = 1,
|
||||
msg = "file not found: %s" % source
|
||||
))
|
||||
sys.exit(1)
|
||||
|
||||
if not os.access(source, os.R_OK):
|
||||
print json.dumps(dict(
|
||||
failed = 1,
|
||||
msg = "file is not readable: %s" % source
|
||||
))
|
||||
sys.exit(1)
|
||||
|
||||
# ==========================================
|
||||
|
||||
data = file(source).read()
|
||||
data = base64.b64encode(data)
|
||||
|
||||
print json.dumps(dict(content=data, encoding='base64'))
|
||||
sys.exit(0)
|
||||
main()
|
||||
|
||||
|
||||
@ -1,24 +1 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# (c) 2012, Michael DeHaan <michael.dehaan@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/>.
|
||||
|
||||
# hey the Ansible template module isn't really a remote transferred
|
||||
# module. All the magic happens in Runner.py making use of the
|
||||
# copy module, and if not running from a playbook, also the 'slurp'
|
||||
# module.
|
||||
|
||||
# this is a virtual module that is entirely implemented server side
|
||||
|
||||
282
library/user
282
library/user
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Stephen Fromm <sfromm@gmail.com>
|
||||
#
|
||||
@ -17,84 +18,50 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
import os
|
||||
import pwd
|
||||
import grp
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import syslog
|
||||
try:
|
||||
import spwd
|
||||
HAVE_SPWD=True
|
||||
except:
|
||||
HAVE_SPWD=False
|
||||
|
||||
USERADD = "/usr/sbin/useradd"
|
||||
USERMOD = "/usr/sbin/usermod"
|
||||
USERDEL = "/usr/sbin/userdel"
|
||||
|
||||
def exit_json(rc=0, **kwargs):
|
||||
if 'name' in kwargs:
|
||||
add_user_info(kwargs)
|
||||
print json.dumps(kwargs)
|
||||
sys.exit(rc)
|
||||
|
||||
def fail_json(**kwargs):
|
||||
kwargs['failed'] = True
|
||||
exit_json(rc=1, **kwargs)
|
||||
|
||||
def add_user_info(kwargs):
|
||||
name = kwargs['name']
|
||||
if user_exists(name):
|
||||
kwargs['state'] = 'present'
|
||||
info = user_info(name)
|
||||
kwargs['uid'] = info[2]
|
||||
kwargs['group'] = info[3]
|
||||
kwargs['comment'] = info[4]
|
||||
kwargs['home'] = info[5]
|
||||
kwargs['shell'] = info[6]
|
||||
kwargs['createhome'] = os.path.exists(info[5])
|
||||
groups = user_group_membership(name)
|
||||
if len(groups) > 0:
|
||||
kwargs['groups'] = groups
|
||||
def get_bin_path(module, arg):
|
||||
if os.path.exists('/usr/sbin/%s' % arg):
|
||||
return '/usr/sbin/%s' % arg
|
||||
elif os.path.exists('/sbin/%s' % arg):
|
||||
return '/sbin/%s' % arg
|
||||
else:
|
||||
kwargs['state'] = 'absent'
|
||||
return kwargs
|
||||
module.fail_json(msg="Cannot find %s" % arg)
|
||||
|
||||
def user_del(user, **kwargs):
|
||||
cmd = [USERDEL]
|
||||
def user_del(module, user, **kwargs):
|
||||
cmd = [get_bin_path(module, 'userdel')]
|
||||
for key in kwargs:
|
||||
if key == 'force' and kwargs[key]:
|
||||
if key == 'force' and kwargs[key] == 'yes':
|
||||
cmd.append('-f')
|
||||
elif key == 'remove' and kwargs[key]:
|
||||
elif key == 'remove' and kwargs[key] == 'yes':
|
||||
cmd.append('-r')
|
||||
cmd.append(user)
|
||||
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if rc == 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(out, err) = p.communicate()
|
||||
rc = p.returncode
|
||||
return (rc, out, err)
|
||||
|
||||
def user_add(user, **kwargs):
|
||||
cmd = [USERADD]
|
||||
def user_add(module, user, **kwargs):
|
||||
cmd = [get_bin_path(module, 'useradd')]
|
||||
for key in kwargs:
|
||||
if key == 'uid' and kwargs[key] is not None:
|
||||
cmd.append('-u')
|
||||
cmd.append(kwargs[key])
|
||||
elif key == 'group' and kwargs[key] is not None:
|
||||
if not group_exists(kwargs[key]):
|
||||
fail_json(msg="Group %s does not exist" % (kwargs[key]))
|
||||
module.fail_json(msg="Group %s does not exist" % (kwargs[key]))
|
||||
cmd.append('-g')
|
||||
cmd.append(kwargs[key])
|
||||
elif key == 'groups' and kwargs[key] is not None:
|
||||
for g in kwargs[key].split(','):
|
||||
if not group_exists(g):
|
||||
fail_json(msg="Group %s does not exist" % (g))
|
||||
module.fail_json(msg="Group %s does not exist" % (g))
|
||||
cmd.append('-G')
|
||||
cmd.append(kwargs[key])
|
||||
elif key == 'comment' and kwargs[key] is not None:
|
||||
@ -118,18 +85,17 @@ def user_add(user, **kwargs):
|
||||
elif key == 'system' and kwargs[key] == 'yes':
|
||||
cmd.append('-r')
|
||||
cmd.append(user)
|
||||
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if rc == 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(out, err) = p.communicate()
|
||||
rc = p.returncode
|
||||
return (rc, out, err)
|
||||
|
||||
"""
|
||||
Without spwd, we would have to resort to reading /etc/shadow
|
||||
to get the encrypted string. For now, punt on idempotent password changes.
|
||||
"""
|
||||
def user_mod(user, **kwargs):
|
||||
cmd = [USERMOD]
|
||||
def user_mod(module, user, **kwargs):
|
||||
cmd = [get_bin_path(module, 'usermod')]
|
||||
info = user_info(user)
|
||||
for key in kwargs:
|
||||
if key == 'uid':
|
||||
@ -138,8 +104,8 @@ def user_mod(user, **kwargs):
|
||||
cmd.append(kwargs[key])
|
||||
elif key == 'group' and kwargs[key] is not None:
|
||||
if not group_exists(kwargs[key]):
|
||||
fail_json(msg="Group %s does not exist" % (kwargs[key]))
|
||||
ginfo = group_info(group)
|
||||
module.fail_json(msg="Group %s does not exist" % (kwargs[key]))
|
||||
ginfo = group_info(kwargs[key])
|
||||
if info[3] != ginfo[2]:
|
||||
cmd.append('-g')
|
||||
cmd.append(kwargs[key])
|
||||
@ -148,7 +114,7 @@ def user_mod(user, **kwargs):
|
||||
groups = kwargs[key].split(',')
|
||||
for g in groups:
|
||||
if not group_exists(g):
|
||||
fail_json(msg="Group %s does not exist" % (g))
|
||||
module.fail_json(msg="Group %s does not exist" % (g))
|
||||
group_diff = set(sorted(current_groups)).symmetric_difference(set(sorted(groups)))
|
||||
groups_need_mod = False
|
||||
|
||||
@ -183,13 +149,12 @@ def user_mod(user, **kwargs):
|
||||
cmd.append(kwargs[key])
|
||||
# skip if no changes to be made
|
||||
if len(cmd) == 1:
|
||||
return False
|
||||
return (None, '', '')
|
||||
cmd.append(user)
|
||||
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if rc == 0:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(out, err) = p.communicate()
|
||||
rc = p.returncode
|
||||
return (rc, out, err)
|
||||
|
||||
def group_exists(group):
|
||||
try:
|
||||
@ -245,93 +210,102 @@ def user_info(user):
|
||||
|
||||
# ===========================================
|
||||
|
||||
if not os.path.exists(USERADD):
|
||||
if os.path.exists("/sbin/useradd"):
|
||||
USERADD = "/sbin/useradd"
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
name=dict(required=True),
|
||||
uid=dict(default=None),
|
||||
group=dict(default=None),
|
||||
groups=dict(default=None),
|
||||
comment=dict(default=None),
|
||||
home=dict(default=None),
|
||||
shell=dict(default=None),
|
||||
password=dict(default=None),
|
||||
# following options are specific to userdel
|
||||
force=dict(default='no', choices=['yes', 'no']),
|
||||
remove=dict(default='no', choices=['yes', 'no']),
|
||||
# following options are specific to useradd
|
||||
createhome=dict(default='yes', choices=['yes', 'no']),
|
||||
system=dict(default='no', choices=['yes', 'no']),
|
||||
# following options are specific to usermod
|
||||
append=dict(default='no', choices=['yes', 'no']),
|
||||
)
|
||||
)
|
||||
|
||||
state = module.params['state']
|
||||
name = module.params['name']
|
||||
uid = module.params['uid']
|
||||
group = module.params['group']
|
||||
groups = module.params['groups']
|
||||
comment = module.params['comment']
|
||||
home = module.params['home']
|
||||
shell = module.params['shell']
|
||||
password = module.params['password']
|
||||
force = module.params['force']
|
||||
remove = module.params['remove']
|
||||
createhome = module.params['createhome']
|
||||
system = module.params['system']
|
||||
append = module.params['append']
|
||||
|
||||
rc = None
|
||||
out = ''
|
||||
err = ''
|
||||
result = {}
|
||||
result['name'] = name
|
||||
result['state'] = state
|
||||
if state == 'absent':
|
||||
if user_exists(name):
|
||||
(rc, out, err) = user_del(module, name, force=force, remove=remove)
|
||||
if rc != 0:
|
||||
module.fail_json(name=name, msg=err, rc=rc)
|
||||
result['force'] = force
|
||||
result['remove'] = remove
|
||||
elif state == 'present':
|
||||
if not user_exists(name):
|
||||
(rc, out, err) = user_add(module,
|
||||
name, uid=uid, group=group, groups=groups,
|
||||
comment=comment, home=home, shell=shell,
|
||||
password=password, createhome=createhome,
|
||||
system=system)
|
||||
result['system'] = system
|
||||
result['createhome'] = createhome
|
||||
else:
|
||||
(rc, out, err) = user_mod(module,
|
||||
name, uid=uid, group=group, groups=groups,
|
||||
comment=comment, home=home, shell=shell,
|
||||
password=password, append=append)
|
||||
result['append'] = append
|
||||
if rc is not None and rc != 0:
|
||||
module.fail_json(name=name, msg=err, rc=rc)
|
||||
if password is not None:
|
||||
result['password'] = 'NOT_LOGGING_PASSWORD'
|
||||
|
||||
if rc is None:
|
||||
result['changed'] = False
|
||||
else:
|
||||
fail_json(msg="Cannot find useradd")
|
||||
if not os.path.exists(USERMOD):
|
||||
if os.path.exists("/sbin/usermod"):
|
||||
USERMOD = "/sbin/usermod"
|
||||
else:
|
||||
fail_json(msg="Cannot find usermod")
|
||||
if not os.path.exists(USERDEL):
|
||||
if os.path.exists("/sbin/userdel"):
|
||||
USERDEL = "/sbin/userdel"
|
||||
else:
|
||||
fail_json(msg="Cannot find userdel")
|
||||
|
||||
argfile = sys.argv[1]
|
||||
args = open(argfile, 'r').read()
|
||||
items = shlex.split(args)
|
||||
syslog.openlog('ansible-%s' % os.path.basename(__file__))
|
||||
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
|
||||
|
||||
if not len(items):
|
||||
fail_json(msg='the module requires arguments -a')
|
||||
sys.exit(1)
|
||||
|
||||
params = {}
|
||||
for x in items:
|
||||
(k, v) = x.split("=")
|
||||
params[k] = v
|
||||
|
||||
state = params.get('state','present')
|
||||
name = params.get('name', None)
|
||||
uid = params.get('uid', None)
|
||||
group = params.get('group', None)
|
||||
groups = params.get('groups', None)
|
||||
comment = params.get('comment', None)
|
||||
home = params.get('home', None)
|
||||
shell = params.get('shell', None)
|
||||
password = params.get('password', None)
|
||||
|
||||
# ===========================================
|
||||
# following options are specific to userdel
|
||||
force = params.get('force', False)
|
||||
remove = params.get('remove', False)
|
||||
|
||||
# ===========================================
|
||||
# following options are specific to useradd
|
||||
createhome = params.get('createhome', 'yes')
|
||||
system = params.get('system', 'no')
|
||||
|
||||
# ===========================================
|
||||
# following options are specific to usermod
|
||||
append = params.get('append', 'no')
|
||||
|
||||
if state not in [ 'present', 'absent' ]:
|
||||
fail_json(msg='invalid state')
|
||||
if createhome not in [ 'yes', 'no' ]:
|
||||
fail_json(msg='invalid createhome')
|
||||
if system not in ['yes', 'no']:
|
||||
fail_json(msg='invalid system')
|
||||
if append not in [ 'yes', 'no' ]:
|
||||
fail_json(msg='invalid append')
|
||||
if name is None:
|
||||
fail_json(msg='name is required')
|
||||
|
||||
changed = False
|
||||
rc = 0
|
||||
if state == 'absent':
|
||||
result['changed'] = True
|
||||
if out:
|
||||
result['stdout'] = out
|
||||
if err:
|
||||
result['stderr'] = err
|
||||
if user_exists(name):
|
||||
changed = user_del(name, force=force, remove=remove)
|
||||
exit_json(name=name, changed=changed, force=force, remove=remove)
|
||||
elif state == 'present':
|
||||
if not user_exists(name):
|
||||
changed = user_add(name, uid=uid, group=group, groups=groups,
|
||||
comment=comment, home=home, shell=shell,
|
||||
password=password, createhome=createhome,
|
||||
system=system)
|
||||
else:
|
||||
changed = user_mod(name, uid=uid, group=group, groups=groups,
|
||||
comment=comment, home=home, shell=shell,
|
||||
password=password, append=append)
|
||||
info = user_info(name)
|
||||
if info == False:
|
||||
result['msg'] = "failed to look up user name: %s" % name
|
||||
result['failed'] = True
|
||||
result['uid'] = info[2]
|
||||
result['group'] = info[3]
|
||||
result['comment'] = info[4]
|
||||
result['home'] = info[5]
|
||||
result['shell'] = info[6]
|
||||
groups = user_group_membership(name)
|
||||
result['uid'] = info[2]
|
||||
if len(groups) > 0:
|
||||
result['groups'] = groups
|
||||
|
||||
if password is not None:
|
||||
exit_json(name=name, changed=changed, password="XXXXXXXX")
|
||||
else:
|
||||
exit_json(name=name, changed=changed)
|
||||
module.exit_json(**result)
|
||||
|
||||
fail_json(name=name, msg='Unexpected position reached')
|
||||
sys.exit(0)
|
||||
# include magic from lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
main()
|
||||
|
||||
180
library/virt
180
library/virt
@ -1,9 +1,11 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Virt management features
|
||||
|
||||
Copyright 2007, 2012 Red Hat, Inc
|
||||
Michael DeHaan <mdehaan@redhat.com>
|
||||
Michael DeHaan <michael.dehaan@gmail.com>
|
||||
Seth Vidal <skvidal@fedoraproject.org>
|
||||
|
||||
This software may be freely redistributed under the terms of the GNU
|
||||
@ -17,29 +19,18 @@ VIRT_FAILED = 1
|
||||
VIRT_SUCCESS = 0
|
||||
VIRT_UNAVAILABLE=2
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
|
||||
# other modules
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import syslog
|
||||
try:
|
||||
import libvirt
|
||||
except ImportError:
|
||||
print json.dumps({
|
||||
"failed" : True,
|
||||
"msg" : "libvirt python module unavailable",
|
||||
"rc": VIRT_UNAVAILABLE,
|
||||
})
|
||||
print "failed=True msg='libvirt python module unavailable'"
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
import shlex
|
||||
|
||||
ALL_COMMANDS = []
|
||||
VM_COMMANDS = ['create','status', 'start', 'stop', 'pause', 'unpause',
|
||||
'shutdown', 'undefine', 'destroy', 'get_xml', 'autostart']
|
||||
HOST_COMMANDS = ['freemem', 'list_vms', 'info', 'nodeinfo', 'virttype']
|
||||
ALL_COMMANDS.extend(VM_COMMANDS)
|
||||
ALL_COMMANDS.extend(HOST_COMMANDS)
|
||||
|
||||
VIRT_STATE_NAME_MAP = {
|
||||
0 : "running",
|
||||
@ -175,7 +166,6 @@ class Virt(object):
|
||||
state.append("%s %s" % (vm,state_blurb))
|
||||
return state
|
||||
|
||||
|
||||
def info(self):
|
||||
vms = self.list_vms()
|
||||
info = dict()
|
||||
@ -227,7 +217,6 @@ class Virt(object):
|
||||
def virttype(self):
|
||||
return self.__get_conn().get_type()
|
||||
|
||||
|
||||
def autostart(self, vmid):
|
||||
self.conn = self.__get_conn()
|
||||
return self.conn.set_autostart(vmid, True)
|
||||
@ -237,87 +226,60 @@ class Virt(object):
|
||||
return self.conn.getFreeMemory()
|
||||
|
||||
def shutdown(self, vmid):
|
||||
"""
|
||||
Make the machine with the given vmid stop running.
|
||||
Whatever that takes.
|
||||
"""
|
||||
""" Make the machine with the given vmid stop running. Whatever that takes. """
|
||||
self.__get_conn()
|
||||
self.conn.shutdown(vmid)
|
||||
return 0
|
||||
|
||||
|
||||
def pause(self, vmid):
|
||||
""" Pause the machine with the given vmid. """
|
||||
|
||||
"""
|
||||
Pause the machine with the given vmid.
|
||||
"""
|
||||
self.__get_conn()
|
||||
return self.conn.suspend(vmid)
|
||||
|
||||
|
||||
def unpause(self, vmid):
|
||||
|
||||
"""
|
||||
Unpause the machine with the given vmid.
|
||||
"""
|
||||
""" Unpause the machine with the given vmid. """
|
||||
|
||||
self.__get_conn()
|
||||
return self.conn.resume(vmid)
|
||||
|
||||
|
||||
def create(self, vmid):
|
||||
""" Start the machine via the given vmid """
|
||||
|
||||
"""
|
||||
Start the machine via the given mac address.
|
||||
"""
|
||||
self.__get_conn()
|
||||
return self.conn.create(vmid)
|
||||
|
||||
def start(self, vmid):
|
||||
""" Start the machine via the given id/name """
|
||||
|
||||
"""
|
||||
Start the machine via the given id/name
|
||||
"""
|
||||
self.__get_conn()
|
||||
return self.conn.create(vmid)
|
||||
|
||||
|
||||
def destroy(self, vmid):
|
||||
|
||||
"""
|
||||
Pull the virtual power from the virtual domain, giving it virtually no
|
||||
time to virtually shut down.
|
||||
"""
|
||||
""" Pull the virtual power from the virtual domain, giving it virtually no time to virtually shut down. """
|
||||
self.__get_conn()
|
||||
return self.conn.destroy(vmid)
|
||||
|
||||
|
||||
def undefine(self, vmid):
|
||||
|
||||
"""
|
||||
Stop a domain, and then wipe it from the face of the earth.
|
||||
by deleting the disk image and its configuration file.
|
||||
"""
|
||||
""" Stop a domain, and then wipe it from the face of the earth. (delete disk/config file) """
|
||||
|
||||
self.__get_conn()
|
||||
return self.conn.undefine(vmid)
|
||||
|
||||
|
||||
def status(self, vmid):
|
||||
|
||||
"""
|
||||
Return a state suitable for server consumption. Aka, codes.py values, not XM output.
|
||||
"""
|
||||
|
||||
self.__get_conn()
|
||||
return self.conn.get_status(vmid)
|
||||
|
||||
|
||||
def get_xml(self, vmid):
|
||||
"""
|
||||
Receive a Vm id as input
|
||||
Return an xml describing vm config returned by a libvirt call
|
||||
"""
|
||||
|
||||
conn = libvirt.openReadOnly(None)
|
||||
if conn == None:
|
||||
return (-1,'Failed to open connection to the hypervisor')
|
||||
@ -327,7 +289,6 @@ class Virt(object):
|
||||
return (-1,'Failed to find the main domain')
|
||||
return domV.XMLDesc(0)
|
||||
|
||||
|
||||
def get_maxVcpus(self, vmid):
|
||||
"""
|
||||
Gets the max number of VCPUs on a guest
|
||||
@ -344,59 +305,19 @@ class Virt(object):
|
||||
self.__get_conn()
|
||||
return self.conn.get_MaxMemory(vmid)
|
||||
|
||||
def main():
|
||||
rc = VIRT_SUCCESS
|
||||
vm_commands = ['create','status', 'start', 'stop', 'pause', 'unpause',
|
||||
'shutdown', 'undefine', 'destroy', 'get_xml', 'autostart']
|
||||
host_commands = ['freemem', 'list_vms', 'info', 'nodeinfo', 'virttype']
|
||||
|
||||
msg = """
|
||||
virtmodule arguments:
|
||||
state=[running|shutdown] guest=guestname
|
||||
command=some_virt_command [guest=guestname]
|
||||
"""
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
return VIRT_FAILED, msg
|
||||
def core(module):
|
||||
|
||||
argfile = sys.argv[1]
|
||||
if not os.path.exists(argfile):
|
||||
msg = "Argument file not found"
|
||||
return VIRT_FAILED, msg
|
||||
|
||||
args = open(argfile, 'r').read()
|
||||
items = shlex.split(args)
|
||||
syslog.openlog('ansible-%s' % os.path.basename(__file__))
|
||||
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
|
||||
|
||||
if not len(items):
|
||||
return VIRT_FAILED, msg
|
||||
|
||||
|
||||
# guest=name state=[running|shutdown|destroyed|undefined]
|
||||
# command=[some command] [guest=name]
|
||||
|
||||
params = {}
|
||||
if '=' not in args:
|
||||
msg = "No proper arguments provided to virt module: %s" % args
|
||||
return VIRT_FAILED, msg
|
||||
|
||||
for x in items:
|
||||
(k, v) = x.split("=")
|
||||
params[k] = v
|
||||
|
||||
state = params.get('state', None)
|
||||
guest = params.get('guest', None)
|
||||
command = params.get('command', None)
|
||||
options = params.get('options', [])
|
||||
state = module.params.get('state', None)
|
||||
guest = module.params.get('name', None)
|
||||
command = module.params.get('command', None)
|
||||
|
||||
v = Virt()
|
||||
res = {}
|
||||
|
||||
|
||||
if state:
|
||||
if not guest:
|
||||
msg = "state change requires a guest specified"
|
||||
return VIRT_FAILED, msg
|
||||
module.fail_json(msg = "state change requires a guest specified")
|
||||
|
||||
res['changed'] = False
|
||||
if state == 'running':
|
||||
@ -407,46 +328,51 @@ def main():
|
||||
if v.status(guest) is not 'shutdown':
|
||||
res['changed'] = True
|
||||
res['msg'] = v.shutdown(guest)
|
||||
|
||||
else:
|
||||
module.fail_json(msg="unexpected state")
|
||||
|
||||
return VIRT_SUCCESS, res
|
||||
|
||||
if command:
|
||||
if command in vm_commands:
|
||||
if command in VM_COMMANDS:
|
||||
if not guest:
|
||||
msg = "%s requires 1 argument: guest" % command
|
||||
return VIRT_FAILED, msg
|
||||
|
||||
module.fail_json(msg = "%s requires 1 argument: guest" % command)
|
||||
res = getattr(v, command)(guest)
|
||||
if type(res) != dict:
|
||||
res = { command: res }
|
||||
return rc, res
|
||||
return VIRT_SUCCESS, res
|
||||
|
||||
elif hasattr(v, command):
|
||||
res = getattr(v, command)()
|
||||
if type(res) != dict:
|
||||
res = { command: res }
|
||||
return rc, res
|
||||
return VIRT_SUCCESS, res
|
||||
|
||||
else:
|
||||
msg = "Command %s not recognized" % basecmd
|
||||
rc = VIRT_FAILED
|
||||
|
||||
return rc, msg
|
||||
module.fail_json(msg="Command %s not recognized" % basecmd)
|
||||
|
||||
module.fail_json(msg="expected state or command parameter to be specified")
|
||||
|
||||
if __name__ == "__main__":
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(argument_spec=dict(
|
||||
name = dict(aliases=['guest']),
|
||||
state = dict(choices=['running', 'shutdown']),
|
||||
command = dict(choices=ALL_COMMANDS),
|
||||
))
|
||||
|
||||
rc = VIRT_SUCCESS
|
||||
try:
|
||||
rc, result = main()
|
||||
rc, result = core(module)
|
||||
except Exception, e:
|
||||
rc = 1
|
||||
result = str(e)
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
if rc != 0: # something went wrong emit the msg
|
||||
print json.dumps({
|
||||
"failed" : rc,
|
||||
"msg" : result,
|
||||
"rc": rc,
|
||||
})
|
||||
sys.exit(rc)
|
||||
module.fail_json(rc=rc, msg=result)
|
||||
else:
|
||||
print json.dumps(result)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
main()
|
||||
|
||||
546
library/yum
546
library/yum
@ -1,4 +1,6 @@
|
||||
#!/usr/bin/python -tt
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2012, Red Hat, Inc
|
||||
# Written by Seth Vidal <skvidal at fedoraproject.org>
|
||||
#
|
||||
@ -19,95 +21,110 @@
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
import sys
|
||||
import yum
|
||||
import subprocess
|
||||
import datetime
|
||||
import shlex
|
||||
import re
|
||||
import traceback
|
||||
import syslog
|
||||
import os
|
||||
|
||||
def_qf = "%{name}-%{version}-%{release}.%{arch}"
|
||||
repoquery='/usr/bin/repoquery'
|
||||
yumbin='/usr/bin/yum'
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
def is_installed(repoq, pkgspec, qf=def_qf):
|
||||
cmd = repoq + "--disablerepo=\* --pkgnarrow=installed --qf '%s' %s " % (qf, pkgspec)
|
||||
rc,out,err = run(cmd)
|
||||
if rc == 0:
|
||||
return [ p for p in out.split('\n') if p.strip() ]
|
||||
|
||||
return []
|
||||
|
||||
def yum_base(conf_file=None, cachedir=False):
|
||||
my = yum.YumBase()
|
||||
my.preconf.debuglevel=0
|
||||
if conf_file and os.path.exists(conf_file):
|
||||
my.preconf.fn = conf_file
|
||||
if cachedir:
|
||||
if hasattr(my, 'setCacheDir'):
|
||||
my.setCacheDir()
|
||||
else:
|
||||
cachedir = yum.misc.getCacheDir()
|
||||
my.repos.setCacheDir(cachedir)
|
||||
my.conf.cache = 0
|
||||
def is_available(repoq, pkgspec, qf=def_qf):
|
||||
cmd = repoq + "--qf '%s' %s " % (qf, pkgspec)
|
||||
rc,out,err = run(cmd)
|
||||
if rc == 0:
|
||||
return [ p for p in out.split('\n') if p.strip() ]
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def is_update(repoq, pkgspec, qf=def_qf):
|
||||
cmd = repoq + "--pkgnarrow=updates --qf '%s' %s " % (qf, pkgspec)
|
||||
rc,out,err = run(cmd)
|
||||
if rc == 0:
|
||||
return set([ p for p in out.split('\n') if p.strip() ])
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def what_provides(repoq, req_spec, qf=def_qf):
|
||||
cmd = repoq + "--qf '%s' --whatprovides %s" % (qf, req_spec)
|
||||
rc,out,err = run(cmd)
|
||||
ret = []
|
||||
if rc == 0:
|
||||
ret = set([ p for p in out.split('\n') if p.strip() ])
|
||||
|
||||
return my
|
||||
return ret
|
||||
|
||||
def pkg_to_dict(po):
|
||||
def local_nvra(path):
|
||||
"""return nvra of a local rpm passed in"""
|
||||
|
||||
cmd = "/bin/rpm -qp --qf='%%{name}-%%{version}-%%{release}.%%{arch}\n' %s'" % path
|
||||
rc, out, err = run(cmd)
|
||||
if rc != 0:
|
||||
return None
|
||||
nvra = out.split('\n')[0]
|
||||
return nvra
|
||||
|
||||
|
||||
def pkg_to_dict(pkgstr):
|
||||
if pkgstr.strip():
|
||||
n,e,v,r,a,repo = pkgstr.split('|')
|
||||
else:
|
||||
return {'error_parsing': pkgstr}
|
||||
|
||||
d = {
|
||||
'name':po.name,
|
||||
'arch':po.arch,
|
||||
'epoch':po.epoch,
|
||||
'release':po.release,
|
||||
'version':po.version,
|
||||
'name':n,
|
||||
'arch':a,
|
||||
'epoch':e,
|
||||
'release':r,
|
||||
'version':v,
|
||||
'repo':repo,
|
||||
'nevra': '%s:%s-%s-%s.%s' % (e,n,v,r,a)
|
||||
}
|
||||
|
||||
if type(po) == yum.rpmsack.RPMInstalledPackage:
|
||||
if repo == 'installed':
|
||||
d['yumstate'] = 'installed'
|
||||
d['repo'] = 'installed'
|
||||
else:
|
||||
d['yumstate'] = 'available'
|
||||
d['repo'] = po.repoid
|
||||
|
||||
if hasattr(po, 'ui_from_repo'):
|
||||
d['repo'] = po.ui_from_repo
|
||||
|
||||
if hasattr(po, 'ui_nevra'):
|
||||
d['_nevra'] = po.ui_nevra
|
||||
else:
|
||||
d['_nevra'] = '%s-%s-%s.%s' % (po.name, po.version, po.release, po.arch)
|
||||
|
||||
|
||||
|
||||
return d
|
||||
|
||||
def list_stuff(my, stuff):
|
||||
# FIXME - there are potential tracebacks that could occur here
|
||||
# need some more catching for them so we can see what happened
|
||||
if stuff == 'installed':
|
||||
return [ pkg_to_dict(po) for po in my.rpmdb ]
|
||||
elif stuff == 'updates':
|
||||
return [ pkg_to_dict(po) for
|
||||
po in my.doPackageLists(pkgnarrow='updates').updates ]
|
||||
elif stuff == 'available':
|
||||
return [ pkg_to_dict(po) for po in my.pkgSack ]
|
||||
elif stuff == 'repos':
|
||||
r = []
|
||||
for repo in my.repos.repos.values():
|
||||
t = {}
|
||||
s = 'disabled'
|
||||
if repo.enabled:
|
||||
s = 'enabled'
|
||||
t[repo.id] = s
|
||||
r.append(t)
|
||||
|
||||
return r
|
||||
else:
|
||||
e,m,u = my.rpmdb.matchPackageNames([stuff])
|
||||
p = e + m
|
||||
e,m,u = my.pkgSack.matchPackageNames([stuff])
|
||||
p = p + e + m
|
||||
return [ pkg_to_dict(po) for po in p ]
|
||||
|
||||
def run_yum(command):
|
||||
def repolist(repoq, qf="%{repoid}"):
|
||||
cmd = repoq + "--qf '%s' -a" % (qf)
|
||||
rc,out,err = run(cmd)
|
||||
ret = []
|
||||
if rc == 0:
|
||||
ret = set([ p for p in out.split('\n') if p.strip() ])
|
||||
|
||||
return ret
|
||||
|
||||
def list_stuff(conf_file, stuff):
|
||||
qf = "%{name}|%{epoch}|%{version}|%{release}|%{arch}|%{repoid}"
|
||||
repoq = '%s --plugins --quiet -q ' % repoquery
|
||||
if conf_file and os.path.exists(conf_file):
|
||||
repoq = '%s -c %s --plugins --quiet -q ' % (repoquery,conf_file)
|
||||
|
||||
|
||||
if stuff == 'installed':
|
||||
return [ pkg_to_dict(p) for p in is_installed(repoq, '-a', qf=qf) if p.strip() ]
|
||||
elif stuff == 'updates':
|
||||
return [ pkg_to_dict(p) for p in is_update(repoq, '-a', qf=qf) if p.strip() ]
|
||||
elif stuff == 'available':
|
||||
return [ pkg_to_dict(p) for p in is_available(repoq, '-a', qf=qf) if p.strip() ]
|
||||
elif stuff == 'repos':
|
||||
return [ dict(repoid=name, state='enabled') for name in repolist(repoq) if name.strip() ]
|
||||
else:
|
||||
return [ pkg_to_dict(p) for p in is_installed(repoq, stuff, qf=qf) + is_available(repoq, stuff, qf=qf) if p.strip() ]
|
||||
|
||||
def run(command):
|
||||
try:
|
||||
cmd = subprocess.Popen(command, shell=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
@ -130,119 +147,170 @@ def run_yum(command):
|
||||
|
||||
return rc, out, err
|
||||
|
||||
def ensure(my, state, pkgspec):
|
||||
yumconf = my.conf.config_file_path
|
||||
|
||||
def install(module, items, repoq, yum_basecmd):
|
||||
res = {}
|
||||
if state == 'installed':
|
||||
# check if pkgspec is installed
|
||||
if re.search('[></=]',pkgspec):
|
||||
try:
|
||||
pkgs = my.returnInstalledPackagesByDep(pkgspec)
|
||||
except yum.Errors.YumBaseError:
|
||||
pkgs = None
|
||||
res['results'] = []
|
||||
res['msg'] = ''
|
||||
res['rc'] = 0
|
||||
res['changed'] = False
|
||||
|
||||
for spec in items:
|
||||
pkg = None
|
||||
|
||||
# check if pkgspec is installed (if possible for idempotence)
|
||||
# localpkg
|
||||
if spec.endswith('.rpm'):
|
||||
# get the pkg name-v-r.arch
|
||||
nvra = local_nvra(spec)
|
||||
# look for them in the rpmdb
|
||||
if is_installed(repoq, nvra):
|
||||
# if they are there, skip it
|
||||
continue
|
||||
pkg = spec
|
||||
#groups :(
|
||||
elif spec.startswith('@'):
|
||||
# complete wild ass guess b/c it's a group
|
||||
pkg = spec
|
||||
|
||||
# range requires or file-requires or pkgname :(
|
||||
else:
|
||||
e,m,u = my.rpmdb.matchPackageNames([pkgspec])
|
||||
pkgs = e +m
|
||||
# look up what pkgs provide this
|
||||
pkglist = what_provides(repoq, spec)
|
||||
if not pkglist:
|
||||
res['msg'] += "No Package matching '%s' found available, installed or updated" % spec
|
||||
res['failed'] = True
|
||||
module.exit_json(**res)
|
||||
|
||||
# if any of them are installed
|
||||
# then nothing to do
|
||||
|
||||
found = False
|
||||
for this in pkglist:
|
||||
if is_installed(repoq, this):
|
||||
found = True
|
||||
res['results'].append('%s providing %s is already installed' % (this, spec))
|
||||
|
||||
if found:
|
||||
continue
|
||||
# if not - then pass in the spec as what to install
|
||||
# we could get here if nothing provides it but that's not
|
||||
# the error we're catching here
|
||||
pkg = spec
|
||||
|
||||
if pkgs:
|
||||
return { 'changed':False }
|
||||
|
||||
# if not see if it is available
|
||||
if re.search('[></=]',pkgspec):
|
||||
try:
|
||||
pkgs = my.returnPackagesByDep(pkgspec)
|
||||
except yum.Errors.YumBaseError:
|
||||
pkgs = None
|
||||
else:
|
||||
e,m,u = my.pkgSack.matchPackageNames([pkgspec])
|
||||
pkgs = e +m
|
||||
|
||||
if not pkgs:
|
||||
msg = "No Package matching %s available" % pkgspec
|
||||
return { 'changed':False, 'failed': True, 'msg':msg }
|
||||
|
||||
my.close()
|
||||
del my
|
||||
cmd = "yum -c %s -d1 -y install '%s'" % (yumconf, pkgspec)
|
||||
rc, out, err = run_yum(cmd)
|
||||
cmd = "%s install '%s'" % (yum_basecmd, pkg)
|
||||
rc, out, err = run(cmd)
|
||||
# FIXME - if we did an install - go and check the rpmdb to see if it actually installed
|
||||
# look for the pkg in rpmdb
|
||||
# look for the pkg via obsoletes
|
||||
if rc:
|
||||
changed = False
|
||||
failed = True
|
||||
res['changed'] = False
|
||||
res['rc'] = rc
|
||||
res['results'].append(out)
|
||||
res['msg'] += err
|
||||
else:
|
||||
changed = True
|
||||
failed = False
|
||||
res['changed'] = True
|
||||
res['rc'] = 0
|
||||
res['results'].append(out)
|
||||
res['msg'] += err
|
||||
|
||||
module.exit_json(**res)
|
||||
|
||||
res['changed'] = changed
|
||||
|
||||
if failed:
|
||||
res['failed'] = failed
|
||||
res['msg'] = err
|
||||
res['results'] = out
|
||||
|
||||
return res
|
||||
|
||||
if state == 'removed':
|
||||
# check if pkgspec is installed
|
||||
# if not return {changed: False, failed=False }
|
||||
# if so try to remove it:
|
||||
# if successfull {changed:True, failed=False, results=stdout, errors=stderr }
|
||||
# if fail {'changed':False, failed='True', results=stdout, errors=stderr }
|
||||
yumconf = my.conf.config_file_path
|
||||
if re.search('[></=]',pkgspec):
|
||||
try:
|
||||
pkgs = my.returnInstalledPackagesByDep(pkgspec)
|
||||
except yum.Errors.YumBaseError:
|
||||
pkgs = None
|
||||
else:
|
||||
e,m,u = my.rpmdb.matchPackageNames([pkgspec])
|
||||
pkgs = e +m
|
||||
|
||||
if not pkgs:
|
||||
return { 'changed':False }
|
||||
def remove(module, items, repoq, yum_basecmd):
|
||||
res = {}
|
||||
res['results'] = []
|
||||
res['msg'] = ''
|
||||
res['changed'] = False
|
||||
res['rc'] = 0
|
||||
|
||||
my.close()
|
||||
del my
|
||||
cmd = "yum -c %s -d1 -y remove '%s'" % (yumconf, pkgspec)
|
||||
rc, out, err = run_yum(cmd)
|
||||
for spec in items:
|
||||
pkg = None
|
||||
|
||||
# group remove - hope you like things dying!
|
||||
if spec.startswith('@'):
|
||||
pkg = spec
|
||||
# req or pkgname remove
|
||||
else:
|
||||
pkglist = what_provides(repoq, spec)
|
||||
if not pkglist:
|
||||
res['msg'] += "No Package matching '%s' found available, installed or updated" % spec
|
||||
res['failed']=True
|
||||
module.exit_json(**res)
|
||||
|
||||
found = False
|
||||
for this in pkglist:
|
||||
if is_installed(repoq, this):
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
res['results'].append('%s is not installed' % spec)
|
||||
continue
|
||||
pkg = spec
|
||||
|
||||
cmd = "%s remove '%s'" % (yum_basecmd, pkg)
|
||||
rc, out, err = run(cmd)
|
||||
|
||||
# FIXME if we ran the remove - check to make sure it actually removed :(
|
||||
# look for the pkg in the rpmdb
|
||||
if rc:
|
||||
changed = False
|
||||
failed = True
|
||||
# look for the pkg in the rpmdb - this is notoriously hard for groups :(
|
||||
if rc != 0:
|
||||
res['changed'] = False
|
||||
res['failed'] = True
|
||||
res['rc'] = rc
|
||||
res['results'].append(out)
|
||||
res['msg'] += err
|
||||
else:
|
||||
changed = True
|
||||
failed = False
|
||||
res['changed'] = True
|
||||
res['rc'] = 0
|
||||
res['results'].append(out)
|
||||
res['msg'] += err
|
||||
|
||||
module.exit_json(**res)
|
||||
|
||||
res['changed'] = changed
|
||||
def latest(module, items, repoq, yum_basecmd):
|
||||
res = {}
|
||||
res['results'] = []
|
||||
res['msg'] = ''
|
||||
res['changed'] = False
|
||||
res['rc'] = 0
|
||||
|
||||
for spec in items:
|
||||
pkg = None
|
||||
|
||||
if failed:
|
||||
res['failed'] = failed
|
||||
res['msg'] = err
|
||||
res['results'] = out
|
||||
|
||||
return res
|
||||
# groups, again
|
||||
if spec.startswith('@'):
|
||||
pkg = spec
|
||||
# dep/pkgname - find it
|
||||
else:
|
||||
pkglist = what_provides(repoq, spec)
|
||||
if not pkglist:
|
||||
res['msg'] += "No Package matching '%s' found available, installed or updated" % spec
|
||||
res['failed']=True
|
||||
module.exit_json(**res)
|
||||
found = False
|
||||
nothing_to_do = False
|
||||
can_be_installed = True
|
||||
for this in pkglist:
|
||||
if is_installed(repoq, this):
|
||||
if is_update(repoq, this):
|
||||
found = True
|
||||
else:
|
||||
nothing_to_do = True
|
||||
|
||||
if nothing_to_do:
|
||||
res['results'].append("All packages providing %s are up to date" % spec)
|
||||
continue
|
||||
|
||||
if state == 'latest':
|
||||
updates = my.doPackageLists(pkgnarrow='updates', patterns=[pkgspec]).updates
|
||||
# sucks but this is for rhel5 - won't matter for rhel6 or fedora or whatnot
|
||||
e,m,u = yum.parsePackages(updates, [pkgspec], casematch=True)
|
||||
updates = e + m
|
||||
avail = my.doPackageLists(pkgnarrow='available', patterns=[pkgspec]).available
|
||||
if not updates and not avail:
|
||||
if not my.doPackageLists(pkgnarrow='installed', patterns=[pkgspec]).installed:
|
||||
msg = "No Package matching '%s' found available, installed or updated" % pkgspec
|
||||
return { 'changed':False, 'failed':True, 'msg': msg }
|
||||
return { 'changed':False,}
|
||||
if not found:
|
||||
basecmd = 'install'
|
||||
else:
|
||||
basecmd = 'update'
|
||||
|
||||
# we have something in updates or available
|
||||
if not updates:
|
||||
cmd = "yum -c %s -d1 -y install '%s'" % (yumconf, pkgspec)
|
||||
else:
|
||||
cmd = "yum -c %s -d1 -y update '%s'" % (yumconf, pkgspec)
|
||||
rc, out, err = run_yum(cmd)
|
||||
|
||||
pkg = spec
|
||||
|
||||
cmd = "%s %s '%s'" % (yum_basecmd, basecmd, pkg)
|
||||
rc, out, err = run(cmd)
|
||||
|
||||
# FIXME if it is - update it and check to see if it applied
|
||||
# check to see if there is no longer an update available for the pkgspec
|
||||
@ -254,24 +322,48 @@ def ensure(my, state, pkgspec):
|
||||
failed = False
|
||||
|
||||
|
||||
res['changed'] = changed
|
||||
|
||||
if failed:
|
||||
res['failed'] = failed
|
||||
res['msg'] = err
|
||||
res['results'] = out
|
||||
|
||||
return res
|
||||
|
||||
return {'changed': False,
|
||||
'failed': True,
|
||||
'results':'',
|
||||
'errors': 'Ensure state %r unknown' % state }
|
||||
if rc:
|
||||
res['changed'] = False
|
||||
res['failed'] = True
|
||||
res['rc'] = rc
|
||||
res['results'].append(out)
|
||||
res['msg'] += err
|
||||
else:
|
||||
res['changed'] = True
|
||||
res['rc'] = 0
|
||||
res['results'].append(out)
|
||||
res['msg'] += err
|
||||
|
||||
module.exit_json(**res)
|
||||
|
||||
|
||||
def update(args):
|
||||
#generic update routine
|
||||
pass
|
||||
|
||||
def ensure(module, state, pkgspec, conf_file):
|
||||
res = {}
|
||||
stdout = ""
|
||||
stderr = ""
|
||||
|
||||
# take multiple args comma separated
|
||||
items = [pkgspec]
|
||||
if pkgspec.find(',') != -1:
|
||||
items = pkgspec.split(',')
|
||||
|
||||
yum_basecmd = '%s -d 1 -y ' % yumbin
|
||||
repoq = '%s --plugins --quiet -q ' % repoquery
|
||||
if conf_file and os.path.exists(conf_file):
|
||||
yum_basecmd = '%s -c %s -d 1 -y' % (yumbin, conf_file)
|
||||
repoq = '%s -c %s --plugins --quiet -q ' % (repoquery,conf_file)
|
||||
|
||||
if state in ['installed', 'present']:
|
||||
install(module, items, repoq, yum_basecmd)
|
||||
elif state in ['removed', 'absent']:
|
||||
remove(module, items, repoq, yum_basecmd)
|
||||
elif state == 'latest':
|
||||
latest(module, items, repoq, yum_basecmd)
|
||||
|
||||
# should be caught by AnsibleModule argument_spec
|
||||
return dict(changed=False, failed=True, results='', errors='unexpected state')
|
||||
|
||||
|
||||
def remove_only(pkgspec):
|
||||
# remove this pkg and only this pkg - fail if it will require more to remove
|
||||
@ -289,73 +381,41 @@ def main():
|
||||
# list=repos
|
||||
# list=pkgspec
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
msg = "the yum module requires arguments (-a)"
|
||||
return 1, msg
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict(
|
||||
pkg=dict(aliases=['name']),
|
||||
# removed==absent, installed==present, these are accepted as aliases
|
||||
state=dict(default='installed', choices=['absent','present','installed','removed','latest']),
|
||||
list=dict(choices=['installed','updates','available','repos','pkgspec']),
|
||||
conf_file=dict(default=None),
|
||||
)
|
||||
)
|
||||
|
||||
argfile = sys.argv[1]
|
||||
if not os.path.exists(argfile):
|
||||
msg = "Argument file not found"
|
||||
return 1, msg
|
||||
params = module.params
|
||||
|
||||
args = open(argfile, 'r').read()
|
||||
items = shlex.split(args)
|
||||
syslog.openlog('ansible-%s' % os.path.basename(__file__))
|
||||
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
|
||||
|
||||
if not len(items):
|
||||
msg = "the yum module requires arguments (-a)"
|
||||
return 1, msg
|
||||
if params['list'] and params['pkg']:
|
||||
module.fail_json(msg="expected 'list=' or 'name=', but not both")
|
||||
|
||||
# if nothing else changes - it fails
|
||||
results = { 'changed':False,
|
||||
'failed':True,
|
||||
'results':'',
|
||||
'errors':'',
|
||||
'msg':args }
|
||||
params = {}
|
||||
for x in items:
|
||||
try:
|
||||
(k, v) = x.split("=", 1)
|
||||
except ValueError:
|
||||
msg = "invalid arguments: %s" % args
|
||||
return 1, msg
|
||||
|
||||
params[k] = v
|
||||
|
||||
if 'conf_file' not in params:
|
||||
params['conf_file'] = None
|
||||
|
||||
|
||||
if 'list' in params:
|
||||
try:
|
||||
my = yum_base(conf_file=params['conf_file'], cachedir=True)
|
||||
results = dict(results=list_stuff(my, params['list']))
|
||||
except Exception, e:
|
||||
return 1, str(e)
|
||||
if not os.path.exists(repoquery):
|
||||
module.fail_json(msg="%s is required to run this module. Please install the yum-utils package." % repoquery)
|
||||
|
||||
if params['list']:
|
||||
results = dict(results=list_stuff(params['conf_file'], params['list']))
|
||||
module.exit_json(**results)
|
||||
|
||||
else:
|
||||
pkg = params.get('pkg', params.get('package', params.get('name', None)))
|
||||
pkg = params['pkg']
|
||||
if 'pkg' is None:
|
||||
results['msg'] = "No pkg specified"
|
||||
module.fail_json(msg="expected 'list=' or 'name='")
|
||||
else:
|
||||
try:
|
||||
my = yum_base(conf_file=params['conf_file'], cachedir=True)
|
||||
state = params.get('state', 'installed')
|
||||
results = ensure(my, state, pkg)
|
||||
except Exception, e:
|
||||
return 1, str(e)
|
||||
state = params['state']
|
||||
res = ensure(module, state, pkg, params['conf_file'])
|
||||
module.fail_json(msg="we should never get here unless this all failed", **res)
|
||||
|
||||
print json.dumps(results)
|
||||
return 0, None
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
|
||||
if __name__ == "__main__":
|
||||
rc, msg = main()
|
||||
if rc != 0: # something went wrong emit the msg
|
||||
print json.dumps({
|
||||
"failed" : rc,
|
||||
"msg" : msg
|
||||
})
|
||||
sys.exit(rc)
|
||||
main()
|
||||
|
||||
|
||||
|
||||
13
packaging/debian/README.md
Normal file
13
packaging/debian/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
Ansible Debian Package
|
||||
======================
|
||||
|
||||
To create an Ansible DEB package:
|
||||
|
||||
sudo apt-get install cdbs debhelper dpkg-dev git-core reprepro
|
||||
git clone git://github.com/ansible/ansible.git
|
||||
cd ansible
|
||||
make deb
|
||||
|
||||
The debian package file will be placed in the `../` directory. This can then be added to an APT repository or installed with `dpkg -l`.
|
||||
|
||||
Note that `dpkg -i` does not resolve dependencies
|
||||
@ -1,11 +0,0 @@
|
||||
I have added a debian folder for use in building a .deb file for ansible. From the ansible directory you can run the following command to construct a debian package of ansible.
|
||||
|
||||
~/ansible$ dpkg-buildpackage -us -uc -rfakeroot
|
||||
|
||||
The debian package files will be placed in the ../ directory and can be installed with the following command:
|
||||
~/$ sudo dpkg -i .deb
|
||||
|
||||
Dpkg -i doesn't resolve dependencies, so if the previous command fails because of dependencies, you will need to run the following to install the dependencies (if needed) and then re-run the dpkg -i command to install the package:
|
||||
$ sudo apt-get -f install
|
||||
|
||||
--Henry Graham
|
||||
@ -1,22 +1,28 @@
|
||||
ansible (0.5) debian; urgency=low
|
||||
ansible (0.6) unstable; urgency=low
|
||||
|
||||
* 0.6 not released yet
|
||||
|
||||
-- Michael DeHaan <michael.dehaan@gmail.com> Wed, 04 Jul 2012 13:40:01 -0400
|
||||
|
||||
ansible (0.5) unstable; urgency=low
|
||||
|
||||
* 0.5 update
|
||||
|
||||
-- Michael DeHaan <michael.dehaan@gmail.com> Wed, 04 Jul 2012 13:40:00 -0400
|
||||
-- Michael DeHaan <michael.dehaan@gmail.com> Wed, 04 Jul 2012 13:40:00 -0400
|
||||
|
||||
ansible (0.4) debian; urgency=low
|
||||
ansible (0.4) unstable; urgency=low
|
||||
|
||||
* 0.4 update
|
||||
|
||||
-- Michael DeHaan <michael.dehaan@gmail.com> Wed, 23 May 2012 19:40:00 -0400
|
||||
-- Michael DeHaan <michael.dehaan@gmail.com> Wed, 23 May 2012 19:40:00 -0400
|
||||
|
||||
ansible (0.3) debian; urgency=low
|
||||
ansible (0.3) unstable; urgency=low
|
||||
|
||||
* 0.3 update
|
||||
|
||||
-- Michael DeHaan <michael.dehaan@gmail.com> Mon, 23 Apr 2012 11:08:00 -0400
|
||||
-- Michael DeHaan <michael.dehaan@gmail.com> Mon, 23 Apr 2012 11:08:00 -0400
|
||||
|
||||
ansible (0.0.2) debian; urgency=low
|
||||
ansible (0.0.2) unstable; urgency=low
|
||||
|
||||
* Initial Release
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot
|
||||
Name: ansible
|
||||
Release: 1%{?dist}
|
||||
Summary: Minimal SSH command and control
|
||||
Version: 0.5
|
||||
Version: 0.6
|
||||
|
||||
Group: Development/Libraries
|
||||
License: GPLv3
|
||||
@ -58,8 +58,11 @@ rm -rf $RPM_BUILD_ROOT
|
||||
|
||||
|
||||
%changelog
|
||||
* Mon Aug 6 2012 Michael DeHaan <michael.dehaan@gmail.com> - 0.6-0
|
||||
- Release of 0.6
|
||||
|
||||
* Wed Jul 4 2012 Michael DeHaan <michael.dehaan@gmail.com> - 0.5-0
|
||||
- Not released yet
|
||||
- Release of 0.5
|
||||
|
||||
* Wed May 23 2012 Michael DeHaan <michael.dehaan@gmail.com> - 0.4-0
|
||||
- Release of 0.4
|
||||
|
||||
@ -14,7 +14,6 @@ class TestInventory(unittest.TestCase):
|
||||
self.inventory_file = os.path.join(self.test_dir, 'simple_hosts')
|
||||
self.complex_inventory_file = os.path.join(self.test_dir, 'complex_hosts')
|
||||
self.inventory_script = os.path.join(self.test_dir, 'inventory_api.py')
|
||||
self.inventory_yaml = os.path.join(self.test_dir, 'yaml_hosts')
|
||||
|
||||
os.chmod(self.inventory_script, 0755)
|
||||
|
||||
@ -35,9 +34,6 @@ class TestInventory(unittest.TestCase):
|
||||
def script_inventory(self):
|
||||
return Inventory(self.inventory_script)
|
||||
|
||||
def yaml_inventory(self):
|
||||
return Inventory(self.inventory_yaml)
|
||||
|
||||
def complex_inventory(self):
|
||||
return Inventory(self.complex_inventory_file)
|
||||
|
||||
@ -48,14 +44,24 @@ class TestInventory(unittest.TestCase):
|
||||
inventory = self.simple_inventory()
|
||||
hosts = inventory.list_hosts()
|
||||
|
||||
expected_hosts=['jupiter', 'saturn', 'zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
|
||||
expected_hosts=['jupiter', 'saturn', 'zeus', 'hera',
|
||||
'cerberus001','cerberus002','cerberus003',
|
||||
'cottus99', 'cottus100',
|
||||
'poseidon', 'thor', 'odin', 'loki',
|
||||
'thrudgelmir0', 'thrudgelmir1', 'thrudgelmir2',
|
||||
'thrudgelmir3', 'thrudgelmir4', 'thrudgelmir5']
|
||||
assert sorted(hosts) == sorted(expected_hosts)
|
||||
|
||||
def test_simple_all(self):
|
||||
inventory = self.simple_inventory()
|
||||
hosts = inventory.list_hosts('all')
|
||||
|
||||
expected_hosts=['jupiter', 'saturn', 'zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
|
||||
expected_hosts=['jupiter', 'saturn', 'zeus', 'hera',
|
||||
'cerberus001','cerberus002','cerberus003',
|
||||
'cottus99', 'cottus100',
|
||||
'poseidon', 'thor', 'odin', 'loki',
|
||||
'thrudgelmir0', 'thrudgelmir1', 'thrudgelmir2',
|
||||
'thrudgelmir3', 'thrudgelmir4', 'thrudgelmir5']
|
||||
assert sorted(hosts) == sorted(expected_hosts)
|
||||
|
||||
def test_simple_norse(self):
|
||||
@ -69,21 +75,29 @@ class TestInventory(unittest.TestCase):
|
||||
inventory = self.simple_inventory()
|
||||
hosts = inventory.list_hosts("ungrouped")
|
||||
|
||||
expected_hosts=['jupiter', 'saturn']
|
||||
expected_hosts=['jupiter', 'saturn',
|
||||
'thrudgelmir0', 'thrudgelmir1', 'thrudgelmir2',
|
||||
'thrudgelmir3', 'thrudgelmir4', 'thrudgelmir5']
|
||||
assert sorted(hosts) == sorted(expected_hosts)
|
||||
|
||||
def test_simple_combined(self):
|
||||
inventory = self.simple_inventory()
|
||||
hosts = inventory.list_hosts("norse:greek")
|
||||
|
||||
expected_hosts=['zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
|
||||
expected_hosts=['zeus', 'hera', 'poseidon',
|
||||
'cerberus001','cerberus002','cerberus003',
|
||||
'cottus99','cottus100',
|
||||
'thor', 'odin', 'loki']
|
||||
assert sorted(hosts) == sorted(expected_hosts)
|
||||
|
||||
def test_simple_restrict(self):
|
||||
inventory = self.simple_inventory()
|
||||
|
||||
restricted_hosts = ['hera', 'poseidon', 'thor']
|
||||
expected_hosts=['zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
|
||||
expected_hosts=['zeus', 'hera', 'poseidon',
|
||||
'cerberus001','cerberus002','cerberus003',
|
||||
'cottus99', 'cottus100',
|
||||
'thor', 'odin', 'loki']
|
||||
|
||||
inventory.restrict_to(restricted_hosts)
|
||||
hosts = inventory.list_hosts("norse:greek")
|
||||
@ -103,11 +117,15 @@ class TestInventory(unittest.TestCase):
|
||||
inventory = self.simple_inventory()
|
||||
|
||||
hosts = inventory.list_hosts("all:!greek")
|
||||
expected_hosts=['jupiter', 'saturn', 'thor', 'odin', 'loki']
|
||||
expected_hosts=['jupiter', 'saturn', 'thor', 'odin', 'loki',
|
||||
'thrudgelmir0', 'thrudgelmir1', 'thrudgelmir2',
|
||||
'thrudgelmir3', 'thrudgelmir4', 'thrudgelmir5']
|
||||
assert sorted(hosts) == sorted(expected_hosts)
|
||||
|
||||
hosts = inventory.list_hosts("all:!norse:!greek")
|
||||
expected_hosts=['jupiter', 'saturn']
|
||||
expected_hosts=['jupiter', 'saturn',
|
||||
'thrudgelmir0', 'thrudgelmir1', 'thrudgelmir2',
|
||||
'thrudgelmir3', 'thrudgelmir4', 'thrudgelmir5']
|
||||
assert sorted(hosts) == sorted(expected_hosts)
|
||||
|
||||
def test_simple_vars(self):
|
||||
@ -116,16 +134,18 @@ class TestInventory(unittest.TestCase):
|
||||
|
||||
print vars
|
||||
assert vars == {'group_names': ['norse'],
|
||||
'inventory_hostname': 'thor'}
|
||||
'inventory_hostname': 'thor',
|
||||
'inventory_hostname_short': 'thor'}
|
||||
|
||||
def test_simple_port(self):
|
||||
inventory = self.simple_inventory()
|
||||
vars = inventory.get_variables('hera')
|
||||
|
||||
print vars
|
||||
expected = {'ansible_ssh_port': 3000,
|
||||
'group_names': ['greek'],
|
||||
'inventory_hostname': 'hera'}
|
||||
expected = { 'ansible_ssh_port': 3000,
|
||||
'group_names': ['greek'],
|
||||
'inventory_hostname': 'hera',
|
||||
'inventory_hostname_short': 'hera' }
|
||||
print expected
|
||||
assert vars == expected
|
||||
|
||||
@ -140,7 +160,7 @@ class TestInventory(unittest.TestCase):
|
||||
|
||||
expected = dict(
|
||||
a='1', b='2', c='3', d='100002', rga='1', rgb='2', rgc='3',
|
||||
inventory_hostname='rtp_a',
|
||||
inventory_hostname='rtp_a', inventory_hostname_short='rtp_a',
|
||||
group_names=[ 'eastcoast', 'nc', 'redundantgroup', 'redundantgroup2', 'redundantgroup3', 'rtp', 'us' ]
|
||||
)
|
||||
print vars
|
||||
@ -214,112 +234,6 @@ class TestInventory(unittest.TestCase):
|
||||
|
||||
assert vars == {'hammer':True,
|
||||
'group_names': ['norse'],
|
||||
'inventory_hostname': 'thor'}
|
||||
|
||||
### Tests for yaml inventory file
|
||||
|
||||
def test_yaml(self):
|
||||
inventory = self.yaml_inventory()
|
||||
hosts = inventory.list_hosts()
|
||||
print hosts
|
||||
expected_hosts=['jupiter', 'saturn', 'mars', 'zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
|
||||
self.compare(hosts, expected_hosts)
|
||||
|
||||
def test_yaml_all(self):
|
||||
inventory = self.yaml_inventory()
|
||||
hosts = inventory.list_hosts('all')
|
||||
|
||||
expected_hosts=['jupiter', 'saturn', 'mars', 'zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
|
||||
self.compare(hosts, expected_hosts)
|
||||
|
||||
def test_yaml_norse(self):
|
||||
inventory = self.yaml_inventory()
|
||||
hosts = inventory.list_hosts("norse")
|
||||
|
||||
expected_hosts=['thor', 'odin', 'loki']
|
||||
self.compare(hosts, expected_hosts)
|
||||
|
||||
def test_yaml_ungrouped(self):
|
||||
inventory = self.yaml_inventory()
|
||||
hosts = inventory.list_hosts("ungrouped")
|
||||
|
||||
expected_hosts=['jupiter', 'mars']
|
||||
self.compare(hosts, expected_hosts)
|
||||
|
||||
def test_yaml_combined(self):
|
||||
inventory = self.yaml_inventory()
|
||||
hosts = inventory.list_hosts("norse:greek")
|
||||
|
||||
expected_hosts=['zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
|
||||
self.compare(hosts, expected_hosts)
|
||||
|
||||
def test_yaml_restrict(self):
|
||||
inventory = self.yaml_inventory()
|
||||
|
||||
restricted_hosts = ['hera', 'poseidon', 'thor']
|
||||
expected_hosts=['zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
|
||||
|
||||
inventory.restrict_to(restricted_hosts)
|
||||
hosts = inventory.list_hosts("norse:greek")
|
||||
|
||||
self.compare(hosts, restricted_hosts)
|
||||
|
||||
inventory.lift_restriction()
|
||||
hosts = inventory.list_hosts("norse:greek")
|
||||
|
||||
self.compare(hosts, expected_hosts)
|
||||
|
||||
def test_yaml_vars(self):
|
||||
inventory = self.yaml_inventory()
|
||||
vars = inventory.get_variables('thor')
|
||||
assert vars == {'group_names': ['norse'],
|
||||
'hammer':True,
|
||||
'inventory_hostname': 'thor'}
|
||||
|
||||
def test_yaml_list_vars(self):
|
||||
inventory = self.yaml_inventory()
|
||||
vars = inventory.get_variables('zeus')
|
||||
assert vars == {'ansible_ssh_port': 3001,
|
||||
'group_names': ['greek', 'ruler'],
|
||||
'inventory_hostname': 'zeus',
|
||||
'ntp_server': 'olympus.example.com'}
|
||||
|
||||
def test_yaml_change_vars(self):
|
||||
inventory = self.yaml_inventory()
|
||||
vars = inventory.get_variables('thor')
|
||||
|
||||
vars["hammer"] = False
|
||||
|
||||
vars = inventory.get_variables('thor')
|
||||
print vars
|
||||
assert vars == {'hammer':True,
|
||||
'inventory_hostname': 'thor',
|
||||
'group_names': ['norse']}
|
||||
|
||||
def test_yaml_host_vars(self):
|
||||
inventory = self.yaml_inventory()
|
||||
vars = inventory.get_variables('saturn')
|
||||
|
||||
print vars
|
||||
assert vars == {'inventory_hostname': 'saturn',
|
||||
'moon': 'titan',
|
||||
'moon2': 'enceladus',
|
||||
'group_names': ['multiple']}
|
||||
|
||||
def test_yaml_port(self):
|
||||
inventory = self.yaml_inventory()
|
||||
vars = inventory.get_variables('hera')
|
||||
|
||||
print vars
|
||||
assert vars == {'ansible_ssh_port': 3000,
|
||||
'inventory_hostname': 'hera',
|
||||
'ntp_server': 'olympus.example.com',
|
||||
'group_names': ['greek']}
|
||||
|
||||
def test_yaml_multiple_groups(self):
|
||||
inventory = self.yaml_inventory()
|
||||
vars = inventory.get_variables('odin')
|
||||
|
||||
assert 'group_names' in vars
|
||||
assert sorted(vars['group_names']) == [ 'norse', 'ruler' ]
|
||||
'inventory_hostname_short': 'thor'}
|
||||
|
||||
|
||||
@ -30,13 +30,10 @@ class TestCallbacks(object):
|
||||
def on_start(self):
|
||||
EVENTS.append('start')
|
||||
|
||||
def on_setup_primary(self):
|
||||
def on_setup(self):
|
||||
EVENTS.append([ 'primary_setup' ])
|
||||
|
||||
def on_setup_secondary(self):
|
||||
EVENTS.append([ 'secondary_setup' ])
|
||||
|
||||
def on_skipped(self, host):
|
||||
def on_skipped(self, host, item=None):
|
||||
EVENTS.append([ 'skipped', [ host ]])
|
||||
|
||||
def on_import_for_host(self, host, filename):
|
||||
@ -57,8 +54,8 @@ class TestCallbacks(object):
|
||||
def on_unreachable(self, host, msg):
|
||||
EVENTS.append([ 'unreachable', [ host, msg ]])
|
||||
|
||||
def on_failed(self, host, results):
|
||||
EVENTS.append([ 'failed', [ host, results ]])
|
||||
def on_failed(self, host, results, ignore_errors):
|
||||
EVENTS.append([ 'failed', [ host, results, ignore_errors ]])
|
||||
|
||||
def on_ok(self, host, result):
|
||||
# delete certain info from host_result to make test comparisons easier
|
||||
@ -86,12 +83,9 @@ class TestCallbacks(object):
|
||||
def on_unreachable(self, host, msg):
|
||||
EVENTS.append([ 'failed/dark', [ host, msg ]])
|
||||
|
||||
def on_setup_primary(self):
|
||||
def on_setup(self):
|
||||
pass
|
||||
|
||||
def on_setup_secondary(self):
|
||||
pass
|
||||
|
||||
def on_no_hosts(self):
|
||||
pass
|
||||
|
||||
@ -144,7 +138,6 @@ class TestPlaybook(unittest.TestCase):
|
||||
runner_callbacks = self.test_callbacks
|
||||
)
|
||||
result = self.playbook.run()
|
||||
# print utils.bigjson(dict(events=EVENTS))
|
||||
return result
|
||||
|
||||
def test_one(self):
|
||||
@ -153,19 +146,20 @@ class TestPlaybook(unittest.TestCase):
|
||||
|
||||
# if different, this will output to screen
|
||||
print "**ACTUAL**"
|
||||
print utils.bigjson(actual)
|
||||
print utils.jsonify(actual, format=True)
|
||||
expected = {
|
||||
"127.0.0.2": {
|
||||
"changed": 9,
|
||||
"failures": 0,
|
||||
"ok": 12,
|
||||
"ok": 11,
|
||||
"skipped": 1,
|
||||
"unreachable": 0
|
||||
}
|
||||
}
|
||||
print "**EXPECTED**"
|
||||
print utils.bigjson(expected)
|
||||
assert utils.bigjson(expected) == utils.bigjson(actual)
|
||||
print utils.jsonify(expected, format=True)
|
||||
|
||||
assert utils.jsonify(expected, format=True) == utils.jsonify(actual,format=True)
|
||||
|
||||
# make sure the template module took options from the vars section
|
||||
data = file('/tmp/ansible_test_data_template.out').read()
|
||||
|
||||
@ -9,6 +9,7 @@ import ansible.runner
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
import tempfile
|
||||
try:
|
||||
import json
|
||||
except:
|
||||
@ -119,28 +120,6 @@ class TestRunner(unittest.TestCase):
|
||||
])
|
||||
assert result['changed'] == False
|
||||
|
||||
def test_template(self):
|
||||
input_ = self._get_test_file('sample.j2')
|
||||
metadata = self._get_test_file('metadata.json')
|
||||
output = self._get_stage_file('sample.out')
|
||||
result = self._run('template', [
|
||||
"src=%s" % input_,
|
||||
"dest=%s" % output,
|
||||
"metadata=%s" % metadata
|
||||
])
|
||||
assert os.path.exists(output)
|
||||
out = file(output).read()
|
||||
assert out.find("duck") != -1
|
||||
assert result['changed'] == True
|
||||
assert 'md5sum' in result
|
||||
assert 'failed' not in result
|
||||
result = self._run('template', [
|
||||
"src=%s" % input_,
|
||||
"dest=%s" % output,
|
||||
"metadata=%s" % metadata
|
||||
])
|
||||
assert result['changed'] == False
|
||||
|
||||
def test_command(self):
|
||||
# test command module, change trigger, etc
|
||||
result = self._run('command', [ "/bin/echo", "hi" ])
|
||||
@ -150,7 +129,7 @@ class TestRunner(unittest.TestCase):
|
||||
assert result['stdout'] == 'hi'
|
||||
assert result['stderr'] == ''
|
||||
|
||||
result = self._run('command', [ "/bin/false" ])
|
||||
result = self._run('command', [ "false" ])
|
||||
assert result['rc'] == 1
|
||||
assert 'failed' not in result
|
||||
|
||||
@ -177,21 +156,6 @@ class TestRunner(unittest.TestCase):
|
||||
assert len(result['stdout']) > 100000
|
||||
assert result['stderr'] == ''
|
||||
|
||||
def test_setup(self):
|
||||
output = self._get_stage_file('output.json')
|
||||
result = self._run('setup', [ "metadata=%s" % output, "a=2", "b=3", "c=4" ])
|
||||
assert 'failed' not in result
|
||||
assert 'md5sum' in result
|
||||
assert result['changed'] == True
|
||||
outds = json.loads(file(output).read())
|
||||
assert outds['c'] == '4'
|
||||
# not bothering to test change hooks here since ohai/facter results change
|
||||
# almost every time so changed is always true, this just tests that
|
||||
# rewriting the file is ok
|
||||
result = self._run('setup', [ "metadata=%s" % output, "a=2", "b=3", "c=4" ])
|
||||
print "RAW RESULT=%s" % result
|
||||
assert 'md5sum' in result
|
||||
|
||||
def test_async(self):
|
||||
# test async launch and job status
|
||||
# of any particular module
|
||||
@ -225,20 +189,33 @@ class TestRunner(unittest.TestCase):
|
||||
assert 'failed' not in result
|
||||
|
||||
def test_git(self):
|
||||
# TODO: tests for the git module
|
||||
pass
|
||||
if not get_binary("yum"):
|
||||
raise SkipTest
|
||||
repo = 'http://github.com/ansible/ansible.git'
|
||||
dest = tempfile.mkdtemp()
|
||||
result = self._run('git', ['repo=%s' % repo, 'dest=%s' % dest])
|
||||
assert 'failed' not in result
|
||||
result = self._run('git', [
|
||||
'repo=%s' % repo,
|
||||
'dest=%s' % dest,
|
||||
'version=master'
|
||||
])
|
||||
assert 'false' not in result
|
||||
try:
|
||||
shutil.rmtree(dest)
|
||||
except OSError, e:
|
||||
print "Failed to remove temp dir %s" % dest
|
||||
|
||||
def test_service(self):
|
||||
# TODO: tests for the service module
|
||||
pass
|
||||
|
||||
def test_assemble(self):
|
||||
input = self._get_test_file('assemble.d')
|
||||
metadata = self._get_test_file('metadata.json')
|
||||
output = self._get_stage_file('sample.out')
|
||||
result = self._run('assemble', [
|
||||
"src=%s" % input,
|
||||
"dest=%s" % output,
|
||||
"metadata=%s" % metadata
|
||||
])
|
||||
assert os.path.exists(output)
|
||||
out = file(output).read()
|
||||
@ -251,7 +228,6 @@ class TestRunner(unittest.TestCase):
|
||||
result = self._run('assemble', [
|
||||
"src=%s" % input,
|
||||
"dest=%s" % output,
|
||||
"metadata=%s" % metadata
|
||||
])
|
||||
assert result['changed'] == False
|
||||
|
||||
|
||||
@ -222,7 +222,7 @@ class TestUtils(unittest.TestCase):
|
||||
'who': 'world',
|
||||
}
|
||||
|
||||
res = ansible.utils.template_from_file("test", "template-basic", vars, {})
|
||||
res = ansible.utils.template_from_file("test", "template-basic", vars)
|
||||
|
||||
assert res == 'hello world'
|
||||
|
||||
@ -231,7 +231,7 @@ class TestUtils(unittest.TestCase):
|
||||
'who': 'world',
|
||||
}
|
||||
|
||||
res = ansible.utils.template_from_file("test", "template-whitespace", vars, {})
|
||||
res = ansible.utils.template_from_file("test", "template-whitespace", vars)
|
||||
|
||||
assert res == 'hello world\n'
|
||||
|
||||
@ -240,7 +240,7 @@ class TestUtils(unittest.TestCase):
|
||||
'who': u'wórld',
|
||||
}
|
||||
|
||||
res = ansible.utils.template_from_file("test", "template-basic", vars, {})
|
||||
res = ansible.utils.template_from_file("test", "template-basic", vars)
|
||||
|
||||
assert res == u'hello wórld'
|
||||
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
{
|
||||
"answer" : "Where will we find a duck and a hose at this hour?"
|
||||
}
|
||||
@ -18,10 +18,10 @@
|
||||
tasks:
|
||||
|
||||
- name: test basic success command
|
||||
action: command /bin/true
|
||||
action: command true
|
||||
|
||||
- name: test basic success command 2
|
||||
action: command /bin/true
|
||||
action: command true
|
||||
|
||||
- name: test basic shell, plus two ways to dereference a variable
|
||||
action: shell echo $HOME $port {{ port }}
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
jupiter
|
||||
saturn
|
||||
thrudgelmir[:5]
|
||||
|
||||
[greek]
|
||||
zeus
|
||||
hera:3000
|
||||
poseidon
|
||||
cerberus[001:003]
|
||||
cottus[99:100]
|
||||
|
||||
[norse]
|
||||
thor
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
---
|
||||
|
||||
- jupiter
|
||||
- host: saturn
|
||||
vars:
|
||||
moon: titan
|
||||
moon2: enceladus
|
||||
|
||||
- host: mars
|
||||
|
||||
- host: zeus
|
||||
vars:
|
||||
- ansible_ssh_port: 3001
|
||||
|
||||
- group: greek
|
||||
hosts:
|
||||
- zeus
|
||||
- hera
|
||||
- poseidon
|
||||
vars:
|
||||
- ansible_ssh_port: 3000
|
||||
- ntp_server: olympus.example.com
|
||||
|
||||
- group: norse
|
||||
hosts:
|
||||
- host: thor
|
||||
vars:
|
||||
- hammer: True
|
||||
- odin
|
||||
- loki
|
||||
|
||||
- group: ruler
|
||||
hosts:
|
||||
- zeus
|
||||
- odin
|
||||
|
||||
- group: multiple
|
||||
hosts:
|
||||
- saturn
|
||||
Reference in New Issue
Block a user