├── .gitignore ├── LICENSE ├── README.md ├── change_audit ├── README.md ├── check.yml ├── lookup_plugins │ └── diff.py ├── tasks │ └── event_tasks.yml ├── templates │ ├── changes.html.j2 │ ├── footer.html.j2 │ └── header.html.j2 └── test.yml ├── collections └── requirements.yml ├── ee-containers ├── README.md ├── build.yml ├── build_files │ ├── tigerlab_ee_supported │ │ ├── bindep.txt │ │ ├── execution-environment.yml │ │ ├── execution-environment.yml-old │ │ ├── galaxy.yml │ │ └── python.txt │ └── vmware_ee_supported │ │ ├── execution-environment.yml │ │ ├── galaxy.yml │ │ └── python.txt ├── tasks │ └── create_ee.yml └── vars │ └── ee.yml ├── prompt_tower_cli ├── call_job.yml ├── set_stat.yml └── tasks │ └── launch_job.yml ├── report_packages ├── report.yml └── templates │ └── packages.csv.j2 ├── tower_cluster_rolling_update └── update_cluster.yml ├── tower_jobs_reports ├── tasks │ └── aap_jobs.yml ├── templates │ └── jobs.csv.j2 └── tower_jobs_report.yml ├── vmware_migration_test └── vmware_migration_test.yml ├── windows_share_report ├── shares_report.yml ├── tasks │ └── share_permissions.yml └── templates │ └── share.csv.j2 ├── windows_vmware_drive_expand └── expand.yml └── wizard ├── README.md ├── action_plugins └── pause.py ├── callback_plugins └── custom_selective.py ├── prompt.yml ├── templates └── custom.config.yml.j2 └── wizard.sh /.gitignore: -------------------------------------------------------------------------------- 1 | */*/__pycache__/* 2 | wizard/custom.config.yml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ansible_misc 2 | Various Ansible playbooks that I have written over the years 3 | -------------------------------------------------------------------------------- /change_audit/README.md: -------------------------------------------------------------------------------- 1 | # Change Auditor 2 | 3 | This playbook (check.yml) if placed in a Workflow with another Job Template, will audit the changes of the first job in that Workflow. 4 | 5 | For instance, if you have a security hardening playbook, you can place the hardening playbook into a Workflow, and then place this playbook after it, and it will audit the previous one for changes. There should be no other playbooks in the workflow (project / inventory syncs, etc.. are fine) 6 | 7 | As this is only an example playbook, I haven't added a task to mail the report out. Instead it just displays the data on the screen. 8 | 9 | ## Credentials 10 | This playbook is currently set to utilize an AAP Controller Credential to access the AAP API. 11 | 12 | ## Variables 13 | For email purposes, you will need to set these variables somewhere (I put it in the workflow extra vars, so I can change it per workflow) 14 | ``` 15 | smtp_server 16 | smtp_port 17 | from_address 18 | to_address 19 | ``` 20 | 21 | ## Show Changes 22 | If you turn on "Show Changes" for the Job template that is making the changes, you will get a lot more detail in the output of the changes for the report. 23 | -------------------------------------------------------------------------------- /change_audit/check.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check previous job for change entries 3 | hosts: localhost 4 | gather_facts: no 5 | connection: local 6 | vars: 7 | tower_server: '{{ lookup("env", "TOWER_HOST") }}' 8 | tower_username: '{{ lookup("env", "TOWER_USERNAME") }}' 9 | tower_password: '{{ lookup("env", "TOWER_PASSWORD") }}' 10 | max: 50 11 | page_size: 200 12 | 13 | # We will be looping over multiple API pages, so its easiest to create individual reports per page, and then combine them together 14 | pre_tasks: 15 | # Lets first check if we have the proper variable to ensure we are not running from command line or outside a workflow 16 | - name: Precheck 17 | ansible.builtin.assert: 18 | that: 19 | - tower_workflow_job_id is defined 20 | - tower_workflow_job_id > 0 21 | fail_msg: "This playbook must be ran from a workflow within Controller" 22 | 23 | # Lets start building out our temporary reports directories 24 | - name: Ensure Reports directories exist 25 | ansible.builtin.file: 26 | state: directory 27 | path: "{{ playbook_dir }}/reports/fragments/" 28 | delegate_to: localhost 29 | run_once: true 30 | 31 | # Lets clean up just in case (shouldn't need to EEs) 32 | - name: Ensure fragments directory is empty 33 | ansible.builtin.file: 34 | state: absent 35 | path: "{{ playbook_dir }}/reports/fragments/*" 36 | delegate_to: localhost 37 | run_once: true 38 | 39 | tasks: 40 | # We have the ID of the currently running workflow job via the variable 'tower_workflow_job_id' 41 | # So we will use that to get the Template jobs that are associated with it 42 | - name: Get the Job ID for the first playbook run 43 | ansible.builtin.uri: 44 | url: https://{{ tower_server }}/api/v2/workflow_jobs/{{ tower_workflow_job_id }}/workflow_nodes/ 45 | method: GET 46 | user: "{{ tower_username }}" 47 | password: "{{ tower_password }}" 48 | body_format: json 49 | validate_certs: False 50 | force_basic_auth: yes 51 | status_code: 52 | - 200 53 | register: response 54 | 55 | # Lets get the job ID of the other job 56 | - ansible.builtin.set_fact: 57 | job_id: "{%- set job = namespace(job=0) -%}\ 58 | {%- for c in response.json.results -%}\ 59 | {%- if c.job != tower_job_id and c.summary_fields.job.status != 'running' and c.summary_fields.job.type == 'job' and c.job > job.job -%}\ 60 | {%- set job.job = c.job -%}\ 61 | {%- endif -%}\ 62 | {%- endfor -%}\ 63 | {{- job.job -}}" 64 | 65 | # Now that we have the Job ID of the first job of the workflow, we need to grab the job events to get the total number of changes 66 | - name: Get the Job Results for the first playbook run 67 | ansible.builtin.uri: 68 | url: https://{{ tower_server }}/api/v2/jobs/{{ job_id }}/job_events/?event=runner_on_ok&changed=true&page_size={{ page_size }} 69 | method: GET 70 | user: "{{ tower_username }}" 71 | password: "{{ tower_password }}" 72 | body_format: json 73 | validate_certs: False 74 | force_basic_auth: yes 75 | status_code: 76 | - 200 77 | register: jdata 78 | 79 | # Display a quick message if we didn't detect any changes. That way they aren't wondering if the job 80 | # didn't complete, etc.. 81 | - ansible.builtin.debug: 82 | msg: No Changes were detected 83 | when: jdata.json.count == 0 84 | 85 | - name: End Play if no changes 86 | ansible.builtin.meta: end_play 87 | when: jdata.json.count == 0 88 | 89 | ##### From here, we only continue if a change was actually made on the previous playbook ##### 90 | 91 | # Set the total pages 92 | - name: Set Page Count 93 | ansible.builtin.set_fact: 94 | pages: "{{ ((jdata.json.count / page_size) | round(0, 'ceil') | int) + 1 }}" 95 | 96 | # We are limiting the number of pages we will pull, and since its sorted in reverse order, we will grab the newest data 97 | - name: Set Start Page Count 98 | ansible.builtin.set_fact: 99 | start: "{{ 1 if pages|int < max|int else ((pages|int) - (max|int)) }}" 100 | 101 | - name: Loop over the event pages backwards 102 | include_tasks: tasks/event_tasks.yml 103 | loop: "{{ range(start|int, pages|int) | list }}" 104 | loop_control: 105 | loop_var: page 106 | 107 | post_tasks: 108 | - name: Concat all the html files 109 | ansible.builtin.assemble: 110 | src: "{{ playbook_dir }}/reports/fragments/" 111 | dest: "{{ playbook_dir }}/reports/changes.html" 112 | run_once: true 113 | 114 | - name: Append the header to the html file 115 | ansible.builtin.lineinfile: 116 | dest: "{{ playbook_dir }}/reports/changes.html" 117 | insertbefore: BOF 118 | line: "{{ lookup('ansible.builtin.template', 'templates/header.html.j2') }}" 119 | 120 | - name: Append the footer to the html file 121 | ansible.builtin.lineinfile: 122 | dest: "{{ playbook_dir }}/reports/changes.html" 123 | insertafter: EOF 124 | line: "{{ lookup('ansible.builtin.template', 'templates/footer.html.j2') }}" 125 | 126 | # For testing purposes, you will want to disable this later 127 | #- name: Display Report 128 | # ansible.builtin.debug: 129 | # var: item 130 | # with_file: 131 | # - "{{ playbook_dir }}/reports/changes.html" 132 | 133 | # Send out the report 134 | - name: Mail Report 135 | community.general.mail: 136 | host: "{{ smtp_server | default('127.0.0.1') }}" 137 | port: "{{ smtp_port | default(25) }}" 138 | subject: Change Report - {{ jdata.json.results[0].summary_fields.job.name }} 139 | body: | 140 | Attached is the change report for {{ jdata.json.results[0].summary_fields.job.name }}

141 | Workflow
142 | Job
143 | from: "{{ from_address }}" 144 | to: 145 | - "{{ to_address }}" 146 | attach: 147 | - "{{ playbook_dir }}/reports/changes.html" 148 | subtype: html 149 | ignore_errors: true 150 | -------------------------------------------------------------------------------- /change_audit/lookup_plugins/diff.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2020 Red Hat 3 | # GNU General Public License v3.0+ 4 | # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | 6 | 7 | """ 8 | The diff lookup plugin 9 | """ 10 | from __future__ import absolute_import, division, print_function 11 | 12 | 13 | __metaclass__ = type 14 | 15 | 16 | DOCUMENTATION = """ 17 | name: diff 18 | author: Jimmy Conner 19 | version_added: "1.0.0" 20 | short_description: Return diff of 2 strings 21 | description: 22 | - Return diff of 2 strings 23 | options: 24 | before: 25 | description: 26 | - The text before the change 27 | type: str 28 | required: True 29 | after: 30 | description: 31 | - The text after the change 32 | type: str 33 | required: True 34 | header: 35 | description: 36 | - The filename 37 | type: str 38 | notes: 39 | """ 40 | 41 | EXAMPLES = r""" 42 | 43 | #### Simple examples 44 | 45 | """ 46 | 47 | RETURN = """ 48 | _raw: 49 | description: 50 | - The diff 51 | """ 52 | import re 53 | 54 | from importlib import import_module 55 | 56 | from ansible.plugins.callback import CallbackBase 57 | 58 | from ansible.errors import AnsibleLookupError, AnsibleParserError 59 | from ansible.plugins.lookup import LookupBase 60 | from ansible.utils.display import Display 61 | 62 | from collections.abc import MutableMapping 63 | import difflib 64 | from ansible import constants as C 65 | from ansible.utils.color import stringc 66 | 67 | #from ansible_collections.ansible.utils.plugins.module_utils.common.argspec_validate import ( 68 | # AnsibleArgSpecValidator, 69 | #) 70 | 71 | display = Display() 72 | 73 | class LookupModule(LookupBase): 74 | def run(self, terms, variables=None, **kwargs): 75 | self.set_options(var_options=variables, direct=kwargs) 76 | res = [] 77 | 78 | if isinstance(terms, list): 79 | keys = [ 80 | "before", 81 | "after", 82 | "header", 83 | ] 84 | terms = dict(zip(keys, terms)) 85 | terms.update(kwargs) 86 | 87 | self.debug = True 88 | diff = FactDiff(terms, variables, self.debug); 89 | 90 | ret = diff.diff() 91 | display.vvvv("------------------------------------") 92 | display.vvvv(ret) 93 | display.vvvv("------------------------------------") 94 | 95 | #display.vvvv(res); 96 | 97 | #display.vvvv("DIFF: %s" % ret) 98 | res.append(ret) 99 | return res 100 | 101 | 102 | class FactDiffBase: 103 | def __init__(self, task_args, task_vars, debug): 104 | self._debug = debug 105 | self._task_args = task_args 106 | self._task_vars = task_vars 107 | 108 | class FactDiff(FactDiffBase): 109 | def _check_valid_regexes(self): 110 | if self._skip_lines: 111 | self._debug("Checking regex in 'split_lines' for validity") 112 | for idx, regex in enumerate(self._skip_lines): 113 | try: 114 | self._skip_lines[idx] = re.compile(regex) 115 | except re.error as exc: 116 | msg = "The regex '{regex}', is not valid. The error was {err}.".format( 117 | regex=regex, 118 | err=str(exc), 119 | ) 120 | self._errors.append(msg) 121 | 122 | def _xform(self): 123 | if self._skip_lines: 124 | if isinstance(self._before, str): 125 | self._debug("'before' is a string, splitting lines") 126 | self._before = self._before.splitlines() 127 | if isinstance(self._after, str): 128 | self._debug("'after' is a string, splitting lines") 129 | self._after = self._after.splitlines() 130 | self._before = [ 131 | line 132 | for line in self._before 133 | if not any(regex.match(str(line)) for regex in self._skip_lines) 134 | ] 135 | self._after = [ 136 | line 137 | for line in self._after 138 | if not any(regex.match(str(line)) for regex in self._skip_lines) 139 | ] 140 | if isinstance(self._before, list): 141 | self._debug("'before' is a list, joining with \n") 142 | self._before = "\n".join(map(str, self._before)) + "\n" 143 | if isinstance(self._after, list): 144 | self._debug("'after' is a list, joining with \n") 145 | self._after = "\n".join(map(str, self._after)) + "\n" 146 | 147 | def diff(self): 148 | self._after = self._task_args["after"] 149 | self._before = self._task_args["before"] 150 | self._header = self._task_args["header"] 151 | self._errors = [] 152 | self._skip_lines = False #self._task_args["plugin"]["vars"].get("skip_lines") 153 | self._check_valid_regexes() 154 | if self._errors: 155 | return {"errors": " ".join(self._errors)} 156 | self._xform() 157 | diff = self._get_diff({"before": self._before, "after": self._after, "header": self._header}) 158 | return diff 159 | 160 | def _get_diff(self, difflist): 161 | 162 | if not isinstance(difflist, list): 163 | difflist = [difflist] 164 | 165 | ret = [] 166 | for diff in difflist: 167 | if 'dst_binary' in diff: 168 | ret.append(u"diff skipped: destination file appears to be binary\n") 169 | if 'src_binary' in diff: 170 | ret.append(u"diff skipped: source file appears to be binary\n") 171 | if 'dst_larger' in diff: 172 | ret.append(u"diff skipped: destination file size is greater than %d\n" % diff['dst_larger']) 173 | if 'src_larger' in diff: 174 | ret.append(u"diff skipped: source file size is greater than %d\n" % diff['src_larger']) 175 | if 'before' in diff and 'after' in diff: 176 | # format complex structures into 'files' 177 | for x in ['before', 'after']: 178 | if isinstance(diff[x], MutableMapping): 179 | diff[x] = self._serialize_diff(diff[x]) 180 | elif diff[x] is None: 181 | diff[x] = '' 182 | if 'header' in diff: 183 | before_header = u"before: %s" % diff['header'] 184 | else: 185 | before_header = u'before' 186 | if 'header' in diff: 187 | after_header = u"after: %s" % diff['header'] 188 | else: 189 | after_header = u'after' 190 | before_lines = diff['before'].splitlines(True) 191 | after_lines = diff['after'].splitlines(True) 192 | if before_lines and not before_lines[-1].endswith(u'\n'): 193 | before_lines[-1] += u'\n\\ No newline at end of file\n' 194 | if after_lines and not after_lines[-1].endswith('\n'): 195 | after_lines[-1] += u'\n\\ No newline at end of file\n' 196 | differ = difflib.unified_diff(before_lines, 197 | after_lines, 198 | fromfile=before_header, 199 | tofile=after_header, 200 | fromfiledate=u'', 201 | tofiledate=u'', 202 | n=C.DIFF_CONTEXT) 203 | difflines = list(differ) 204 | has_diff = False 205 | for line in difflines: 206 | has_diff = True 207 | if line.startswith(u'+'): 208 | # line = stringc(line, C.COLOR_DIFF_ADD) 209 | line = "" + line + "" 210 | elif line.startswith(u'-'): 211 | # line = stringc(line, C.COLOR_DIFF_REMOVE) 212 | line = "" + line + "" 213 | elif line.startswith(u'@@'): 214 | # line = stringc(line, C.COLOR_DIFF_LINES) 215 | line = "" + line + "" 216 | ret.append(line) 217 | if has_diff: 218 | ret.append('\n') 219 | if 'prepared' in diff: 220 | ret.append(diff['prepared']) 221 | return u''.join(ret) 222 | 223 | def _serialize_diff(self, diff): 224 | try: 225 | result_format = self.get_option('result_format') 226 | except KeyError: 227 | # Callback does not declare result_format nor extend result_format_callback 228 | result_format = 'json' 229 | 230 | try: 231 | pretty_results = self.get_option('pretty_results') 232 | except KeyError: 233 | # Callback does not declare pretty_results nor extend result_format_callback 234 | pretty_results = None 235 | 236 | if result_format == 'json': 237 | return json.dumps(diff, sort_keys=True, indent=4, separators=(u',', u': ')) + u'\n' 238 | elif result_format == 'yaml': 239 | # None is a sentinel in this case that indicates default behavior 240 | # default behavior for yaml is to prettify results 241 | lossy = pretty_results in (None, True) 242 | return '%s\n' % textwrap.indent( 243 | yaml.dump( 244 | diff, 245 | allow_unicode=True, 246 | Dumper=_AnsibleCallbackDumper(lossy=lossy), 247 | default_flow_style=False, 248 | indent=4, 249 | # sort_keys=sort_keys # This requires PyYAML>=5.1 250 | ), 251 | ' ' 252 | ) 253 | -------------------------------------------------------------------------------- /change_audit/tasks/event_tasks.yml: -------------------------------------------------------------------------------- 1 | 2 | # We will now grab each page of the Job Events 3 | - name: Get the events on page {{ page }} 4 | ansible.builtin.uri: 5 | url: https://{{ tower_server }}/api/v2/jobs/{{ job_id }}/job_events/?event=runner_on_ok&changed=true&page_size={{ page_size }}&page={{ page }} 6 | method: GET 7 | user: "{{ tower_username }}" 8 | password: "{{ tower_password }}" 9 | body_format: json 10 | validate_certs: False 11 | force_basic_auth: yes 12 | status_code: 13 | - 200 14 | register: events 15 | 16 | # You can also add in a URL link back to the actual job run by using 17 | # Workflow: https://{{ tower_server }}/#/jobs/workflow/{{ tower_workflow_job_id }}/output 18 | # Job: https://{{ tower_server }}/#/jobs/playbook/{{ job_id }}/output 19 | # so they can more easily view the results 20 | - name: Create report of changes on page {{ page }} 21 | ansible.builtin.template: 22 | src: templates/changes.html.j2 23 | dest: "{{ playbook_dir }}/reports/fragments/page-{{ page }}.html" 24 | -------------------------------------------------------------------------------- /change_audit/templates/changes.html.j2: -------------------------------------------------------------------------------- 1 | {% for c in events.json.results %} 2 | 3 | {{ c.modified[:19] | replace("T", " ") }}{{ c.host_name }}{{ c.playbook }}{{ c.task }}{{ c.event_data.resolved_action }}
 4 | {% if c.event_data.res.diff is defined %}
 5 | {% for d in c.event_data.res.diff %}
 6 | {% if d.before_header is defined and d.before is defined %}
 7 | {{ lookup('diff', before=d.before, after=d.after, header=d.before_header) | default("") | to_nice_yaml(width=1337) | replace ("\\n", "\n") }}
 8 | {% else %}
 9 | {{ d | default("") | to_nice_yaml(width=1337) | replace ("\\n", "\n        ") }}
10 | {% endif %}
11 | {% endfor %}
12 | {% endif %}
13 | 
14 | 15 | {% endfor %} 16 | -------------------------------------------------------------------------------- /change_audit/templates/footer.html.j2: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /change_audit/templates/header.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 |

{{ jdata.json.results[0].summary_fields.job.name }}



12 | 13 | 14 | -------------------------------------------------------------------------------- /change_audit/test.yml: -------------------------------------------------------------------------------- 1 | - name: Sample Playbook that pretends to make some changes 2 | hosts: localhost 3 | gather_facts: no 4 | tasks: 5 | 6 | - name: Touch a file 7 | file: 8 | path: myfile.conf 9 | state: touch 10 | 11 | - name: Touch a file 2 12 | file: 13 | path: myfile2.conf 14 | state: touch 15 | changed_when: false 16 | 17 | - name: Touch a file 3 18 | file: 19 | path: myfile3.conf 20 | state: touch -------------------------------------------------------------------------------- /collections/requirements.yml: -------------------------------------------------------------------------------- 1 | collections: 2 | - name: awx.awx 3 | version: 21.10.2 4 | - name: community.general 5 | version: 6.2.0 6 | # - ansible.utils 7 | 8 | -------------------------------------------------------------------------------- /ee-containers/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ee-containers/build.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Build / Push / Install Custom EEs 3 | hosts: all 4 | vars_files: 5 | - vars/ee.yml 6 | vars: 7 | # what is the path where the EE build files are stored 8 | path_ee: "{{ playbook_dir }}/build_files" 9 | 10 | # remote directory to store the EE Build files 11 | remote_path: /tmp 12 | 13 | # private automation hub host 14 | #pah_host: lab-aaphub.tiger.lab 15 | 16 | # AAP host 17 | #aap_host: lab-aapc1.tiger.lab 18 | 19 | 20 | # credential name used in AAP 21 | pah_cred: Automation Hub Container Registry 22 | 23 | tasks: 24 | - include_tasks: tasks/create_ee.yml 25 | loop: "{{ execution_envs }}" 26 | loop_control: 27 | loop_var: ee 28 | label: "{{ ee }}" 29 | -------------------------------------------------------------------------------- /ee-containers/build_files/tigerlab_ee_supported/bindep.txt: -------------------------------------------------------------------------------- 1 | iputils 2 | -------------------------------------------------------------------------------- /ee-containers/build_files/tigerlab_ee_supported/execution-environment.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | 4 | 5 | #build_arg_defaults: 6 | #EE_BASE_IMAGE: jc-aap23-hub/ee-minimal-rhel8:latest 7 | #EE_BASE_IMAGE: 'registry.redhat.io/ansible-automation-platform-21/ee-supported-rhel8' 8 | #EE_BASE_IMAGE: 'registry.redhat.io/ansible-automation-platform-20-early-access/ee-supported-rhel8:2.0.0' 9 | 10 | # ansible_config: 'ansible.cfg' 11 | 12 | dependencies: 13 | galaxy: galaxy.yml 14 | python: python.txt 15 | system: bindep.txt 16 | 17 | images: 18 | base_image: 19 | name: jc-aap23-hub.tiger.lab/ee-minimal-rhel8:latest 20 | builder_image: 21 | name: jc-aap23-hub.tiger.lab/ansible-builder-rhel8:latest 22 | # signature_original_name: registry.redhat.io/ansible-automation-platform-21/ansible-builder-rhel8:latest 23 | -------------------------------------------------------------------------------- /ee-containers/build_files/tigerlab_ee_supported/execution-environment.yml-old: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | 4 | build_arg_defaults: 5 | EE_BASE_IMAGE: jc-aap23-hub.tiger.lab/ee-supported-rhel8:latest 6 | #EE_BASE_IMAGE: 'registry.redhat.io/ansible-automation-platform-21/ee-supported-rhel8' 7 | #EE_BASE_IMAGE: 'registry.redhat.io/ansible-automation-platform-20-early-access/ee-supported-rhel8:2.0.0' 8 | 9 | # ansible_config: 'ansible.cfg' 10 | 11 | dependencies: 12 | galaxy: galaxy.yml 13 | python: python.txt 14 | -------------------------------------------------------------------------------- /ee-containers/build_files/tigerlab_ee_supported/galaxy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | collections: 3 | - name: community.vmware 4 | version: 3.2.0 5 | - name: community.routeros 6 | version: 2.6.0 7 | - name: community.general 8 | version: 6.5.0 9 | - name: ansible.netcommon 10 | version: 4.1.0 11 | - name: ansible.utils 12 | version: 2.8.0 13 | - name: community.windows 14 | version: 1.12.0 15 | - name: ansible.posix 16 | version: 1.4.0 17 | -------------------------------------------------------------------------------- /ee-containers/build_files/tigerlab_ee_supported/python.txt: -------------------------------------------------------------------------------- 1 | pyVmomi>=6.7 2 | git+https://github.com/vmware/vsphere-automation-sdk-python.git ; python_version >= '2.7' # Python 2.6 is not supported 3 | packaging 4 | requests[security] 5 | xmltodict -------------------------------------------------------------------------------- /ee-containers/build_files/vmware_ee_supported/execution-environment.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | 4 | 5 | #build_arg_defaults: 6 | #EE_BASE_IMAGE: jc-aap23-hub/ee-minimal-rhel8:latest 7 | #EE_BASE_IMAGE: 'registry.redhat.io/ansible-automation-platform-21/ee-supported-rhel8' 8 | #EE_BASE_IMAGE: 'registry.redhat.io/ansible-automation-platform-20-early-access/ee-supported-rhel8:2.0.0' 9 | 10 | # ansible_config: 'ansible.cfg' 11 | 12 | dependencies: 13 | galaxy: galaxy.yml 14 | python: python.txt 15 | 16 | images: 17 | base_image: 18 | name: jc-aap23-hub.tiger.lab/ee-minimal-rhel8:latest 19 | builder_image: 20 | name: jc-aap23-hub.tiger.lab/ansible-builder-rhel8:latest 21 | # signature_original_name: registry.redhat.io/ansible-automation-platform-21/ansible-builder-rhel8:latest 22 | -------------------------------------------------------------------------------- /ee-containers/build_files/vmware_ee_supported/galaxy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | collections: 3 | - name: community.vmware 4 | -------------------------------------------------------------------------------- /ee-containers/build_files/vmware_ee_supported/python.txt: -------------------------------------------------------------------------------- 1 | pyVmomi>=6.7 2 | git+https://github.com/vmware/vsphere-automation-sdk-python.git ; python_version >= '2.7' # Python 2.6 is not supported 3 | -------------------------------------------------------------------------------- /ee-containers/tasks/create_ee.yml: -------------------------------------------------------------------------------- 1 | - name: Create remote directory 2 | ansible.builtin.file: 3 | path: "{{ remote_path }}/{{ ee.name }}/" 4 | state: directory 5 | 6 | - name: Copy the EE files 7 | ansible.builtin.copy: 8 | src: "{{ playbook_dir }}/build_files/{{ ee.name }}/" 9 | dest: "{{ remote_path }}/{{ ee.name }}/" 10 | 11 | - name: Include the EE file as variables 12 | include_vars: 13 | file: "{{ path_ee }}/{{ ee.name }}/execution-environment.yml" 14 | name: inc_vars 15 | 16 | - name: Setup some base variables 17 | ansible.builtin.set_fact: 18 | base_ee: "{{ inc_vars.images.base_image.name }}" 19 | # base_ee: "{{ inc_vars.build_arg_defaults.EE_BASE_IMAGE }}" 20 | new_ee_ver: "{{ ee.version }}" 21 | 22 | - name: Grab the list of EEs to ensure the correct EE exists 23 | ansible.builtin.shell: podman image list 24 | changed_when: false 25 | register: ee_list 26 | 27 | - name: Install the required base EE if not already present 28 | when: ee_list.stdout_lines is not search((base_ee|split(':'))[0]) 29 | ansible.builtin.shell: "podman pull --creds={{ ee_username }}:{{ ee_password }} {{ inc_vars.images.base_image.name }}" 30 | # ansible.builtin.shell: "podman pull --creds={{ ee_username }}:{{ ee_password }} {{ inc_vars.build_arg_defaults.EE_BASE_IMAGE }}" 31 | #no_log: true 32 | 33 | - name: Run the build on the selected new EE 34 | ansible.builtin.shell: "ansible-builder build --tag {{ ee.name }}" 35 | args: 36 | chdir: "{{ remote_path }}/{{ ee.name }}" 37 | register: ee_build_out 38 | failed_when: ee_build_out.stdout_lines is not search('Complete!') 39 | 40 | - name: Push EE to container registry 41 | ansible.builtin.shell: "podman push --creds={{ ee_username }}:{{ ee_password }} localhost/{{ ee.name }}:latest docker://{{ pah_host }}/{{ ee.name }}:{{ new_ee_ver }} --tls-verify=false" 42 | args: 43 | chdir: "{{ remote_path }}/{{ ee.name }}" 44 | register: pah_push_out 45 | no_log: true 46 | failed_when: pah_push_out.stderr_lines is not search('Storing signatures') 47 | 48 | - name: Collect credentials 49 | uri: 50 | url: "https://{{ aap_host }}/api/v2/credentials/?name={{ pah_cred | replace(' ', '%20') }}" 51 | user: "{{ ee_username }}" 52 | password: "{{ ee_password }}" 53 | method: GET 54 | validate_certs: false 55 | force_basic_auth: true 56 | status_code: 57 | - 200 58 | - 201 59 | - 204 60 | changed_when: false 61 | register: aap_cred_out 62 | 63 | - name: Set ansible credential name to ID 64 | when: item.name == pah_cred 65 | ansible.builtin.set_fact: 66 | pah_cred_id: "{{ item.id }}" 67 | loop: "{{ aap_cred_out.json.results }}" 68 | 69 | - name: Collect EEs to check if it already exists 70 | uri: 71 | url: "https://{{ aap_host }}/api/v2/execution_environments/?name={{ ee.name | replace(' ', '%20') }}" 72 | user: "{{ ee_username }}" 73 | password: "{{ ee_password }}" 74 | method: GET 75 | validate_certs: false 76 | force_basic_auth: true 77 | status_code: 78 | - 200 79 | - 201 80 | - 204 81 | changed_when: false 82 | register: aap_ee_out 83 | 84 | - name: Set ansible ee id if name already exists 85 | when: item.name == ee.name 86 | ansible.builtin.set_fact: 87 | new_ee_id: "{{ item.id }}" 88 | loop: "{{ aap_ee_out.json.results }}" 89 | 90 | - name: Configure AAP for new EE 91 | when: new_ee_id is undefined 92 | uri: 93 | url: "https://{{ aap_host }}/api/v2/execution_environments/" 94 | user: "{{ ee_username }}" 95 | password: "{{ ee_password }}" 96 | method: POST 97 | validate_certs: false 98 | force_basic_auth: true 99 | body_format: json 100 | body: | 101 | { 102 | "name": "{{ ee.name }}", 103 | "image": "{{ pah_host }}/{{ ee.name }}:{{ new_ee_ver }}", 104 | "description": "added by automation", 105 | "pull": "missing", 106 | "credential": "{{ pah_cred_id }}" 107 | } 108 | status_code: 109 | - 200 110 | - 201 111 | - 204 112 | failed_when: aap_conf_out.status != 201 and aap_conf_out.json.name is not search('already exists') 113 | register: aap_conf_out 114 | 115 | - name: If EE already exists on AAP, update version number 116 | when: new_ee_id is defined 117 | uri: 118 | url: "https://{{ aap_host }}/api/v2/execution_environments/{{ new_ee_id }}/" 119 | user: "{{ ee_username }}" 120 | password: "{{ ee_password }}" 121 | method: PUT 122 | validate_certs: false 123 | force_basic_auth: true 124 | body_format: json 125 | body: | 126 | { 127 | "name": "{{ ee.name }}", 128 | "image": "{{ pah_host }}/{{ ee.name }}:{{ new_ee_ver }}", 129 | "description": "added by automation", 130 | "pull": "missing", 131 | "credential": "{{ pah_cred_id }}" 132 | } 133 | status_code: 134 | - 200 135 | - 201 136 | - 204 137 | 138 | 139 | -------------------------------------------------------------------------------- /ee-containers/vars/ee.yml: -------------------------------------------------------------------------------- 1 | execution_envs: 2 | - name: vmware_ee_supported 3 | version: 2 4 | - name: tigerlab_ee_supported 5 | version: 5 6 | -------------------------------------------------------------------------------- /prompt_tower_cli/call_job.yml: -------------------------------------------------------------------------------- 1 | - name: Test of vars prompt / Tower Survey 2 | hosts: localhost 3 | connection: local 4 | gather_facts: false 5 | vars: 6 | extra_vars: 7 | test: "Test" 8 | apps: 9 | - Test1 10 | - Test2 11 | tasks: 12 | - name: Call our job 13 | include_tasks: tasks/launch_job.yml 14 | loop: "{{ apps }}" 15 | loop_control: 16 | loop_var: app 17 | 18 | -------------------------------------------------------------------------------- /prompt_tower_cli/set_stat.yml: -------------------------------------------------------------------------------- 1 | - name: Test of set_stats 2 | hosts: localhost 3 | connection: local 4 | gather_facts: false 5 | tasks: 6 | 7 | - set_stats: 8 | data: 9 | myvariable: "Test" 10 | -------------------------------------------------------------------------------- /prompt_tower_cli/tasks/launch_job.yml: -------------------------------------------------------------------------------- 1 | - name: Call our job 2 | awx.awx.job_launch: 3 | name: Set Stat 4 | extra_vars: "{{ extra_vars }}" 5 | wait: yes 6 | register: job 7 | 8 | - name: Get the Job Results 9 | ansible.builtin.uri: 10 | url: https://{{ lookup('env', 'CONTROLLER_HOST') }}/api/v2/jobs/{{ job.id }}/ 11 | method: GET 12 | user: "{{ lookup('env', 'CONTROLLER_USERNAME') }}" 13 | password: "{{ lookup('env', 'CONTROLLER_PASSWORD') }}" 14 | body_format: json 15 | validate_certs: False 16 | force_basic_auth: yes 17 | status_code: 18 | - 200 19 | register: jdata 20 | 21 | - name: Save artifacts 22 | set_fact: 23 | extra_vars: "{{ extra_vars | combine(jdata.json.artifacts) }}" 24 | when: jdata.json.artifacts is defined 25 | -------------------------------------------------------------------------------- /report_packages/report.yml: -------------------------------------------------------------------------------- 1 | - name: Create Packages Report 2 | hosts: all 3 | vars: 4 | report_packages: 5 | - kernel 6 | - httpd 7 | - nginx 8 | 9 | pre_tasks: 10 | - name: Ensure Reports directories exist 11 | ansible.builtin.file: 12 | state: directory 13 | path: "{{ playbook_dir }}/reports/fragments/" 14 | delegate_to: localhost 15 | run_once: true 16 | 17 | - name: Ensure fragments directory is empty 18 | ansible.builtin.file: 19 | state: absent 20 | path: "{{ playbook_dir }}/reports/fragments/*" 21 | delegate_to: localhost 22 | run_once: true 23 | 24 | tasks: 25 | - name: Get Packages 26 | ansible.builtin.package_facts: 27 | register: packages 28 | 29 | - name: Render the Host Report Template 30 | ansible.builtin.template: 31 | src: "templates/packages.csv.j2" 32 | dest: "{{ playbook_dir }}/reports/fragments/{{ inventory_hostname }}-packages.csv" 33 | delegate_to: localhost 34 | 35 | post_tasks: 36 | - name: Concat all the csv files 37 | ansible.builtin.assemble: 38 | src: "{{ playbook_dir }}/reports/fragments/" 39 | dest: "{{ playbook_dir }}/reports/packages.csv" 40 | delegate_to: localhost 41 | run_once: true 42 | 43 | - name: Append the header to the csv file 44 | ansible.builtin.lineinfile: 45 | dest: "{{ playbook_dir }}/reports/packages.csv" 46 | insertbefore: BOF 47 | line: "Hostname,Package,Installed,Version" 48 | delegate_to: localhost 49 | run_once: true 50 | 51 | - name: Mail Report 52 | community.general.mail: 53 | host: "{{ smtp_server | default('127.0.0.1') }}" 54 | port: "{{ smtp_port | default(25) }}" 55 | subject: Packages Report 56 | body: Here is the report of the required Packages 57 | from: "{{ from_address }}" 58 | to: 59 | - "{{ to_address }}" 60 | attach: 61 | - "{{ playbook_dir }}/reports/packages.csv" 62 | ignore_errors: true 63 | delegate_to: localhost 64 | run_once: true 65 | -------------------------------------------------------------------------------- /report_packages/templates/packages.csv.j2: -------------------------------------------------------------------------------- 1 | {% for r in report_packages %} 2 | {% if ansible_facts.packages[r] is defined %} 3 | {{ inventory_hostname }},{{ r }},Yes,{{ ansible_facts.packages[r][0].version }} 4 | {% else %} 5 | {{ inventory_hostname }},{{ r }},No, 6 | {% endif %} 7 | {% endfor %} 8 | -------------------------------------------------------------------------------- /tower_cluster_rolling_update/update_cluster.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Pull all instances 3 | hosts: localhost 4 | gather_facts: no 5 | tasks: 6 | - name: Pull Instances from API 7 | uri: 8 | url: https://localhost/api/v2/instances/ 9 | method: GET 10 | user: "{{ tower_username }}" 11 | password: "{{ tower_password }}" 12 | validate_certs: False 13 | force_basic_auth: yes 14 | status_code: 200 15 | register: instances 16 | 17 | - name: Add Instances as Hosts 18 | add_host: 19 | name: "{{ item.hostname }}" 20 | group: tower 21 | instance_id: "{{ item.id }}" 22 | loop: "{{ instances.json.results }}" 23 | when: "'isolated' not in item.hostname" 24 | 25 | - name: Update Cluster servers individually 26 | hosts: tower 27 | serial: 1 28 | tasks: 29 | - name: Disable Instance in Tower API 30 | uri: 31 | url: https://{{ ansible_hostname }}/api/v2/instances/{{ instance_id }}/ 32 | method: PATCH 33 | user: "{{ tower_username }}" 34 | password: "{{ tower_password }}" 35 | body: 36 | enabled: false 37 | body_format: json 38 | validate_certs: False 39 | force_basic_auth: yes 40 | status_code: 200 41 | delegate_to: 127.0.0.1 42 | 43 | - name: Wait for Jobs to Complete 44 | uri: 45 | url: https://{{ ansible_hostname }}/api/v2/instances/{{ instance_id }}/ 46 | method: GET 47 | user: "{{ tower_username }}" 48 | password: "{{ tower_password }}" 49 | validate_certs: False 50 | force_basic_auth: yes 51 | status_code: 200 52 | register: job 53 | until: job.json.jobs_running == 0 54 | retries: 100 55 | delay: 5 56 | delegate_to: 127.0.0.1 57 | 58 | - name: Update Packages 59 | yum: 60 | name: "*" 61 | state: latest 62 | exclude: kernel* 63 | register: updates 64 | 65 | - name: Update Kernel Packages 66 | yum: 67 | name: "kernel" 68 | state: latest 69 | register: kernel_updates 70 | 71 | - name: Reboot if necessary 72 | reboot: 73 | when: kernel_updates.changed 74 | 75 | - name: Enable Instance in Tower API 76 | uri: 77 | url: https://{{ ansible_hostname }}/api/v2/instances/{{ instance_id }}/ 78 | method: PATCH 79 | user: "{{ tower_username }}" 80 | password: "{{ tower_password }}" 81 | body: 82 | enabled: true 83 | body_format: json 84 | validate_certs: False 85 | force_basic_auth: yes 86 | status_code: 200 87 | delegate_to: 127.0.0.1 88 | 89 | 90 | -------------------------------------------------------------------------------- /tower_jobs_reports/tasks/aap_jobs.yml: -------------------------------------------------------------------------------- 1 | - name: Get AAP Job Count 2 | uri: 3 | url: "https://localhost/api/v2/jobs/?page={{ page }}" 4 | user: "{{ tower_username }}" 5 | password: "{{ tower_password }}" 6 | method: GET 7 | validate_certs: false 8 | force_basic_auth: true 9 | status_code: 10 | - 200 11 | - 201 12 | - 204 13 | changed_when: false 14 | register: jobs 15 | 16 | - name: Render the Host Report Template 17 | template: 18 | src: "templates/jobs.csv.j2" 19 | dest: "{{ playbook_dir }}/reports/fragments/page-{{ page }}.csv" 20 | -------------------------------------------------------------------------------- /tower_jobs_reports/templates/jobs.csv.j2: -------------------------------------------------------------------------------- 1 | {% for job in jobs.json.results %} 2 | {{ job.name}},{{ job.started }},{{ job.finished }},{{ job.elapsed }},{{ job.summary_fields.created_by.username|default('') }},{{ job.job_type }},{{ job.summary_fields.inventory.name|default('') }},{{ job.limit }} 3 | {% endfor %} 4 | -------------------------------------------------------------------------------- /tower_jobs_reports/tower_jobs_report.yml: -------------------------------------------------------------------------------- 1 | - name: Create Report 2 | hosts: localhost 3 | connection: local 4 | gather_facts: no 5 | vars: 6 | max: 50 7 | 8 | pre_tasks: 9 | - name: Ensure Reports directories exist 10 | file: 11 | state: directory 12 | path: "{{ playbook_dir }}/reports/fragments/" 13 | delegate_to: localhost 14 | run_once: true 15 | 16 | - name: Ensure fragments directory is empty 17 | file: 18 | state: absent 19 | path: "{{ playbook_dir }}/reports/fragments/*" 20 | delegate_to: localhost 21 | run_once: true 22 | 23 | tasks: 24 | - name: Get AAP Job Count 25 | uri: 26 | url: "https://localhost/api/v2/jobs/" 27 | user: "{{ tower_username }}" 28 | password: "{{ tower_password }}" 29 | method: GET 30 | validate_certs: false 31 | force_basic_auth: true 32 | status_code: 33 | - 200 34 | - 201 35 | - 204 36 | changed_when: false 37 | register: aap_jobs_count 38 | 39 | - name: Set Page Count 40 | set_fact: 41 | pages: "{{ ((aap_jobs_count.json.count/25)|int)+1 }}" 42 | 43 | - name: Set Start Page Count 44 | set_fact: 45 | start: "{{ 1 if pages < max else ((pages|int) - (max|int)) }}" 46 | 47 | - name: Loop over the job pages backwards 48 | include_tasks: tasks/aap_jobs.yml 49 | loop: "{{ range(start|int, pages|int) | list }}" 50 | loop_control: 51 | loop_var: page 52 | 53 | post_tasks: 54 | - name: Concat all the csv files 55 | assemble: 56 | src: "{{ playbook_dir }}/reports/fragments/" 57 | dest: "{{ playbook_dir }}/reports/jobs.csv" 58 | delegate_to: localhost 59 | run_once: true 60 | 61 | - name: Append the header to the csv file 62 | lineinfile: 63 | dest: "{{ playbook_dir }}/reports/jobs.csv" 64 | insertbefore: BOF 65 | line: "Name,Started,Finished,Elapsed,Username,Job Type,Inventory,Limit" 66 | delegate_to: localhost 67 | run_once: true 68 | 69 | - name: Display Report 70 | debug: var=item 71 | with_file: 72 | - "{{ playbook_dir }}/reports/jobs.csv" 73 | 74 | -------------------------------------------------------------------------------- /vmware_migration_test/vmware_migration_test.yml: -------------------------------------------------------------------------------- 1 | - name: VMWare Functionality test 2 | hosts: localhost 3 | connection: local 4 | gather_facts: no 5 | vars: 6 | vCenterDatacenter: MNS 7 | vCenterCluster: MNS 8 | vCenterFolder: /POC/ 9 | vm_name: MigrationTest 10 | vCenterCloneSource: CentOS8 11 | VCenterCloneSnapshot: Linked 12 | vCenterPortGroup: Tower 13 | VMIpAddress: 10.0.110.250 14 | VMSubnetMask: 255.255.255.0 15 | VMGateway: 10.0.110.1 16 | dns_server1: 10.0.110.1 17 | esxi_host1: mns01.lab.rhlabs.net 18 | esxi_host2: mns02.lab.rhlabs.net 19 | storage1: SSD 20 | storage2: ISO 21 | tasks: 22 | - name: Clone template or virtual machine and customize 23 | vmware_guest: 24 | datacenter: "{{ vCenterDatacenter }}" 25 | cluster: "{{ vCenterCluster }}" 26 | folder: "/{{ vCenterFolder }}" 27 | name: "{{ vm_name }}" 28 | template: "{{ vCenterCloneSource }}" 29 | snapshot_src: "{{ VCenterCloneSnapshot }}" 30 | linked_clone: True 31 | state: poweredon 32 | wait_for_ip_address: true 33 | validate_certs: False 34 | networks: 35 | - name: "{{ vCenterPortGroup }}" 36 | ip: "{{ VMIpAddress }}" 37 | netmask: "{{ VMSubnetMask }}" 38 | gateway: "{{ VMGateway }}" 39 | dns_servers: 40 | - "{{ dns_server1 }}" 41 | register: newvm 42 | delegate_to: localhost 43 | 44 | - name: Wait until network is working 45 | wait_for: 46 | host: "{{ VMIpAddress }}" 47 | port: 22 48 | delay: 20 49 | timeout: 600 50 | when: newvm.changed 51 | delegate_to: localhost 52 | 53 | - name: Gather cluster info from given datacenter 54 | vmware_cluster_info: 55 | datacenter: "{{ vCenterDatacenter }}" 56 | validate_certs: no 57 | delegate_to: localhost 58 | register: cluster_info 59 | 60 | - name: Perform Host vMotion of virtual machine 61 | vmware_vmotion: 62 | vm_name: "{{ vm_name }}" 63 | destination_host: "{% if newvm.instance.hw_esxi_host != esxi_host1 %}{{ esxi_host1 }}{% else %}{{ esxi_host2 }}{% endif %}" 64 | validate_certs: no 65 | 66 | - name: Perform Storage vMotion of virtual machine 67 | vmware_vmotion: 68 | vm_name: "{{ vm_name }}" 69 | destination_datastore: "{% if newvm.instance.hw_datastores[0] != storage1 %}{{ storage1 }}{% else %}{{ storage2 }}{% endif %}" 70 | validate_certs: no 71 | 72 | - name: Destroy the VM 73 | vmware_guest: 74 | datacenter: "{{ vCenterDatacenter }}" 75 | cluster: "{{ vCenterCluster }}" 76 | folder: "/{{ vCenterFolder }}" 77 | name: "{{ vm_name }}" 78 | state: absent 79 | force: true 80 | validate_certs: False 81 | delegate_to: localhost 82 | 83 | -------------------------------------------------------------------------------- /windows_share_report/shares_report.yml: -------------------------------------------------------------------------------- 1 | - name: Create Report 2 | hosts: all 3 | 4 | pre_tasks: 5 | - name: Ensure Reports directories exist 6 | ansible.builtin.file: 7 | state: directory 8 | path: "{{ playbook_dir }}/reports/fragments/" 9 | delegate_to: localhost 10 | run_once: true 11 | 12 | - name: Ensure fragments directory is empty 13 | ansible.builtin.file: 14 | state: absent 15 | path: "{{ playbook_dir }}/reports/fragments/*" 16 | delegate_to: localhost 17 | run_once: true 18 | 19 | tasks: 20 | - name: Powershell | Get-SMBShare 21 | ansible.windows.win_shell: Get-SMBShare | Select-Object -Property Name,ScopeName,Path,Description,CimSystemProperties | ConvertTo-JSON 22 | register: shares 23 | 24 | - name: include Permissions report 25 | ansible.builtin.include_tasks: tasks/share_permissions.yml 26 | loop: "{{ shares.stdout | from_json }}" 27 | loop_control: 28 | loop_var: share 29 | label: "{{ share.Name }}" 30 | 31 | post_tasks: 32 | - name: Concat all the csv files 33 | ansible.builtin.assemble: 34 | src: "{{ playbook_dir }}/reports/fragments/" 35 | dest: "{{ playbook_dir }}/reports/shares.csv" 36 | delegate_to: localhost 37 | run_once: true 38 | 39 | - name: Append the header to the csv file 40 | ansible.builtin.lineinfile: 41 | dest: "{{ playbook_dir }}/reports/shares.csv" 42 | insertbefore: BOF 43 | line: "Hostname,Share_Name,Share_Value,FullSharePath,Share_Mapping,Share_Permissions,NTFS_Permissions,Description" 44 | delegate_to: localhost 45 | run_once: true 46 | 47 | - name: Mail Report 48 | community.general.mail: 49 | host: "{{ smtp_server | default('127.0.0.1') }}" 50 | port: "{{ smtp_port | default(25) }}" 51 | subject: Windows Share Report 52 | body: Here is the report of all Windows Shares 53 | from: "{{ from_address }}" 54 | to: 55 | - "{{ to_address }}" 56 | attach: 57 | - "{{ playbook_dir }}/reports/shares.csv" 58 | ignore_errors: true 59 | delegate_to: localhost 60 | run_once: true 61 | -------------------------------------------------------------------------------- /windows_share_report/tasks/share_permissions.yml: -------------------------------------------------------------------------------- 1 | - name: Powershell | Get-SMBShareAccess 2 | ansible.windows.win_shell: Get-SMBShareAccess -Name "{{ share.Name }}" | Select-Object -Property AccountName,AccessRight | ConvertTo-JSON 3 | register: permission 4 | 5 | - name: Set Permission variable 6 | ansible.builtin.set_fact: 7 | permission: "{{ permission.stdout | from_json }}" 8 | 9 | - name: Combine Share Permissions 10 | ansible.builtin.set_fact: 11 | pcom: '{%- set r = [] -%} 12 | {%- if permission.AccountName is defined -%} 13 | {%- set permission = [permission] -%} 14 | {%- endif -%} 15 | {%- for item in permission -%} 16 | {%- set v = item.AccountName | replace("NT AUTHORITY\\", "") | replace("BUILTIN\\", "") | replace("NT SERVICE\\", "") -%} 17 | {%- if v not in r -%} 18 | {{- r.append(v) -}} 19 | {%- endif -%} 20 | {%- endfor -%} 21 | {{- r | sort | join(";") -}}' 22 | 23 | - block: 24 | - name: Powershell | Get-ACL 25 | ansible.windows.win_shell: Get-ACL "{{ share.Path }}" | Select -ExpandProperty Access | Select -ExpandProperty IdentityReference | ConvertTo-JSON 26 | register: acl 27 | 28 | - name: Combine ACL Permissions 29 | ansible.builtin.set_fact: 30 | pacl: '{%- set r = [] -%} 31 | {%- for item in acl.stdout | from_json -%} 32 | {%- set v = item.Value | replace("NT AUTHORITY\\", "") | replace("BUILTIN\\", "") | replace("NT SERVICE\\", "") -%} 33 | {%- if v not in r -%} 34 | {{- r.append(v) -}} 35 | {%- endif -%} 36 | {%- endfor -%} 37 | {{- r | sort | join(";") -}}' 38 | rescue: 39 | - name: Set ACL Permission Blank 40 | ansible.builtin.set_fact: 41 | pacl: "" 42 | 43 | - name: Check for Everyone Permission 44 | ansible.builtin.set_fact: 45 | open: "{% if pcom is search('Everyone') %}Open{% else %}Secured{% endif %}" 46 | 47 | - name: Render the Host Report Template 48 | ansible.builtin.template: 49 | src: "templates/share.csv.j2" 50 | dest: "{{ playbook_dir }}/reports/fragments/{{ inventory_hostname }}-{{ share.Name }}.csv" 51 | delegate_to: localhost 52 | -------------------------------------------------------------------------------- /windows_share_report/templates/share.csv.j2: -------------------------------------------------------------------------------- 1 | {{ inventory_hostname }},{{ share.Name }},{{ open }},\\{{ share.CimSystemProperties.ServerName}}\{{ share.Name }},{{ share.Path }},{{ pcom }},{{ pacl }},{{ share.Description }} 2 | -------------------------------------------------------------------------------- /windows_vmware_drive_expand/expand.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check Windows C Drive and Expand if Utilization > 75% 3 | hosts: all 4 | tasks: 5 | 6 | - name: Check that we are Windows 7 | assert: 8 | that: "ansible_os_family == 'Windows'" 9 | 10 | - name: Gather Windows Drive Percentage 11 | win_shell: Get-Volume -DriveLetter C | Select @{ Label=”PercentFree”; Expression={ 100 * ($_.SizeRemaining / $_.Size) }} | ft -hide 12 | register: f 13 | changed_when: false 14 | 15 | - name: Set FreeSpace Fact 16 | set_fact: 17 | fs: "{{ f.stdout | trim | int }}" 18 | 19 | - debug: 20 | msg: "Disk Space Free: {{ fs }}%" 21 | 22 | - block: 23 | - name: Gather VMWare disk facts about VM 24 | vmware_guest_disk_facts: 25 | datacenter: "{{ vCenterDatacenter }}" 26 | validate_certs: no 27 | name: "{{ inventory_hostname }}" 28 | register: disk_facts 29 | delegate_to: localhost 30 | 31 | - debug: 32 | msg: 33 | - "Old Size: {{ (disk_facts.guest_disk_facts['0'].capacity_in_kb|int / 1024 / 1024)|int }} Gb" 34 | - "New Size: {{ (disk_facts.guest_disk_facts['0'].capacity_in_kb|int / 1024 / 1024 * 1.25)|int }} Gb" 35 | 36 | - name: Create vmware_guest module compatible disk list 37 | set_fact: 38 | disks: "{% set output = [] %}\ 39 | {% for d in disk_facts.guest_disk_facts|sort %}\ 40 | {%- if loop.first -%}\ 41 | {{ output.append({'size_kb' : disk_facts.guest_disk_facts[d].capacity_in_kb|int * 1.25 }) }}\ 42 | {%- else -%} 43 | {{ output.append({'size_kb' : disk_facts.guest_disk_facts[d].capacity_in_kb }) }}\ 44 | {%- endif -%} 45 | {% endfor %}\ 46 | {{ output }}" 47 | 48 | - name: Expand Disk in VMWare 49 | vmware_guest: 50 | datacenter: "{{ vCenterDatacenter }}" 51 | cluster: "{{ vCenterCluster }}" 52 | folder: "/{{ vCenterFolder }}" 53 | name: "{{ inventory_hostname }}" 54 | state: present 55 | validate_certs: False 56 | disk: "{{ disks }}" 57 | delegate_to: localhost 58 | 59 | - name: Expand Local Windows Disk 60 | win_shell: Update-HostStorageCache; $MaxSize = (Get-PartitionSupportedSize -DriveLetter c).sizeMax; Resize-Partition -DriveLetter c -Size $MaxSize 61 | 62 | when: fs|int < 25 63 | -------------------------------------------------------------------------------- /wizard/README.md: -------------------------------------------------------------------------------- 1 | A quick example that shows using a callback plugin and various Ansible options to disable most output from a playbook. I also do a quick modification of the pause module to make it's output cleaner. 2 | 3 | This allows you to have Ansible look and feel like a command line interface, but with all the extras that Ansible brings to the table, like retries on invalid entries or templating of files. 4 | -------------------------------------------------------------------------------- /wizard/action_plugins/pause.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012, Tim Bielawa 2 | # 3 | # This file is part of Ansible 4 | # 5 | # Ansible is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Ansible is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with Ansible. If not, see . 17 | from __future__ import (absolute_import, division, print_function) 18 | __metaclass__ = type 19 | 20 | import datetime 21 | import signal 22 | import sys 23 | import termios 24 | import time 25 | import tty 26 | 27 | from os import ( 28 | getpgrp, 29 | isatty, 30 | tcgetpgrp, 31 | ) 32 | from ansible.errors import AnsibleError 33 | from ansible.module_utils._text import to_text, to_native 34 | from ansible.module_utils.parsing.convert_bool import boolean 35 | from ansible.plugins.action import ActionBase 36 | from ansible.utils.display import Display 37 | 38 | display = Display() 39 | 40 | try: 41 | import curses 42 | import io 43 | 44 | # Nest the try except since curses.error is not available if curses did not import 45 | try: 46 | curses.setupterm() 47 | HAS_CURSES = True 48 | except (curses.error, TypeError, io.UnsupportedOperation): 49 | HAS_CURSES = False 50 | except ImportError: 51 | HAS_CURSES = False 52 | 53 | MOVE_TO_BOL = b'\r' 54 | CLEAR_TO_EOL = b'\x1b[K' 55 | if HAS_CURSES: 56 | # curses.tigetstr() returns None in some circumstances 57 | MOVE_TO_BOL = curses.tigetstr('cr') or MOVE_TO_BOL 58 | CLEAR_TO_EOL = curses.tigetstr('el') or CLEAR_TO_EOL 59 | 60 | 61 | def setraw(fd, when=termios.TCSAFLUSH): 62 | """Put terminal into a raw mode. 63 | 64 | Copied from ``tty`` from CPython 3.11.0, and modified to not remove OPOST from OFLAG 65 | 66 | OPOST is kept to prevent an issue with multi line prompts from being corrupted now that display 67 | is proxied via the queue from forks. The problem is a race condition, in that we proxy the display 68 | over the fork, but before it can be displayed, this plugin will have continued executing, potentially 69 | setting stdout and stdin to raw which remove output post processing that commonly converts NL to CRLF 70 | """ 71 | mode = termios.tcgetattr(fd) 72 | mode[tty.IFLAG] = mode[tty.IFLAG] & ~(termios.BRKINT | termios.ICRNL | termios.INPCK | termios.ISTRIP | termios.IXON) 73 | # mode[tty.OFLAG] = mode[tty.OFLAG] & ~(termios.OPOST) 74 | mode[tty.CFLAG] = mode[tty.CFLAG] & ~(termios.CSIZE | termios.PARENB) 75 | mode[tty.CFLAG] = mode[tty.CFLAG] | termios.CS8 76 | mode[tty.LFLAG] = mode[tty.LFLAG] & ~(termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG) 77 | mode[tty.CC][termios.VMIN] = 1 78 | mode[tty.CC][termios.VTIME] = 0 79 | termios.tcsetattr(fd, when, mode) 80 | 81 | 82 | class AnsibleTimeoutExceeded(Exception): 83 | pass 84 | 85 | 86 | def timeout_handler(signum, frame): 87 | raise AnsibleTimeoutExceeded 88 | 89 | 90 | def clear_line(stdout): 91 | stdout.write(b'\x1b[%s' % MOVE_TO_BOL) 92 | stdout.write(b'\x1b[%s' % CLEAR_TO_EOL) 93 | 94 | 95 | def is_interactive(fd=None): 96 | if fd is None: 97 | return False 98 | 99 | if isatty(fd): 100 | # Compare the current process group to the process group associated 101 | # with terminal of the given file descriptor to determine if the process 102 | # is running in the background. 103 | return getpgrp() == tcgetpgrp(fd) 104 | else: 105 | return False 106 | 107 | 108 | class ActionModule(ActionBase): 109 | ''' pauses execution for a length or time, or until input is received ''' 110 | 111 | BYPASS_HOST_LOOP = True 112 | 113 | def run(self, tmp=None, task_vars=None): 114 | ''' run the pause action module ''' 115 | if task_vars is None: 116 | task_vars = dict() 117 | 118 | result = super(ActionModule, self).run(tmp, task_vars) 119 | del tmp # tmp no longer has any effect 120 | 121 | validation_result, new_module_args = self.validate_argument_spec( 122 | argument_spec={ 123 | 'echo': {'type': 'bool', 'default': True}, 124 | 'minutes': {'type': int}, # Don't break backwards compat, allow floats, by using int callable 125 | 'seconds': {'type': int}, # Don't break backwards compat, allow floats, by using int callable 126 | 'prompt': {'type': 'str'}, 127 | }, 128 | mutually_exclusive=( 129 | ('minutes', 'seconds'), 130 | ), 131 | ) 132 | 133 | duration_unit = 'minutes' 134 | prompt = None 135 | seconds = None 136 | echo = new_module_args['echo'] 137 | echo_prompt = '' 138 | result.update(dict( 139 | changed=False, 140 | rc=0, 141 | stderr='', 142 | stdout='', 143 | start=None, 144 | stop=None, 145 | delta=None, 146 | echo=echo 147 | )) 148 | 149 | # Add a note saying the output is hidden if echo is disabled 150 | if not echo: 151 | echo_prompt = ' (output is hidden)' 152 | 153 | if new_module_args['prompt']: 154 | prompt = "%s\n\n%s%s" % (self._task.get_name().strip(), new_module_args['prompt'], echo_prompt) 155 | else: 156 | # If no custom prompt is specified, set a default prompt 157 | prompt = "[%s]\n%s%s:" % (self._task.get_name().strip(), 'Press enter to continue, Ctrl+C to interrupt', echo_prompt) 158 | 159 | if new_module_args['minutes'] is not None: 160 | seconds = new_module_args['minutes'] * 60 161 | elif new_module_args['seconds'] is not None: 162 | seconds = new_module_args['seconds'] 163 | duration_unit = 'seconds' 164 | 165 | ######################################################################## 166 | # Begin the hard work! 167 | 168 | start = time.time() 169 | result['start'] = to_text(datetime.datetime.now()) 170 | result['user_input'] = b'' 171 | 172 | stdin_fd = None 173 | old_settings = None 174 | try: 175 | if seconds is not None: 176 | if seconds < 1: 177 | seconds = 1 178 | 179 | # setup the alarm handler 180 | signal.signal(signal.SIGALRM, timeout_handler) 181 | signal.alarm(seconds) 182 | 183 | # show the timer and control prompts 184 | display.display("Pausing for %d seconds%s" % (seconds, echo_prompt)) 185 | display.display("(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)\r"), 186 | 187 | # show the prompt specified in the task 188 | if new_module_args['prompt']: 189 | display.display(prompt) 190 | 191 | else: 192 | display.display(prompt) 193 | 194 | # save the attributes on the existing (duped) stdin so 195 | # that we can restore them later after we set raw mode 196 | stdin_fd = None 197 | stdout_fd = None 198 | try: 199 | stdin = self._connection._new_stdin.buffer 200 | stdout = sys.stdout.buffer 201 | stdin_fd = stdin.fileno() 202 | stdout_fd = stdout.fileno() 203 | except (ValueError, AttributeError): 204 | # ValueError: someone is using a closed file descriptor as stdin 205 | # AttributeError: someone is using a null file descriptor as stdin on windoze 206 | stdin = None 207 | interactive = is_interactive(stdin_fd) 208 | if interactive: 209 | # grab actual Ctrl+C sequence 210 | try: 211 | intr = termios.tcgetattr(stdin_fd)[6][termios.VINTR] 212 | except Exception: 213 | # unsupported/not present, use default 214 | intr = b'\x03' # value for Ctrl+C 215 | 216 | # get backspace sequences 217 | try: 218 | backspace = termios.tcgetattr(stdin_fd)[6][termios.VERASE] 219 | except Exception: 220 | backspace = [b'\x7f', b'\x08'] 221 | 222 | old_settings = termios.tcgetattr(stdin_fd) 223 | setraw(stdin_fd) 224 | 225 | # Only set stdout to raw mode if it is a TTY. This is needed when redirecting 226 | # stdout to a file since a file cannot be set to raw mode. 227 | if isatty(stdout_fd): 228 | setraw(stdout_fd) 229 | 230 | # Only echo input if no timeout is specified 231 | if not seconds and echo: 232 | new_settings = termios.tcgetattr(stdin_fd) 233 | new_settings[3] = new_settings[3] | termios.ECHO 234 | termios.tcsetattr(stdin_fd, termios.TCSANOW, new_settings) 235 | 236 | # flush the buffer to make sure no previous key presses 237 | # are read in below 238 | termios.tcflush(stdin, termios.TCIFLUSH) 239 | 240 | while True: 241 | if not interactive: 242 | if seconds is None: 243 | display.warning("Not waiting for response to prompt as stdin is not interactive") 244 | if seconds is not None: 245 | # Give the signal handler enough time to timeout 246 | time.sleep(seconds + 1) 247 | break 248 | 249 | try: 250 | key_pressed = stdin.read(1) 251 | 252 | if key_pressed == intr: # value for Ctrl+C 253 | clear_line(stdout) 254 | raise KeyboardInterrupt 255 | 256 | if not seconds: 257 | # read key presses and act accordingly 258 | if key_pressed in (b'\r', b'\n'): 259 | clear_line(stdout) 260 | if echo: 261 | stdout.write(result['user_input']) 262 | stdout.write((b'\n')) 263 | 264 | stdout.flush() 265 | 266 | break 267 | elif key_pressed in backspace: 268 | # delete a character if backspace is pressed 269 | result['user_input'] = result['user_input'][:-1] 270 | clear_line(stdout) 271 | if echo: 272 | stdout.write(result['user_input']) 273 | stdout.flush() 274 | else: 275 | result['user_input'] += key_pressed 276 | 277 | except KeyboardInterrupt: 278 | signal.alarm(0) 279 | display.display("Press 'C' to continue the play or 'A' to abort \r"), 280 | if self._c_or_a(stdin): 281 | clear_line(stdout) 282 | break 283 | 284 | clear_line(stdout) 285 | 286 | raise AnsibleError('user requested abort!') 287 | 288 | except AnsibleTimeoutExceeded: 289 | # this is the exception we expect when the alarm signal 290 | # fires, so we simply ignore it to move into the cleanup 291 | pass 292 | finally: 293 | # cleanup and save some information 294 | # restore the old settings for the duped stdin stdin_fd 295 | if not (None in (stdin_fd, old_settings)) and isatty(stdin_fd): 296 | termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_settings) 297 | 298 | duration = time.time() - start 299 | result['stop'] = to_text(datetime.datetime.now()) 300 | result['delta'] = int(duration) 301 | 302 | if duration_unit == 'minutes': 303 | duration = round(duration / 60.0, 2) 304 | else: 305 | duration = round(duration, 2) 306 | result['stdout'] = "Paused for %s %s" % (duration, duration_unit) 307 | 308 | result['user_input'] = to_text(result['user_input'], errors='surrogate_or_strict') 309 | return result 310 | 311 | def _c_or_a(self, stdin): 312 | while True: 313 | key_pressed = stdin.read(1) 314 | if key_pressed.lower() == b'a': 315 | return False 316 | elif key_pressed.lower() == b'c': 317 | return True 318 | -------------------------------------------------------------------------------- /wizard/callback_plugins/custom_selective.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) Fastly, inc 2016 3 | # Copyright (c) 2017 Ansible Project 4 | # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) 5 | # SPDX-License-Identifier: GPL-3.0-or-later 6 | 7 | from __future__ import (absolute_import, division, print_function) 8 | __metaclass__ = type 9 | 10 | DOCUMENTATION = ''' 11 | author: Unknown (!UNKNOWN) 12 | name: custom_selective 13 | type: stdout 14 | requirements: 15 | - set as main display callback 16 | short_description: only print certain tasks 17 | description: 18 | - This callback only prints tasks that have been tagged with C(print_action) or that have failed. 19 | This allows operators to focus on the tasks that provide value only. 20 | - Tasks that are not printed are placed with a C(.). 21 | - If you increase verbosity all tasks are printed. 22 | options: 23 | nocolor: 24 | default: false 25 | description: This setting allows suppressing colorizing output. 26 | env: 27 | - name: ANSIBLE_NOCOLOR 28 | - name: ANSIBLE_SELECTIVE_DONT_COLORIZE 29 | ini: 30 | - section: defaults 31 | key: nocolor 32 | type: boolean 33 | ''' 34 | 35 | EXAMPLES = """ 36 | - ansible.builtin.debug: msg="This will not be printed" 37 | - ansible.builtin.debug: msg="But this will" 38 | tags: [print_action] 39 | """ 40 | 41 | import difflib 42 | 43 | from ansible import constants as C 44 | from ansible.plugins.callback import CallbackBase 45 | from ansible.module_utils.common.text.converters import to_text 46 | 47 | 48 | DONT_COLORIZE = False 49 | COLORS = { 50 | 'normal': '\033[0m', 51 | 'ok': '\033[{0}m'.format(C.COLOR_CODES[C.COLOR_OK]), 52 | 'bold': '\033[1m', 53 | 'not_so_bold': '\033[1m\033[34m', 54 | 'changed': '\033[{0}m'.format(C.COLOR_CODES[C.COLOR_CHANGED]), 55 | 'failed': '\033[{0}m'.format(C.COLOR_CODES[C.COLOR_ERROR]), 56 | 'endc': '\033[0m', 57 | 'skipped': '\033[{0}m'.format(C.COLOR_CODES[C.COLOR_SKIP]), 58 | } 59 | 60 | 61 | def dict_diff(prv, nxt): 62 | """Return a dict of keys that differ with another config object.""" 63 | keys = set(list(prv.keys()) + list(nxt.keys())) 64 | result = {} 65 | for k in keys: 66 | if prv.get(k) != nxt.get(k): 67 | result[k] = (prv.get(k), nxt.get(k)) 68 | return result 69 | 70 | 71 | def colorize(msg, color): 72 | """Given a string add necessary codes to format the string.""" 73 | if DONT_COLORIZE: 74 | return msg 75 | else: 76 | return '{0}{1}{2}'.format(COLORS[color], msg, COLORS['endc']) 77 | 78 | 79 | class CallbackModule(CallbackBase): 80 | """custom_selective.py callback plugin.""" 81 | 82 | CALLBACK_VERSION = 2.0 83 | CALLBACK_TYPE = 'stdout' 84 | CALLBACK_NAME = 'custom_selective' 85 | 86 | def __init__(self, display=None): 87 | """custom_selective.py callback plugin.""" 88 | super(CallbackModule, self).__init__(display) 89 | self.last_skipped = False 90 | self.last_task_name = None 91 | self.printed_last_task = False 92 | 93 | def set_options(self, task_keys=None, var_options=None, direct=None): 94 | 95 | super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct) 96 | 97 | global DONT_COLORIZE 98 | DONT_COLORIZE = self.get_option('nocolor') 99 | 100 | def _print_task(self, task_name=None): 101 | if task_name is None: 102 | task_name = self.last_task_name 103 | 104 | if not self.printed_last_task: 105 | self.printed_last_task = True 106 | line_length = 120 107 | if self.last_skipped: 108 | print() 109 | line = "# {0} ".format(task_name) 110 | msg = colorize("{0}{1}".format(line, '*' * (line_length - len(line))), 'bold') 111 | print(msg) 112 | 113 | def _indent_text(self, text, indent_level): 114 | lines = text.splitlines() 115 | result_lines = [] 116 | for l in lines: 117 | result_lines.append("{0}{1}".format(' ' * indent_level, l)) 118 | return '\n'.join(result_lines) 119 | 120 | def _print_diff(self, diff, indent_level): 121 | if isinstance(diff, dict): 122 | try: 123 | diff = '\n'.join(difflib.unified_diff(diff['before'].splitlines(), 124 | diff['after'].splitlines(), 125 | fromfile=diff.get('before_header', 126 | 'new_file'), 127 | tofile=diff['after_header'])) 128 | except AttributeError: 129 | diff = dict_diff(diff['before'], diff['after']) 130 | if diff: 131 | diff = colorize(str(diff), 'changed') 132 | print(self._indent_text(diff, indent_level + 4)) 133 | 134 | def _print_host_or_item(self, host_or_item, changed, msg, diff, is_host, error, stdout, stderr): 135 | if is_host: 136 | indent_level = 0 137 | name = colorize(host_or_item.name, 'not_so_bold') 138 | else: 139 | indent_level = 4 140 | if isinstance(host_or_item, dict): 141 | if 'key' in host_or_item.keys(): 142 | host_or_item = host_or_item['key'] 143 | name = colorize(to_text(host_or_item), 'bold') 144 | 145 | if error: 146 | color = 'failed' 147 | change_string = colorize('FAILED!!!', color) 148 | else: 149 | color = 'changed' if changed else 'ok' 150 | change_string = colorize("changed={0}".format(changed), color) 151 | 152 | msg = colorize(msg, color) 153 | 154 | line_length = 120 155 | spaces = ' ' * (40 - len(name) - indent_level) 156 | line = "{0} * {1}{2}- {3}".format(' ' * indent_level, name, spaces, change_string) 157 | 158 | if len(msg) < 50: 159 | line += ' -- {0}'.format(msg) 160 | print("{0} {1}---------".format(line, '-' * (line_length - len(line)))) 161 | else: 162 | print("{0} {1}".format(line, '-' * (line_length - len(line)))) 163 | print(self._indent_text(msg, indent_level + 4)) 164 | 165 | if diff: 166 | self._print_diff(diff, indent_level) 167 | if stdout: 168 | stdout = colorize(stdout, 'failed') 169 | print(self._indent_text(stdout, indent_level + 4)) 170 | if stderr: 171 | stderr = colorize(stderr, 'failed') 172 | print(self._indent_text(stderr, indent_level + 4)) 173 | 174 | def v2_playbook_on_play_start(self, play): 175 | """Run on start of the play.""" 176 | pass 177 | 178 | def v2_playbook_on_task_start(self, task, **kwargs): 179 | """Run when a task starts.""" 180 | self.last_task_name = task.get_name() 181 | self.printed_last_task = False 182 | 183 | def _print_task_result(self, result, error=False, **kwargs): 184 | """Run when a task finishes correctly.""" 185 | 186 | if 'print_action' in result._task.tags or error or self._display.verbosity > 1: 187 | self._print_task() 188 | self.last_skipped = False 189 | msg = to_text(result._result.get('msg', '')) or\ 190 | to_text(result._result.get('reason', '')) 191 | 192 | stderr = [result._result.get('exception', None), 193 | result._result.get('module_stderr', None)] 194 | stderr = "\n".join([e for e in stderr if e]).strip() 195 | 196 | self._print_host_or_item(result._host, 197 | result._result.get('changed', False), 198 | msg, 199 | result._result.get('diff', None), 200 | is_host=True, 201 | error=error, 202 | stdout=result._result.get('module_stdout', None), 203 | stderr=stderr.strip(), 204 | ) 205 | if 'results' in result._result: 206 | for r in result._result['results']: 207 | failed = 'failed' in r and r['failed'] 208 | 209 | stderr = [r.get('exception', None), r.get('module_stderr', None)] 210 | stderr = "\n".join([e for e in stderr if e]).strip() 211 | 212 | self._print_host_or_item(r['item'], 213 | r.get('changed', False), 214 | to_text(r.get('msg', '')), 215 | r.get('diff', None), 216 | is_host=False, 217 | error=failed, 218 | stdout=r.get('module_stdout', None), 219 | stderr=stderr.strip(), 220 | ) 221 | else: 222 | self.last_skipped = True 223 | print("\n\n", end="") 224 | 225 | def v2_playbook_on_stats(self, stats): 226 | """Display info about playbook statistics.""" 227 | print() 228 | self.printed_last_task = False 229 | #self._print_task('STATS') 230 | 231 | hosts = sorted(stats.processed.keys()) 232 | for host in hosts: 233 | s = stats.summarize(host) 234 | 235 | if s['failures'] or s['unreachable']: 236 | color = 'failed' 237 | elif s['changed']: 238 | color = 'changed' 239 | else: 240 | color = 'ok' 241 | 242 | msg = '{0} : ok={1}\tchanged={2}\tfailed={3}\tunreachable={4}\trescued={5}\tignored={6}'.format( 243 | host, s['ok'], s['changed'], s['failures'], s['unreachable'], s['rescued'], s['ignored']) 244 | msg = '' 245 | print(colorize(msg, color)) 246 | 247 | def v2_runner_on_skipped(self, result, **kwargs): 248 | """Run when a task is skipped.""" 249 | if self._display.verbosity > 1: 250 | self._print_task() 251 | self.last_skipped = False 252 | 253 | line_length = 120 254 | spaces = ' ' * (31 - len(result._host.name) - 4) 255 | 256 | line = " * {0}{1}- {2}".format(colorize(result._host.name, 'not_so_bold'), 257 | spaces, 258 | colorize("skipped", 'skipped'),) 259 | 260 | reason = result._result.get('skipped_reason', '') or \ 261 | result._result.get('skip_reason', '') 262 | if len(reason) < 50: 263 | line += ' -- {0}'.format(reason) 264 | print("{0} {1}---------".format(line, '-' * (line_length - len(line)))) 265 | else: 266 | print("{0} {1}".format(line, '-' * (line_length - len(line)))) 267 | print(self._indent_text(reason, 8)) 268 | print(reason) 269 | 270 | def v2_runner_on_ok(self, result, **kwargs): 271 | self._print_task_result(result, error=False, **kwargs) 272 | 273 | def v2_runner_on_failed(self, result, **kwargs): 274 | self._print_task_result(result, error=True, **kwargs) 275 | 276 | def v2_runner_on_unreachable(self, result, **kwargs): 277 | self._print_task_result(result, error=True, **kwargs) 278 | 279 | v2_playbook_on_handler_task_start = v2_playbook_on_task_start 280 | -------------------------------------------------------------------------------- /wizard/prompt.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: localhost 3 | connection: local 4 | vars: 5 | platforms: 6 | - k3s 7 | - eks 8 | - dkp 9 | lb_protocols: 10 | - http 11 | - https 12 | 13 | tasks: 14 | - name: "\nSelect the number of the Kubernetes platform you are using to install Ascender:" 15 | pause: 16 | prompt: | 17 | {% for a in platforms %} 18 | {{ loop.index0 + 1}} - {{ a }} 19 | {% endfor -%} 20 | Enter your selection 21 | register: platform 22 | until: platform.user_input|default('') in lookup('sequence', 'end=' + (platforms | count | string) + ' start=1') 23 | retries: 100 24 | delay: 0 25 | 26 | # - debug: 27 | # msg: "{{ platform }}" 28 | # tags: [print_action] 29 | 30 | - set_fact: 31 | k8s_platform: "{{ platforms[platform.user_input | int - 1] }}" 32 | 33 | - name: "\nSelect the number of the prototol you want to support. Selecting 'https' requires the SSL certificate being present:" 34 | pause: 35 | prompt: | 36 | {% for a in lb_protocols %} 37 | {{ loop.index0 + 1 }} - {{ a }} 38 | {% endfor -%} 39 | Enter your selection 40 | register: lb_protocol 41 | until: lb_protocol.user_input|default('') in lookup('sequence', 'end=' + (lb_protocols | count | string) + ' start=1') 42 | retries: 100 43 | delay: 0 44 | 45 | - set_fact: 46 | k8s_lb_protocol: "{{ lb_protocols[lb_protocol.user_input | int - 1] }}" 47 | 48 | - template: 49 | src: templates/custom.config.yml.j2 50 | dest: custom.config.yml -------------------------------------------------------------------------------- /wizard/templates/custom.config.yml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | # ---Kubernetes-specific variables--- 3 | 4 | # This variable specificies which Kubernetes platform Ascender and its components will be installed on. 5 | k8s_platform: {{ k8s_platform }} # Options include k3s, eks and dkp, with more to come. 6 | 7 | # Determines whether to use HTTP or HTTPS for Ascender and Ledger. 8 | # If set to https, you MUST provide certificate/key options for the Installer to use. 9 | k8s_lb_protocol: {{ k8s_lb_protocol }} #options include http and https 10 | -------------------------------------------------------------------------------- /wizard/wizard.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ANSIBLE_STDOUT_CALLBACK=custom_selective ANSIBLE_SELECTIVE_DONT_COLORIZE=True ANSIBLE_LOCALHOST_WARNING=False ANSIBLE_INVENTORY_UNPARSED_WARNING=False ANSIBLE_DISPLAY_SKIPPED_HOSTS=False ANSIBLE_DISPLAY_OK_HOSTS=False ANSIBLE_SHOW_CUSTOM_STATS=no ANSIBLE_SHOW_PER_HOST_START=no ansible-playbook prompt.yml 3 | --------------------------------------------------------------------------------
DateServerPlaybookTaskModuleChange