diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-14 20:03:01 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-14 20:03:01 +0000 |
commit | a453ac31f3428614cceb99027f8efbdb9258a40b (patch) | |
tree | f61f87408f32a8511cbd91799f9cececb53e0374 /collections-debian-merged/ansible_collections/awx | |
parent | Initial commit. (diff) | |
download | ansible-upstream.tar.xz ansible-upstream.zip |
Adding upstream version 2.10.7+merged+base+2.10.8+dfsg.upstream/2.10.7+merged+base+2.10.8+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'collections-debian-merged/ansible_collections/awx')
121 files changed, 18031 insertions, 0 deletions
diff --git a/collections-debian-merged/ansible_collections/awx/awx/COPYING b/collections-debian-merged/ansible_collections/awx/awx/COPYING new file mode 100644 index 00000000..b743e04e --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/COPYING @@ -0,0 +1,674 @@ +GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + + Preamble + +The GNU General Public License is a free, copyleft license for +software and other kinds of works. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + +Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + +Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +1. Source Code. + +The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + +a) The work must carry prominent notices stating that you modified +it, and giving a relevant date. + +b) The work must carry prominent notices stating that it is +released under this License and any conditions added under section +7. This requirement modifies the requirement in section 4 to +"keep intact all notices". + +c) You must license the entire work, as a whole, under this +License to anyone who comes into possession of a copy. This +License will therefore apply, along with any applicable section 7 +additional terms, to the whole of the work, and all its parts, +regardless of how they are packaged. This License gives no +permission to license the work in any other way, but it does not +invalidate such permission if you have separately received it. + +d) If the work has interactive user interfaces, each must display +Appropriate Legal Notices; however, if the Program has interactive +interfaces that do not display Appropriate Legal Notices, your +work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + +a) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by the +Corresponding Source fixed on a durable physical medium +customarily used for software interchange. + +b) Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by a +written offer, valid for at least three years and valid for as +long as you offer spare parts or customer support for that product +model, to give anyone who possesses the object code either (1) a +copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical +medium customarily used for software interchange, for a price no +more than your reasonable cost of physically performing this +conveying of source, or (2) access to copy the +Corresponding Source from a network server at no charge. + +c) Convey individual copies of the object code with a copy of the +written offer to provide the Corresponding Source. This +alternative is allowed only occasionally and noncommercially, and +only if you received the object code with such an offer, in accord +with subsection 6b. + +d) Convey the object code by offering access from a designated +place (gratis or for a charge), and offer equivalent access to the +Corresponding Source in the same way through the same place at no +further charge. You need not require recipients to copy the +Corresponding Source along with the object code. If the place to +copy the object code is a network server, the Corresponding Source +may be on a different server (operated by you or a third party) +that supports equivalent copying facilities, provided you maintain +clear directions next to the object code saying where to find the +Corresponding Source. Regardless of what server hosts the +Corresponding Source, you remain obligated to ensure that it is +available for as long as needed to satisfy these requirements. + +e) Convey the object code using peer-to-peer transmission, provided +you inform other peers where the object code and Corresponding +Source of the work are being offered to the general public at no +charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + +a) Disclaiming warranty or limiting liability differently from the +terms of sections 15 and 16 of this License; or + +b) Requiring preservation of specified reasonable legal notices or +author attributions in that material or in the Appropriate Legal +Notices displayed by works containing it; or + +c) Prohibiting misrepresentation of the origin of that material, or +requiring that modified versions of such material be marked in +reasonable ways as different from the original version; or + +d) Limiting the use for publicity purposes of names of licensors or +authors of the material; or + +e) Declining to grant rights under trademark law for use of some +trade names, trademarks, or service marks; or + +f) Requiring indemnification of licensors and authors of that +material by anyone who conveys the material (or modified versions of +it) with contractual assumptions of liability to the recipient, for +any liability that these contractual assumptions directly impose on +those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + +<one line to give the program's name and a brief idea of what it does.> +Copyright (C) <year> <name of author> + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + +<program> Copyright (C) <year> <name of author> +This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. +This is free software, and you are welcome to redistribute it +under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + +The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. diff --git a/collections-debian-merged/ansible_collections/awx/awx/FILES.json b/collections-debian-merged/ansible_collections/awx/awx/FILES.json new file mode 100644 index 00000000..6156d893 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/FILES.json @@ -0,0 +1,1419 @@ +{ + "files": [ + { + "format": 1, + "ftype": "dir", + "chksum_sha256": null, + "name": ".", + "chksum_type": null + }, + { + "ftype": "file", + "chksum_sha256": "7c50cd9b85e2b7eebaea2b5618b402862b01d5a66befff8e41401ef3f14e471a", + "name": "COPYING", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "971ddb2ef90aabfb6d49519e15688181f41a8abd580f4fe9d6ac430d1039a9b2", + "name": "README.md", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "meta", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "ddf9dd01549307a457f256c839eb76b271472362466c29c9dcf3776a863591ab", + "name": "meta/runtime.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "plugins", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "plugins/doc_fragments", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "aaaf77e111937fb586f689d18b41f8c5a3d656390645d7617e798e031d31b0e6", + "name": "plugins/doc_fragments/auth.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "740c0c506c28ee8693546638aca964ccc3ed332446a5574a8a213716d6acebcb", + "name": "plugins/doc_fragments/auth_legacy.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "526100f565401e2811e049e966d0c9afcc0102c9b0b8c62ba39cded962bc5222", + "name": "plugins/doc_fragments/auth_plugin.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "plugins/inventory", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "7fb770eacf91c4fdc0ac775992177ee5b4a5c06aaec433498b5714ae2e440729", + "name": "plugins/inventory/tower.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "plugins/lookup", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "b00fe585d53b403723975974ab2ef744ea9104bd72df1b62b814a96d38d9148d", + "name": "plugins/lookup/tower_api.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "9b23ead83ef4a9e98c0c47b4491dbec8677902ef39452f184cc695e6d71886f1", + "name": "plugins/lookup/tower_schedule_rrule.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "plugins/module_utils", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "9fce90ba980270326df6c28d269f8bdf36bb3a9d23412c808c05bc76efe181e4", + "name": "plugins/module_utils/tower_api.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "bc4c977bb08d5a815d72f1dfe3bba2e8ec6d322233c638e919bfb4f340379ee2", + "name": "plugins/module_utils/tower_awxkit.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "162ff0173de655277cc6eeb02ae0481fcab98a5da1705d6d2963b822be44f0fd", + "name": "plugins/module_utils/tower_legacy.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "201f0e767a1c03a5ed68b582a4a2b5d72181e3adb96ddf18dade169dbd052920", + "name": "plugins/module_utils/tower_module.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "plugins/modules", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "name": "plugins/modules/__init__.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "645f3d07b54820a9471dace5a04e1e376c3c9fc30424531253e5d9b992ce1d12", + "name": "plugins/modules/tower_credential.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "3d198d9ed6a981137e40f6fd8f88fe7f51e666e59588ed7b2eeda8302c92d7a3", + "name": "plugins/modules/tower_credential_input_source.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "97fe4bc8a2dba7699751c68219bbaa7f49d92f222ebbebe6d9a3a6c0e66a86b7", + "name": "plugins/modules/tower_credential_type.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "368613ada4bb4a4a06fa8041e18ae8d99caea041b4aace9dc5cb447a61e1f6f0", + "name": "plugins/modules/tower_export.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "54b67799c8eeb585d7c354add356144846276d8993541f8a3b372767300b306a", + "name": "plugins/modules/tower_group.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "f3b83b4c793fe1251a0178e0c9fc54227c08ff191fdeca21ffc80f6cb961e688", + "name": "plugins/modules/tower_host.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "b1bebd0fe86f8ad9fc371aca1feec6d3949f0c1c6fff72c9ecb742744e5748e4", + "name": "plugins/modules/tower_import.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "3ce630f169816d027bc091219cb9e5108fd5ec54e6da5927f0306b5f4ab09f34", + "name": "plugins/modules/tower_inventory.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "f5e4f89c29f7f72716486c69db63862a9e6244ffef3508c1fc1fa3242eff0093", + "name": "plugins/modules/tower_inventory_source.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "9b333d5dda7aa132e3c487ea79208094859972055c7e6ce6c643f8ddc595cd16", + "name": "plugins/modules/tower_job_cancel.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "7798b0f881a292558e7a7d64b2de6556fce146b4f4a354a33387ed0873d25971", + "name": "plugins/modules/tower_job_launch.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "2afc1b226b9a3a6fcb58e25b539518dc16a658c235235464457a104832ba8618", + "name": "plugins/modules/tower_job_list.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "7afd00e7fdd160402b10ca9e4c8ebe9bd031c54a69a915109dfca90eafb3134c", + "name": "plugins/modules/tower_job_template.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "652955a7e41800e45337f458c687e3a5f5dd881e2b502e000644752068d74e77", + "name": "plugins/modules/tower_job_wait.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "3f1d8e4ed4d3e029164e59cb8ffda74d240312523c249a1dd3ede149e10b90c3", + "name": "plugins/modules/tower_label.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "6ec7cfb6eed6ce2913f2d4dac15e71fe4e0e93aae336c602e64f6ac314bbcd75", + "name": "plugins/modules/tower_license.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "da33b9b93cc056cee3d6e4fead16278745a8c968bc707aff7ee2fbf4b89ab075", + "name": "plugins/modules/tower_meta.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "b527e9eafc9e87bcc2dfcf88ada303af1723ff72f3c745f596eec590b3764658", + "name": "plugins/modules/tower_notification.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "086dcc99240d80d4defc30801469add9ff2344843cc27422a461f8ff544e2110", + "name": "plugins/modules/tower_organization.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "692aa22bc72618dcb5b4f630ab1c255b42d8faec8dd983535d53d73a76dc70b2", + "name": "plugins/modules/tower_project.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "8468afc15da7465cbf3c72d87e6e1aedbf4268b9bcade28facea7272f0623965", + "name": "plugins/modules/tower_receive.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "8193893c71e05ee65f26a0c8f0ed121efe0e554b4525f88b0bdb867518169f47", + "name": "plugins/modules/tower_role.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "39890d17bdc03d53cb246fef306e914389b81794c95ebfc06323da3529b5732d", + "name": "plugins/modules/tower_schedule.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "14b7227afaadcc2d89bdeed1bc82790bda08eb50ea8084f47088542d965d239d", + "name": "plugins/modules/tower_send.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "570dc79d3ce4ba6b60cfd8d6b9edaf3ebfc905701cebe628a018dc64433e4aba", + "name": "plugins/modules/tower_settings.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "ff79eadbabd993e0f5d210944045333aed26fba301c61fef2e130e48092fe3d5", + "name": "plugins/modules/tower_team.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "232ae8b52e33d38e72327d78892dca561a8c68d7660377f2593b615b51f20f52", + "name": "plugins/modules/tower_token.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "a585763e351494021f979b702d1a7e32be7031592e1c360627d88fb1491b1788", + "name": "plugins/modules/tower_user.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "c190b0dcfdad7ad31f38707178218804f42576ab2a50dff4b370bcdb38cbaf7d", + "name": "plugins/modules/tower_workflow_job_template.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "9a7d6e8bae3a9f0abe264061abb6a58cd7f3e497e7ba61b7e555f2d99af5a7b4", + "name": "plugins/modules/tower_workflow_job_template_node.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "94a9c31486a20093e745c2c875872a0b6afe2699b2b954fefb5280c87ce2371a", + "name": "plugins/modules/tower_workflow_launch.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "502923a998cfc50d5389279c078ff3a03d55463d095aa2588c2aa481d3fcd7eb", + "name": "plugins/modules/tower_workflow_template.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "b4e0a72acdb5b80917bc3af3e9872869d11265e98a6867f0a46664305008bd7d", + "name": "requirements.txt", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "05955e13ceb24a7db106aff7d1242bc4ed62f832b13e183731f1fddc0dbf6449", + "name": "setup.cfg", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "test", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "test/awx", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "95f3534ff1ec6a31c83f1cff777510f571246d71ed5a2eda85e39ca2088b7ed1", + "name": "test/awx/conftest.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "60308ed4fe0f146de32fac0e28d82b259575f9a99feea2f3e7c8e4f3aafef8cf", + "name": "test/awx/test_credential.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "e05f99a513e8b869183f37c55030812d11b32a694cf41c1fa17a7f7c554bffbe", + "name": "test/awx/test_credential_input_source.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "6a22345c0ac27828fcdd314ab78f8c2e1c05dd59e5aedeee8ab00a149dffc424", + "name": "test/awx/test_credential_type.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "5884ef919d19ace125306e02afd9c3e6a7bc2154430027a8fbc31e69b3f15a1b", + "name": "test/awx/test_group.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "5c5d11da5c45343357c88274c6431e8cc05caea00e50939f9eb4ebf70486f728", + "name": "test/awx/test_inventory.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "7aa431446502d53fd21180cf090341091c8752012b14758a274855794479560f", + "name": "test/awx/test_inventory_source.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "92e5a2742224842fbae2314b353a7974cd611d683a304a46b8b67bbb9eb0191d", + "name": "test/awx/test_job.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "f9f1456d56ceba5485fb422a5b014ad360cda53a4d4e2316db8f8886c1066208", + "name": "test/awx/test_job_template.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "cf6c2eb53bb9813a67c88d03a9c69fe1418e30ec1b4f65e5b97d91020b92b32d", + "name": "test/awx/test_label.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "7d856f69a0da54b5882293f3d78a0f38c4ac0a0c93a324d47e21138e7dd9cfaf", + "name": "test/awx/test_module_utils.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "08ee57748e7c99b4337731c9573124ad701c8143ab9a145efb4b9c0b405222e0", + "name": "test/awx/test_notification.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "8ba6c8395909413b6638583c4f5bc6e927269336e8ef1fb376b3f12be71fee88", + "name": "test/awx/test_organization.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "9815c0c98a474118b60ef21a3640111d957e823084686c7a3817351cd625d9fd", + "name": "test/awx/test_project.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "65dcfea204f2544de698a5edbc0e41eb260fd460cb256ef6c9f3068c333f6a22", + "name": "test/awx/test_role.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "f6bef79884ffe9db7ae581e9392c6b4bd794760fdfc4a4c23e1290994d1ddf12", + "name": "test/awx/test_schedule.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "d8e5dd41ca1221ee30ec5e6468a838e90fe041be7bc4740b9228c3ba3c6b0657", + "name": "test/awx/test_send_receive.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "10004dff541cd01f1d9f6c2e5ef89d716cd8775b51f97cb9ed40dc839958c823", + "name": "test/awx/test_settings.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "9768fd13b29b7c7f79ff98fa48cd9b765d05cda2f39de9a91810a31534aa5198", + "name": "test/awx/test_team.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "8d19762c0c8f84b31cb58c43ef699acd4799d57594a3e23dc067ce6373f89e61", + "name": "test/awx/test_token.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "067e1e8bc996911aade13805ccd0edbfead9c56bcaf3042c47ef0b5ce9393bc1", + "name": "test/awx/test_user.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "baee4c65d178e67a5e36397c947bcbb96fdb15eb655141d6de3757e50cf94245", + "name": "test/awx/test_workflow_job_template.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "d7e8d6ecc333aa469ad91174a48fea9f56ea26bb89bc1e876b616a44f4b85d08", + "name": "test/awx/test_workflow_job_template_node.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "a4c6141d13ee1cff044e7a4b401faa755a20be98cd85e9a2e4d97af00fb408ae", + "name": "test/awx/test_workflow_template.py", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/demo_data", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/demo_data/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "e9266afb6c233a959b22aebb9d644712f5fbd1b821e64f0b636e90e1e9391442", + "name": "tests/integration/targets/demo_data/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_credential", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_credential/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "90ce93d3ff55756c991acbed6ef3895665b250a636cabd8395fc6ee681e53d1f", + "name": "tests/integration/targets/tower_credential/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_credential_input_source", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_credential_input_source/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "bf92e526888340685b170b2608b2428683596b76d32c12787af023e518ef9024", + "name": "tests/integration/targets/tower_credential_input_source/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_credential_type", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_credential_type/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "44d30e6ad90487d741984303474d43d095f494a35f360c1dedd2b67a9068041e", + "name": "tests/integration/targets/tower_credential_type/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_export", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "52e1315ef042495cdf2b0ce22d8ba47f726dce15b968e301a795be1f69045f20", + "name": "tests/integration/targets/tower_export/aliases", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_export/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "2b09e1a9cc24ce4637d72580f0b70525cb4c9b55b9d1b737a5512b4501558b35", + "name": "tests/integration/targets/tower_export/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_group", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_group/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "c95a0700d2196bfede38981422a23328f2ac4718c77c606ae91c5d57f9d92418", + "name": "tests/integration/targets/tower_group/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_host", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_host/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "4de178adbcad25f8936c0c65bf082422d7a5e07e718e509b783c003bb2d2f129", + "name": "tests/integration/targets/tower_host/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_import", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "52e1315ef042495cdf2b0ce22d8ba47f726dce15b968e301a795be1f69045f20", + "name": "tests/integration/targets/tower_import/aliases", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_import/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "a3d018cc57193a4a2d052f244643d05b1905d9f2f926189e0890622a56b603af", + "name": "tests/integration/targets/tower_import/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_inventory", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_inventory/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "2351ca1932e4d77756d138bf3424ce4b010c156498184aeb8fbbe154b48866d6", + "name": "tests/integration/targets/tower_inventory/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_inventory_source", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_inventory_source/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "3929ccd0ca2946624609370129664d915bbb501eb0f9adba486e608d95dc03cd", + "name": "tests/integration/targets/tower_inventory_source/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_job_cancel", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_job_cancel/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "84082b74bb0ae8ddabda3c3e9ff10ecffdd9b7030ae97bab95ae43c39a452a93", + "name": "tests/integration/targets/tower_job_cancel/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_job_launch", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_job_launch/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "c94d0f4fe761a75ba34b2a9cb8b63299ccc7a80f9e020e9b2d368a6c378706b5", + "name": "tests/integration/targets/tower_job_launch/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_job_list", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_job_list/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "b6ed99f7f196254e088a7c431027f44b550ec8cf4bb0aad6db5ccb80cc0ebeeb", + "name": "tests/integration/targets/tower_job_list/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_job_template", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_job_template/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "7c753026216baebc4301276e5569b32f59bcc9a678158dc734fdf8d466c4e1e6", + "name": "tests/integration/targets/tower_job_template/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_job_wait", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_job_wait/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "9e496726eec92a1ff679a3bf68ffba4219b76b86e4fe1901b63b323125d5a6a8", + "name": "tests/integration/targets/tower_job_wait/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_label", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_label/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "3a27d38eae3669311f69ef9f649780fad79ef27f10114f7383cb9b92f74d1634", + "name": "tests/integration/targets/tower_label/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_lookup_api_plugin", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_lookup_api_plugin/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "9e910d52586572d72bf4b2c416df3a7517df31d0ffa1fb1ab5c0bd6da20ac206", + "name": "tests/integration/targets/tower_lookup_api_plugin/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_notification", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_notification/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "585c970aa38f77e16abbb18402f2e4357b9ff90d9812f6de4535496afd5c3668", + "name": "tests/integration/targets/tower_notification/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_organization", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_organization/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "1abecb4347cfb1b77d8e2b2fe90d64632c7d2e39cefd21318f25d07d91c70e70", + "name": "tests/integration/targets/tower_organization/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_project", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_project/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "c456db8e1a6f513a6367c55d11d41d638dd68e99211756cea035c82f8cda7ed1", + "name": "tests/integration/targets/tower_project/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_project_manual", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_project_manual/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "218e9fc99f54cb65146cc5c41139165111060e8b721f5567a86409ea2bdcfb51", + "name": "tests/integration/targets/tower_project_manual/tasks/create_project_dir.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "190a73a479cd59e17357aa09bc5ca4988c615533d5113560f06d61b64229ae91", + "name": "tests/integration/targets/tower_project_manual/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_role", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_role/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "4928fb6ac44c0be4497b1cb3a42b32502b132d92ac739714d1debf49b0349cb2", + "name": "tests/integration/targets/tower_role/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_schedule", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_schedule/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "ad0ede2f4a20b061bb8ef3c99fb383fa32113fb4fb363747408393f5ac684bcf", + "name": "tests/integration/targets/tower_schedule/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_schedule_rrule", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_schedule_rrule/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "cc0e845e9168ce30e9587cbe702402873f476643fbe5cde3c6fafadcd14a7cf3", + "name": "tests/integration/targets/tower_schedule_rrule/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_settings", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_settings/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "8cb8aa8fd34ad4b10fa2a5b10e9feba359c73e414694746e1920ddd8167dc2d3", + "name": "tests/integration/targets/tower_settings/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_team", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_team/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "427187eac35d6486853ce358ea3bca7ad5f822c19063d3e593f9c2b03e01fe79", + "name": "tests/integration/targets/tower_team/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_token", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_token/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "197e2ec188bd2270b417a8806c98cfa477feecb6c6f9663b10c480f2f3ba91f9", + "name": "tests/integration/targets/tower_token/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_user", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_user/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "9b70d3ec85846ced6c006ef90b6eb766313e6762b4f220617cd6655b6194abb0", + "name": "tests/integration/targets/tower_user/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_workflow_job_template", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_workflow_job_template/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "22dd3978317b05626e1f2be7410e67043c0c52227f3fa7e321db33016cf088cd", + "name": "tests/integration/targets/tower_workflow_job_template/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_workflow_launch", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/integration/targets/tower_workflow_launch/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "f97fddca20e1a459850373b1c4e87fb3da9df4833abb606ba1305b787f6b9b1a", + "name": "tests/integration/targets/tower_workflow_launch/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tests/sanity", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "d130191bab94e26ce61f8205a9431946b94235730e5cb8e5c3c0b0728fe965eb", + "name": "tests/sanity/ignore-2.10.txt", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "00ee77efbc55acc6500f6052038e79541f6c7c8563d20a7852a5f525f5dcf5d9", + "name": "tests/sanity/ignore-2.9.txt", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tools", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "aa0d7cefef5234cf5e143c769fa537c4142d411e815dd2b2599891f137ac2347", + "name": "tools/generate.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tools/roles", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tools/roles/generate", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tools/roles/generate/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "3aecc6ba819a7b18e755f80207316a9960c8a8e8ce4eeab7fee798ba91a989fb", + "name": "tools/roles/generate/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tools/roles/generate/templates", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "10a53db0de9332b1ba639d9fb7ab5c236df50a57220fcc6f27c44fb2409d49ba", + "name": "tools/roles/generate/templates/tower_module.j2", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tools/roles/template_galaxy", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tools/roles/template_galaxy/tasks", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "7a1c0a42110ad8816c45291cd870dfc3c0a8486fa12d0ce11c56b59d465e7328", + "name": "tools/roles/template_galaxy/tasks/main.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tools/roles/template_galaxy/templates", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "7caad7492660b716f55b21176b33db1687c34bc6829786400981778db7501a79", + "name": "tools/roles/template_galaxy/templates/README.md.j2", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "9d549e115b3fbc22b12cdac959607c73b302183060374bd1be915fd02afd95ec", + "name": "tools/roles/template_galaxy/templates/galaxy.yml.j2", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "87328b1102cb42e5c49a439e725eef477d8013fe6a61889b3251be061c9749de", + "name": "tools/template_galaxy.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "dir", + "chksum_sha256": null, + "name": "tools/vars", + "chksum_type": null, + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "1b7881b3f397d274bf8758f1df94b82f95dcf2438ae0288c5e59e0a92da0c47a", + "name": "tools/vars/aliases.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "47f50cc9015ea57b2217c42d0022476e846acde3c9d3e39745339dc5f08f7ded", + "name": "tools/vars/associations.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "c4cfcf4390c7d64356e4ea14521ebda56db71d2a1cb0eebcd83002f4c706d726", + "name": "tools/vars/examples.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "2af0e6f768d2c9d05a62eb01b6dc125d1c7d3a7de7f8a7267dfc5bcd307f0773", + "name": "tools/vars/generate_for.yml", + "chksum_type": "sha256", + "format": 1 + }, + { + "ftype": "file", + "chksum_sha256": "17eb9fa13ff5c6d88b48a1ac2dc4c32b58181d4a277cacbb31ad09b25a81d84a", + "name": "tools/vars/resolution.yml", + "chksum_type": "sha256", + "format": 1 + } + ], + "format": 1 +}
\ No newline at end of file diff --git a/collections-debian-merged/ansible_collections/awx/awx/MANIFEST.json b/collections-debian-merged/ansible_collections/awx/awx/MANIFEST.json new file mode 100644 index 00000000..ebf10ad6 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/MANIFEST.json @@ -0,0 +1,36 @@ +{ + "collection_info": { + "description": "Ansible content that interacts with the AWX or Ansible Tower API.", + "repository": "https://github.com/ansible/awx", + "tags": [ + "cloud", + "infrastructure", + "awx", + "ansible", + "automation" + ], + "dependencies": {}, + "authors": [ + "AWX Project Contributors <awx-project@googlegroups.com>" + ], + "issues": "https://github.com/ansible/awx/issues?q=is%3Aissue+label%3Acomponent%3Aawx_collection", + "name": "awx", + "license": [ + "GPL-3.0-only" + ], + "documentation": "https://github.com/ansible/awx/blob/devel/awx_collection/README.md", + "namespace": "awx", + "version": "14.1.0", + "readme": "README.md", + "license_file": null, + "homepage": "https://www.ansible.com/" + }, + "file_manifest_file": { + "format": 1, + "ftype": "file", + "chksum_sha256": "12c59de0bf1e310de1fec6e5c659b9e4483ba517a7ab02d888ae2027b371f3f9", + "name": "FILES.json", + "chksum_type": "sha256" + }, + "format": 1 +}
\ No newline at end of file diff --git a/collections-debian-merged/ansible_collections/awx/awx/README.md b/collections-debian-merged/ansible_collections/awx/awx/README.md new file mode 100644 index 00000000..47b517c6 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/README.md @@ -0,0 +1,135 @@ +# AWX Ansible Collection + +[comment]: # (*******************************************************) +[comment]: # (* *) +[comment]: # (* WARNING *) +[comment]: # (* *) +[comment]: # (* This file is templated and not to be *) +[comment]: # (* edited directly! Instead modify: *) +[comment]: # (* tools/roles/template_galaxy/templates/README.md.j2 *) +[comment]: # (* *) +[comment]: # (* Changes to the base README.md file are refreshed *) +[comment]: # (* upon build of the collection *) +[comment]: # (*******************************************************) + +This Ansible collection allows for easy interaction with an AWX server via Ansible playbooks. + +This source for this collection lives in the `awx_collection` folder inside of the +AWX source. +The previous home for this collection was inside the folder [lib/ansible/modules/web_infrastructure/ansible_tower](https://github.com/ansible/ansible/tree/stable-2.9/lib/ansible/modules/web_infrastructure/ansible_tower) in the Ansible repo, +as well as other places for the inventory plugin, module utils, and +doc fragment. + +## Building and Installing + +This collection templates the `galaxy.yml` file it uses. +Run `make build_collection` from the root folder of the AWX source tree. +This will create the `tar.gz` file inside the `awx_collection` folder +with the current AWX version, for example: `awx_collection/awx-awx-9.2.0.tar.gz`. + +Installing the `tar.gz` involves no special instructions. + +## Running + +Non-deprecated modules in this collection have no Python requirements, but +may require the official [AWX CLI](https://docs.ansible.com/ansible-tower/latest/html/towercli/index.html) +in the future. The `DOCUMENTATION` for each module will report this. + +You can specify authentication by a combination of either: + + - host, username, password + - host, OAuth2 token + +The OAuth2 token is the preferred method. You can obtain a token via the +AWX CLI [login](https://docs.ansible.com/ansible-tower/latest/html/towercli/reference.html#awx-login) +command. + +These can be specified via (from highest to lowest precedence): + + - direct module parameters + - environment variables (most useful when running against localhost) + - a config file path specified by the `tower_config_file` parameter + - a config file at `~/.tower_cli.cfg` + - a config file at `/etc/tower/tower_cli.cfg` + +Config file syntax looks like this: + +``` +[general] +host = https://localhost:8043 +verify_ssl = true +oauth_token = LEdCpKVKc4znzffcpQL5vLG8oyeku6 +``` + +## Release and Upgrade Notes + +Notable releases of the `awx.awx` collection: + + - 7.0.0 is intended to be identical to the content prior to the migration, aside from changes necessary to function as a collection. + - 11.0.0 has no non-deprecated modules that depend on the deprecated `tower-cli` [PyPI](https://pypi.org/project/ansible-tower-cli/). + - 0.0.1-devel is the version you should see if installing from source, which is intended for development and expected to be unstable. + +The following notes are changes that may require changes to playbooks: + + - When a project is created, it will wait for the update/sync to finish by default; this can be turned off with the `wait` parameter, if desired. + - Creating a "scan" type job template is no longer supported. + - Specifying a custom certificate via the `TOWER_CERTIFICATE` environment variable no longer works. + - Type changes of variable fields: + + - `extra_vars` in the `tower_job_launch` module worked with a `list` previously, but now only works with a `dict` type + - `extra_vars` in the `tower_workflow_job_template` module worked with a `string` previously but now expects a `dict` + - When the `extra_vars` parameter is used with the `tower_job_launch` module, the launch will fail unless `ask_extra_vars` or `survey_enabled` is explicitly set to `True` on the Job Template + - The `variables` parameter in the `tower_group`, `tower_host` and `tower_inventory` modules now expects a `dict` type and no longer supports the use of `@` syntax for a file + + + - Type changes of other types of fields: + + - `inputs` or `injectors` in the `tower_credential_type` module worked with a string previously but now expects a `dict` + - `schema` in the `tower_workflow_job_template` module worked with a `string` previously but not expects a `list` of `dict`s + + - `tower_group` used to also service inventory sources, but this functionality has been removed from this module; use `tower_inventory_source` instead. + - Specified `tower_config` file used to handle `k=v` pairs on a single line; this is no longer supported. Please use a file formatted as `yaml`, `json` or `ini` only. + - Some return values (e.g., `credential_type`) have been removed. Use of `id` is recommended. + - `tower_job_template` no longer supports the deprecated `extra_vars_path` parameter, please use `extra_vars` with the lookup plugin to replace this functionality. + - The `notification_configuration` parameter of `tower_notification` has changed from a string to a dict. Please use the `lookup` plugin to read an existing file into a dict. + - `tower_credential` no longer supports passing a file name to ssh_key_data. + - The HipChat `notification_type` has been removed and can no longer be created using the `tower_notification` module. + +## Running Unit Tests + +Tests to verify compatibility with the most recent AWX code are in `awx_collection/test/awx`. +These can be ran by `make test_collection` in the development container. + +To run outside of the development container, or to run against +Ansible source, set up a working environment: + +``` +mkvirtualenv my_new_venv +# may need to replace psycopg2 with psycopg2-binary in requirements/requirements.txt +pip install -r requirements/requirements.txt -r requirements/requirements_dev.txt -r requirements/requirements_git.txt +make clean-api +pip install -e <path to your Ansible> +pip install -e . +pip install -e awxkit +py.test awx_collection/test/awx/ +``` + +## Running Integration Tests + +The integration tests require a virtualenv with `ansible` >= 2.9 and `tower_cli`. +The collection must first be installed, which can be done using `make install_collection`. +You also need a configuration file, as described in the running section. + +Run the tests: + +``` +# ansible-test must be run from the directory in which the collection is installed +cd ~/.ansible/collections/ansible_collections/awx/awx/ +ansible-test integration +``` + +## Licensing + +All content in this folder is licensed under the same license as Ansible, +which is the same as license that applied before the split into an +independent collection. diff --git a/collections-debian-merged/ansible_collections/awx/awx/meta/runtime.yml b/collections-debian-merged/ansible_collections/awx/awx/meta/runtime.yml new file mode 100644 index 00000000..3980ccc1 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/meta/runtime.yml @@ -0,0 +1,15 @@ +--- +plugin_routing: + modules: + tower_receive: + deprecation: + removal_date: TBD + warning_text: see plugin documentation for details + tower_send: + deprecation: + removal_date: TBD + warning_text: see plugin documentation for details + tower_workflow_template: + deprecation: + removal_date: TBD + warning_text: see plugin documentation for details diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/doc_fragments/auth.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/doc_fragments/auth.py new file mode 100644 index 00000000..1e77a63b --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/doc_fragments/auth.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Wayne Witzel III <wayne@riotousliving.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class ModuleDocFragment(object): + + # Ansible Tower documentation fragment + DOCUMENTATION = r''' +options: + tower_host: + description: + - URL to your Tower or AWX instance. + - If value not set, will try environment variable C(TOWER_HOST) and then config files + - If value not specified by any means, the value of C(127.0.0.1) will be used + type: str + tower_username: + description: + - Username for your Tower or AWX instance. + - If value not set, will try environment variable C(TOWER_USERNAME) and then config files + type: str + tower_password: + description: + - Password for your Tower or AWX instance. + - If value not set, will try environment variable C(TOWER_PASSWORD) and then config files + type: str + tower_oauthtoken: + description: + - The Tower OAuth token to use. + - This value can be in one of two formats. + - A string which is the token itself. (i.e. bqV5txm97wqJqtkxlMkhQz0pKhRMMX) + - A dictionary structure as returned by the tower_token module. + - If value not set, will try environment variable C(TOWER_OAUTH_TOKEN) and then config files + type: raw + version_added: "3.7" + validate_certs: + description: + - Whether to allow insecure connections to Tower or AWX. + - If C(no), SSL certificates will not be validated. + - This should only be used on personally controlled sites using self-signed certificates. + - If value not set, will try environment variable C(TOWER_VERIFY_SSL) and then config files + type: bool + aliases: [ tower_verify_ssl ] + tower_config_file: + description: + - Path to the Tower or AWX config file. + - If provided, the other locations for config files will not be considered. + type: path + +notes: +- If no I(config_file) is provided we will attempt to use the tower-cli library + defaults to find your Tower host information. +- I(config_file) should contain Tower configuration in the following format + host=hostname + username=username + password=password +''' diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/doc_fragments/auth_legacy.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/doc_fragments/auth_legacy.py new file mode 100644 index 00000000..bf0ea288 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/doc_fragments/auth_legacy.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Wayne Witzel III <wayne@riotousliving.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class ModuleDocFragment(object): + + # Ansible Tower documentation fragment + DOCUMENTATION = r''' +options: + tower_host: + description: + - URL to your Tower or AWX instance. + - If value not set, will try environment variable C(TOWER_HOST) and then config files + - If value not specified by any means, the value of C(127.0.0.1) will be used + type: str + tower_username: + description: + - Username for your Tower or AWX instance. + - If value not set, will try environment variable C(TOWER_USERNAME) and then config files + type: str + tower_password: + description: + - Password for your Tower or AWX instance. + - If value not set, will try environment variable C(TOWER_PASSWORD) and then config files + type: str + validate_certs: + description: + - Whether to allow insecure connections to Tower or AWX. + - If C(no), SSL certificates will not be validated. + - This should only be used on personally controlled sites using self-signed certificates. + - If value not set, will try environment variable C(TOWER_VERIFY_SSL) and then config files + type: bool + aliases: [ tower_verify_ssl ] + tower_config_file: + description: + - Path to the Tower or AWX config file. + - If provided, the other locations for config files will not be considered. + type: path + +notes: +- If no I(config_file) is provided we will attempt to use the tower-cli library + defaults to find your Tower host information. +- I(config_file) should contain Tower configuration in the following format + host=hostname + username=username + password=password +''' diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/doc_fragments/auth_plugin.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/doc_fragments/auth_plugin.py new file mode 100644 index 00000000..527054ed --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/doc_fragments/auth_plugin.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Ansible by Red Hat, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class ModuleDocFragment(object): + + # Ansible Tower documentation fragment + DOCUMENTATION = r''' +options: + host: + description: The network address of your Ansible Tower host. + env: + - name: TOWER_HOST + username: + description: The user that you plan to use to access inventories on Ansible Tower. + env: + - name: TOWER_USERNAME + password: + description: The password for your Ansible Tower user. + env: + - name: TOWER_PASSWORD + oauth_token: + description: + - The Tower OAuth token to use. + env: + - name: TOWER_OAUTH_TOKEN + verify_ssl: + description: + - Specify whether Ansible should verify the SSL certificate of Ansible Tower host. + - Defaults to True, but this is handled by the shared module_utils code + type: bool + env: + - name: TOWER_VERIFY_SSL + aliases: [ validate_certs ] + +notes: +- If no I(config_file) is provided we will attempt to use the tower-cli library + defaults to find your Tower host information. +- I(config_file) should contain Tower configuration in the following format + host=hostname + username=username + password=password +''' diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/inventory/tower.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/inventory/tower.py new file mode 100644 index 00000000..7dc4aaa1 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/inventory/tower.py @@ -0,0 +1,174 @@ +# Copyright (c) 2018 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +DOCUMENTATION = ''' +name: tower +plugin_type: inventory +author: + - Matthew Jones (@matburt) + - Yunfan Zhang (@YunfanZhang42) +short_description: Ansible dynamic inventory plugin for Ansible Tower. +description: + - Reads inventories from Ansible Tower. + - Supports reading configuration from both YAML config file and environment variables. + - If reading from the YAML file, the file name must end with tower.(yml|yaml) or tower_inventory.(yml|yaml), + the path in the command would be /path/to/tower_inventory.(yml|yaml). If some arguments in the config file + are missing, this plugin will try to fill in missing arguments by reading from environment variables. + - If reading configurations from environment variables, the path in the command must be @tower_inventory. +extends_documentation_fragment: awx.awx.auth_plugin +options: + inventory_id: + description: + - The ID of the Ansible Tower inventory that you wish to import. + - This is allowed to be either the inventory primary key or its named URL slug. + - Primary key values will be accepted as strings or integers, and URL slugs must be strings. + - Named URL slugs follow the syntax of "inventory_name++organization_name". + type: raw + env: + - name: TOWER_INVENTORY + required: True + include_metadata: + description: Make extra requests to provide all group vars with metadata about the source Ansible Tower host. + type: bool + default: False +''' + +EXAMPLES = ''' +# Before you execute the following commands, you should make sure this file is in your plugin path, +# and you enabled this plugin. + +# Example for using tower_inventory.yml file + +plugin: awx.awx.tower +host: your_ansible_tower_server_network_address +username: your_ansible_tower_username +password: your_ansible_tower_password +inventory_id: the_ID_of_targeted_ansible_tower_inventory +# Then you can run the following command. +# If some of the arguments are missing, Ansible will attempt to read them from environment variables. +# ansible-inventory -i /path/to/tower_inventory.yml --list + +# Example for reading from environment variables: + +# Set environment variables: +# export TOWER_HOST=YOUR_TOWER_HOST_ADDRESS +# export TOWER_USERNAME=YOUR_TOWER_USERNAME +# export TOWER_PASSWORD=YOUR_TOWER_PASSWORD +# export TOWER_INVENTORY=THE_ID_OF_TARGETED_INVENTORY +# Read the inventory specified in TOWER_INVENTORY from Ansible Tower, and list them. +# The inventory path must always be @tower_inventory if you are reading all settings from environment variables. +# ansible-inventory -i @tower_inventory --list +''' + +import os + +from ansible.module_utils import six +from ansible.module_utils._text import to_text, to_native +from ansible.errors import AnsibleParserError, AnsibleOptionsError +from ansible.plugins.inventory import BaseInventoryPlugin +from ansible.config.manager import ensure_type + +from ..module_utils.tower_api import TowerAPIModule + + +def handle_error(**kwargs): + raise AnsibleParserError(to_native(kwargs.get('msg'))) + + +class InventoryModule(BaseInventoryPlugin): + NAME = 'awx.awx.tower' # REPLACE + # Stays backward compatible with tower inventory script. + # If the user supplies '@tower_inventory' as path, the plugin will read from environment variables. + no_config_file_supplied = False + + def verify_file(self, path): + if path.endswith('@tower_inventory'): + self.no_config_file_supplied = True + return True + elif super(InventoryModule, self).verify_file(path): + return path.endswith(('tower_inventory.yml', 'tower_inventory.yaml', 'tower.yml', 'tower.yaml')) + else: + return False + + def warn_callback(self, warning): + self.display.warning(warning) + + def parse(self, inventory, loader, path, cache=True): + super(InventoryModule, self).parse(inventory, loader, path) + if not self.no_config_file_supplied and os.path.isfile(path): + self._read_config_data(path) + + # Defer processing of params to logic shared with the modules + module_params = {} + for plugin_param, module_param in TowerAPIModule.short_params.items(): + opt_val = self.get_option(plugin_param) + if opt_val is not None: + module_params[module_param] = opt_val + + module = TowerAPIModule( + argument_spec={}, direct_params=module_params, + error_callback=handle_error, warn_callback=self.warn_callback + ) + + # validate type of inventory_id because we allow two types as special case + inventory_id = self.get_option('inventory_id') + if isinstance(inventory_id, int): + inventory_id = to_text(inventory_id, nonstring='simplerepr') + else: + try: + inventory_id = ensure_type(inventory_id, 'str') + except ValueError as e: + raise AnsibleOptionsError( + 'Invalid type for configuration option inventory_id, ' + 'not integer, and cannot convert to string: {err}'.format(err=to_native(e)) + ) + inventory_id = inventory_id.replace('/', '') + inventory_url = '/api/v2/inventories/{inv_id}/script/'.format(inv_id=inventory_id) + + inventory = module.get_endpoint( + inventory_url, data={'hostvars': '1', 'towervars': '1', 'all': '1'} + )['json'] + + # To start with, create all the groups. + for group_name in inventory: + if group_name != '_meta': + self.inventory.add_group(group_name) + + # Then, create all hosts and add the host vars. + all_hosts = inventory['_meta']['hostvars'] + for host_name, host_vars in six.iteritems(all_hosts): + self.inventory.add_host(host_name) + for var_name, var_value in six.iteritems(host_vars): + self.inventory.set_variable(host_name, var_name, var_value) + + # Lastly, create to group-host and group-group relationships, and set group vars. + for group_name, group_content in six.iteritems(inventory): + if group_name != 'all' and group_name != '_meta': + # First add hosts to groups + for host_name in group_content.get('hosts', []): + self.inventory.add_host(host_name, group_name) + # Then add the parent-children group relationships. + for child_group_name in group_content.get('children', []): + self.inventory.add_child(group_name, child_group_name) + # Set the group vars. Note we should set group var for 'all', but not '_meta'. + if group_name != '_meta': + for var_name, var_value in six.iteritems(group_content.get('vars', {})): + self.inventory.set_variable(group_name, var_name, var_value) + + # Fetch extra variables if told to do so + if self.get_option('include_metadata'): + + config_data = module.get_endpoint('/api/v2/config/')['json'] + + server_data = {} + server_data['license_type'] = config_data.get('license_info', {}).get('license_type', 'unknown') + for key in ('version', 'ansible_version'): + server_data[key] = config_data.get(key, 'unknown') + self.inventory.set_variable('all', 'tower_metadata', server_data) + + # Clean up the inventory. + self.inventory.reconcile_inventory() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/lookup/tower_api.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/lookup/tower_api.py new file mode 100644 index 00000000..76b32be6 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/lookup/tower_api.py @@ -0,0 +1,196 @@ +# (c) 2020 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ +lookup: tower_api +author: John Westcott IV (@john-westcott-iv) +short_description: Search the API for objects +requirements: + - None +description: + - Returns GET requests from the Ansible Tower API. See + U(https://docs.ansible.com/ansible-tower/latest/html/towerapi/index.html) for API usage. + - For use that is cross-compatible between the awx.awx and ansible.tower collection + see the tower_meta module +extends_documentation_fragment: awx.awx.auth_plugin +options: + _terms: + description: + - The endpoint to query, i.e. teams, users, tokens, job_templates, etc. + required: True + query_params: + description: + - The query parameters to search for in the form of key/value pairs. + type: dict + required: False + aliases: [query, data, filter, params] + expect_objects: + description: + - Error if the response does not contain either a detail view or a list view. + type: boolean + default: False + aliases: [expect_object] + expect_one: + description: + - Error if the response contains more than one object. + type: boolean + default: False + return_objects: + description: + - If a list view is returned, promote the list of results to the top-level of list returned. + - Allows using this lookup plugin to loop over objects without additional work. + type: boolean + default: True + return_all: + description: + - If the response is paginated, return all pages. + type: boolean + default: False + return_ids: + description: + - If response contains objects, promote the id key to the top-level entries in the list. + - Allows looking up a related object and passing it as a parameter to another module. + - This will convert the return to a string or list of strings depending on the number of selected items. + type: boolean + aliases: [return_id] + default: False + max_objects: + description: + - if C(return_all) is true, this is the maximum of number of objects to return from the list. + - If a list view returns more an max_objects an exception will be raised + type: integer + default: 1000 + +notes: + - If the query is not filtered properly this can cause a performance impact. +""" + +EXAMPLES = """ +- name: Load the UI settings + set_fact: + tower_settings: "{{ lookup('awx.awx.tower_api', 'settings/ui') }}" + +- name: Report the usernames of all users with admin privs + debug: + msg: "Admin users: {{ query('awx.awx.tower_api', 'users', query_params={ 'is_superuser': true }) | map(attribute='username') | join(', ') }}" + +- name: debug all organizations in a loop # use query to return a list + debug: + msg: "Organization description={{ item['description'] }} id={{ item['id'] }}" + loop: "{{ query('awx.awx.tower_api', 'organizations') }}" + loop_control: + label: "{{ item['name'] }}" + +- name: Make sure user 'john' is an org admin of the default org if the user exists + tower_role: + organization: Default + role: admin + user: john + when: "lookup('awx.awx.tower_api', 'users', query_params={ 'username': 'john' }) | length == 1" + +- name: Create an inventory group with all 'foo' hosts + tower_group: + name: "Foo Group" + inventory: "Demo Inventory" + hosts: >- + {{ query( + 'awx.awx.tower_api', + 'hosts', + query_params={ 'name__startswith' : 'foo', }, + ) | map(attribute='name') | list }} + register: group_creation +""" + +RETURN = """ +_raw: + description: + - Response from the API + type: dict + returned: on successful request +""" + +from ansible.plugins.lookup import LookupBase +from ansible.errors import AnsibleError +from ansible.module_utils._text import to_native +from ansible.utils.display import Display +from ..module_utils.tower_api import TowerAPIModule + + +class LookupModule(LookupBase): + display = Display() + + def handle_error(self, **kwargs): + raise AnsibleError(to_native(kwargs.get('msg'))) + + def warn_callback(self, warning): + self.display.warning(warning) + + def run(self, terms, variables=None, **kwargs): + if len(terms) != 1: + raise AnsibleError('You must pass exactly one endpoint to query') + + # Defer processing of params to logic shared with the modules + module_params = {} + for plugin_param, module_param in TowerAPIModule.short_params.items(): + opt_val = self.get_option(plugin_param) + if opt_val is not None: + module_params[module_param] = opt_val + + # Create our module + module = TowerAPIModule( + argument_spec={}, direct_params=module_params, + error_callback=self.handle_error, warn_callback=self.warn_callback + ) + + self.set_options(direct=kwargs) + + response = module.get_endpoint(terms[0], data=self.get_option('query_params', {})) + + if 'status_code' not in response: + raise AnsibleError("Unclear response from API: {0}".format(response)) + + if response['status_code'] != 200: + raise AnsibleError("Failed to query the API: {0}".format(response['json'].get('detail', response['json']))) + + return_data = response['json'] + + if self.get_option('expect_objects') or self.get_option('expect_one'): + if ('id' not in return_data) and ('results' not in return_data): + raise AnsibleError( + 'Did not obtain a list or detail view at {0}, and ' + 'expect_objects or expect_one is set to True'.format(terms[0]) + ) + + if self.get_option('expect_one'): + if 'results' in return_data and len(return_data['results']) != 1: + raise AnsibleError( + 'Expected one object from endpoint {0}, ' + 'but obtained {1} from API'.format(terms[0], len(return_data['results'])) + ) + + if self.get_option('return_all') and 'results' in return_data: + if return_data['count'] > self.get_option('max_objects'): + raise AnsibleError( + 'List view at {0} returned {1} objects, which is more than the maximum allowed ' + 'by max_objects, {2}'.format(terms[0], return_data['count'], self.get_option('max_objects')) + ) + + next_page = return_data['next'] + while next_page is not None: + next_response = module.get_endpoint(next_page) + return_data['results'] += next_response['json']['results'] + next_page = next_response['json']['next'] + return_data['next'] = None + + if self.get_option('return_ids'): + if 'results' in return_data: + return_data['results'] = [str(item['id']) for item in return_data['results']] + elif 'id' in return_data: + return_data = str(return_data['id']) + + if self.get_option('return_objects') and 'results' in return_data: + return return_data['results'] + else: + return [return_data] diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/lookup/tower_schedule_rrule.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/lookup/tower_schedule_rrule.py new file mode 100644 index 00000000..918b9fa1 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/lookup/tower_schedule_rrule.py @@ -0,0 +1,248 @@ +# (c) 2020 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + lookup: tower_schedule_rrule + author: John Westcott IV (@john-westcott-iv) + short_description: Generate an rrule string which can be used for Tower Schedules + requirements: + - pytz + - python.dateutil >= 2.7.0 + description: + - Returns a string based on criteria which represents an rrule + options: + _terms: + description: + - The frequency of the schedule + - none - Run this schedule once + - minute - Run this schedule every x minutes + - hour - Run this schedule every x hours + - day - Run this schedule every x days + - week - Run this schedule weekly + - month - Run this schedule monthly + required: True + choices: ['none', 'minute', 'hour', 'day', 'week', 'month'] + start_date: + description: + - The date to start the rule + - Used for all frequencies + - Format should be YYYY-MM-DD [HH:MM:SS] + type: str + timezone: + description: + - The timezone to use for this rule + - Used for all frequencies + - Format should be as US/Eastern + - Defaults to America/New_York + type: str + every: + description: + - The repetition in months, weeks, days hours or minutes + - Used for all types except none + type: int + end_on: + description: + - How to end this schedule + - If this is not defined, this schedule will never end + - If this is a positive integer, this schedule will end after this number of occurences + - If this is a date in the format YYYY-MM-DD [HH:MM:SS], this schedule ends after this date + - Used for all types except none + type: str + on_days: + description: + - The days to run this schedule on + - A comma-separated list which can contain values sunday, monday, tuesday, wednesday, thursday, friday + - Used for week type schedules + month_day_number: + description: + - The day of the month this schedule will run on (0-31) + - Used for month type schedules + - Cannot be used with on_the parameter + type: int + on_the: + description: + - A description on when this schedule will run + - Two strings separated by a space + - First string is one of first, second, third, fourth, last + - Second string is one of sunday, monday, tuesday, wednesday, thursday, friday + - Used for month type schedules + - Cannot be used with month_day_number parameters +""" + +EXAMPLES = """ + - name: Create a string for a schedule + debug: + msg: "{{ query('awx.awx.tower_schedule_rrule', 'none', start_date='1979-09-13 03:45:07') }}" +""" + +RETURN = """ +_raw: + description: + - String in the rrule format + type: string +""" + +from ansible.plugins.lookup import LookupBase +from ansible.errors import AnsibleError +from datetime import datetime +import re +from distutils.version import LooseVersion + +missing_modules = [] +try: + import pytz +except ImportError: + missing_modules.append('pytz') + +try: + from dateutil import rrule +except ImportError: + missing_modules.append('python.dateutil') + +# Validate the version of python.dateutil +try: + import dateutil + if LooseVersion(dateutil.__version__) < LooseVersion("2.7.0"): + raise Exception +except Exception: + missing_modules.append('python.dateutil>=2.7.0') + +if len(missing_modules) > 0: + raise AnsibleError('You are missing the modules {0}'.format(', '.join(missing_modules))) + + +class LookupModule(LookupBase): + frequencies = { + 'none': rrule.DAILY, + 'minute': rrule.MINUTELY, + 'hour': rrule.HOURLY, + 'day': rrule.DAILY, + 'week': rrule.WEEKLY, + 'month': rrule.MONTHLY, + } + + weekdays = { + 'monday': rrule.MO, + 'tuesday': rrule.TU, + 'wednesday': rrule.WE, + 'thursday': rrule.TH, + 'friday': rrule.FR, + 'saturday': rrule.SA, + 'sunday': rrule.SU, + } + + set_positions = { + 'first': 1, + 'second': 2, + 'third': 3, + 'fourth': 4, + 'last': -1, + } + + @staticmethod + def parse_date_time(date_string): + try: + return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S') + except ValueError: + return datetime.strptime(date_string, '%Y-%m-%d') + + def run(self, terms, variables=None, **kwargs): + if len(terms) != 1: + raise AnsibleError('You may only pass one schedule type in at a time') + + frequency = terms[0].lower() + + return self.get_rrule(frequency, kwargs) + + @staticmethod + def get_rrule(frequency, kwargs): + + if frequency not in LookupModule.frequencies: + raise AnsibleError('Frequency of {0} is invalid'.format(frequency)) + + rrule_kwargs = { + 'freq': LookupModule.frequencies[frequency], + 'interval': kwargs.get('every', 1), + } + + # All frequencies can use a start date + if 'start_date' in kwargs: + try: + rrule_kwargs['dtstart'] = LookupModule.parse_date_time(kwargs['start_date']) + except Exception: + raise AnsibleError('Parameter start_date must be in the format YYYY-MM-DD [HH:MM:SS]') + + # If we are a none frequency we don't need anything else + if frequency == 'none': + rrule_kwargs['count'] = 1 + else: + # All non-none frequencies can have an end_on option + if 'end_on' in kwargs: + end_on = kwargs['end_on'] + if re.match(r'^\d+$', end_on): + rrule_kwargs['count'] = end_on + else: + try: + rrule_kwargs['until'] = LookupModule.parse_date_time(end_on) + except Exception: + raise AnsibleError('Parameter end_on must either be an integer or in the format YYYY-MM-DD [HH:MM:SS]') + + # A week-based frequency can also take the on_days parameter + if frequency == 'week' and 'on_days' in kwargs: + days = [] + for day in kwargs['on_days'].split(','): + day = day.strip() + if day not in LookupModule.weekdays: + raise AnsibleError('Parameter on_days must only contain values {0}'.format(', '.join(LookupModule.weekdays.keys()))) + days.append(LookupModule.weekdays[day]) + + rrule_kwargs['byweekday'] = days + + # A month-based frequency can also deal with month_day_number and on_the options + if frequency == 'month': + if 'month_day_number' in kwargs and 'on_the' in kwargs: + raise AnsibleError('Month based frequencies can have month_day_number or on_the but not both') + + if 'month_day_number' in kwargs: + try: + my_month_day = int(kwargs['month_day_number']) + if my_month_day < 1 or my_month_day > 31: + raise Exception() + except Exception: + raise AnsibleError('month_day_number must be between 1 and 31') + + rrule_kwargs['bymonthday'] = my_month_day + + if 'on_the' in kwargs: + try: + (occurance, weekday) = kwargs['on_the'].split(' ') + except Exception: + raise AnsibleError('on_the parameter must be two words separated by a space') + + if weekday not in LookupModule.weekdays: + raise AnsibleError('Weekday portion of on_the parameter is not valid') + if occurance not in LookupModule.set_positions: + raise AnsibleError('The first string of the on_the parameter is not valid') + + rrule_kwargs['byweekday'] = LookupModule.weekdays[weekday] + rrule_kwargs['bysetpos'] = LookupModule.set_positions[occurance] + + my_rule = rrule.rrule(**rrule_kwargs) + + # All frequencies can use a timezone but rrule can't support the format that Tower uses. + # So we will do a string manip here if we need to + timezone = 'America/New_York' + if 'timezone' in kwargs: + if kwargs['timezone'] not in pytz.all_timezones: + raise AnsibleError('Timezone parameter is not valid') + timezone = kwargs['timezone'] + + # rrule puts a \n in the rule instad of a space and can't handle timezones + return_rrule = str(my_rule).replace('\n', ' ').replace('DTSTART:', 'DTSTART;TZID={0}:'.format(timezone)) + # Tower requires an interval. rrule will not add interval if it's set to 1 + if kwargs.get('every', 1) == 1: + return_rrule = "{0};INTERVAL=1".format(return_rrule) + + return return_rrule diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/module_utils/tower_api.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/module_utils/tower_api.py new file mode 100644 index 00000000..8ffbd3e5 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/module_utils/tower_api.py @@ -0,0 +1,583 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from . tower_module import TowerModule +from ansible.module_utils.urls import Request, SSLValidationError, ConnectionError +from ansible.module_utils.six import PY2 +from ansible.module_utils.six.moves.urllib.parse import urlencode +from ansible.module_utils.six.moves.urllib.error import HTTPError +from ansible.module_utils.six.moves.http_cookiejar import CookieJar +import re +from json import loads, dumps + + +class TowerAPIModule(TowerModule): + # TODO: Move the collection version check into tower_module.py + # This gets set by the make process so whatever is in here is irrelevant + _COLLECTION_VERSION = "14.1.0" + _COLLECTION_TYPE = "awx" + # This maps the collections type (awx/tower) to the values returned by the API + # Those values can be found in awx/api/generics.py line 204 + collection_to_version = { + 'awx': 'AWX', + 'tower': 'Red Hat Ansible Tower', + } + session = None + cookie_jar = CookieJar() + + def __init__(self, argument_spec, direct_params=None, error_callback=None, warn_callback=None, **kwargs): + kwargs['supports_check_mode'] = True + + super(TowerAPIModule, self).__init__(argument_spec=argument_spec, direct_params=direct_params, + error_callback=error_callback, warn_callback=warn_callback, **kwargs) + self.session = Request(cookies=CookieJar(), validate_certs=self.verify_ssl) + + @staticmethod + def param_to_endpoint(name): + exceptions = { + 'inventory': 'inventories', + 'target_team': 'teams', + 'workflow': 'workflow_job_templates' + } + return exceptions.get(name, '{0}s'.format(name)) + + def head_endpoint(self, endpoint, *args, **kwargs): + return self.make_request('HEAD', endpoint, **kwargs) + + def get_endpoint(self, endpoint, *args, **kwargs): + return self.make_request('GET', endpoint, **kwargs) + + def patch_endpoint(self, endpoint, *args, **kwargs): + # Handle check mode + if self.check_mode: + self.json_output['changed'] = True + self.exit_json(**self.json_output) + + return self.make_request('PATCH', endpoint, **kwargs) + + def post_endpoint(self, endpoint, *args, **kwargs): + # Handle check mode + if self.check_mode: + self.json_output['changed'] = True + self.exit_json(**self.json_output) + + return self.make_request('POST', endpoint, **kwargs) + + def delete_endpoint(self, endpoint, *args, **kwargs): + # Handle check mode + if self.check_mode: + self.json_output['changed'] = True + self.exit_json(**self.json_output) + + return self.make_request('DELETE', endpoint, **kwargs) + + def get_all_endpoint(self, endpoint, *args, **kwargs): + response = self.get_endpoint(endpoint, *args, **kwargs) + if 'next' not in response['json']: + raise RuntimeError('Expected list from API at {0}, got: {1}'.format(endpoint, response)) + next_page = response['json']['next'] + + if response['json']['count'] > 10000: + self.fail_json(msg='The number of items being queried for is higher than 10,000.') + + while next_page is not None: + next_response = self.get_endpoint(next_page) + response['json']['results'] = response['json']['results'] + next_response['json']['results'] + next_page = next_response['json']['next'] + response['json']['next'] = next_page + return response + + def get_one(self, endpoint, *args, **kwargs): + response = self.get_endpoint(endpoint, *args, **kwargs) + if response['status_code'] != 200: + fail_msg = "Got a {0} response when trying to get one from {1}".format(response['status_code'], endpoint) + if 'detail' in response.get('json', {}): + fail_msg += ', detail: {0}'.format(response['json']['detail']) + self.fail_json(msg=fail_msg) + + if 'count' not in response['json'] or 'results' not in response['json']: + self.fail_json(msg="The endpoint did not provide count and results") + + if response['json']['count'] == 0: + return None + elif response['json']['count'] > 1: + self.fail_json(msg="An unexpected number of items was returned from the API ({0})".format(response['json']['count'])) + + return response['json']['results'][0] + + def resolve_name_to_id(self, endpoint, name_or_id): + # Try to resolve the object by name + name_field = 'name' + if endpoint == 'users': + name_field = 'username' + + response = self.get_endpoint(endpoint, **{'data': {name_field: name_or_id}}) + if response['status_code'] == 400: + self.fail_json(msg="Unable to try and resolve {0} for {1} : {2}".format(endpoint, name_or_id, response['json']['detail'])) + + if response['json']['count'] == 1: + return response['json']['results'][0]['id'] + elif response['json']['count'] == 0: + try: + int(name_or_id) + # If we got 0 items by name, maybe they gave us an ID, let's try looking it up by ID + response = self.head_endpoint("{0}/{1}".format(endpoint, name_or_id), **{'return_none_on_404': True}) + if response is not None: + return name_or_id + except ValueError: + # If we got a value error than we didn't have an integer so we can just pass and fall down to the fail + pass + + self.fail_json(msg="The {0} {1} was not found on the Tower server".format(endpoint, name_or_id)) + else: + self.fail_json(msg="Found too many names {0} at endpoint {1} try using an ID instead of a name".format(name_or_id, endpoint)) + + def make_request(self, method, endpoint, *args, **kwargs): + # In case someone is calling us directly; make sure we were given a method, let's not just assume a GET + if not method: + raise Exception("The HTTP method must be defined") + + # Make sure we start with /api/vX + if not endpoint.startswith("/"): + endpoint = "/{0}".format(endpoint) + if not endpoint.startswith("/api/"): + endpoint = "/api/v2{0}".format(endpoint) + if not endpoint.endswith('/') and '?' not in endpoint: + endpoint = "{0}/".format(endpoint) + + # Extract the headers, this will be used in a couple of places + headers = kwargs.get('headers', {}) + + # Authenticate to Tower (if we don't have a token and if not already done so) + if not self.oauth_token and not self.authenticated: + # This method will set a cookie in the cookie jar for us and also an oauth_token + self.authenticate(**kwargs) + if self.oauth_token: + # If we have a oauth token, we just use a bearer header + headers['Authorization'] = 'Bearer {0}'.format(self.oauth_token) + + # Update the URL path with the endpoint + self.url = self.url._replace(path=endpoint) + + if method in ['POST', 'PUT', 'PATCH']: + headers.setdefault('Content-Type', 'application/json') + kwargs['headers'] = headers + elif kwargs.get('data'): + self.url = self.url._replace(query=urlencode(kwargs.get('data'))) + + data = None # Important, if content type is not JSON, this should not be dict type + if headers.get('Content-Type', '') == 'application/json': + data = dumps(kwargs.get('data', {})) + + try: + response = self.session.open(method, self.url.geturl(), headers=headers, validate_certs=self.verify_ssl, follow_redirects=True, data=data) + except(SSLValidationError) as ssl_err: + self.fail_json(msg="Could not establish a secure connection to your host ({1}): {0}.".format(self.url.netloc, ssl_err)) + except(ConnectionError) as con_err: + self.fail_json(msg="There was a network error of some kind trying to connect to your host ({1}): {0}.".format(self.url.netloc, con_err)) + except(HTTPError) as he: + # Sanity check: Did the server send back some kind of internal error? + if he.code >= 500: + self.fail_json(msg='The host sent back a server error ({1}): {0}. Please check the logs and try again later'.format(self.url.path, he)) + # Sanity check: Did we fail to authenticate properly? If so, fail out now; this is always a failure. + elif he.code == 401: + self.fail_json(msg='Invalid Tower authentication credentials for {0} (HTTP 401).'.format(self.url.path)) + # Sanity check: Did we get a forbidden response, which means that the user isn't allowed to do this? Report that. + elif he.code == 403: + self.fail_json(msg="You don't have permission to {1} to {0} (HTTP 403).".format(self.url.path, method)) + # Sanity check: Did we get a 404 response? + # Requests with primary keys will return a 404 if there is no response, and we want to consistently trap these. + elif he.code == 404: + if kwargs.get('return_none_on_404', False): + return None + self.fail_json(msg='The requested object could not be found at {0}.'.format(self.url.path)) + # Sanity check: Did we get a 405 response? + # A 405 means we used a method that isn't allowed. Usually this is a bad request, but it requires special treatment because the + # API sends it as a logic error in a few situations (e.g. trying to cancel a job that isn't running). + elif he.code == 405: + self.fail_json(msg="The Tower server says you can't make a request with the {0} method to this endpoing {1}".format(method, self.url.path)) + # Sanity check: Did we get some other kind of error? If so, write an appropriate error message. + elif he.code >= 400: + # We are going to return a 400 so the module can decide what to do with it + page_data = he.read() + try: + return {'status_code': he.code, 'json': loads(page_data)} + # JSONDecodeError only available on Python 3.5+ + except ValueError: + return {'status_code': he.code, 'text': page_data} + elif he.code == 204 and method == 'DELETE': + # A 204 is a normal response for a delete function + pass + else: + self.fail_json(msg="Unexpected return code when calling {0}: {1}".format(self.url.geturl(), he)) + except(Exception) as e: + self.fail_json(msg="There was an unknown error when trying to connect to {2}: {0} {1}".format(type(e).__name__, e, self.url.geturl())) + finally: + self.url = self.url._replace(query=None) + + if not self.version_checked: + # In PY2 we get back an HTTPResponse object but PY2 is returning an addinfourl + # First try to get the headers in PY3 format and then drop down to PY2. + try: + tower_type = response.getheader('X-API-Product-Name', None) + tower_version = response.getheader('X-API-Product-Version', None) + except Exception: + tower_type = response.info().getheader('X-API-Product-Name', None) + tower_version = response.info().getheader('X-API-Product-Version', None) + + if self._COLLECTION_TYPE not in self.collection_to_version or self.collection_to_version[self._COLLECTION_TYPE] != tower_type: + self.warn("You are using the {0} version of this collection but connecting to {1}".format( + self._COLLECTION_TYPE, tower_type + )) + elif self._COLLECTION_VERSION != tower_version: + self.warn("You are running collection version {0} but connecting to tower version {1}".format( + self._COLLECTION_VERSION, tower_version + )) + self.version_checked = True + + response_body = '' + try: + response_body = response.read() + except(Exception) as e: + self.fail_json(msg="Failed to read response body: {0}".format(e)) + + response_json = {} + if response_body and response_body != '': + try: + response_json = loads(response_body) + except(Exception) as e: + self.fail_json(msg="Failed to parse the response json: {0}".format(e)) + + if PY2: + status_code = response.getcode() + else: + status_code = response.status + return {'status_code': status_code, 'json': response_json} + + def authenticate(self, **kwargs): + if self.username and self.password: + # Attempt to get a token from /api/v2/tokens/ by giving it our username/password combo + # If we have a username and password, we need to get a session cookie + login_data = { + "description": "Ansible Tower Module Token", + "application": None, + "scope": "write", + } + # Post to the tokens endpoint with baisc auth to try and get a token + api_token_url = (self.url._replace(path='/api/v2/tokens/')).geturl() + + try: + response = self.session.open( + 'POST', api_token_url, + validate_certs=self.verify_ssl, follow_redirects=True, + force_basic_auth=True, url_username=self.username, url_password=self.password, + data=dumps(login_data), headers={'Content-Type': 'application/json'} + ) + except HTTPError as he: + try: + resp = he.read() + except Exception as e: + resp = 'unknown {0}'.format(e) + self.fail_json(msg='Failed to get token: {0}'.format(he), response=resp) + except(Exception) as e: + # Sanity check: Did the server send back some kind of internal error? + self.fail_json(msg='Failed to get token: {0}'.format(e)) + + token_response = None + try: + token_response = response.read() + response_json = loads(token_response) + self.oauth_token_id = response_json['id'] + self.oauth_token = response_json['token'] + except(Exception) as e: + self.fail_json(msg="Failed to extract token information from login response: {0}".format(e), **{'response': token_response}) + + # If we have neither of these, then we can try un-authenticated access + self.authenticated = True + + def delete_if_needed(self, existing_item, on_delete=None): + # This will exit from the module on its own. + # If the method successfully deletes an item and on_delete param is defined, + # the on_delete parameter will be called as a method pasing in this object and the json from the response + # This will return one of two things: + # 1. None if the existing_item is not defined (so no delete needs to happen) + # 2. The response from Tower from calling the delete on the endpont. It's up to you to process the response and exit from the module + # Note: common error codes from the Tower API can cause the module to fail + if existing_item: + # If we have an item, we can try to delete it + try: + item_url = existing_item['url'] + item_type = existing_item['type'] + item_id = existing_item['id'] + except KeyError as ke: + self.fail_json(msg="Unable to process delete of item due to missing data {0}".format(ke)) + + if 'name' in existing_item: + item_name = existing_item['name'] + elif 'username' in existing_item: + item_name = existing_item['username'] + elif 'identifier' in existing_item: + item_name = existing_item['identifier'] + elif item_type == 'o_auth2_access_token': + # An oauth2 token has no name, instead we will use its id for any of the messages + item_name = existing_item['id'] + elif item_type == 'credential_input_source': + # An credential_input_source has no name, instead we will use its id for any of the messages + item_name = existing_item['id'] + else: + self.fail_json(msg="Unable to process delete of {0} due to missing name".format(item_type)) + + response = self.delete_endpoint(item_url) + + if response['status_code'] in [202, 204]: + if on_delete: + on_delete(self, response['json']) + self.json_output['changed'] = True + self.json_output['id'] = item_id + self.exit_json(**self.json_output) + else: + if 'json' in response and '__all__' in response['json']: + self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['json']['__all__'][0])) + elif 'json' in response: + # This is from a project delete (if there is an active job against it) + if 'error' in response['json']: + self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['json']['error'])) + else: + self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['json'])) + else: + self.fail_json(msg="Unable to delete {0} {1}: {2}".format(item_type, item_name, response['status_code'])) + else: + self.exit_json(**self.json_output) + + def modify_associations(self, association_endpoint, new_association_list): + # if we got None instead of [] we are not modifying the association_list + if new_association_list is None: + return + + # First get the existing associations + response = self.get_all_endpoint(association_endpoint) + existing_associated_ids = [association['id'] for association in response['json']['results']] + + # Disassociate anything that is in existing_associated_ids but not in new_association_list + ids_to_remove = list(set(existing_associated_ids) - set(new_association_list)) + for an_id in ids_to_remove: + response = self.post_endpoint(association_endpoint, **{'data': {'id': int(an_id), 'disassociate': True}}) + if response['status_code'] == 204: + self.json_output['changed'] = True + else: + self.fail_json(msg="Failed to disassociate item {0}".format(response['json']['detail'])) + + # Associate anything that is in new_association_list but not in `association` + for an_id in list(set(new_association_list) - set(existing_associated_ids)): + response = self.post_endpoint(association_endpoint, **{'data': {'id': int(an_id)}}) + if response['status_code'] == 204: + self.json_output['changed'] = True + else: + self.fail_json(msg="Failed to associate item {0}".format(response['json']['detail'])) + + def create_if_needed(self, existing_item, new_item, endpoint, on_create=None, item_type='unknown', associations=None): + + # This will exit from the module on its own + # If the method successfully creates an item and on_create param is defined, + # the on_create parameter will be called as a method pasing in this object and the json from the response + # This will return one of two things: + # 1. None if the existing_item is already defined (so no create needs to happen) + # 2. The response from Tower from calling the patch on the endpont. It's up to you to process the response and exit from the module + # Note: common error codes from the Tower API can cause the module to fail + + if not endpoint: + self.fail_json(msg="Unable to create new {0} due to missing endpoint".format(item_type)) + + item_url = None + if existing_item: + try: + item_url = existing_item['url'] + except KeyError as ke: + self.fail_json(msg="Unable to process create of item due to missing data {0}".format(ke)) + else: + # If we don't have an exisitng_item, we can try to create it + + # We have to rely on item_type being passed in since we don't have an existing item that declares its type + # We will pull the item_name out from the new_item, if it exists + for key in ('name', 'username', 'identifier', 'hostname'): + if key in new_item: + item_name = new_item[key] + break + else: + item_name = 'unknown' + + response = self.post_endpoint(endpoint, **{'data': new_item}) + if response['status_code'] == 201: + self.json_output['name'] = 'unknown' + for key in ('name', 'username', 'identifier', 'hostname'): + if key in response['json']: + self.json_output['name'] = response['json'][key] + self.json_output['id'] = response['json']['id'] + self.json_output['changed'] = True + item_url = response['json']['url'] + else: + if 'json' in response and '__all__' in response['json']: + self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['json']['__all__'][0])) + elif 'json' in response: + self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['json'])) + else: + self.fail_json(msg="Unable to create {0} {1}: {2}".format(item_type, item_name, response['status_code'])) + + # Process any associations with this item + if associations is not None: + for association_type in associations: + sub_endpoint = '{0}{1}/'.format(item_url, association_type) + self.modify_associations(sub_endpoint, associations[association_type]) + + # If we have an on_create method and we actually changed something we can call on_create + if on_create is not None and self.json_output['changed']: + on_create(self, response['json']) + else: + self.exit_json(**self.json_output) + + def _encrypted_changed_warning(self, field, old, warning=False): + if not warning: + return + self.warn( + 'The field {0} of {1} {2} has encrypted data and may inaccurately report task is changed.'.format( + field, old.get('type', 'unknown'), old.get('id', 'unknown') + )) + + @staticmethod + def has_encrypted_values(obj): + """Returns True if JSON-like python content in obj has $encrypted$ + anywhere in the data as a value + """ + if isinstance(obj, dict): + for val in obj.values(): + if TowerAPIModule.has_encrypted_values(val): + return True + elif isinstance(obj, list): + for val in obj: + if TowerAPIModule.has_encrypted_values(val): + return True + elif obj == TowerAPIModule.ENCRYPTED_STRING: + return True + return False + + def objects_could_be_different(self, old, new, field_set=None, warning=False): + if field_set is None: + field_set = set(fd for fd in new.keys() if fd not in ('modified', 'related', 'summary_fields')) + for field in field_set: + new_field = new.get(field, None) + old_field = old.get(field, None) + if old_field != new_field: + return True # Something doesn't match + elif self.has_encrypted_values(new_field) or field not in new: + # case of 'field not in new' - user password write-only field that API will not display + self._encrypted_changed_warning(field, old, warning=warning) + return True + return False + + def update_if_needed(self, existing_item, new_item, on_update=None, associations=None): + # This will exit from the module on its own + # If the method successfully updates an item and on_update param is defined, + # the on_update parameter will be called as a method pasing in this object and the json from the response + # This will return one of two things: + # 1. None if the existing_item does not need to be updated + # 2. The response from Tower from patching to the endpoint. It's up to you to process the response and exit from the module. + # Note: common error codes from the Tower API can cause the module to fail + response = None + if existing_item: + + # If we have an item, we can see if it needs an update + try: + item_url = existing_item['url'] + item_type = existing_item['type'] + if item_type == 'user': + item_name = existing_item['username'] + elif item_type == 'workflow_job_template_node': + item_name = existing_item['identifier'] + elif item_type == 'credential_input_source': + item_name = existing_item['id'] + else: + item_name = existing_item['name'] + item_id = existing_item['id'] + except KeyError as ke: + self.fail_json(msg="Unable to process update of item due to missing data {0}".format(ke)) + + # Check to see if anything within the item requires the item to be updated + needs_patch = self.objects_could_be_different(existing_item, new_item) + + # If we decided the item needs to be updated, update it + self.json_output['id'] = item_id + if needs_patch: + response = self.patch_endpoint(item_url, **{'data': new_item}) + if response['status_code'] == 200: + # compare apples-to-apples, old API data to new API data + # but do so considering the fields given in parameters + self.json_output['changed'] = self.objects_could_be_different( + existing_item, response['json'], field_set=new_item.keys(), warning=True) + elif 'json' in response and '__all__' in response['json']: + self.fail_json(msg=response['json']['__all__']) + else: + self.fail_json(**{'msg': "Unable to update {0} {1}, see response".format(item_type, item_name), 'response': response}) + + else: + raise RuntimeError('update_if_needed called incorrectly without existing_item') + + # Process any associations with this item + if associations is not None: + for association_type, id_list in associations.items(): + endpoint = '{0}{1}/'.format(item_url, association_type) + self.modify_associations(endpoint, id_list) + + # If we change something and have an on_change call it + if on_update is not None and self.json_output['changed']: + if response is None: + last_data = existing_item + else: + last_data = response['json'] + on_update(self, last_data) + else: + self.exit_json(**self.json_output) + + def create_or_update_if_needed(self, existing_item, new_item, endpoint=None, item_type='unknown', on_create=None, on_update=None, associations=None): + if existing_item: + return self.update_if_needed(existing_item, new_item, on_update=on_update, associations=associations) + else: + return self.create_if_needed(existing_item, new_item, endpoint, on_create=on_create, item_type=item_type, associations=associations) + + def logout(self): + if self.authenticated: + # Attempt to delete our current token from /api/v2/tokens/ + # Post to the tokens endpoint with baisc auth to try and get a token + api_token_url = ( + self.url._replace( + path='/api/v2/tokens/{0}/'.format(self.oauth_token_id), + query=None # in error cases, fail_json exists before exception handling + ) + ).geturl() + + try: + self.session.open( + 'DELETE', + api_token_url, + validate_certs=self.verify_ssl, + follow_redirects=True, + force_basic_auth=True, + url_username=self.username, + url_password=self.password + ) + self.oauth_token_id = None + self.authenticated = False + except HTTPError as he: + try: + resp = he.read() + except Exception as e: + resp = 'unknown {0}'.format(e) + self.warn('Failed to release tower token: {0}, response: {1}'.format(he, resp)) + except(Exception) as e: + # Sanity check: Did the server send back some kind of internal error? + self.warn('Failed to release tower token {0}: {1}'.format(self.oauth_token_id, e)) + + def is_job_done(self, job_status): + if job_status in ['new', 'pending', 'waiting', 'running']: + return False + else: + return True diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/module_utils/tower_awxkit.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/module_utils/tower_awxkit.py new file mode 100644 index 00000000..fc4e232f --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/module_utils/tower_awxkit.py @@ -0,0 +1,53 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from . tower_module import TowerModule +from ansible.module_utils.basic import missing_required_lib + +try: + from awxkit.api.client import Connection + from awxkit.api.pages.api import ApiV2 + from awxkit.api import get_registered_page + HAS_AWX_KIT = True +except ImportError: + HAS_AWX_KIT = False + + +class TowerAWXKitModule(TowerModule): + connection = None + apiV2Ref = None + + def __init__(self, argument_spec, **kwargs): + kwargs['supports_check_mode'] = False + + super(TowerAWXKitModule, self).__init__(argument_spec=argument_spec, **kwargs) + + # Die if we don't have AWX_KIT installed + if not HAS_AWX_KIT: + self.exit_json(msg=missing_required_lib('awxkit')) + + # Establish our conneciton object + self.connection = Connection(self.host, verify=self.verify_ssl) + + def authenticate(self): + try: + if self.oauth_token: + self.connection.login(None, None, token=self.oauth_token) + self.authenticated = True + elif self.username: + self.connection.login(username=self.username, password=self.password) + self.authenticated = True + except Exception: + self.exit_json("Failed to authenticate") + + def get_api_v2_object(self): + if not self.apiV2Ref: + if not self.authenticated: + self.authenticate() + v2_index = get_registered_page('/api/v2/')(self.connection).get() + self.api_ref = ApiV2(connection=self.connection, **{'json': v2_index}) + return self.api_ref + + def logout(self): + if self.authenticated: + self.connection.logout() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/module_utils/tower_legacy.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/module_utils/tower_legacy.py new file mode 100644 index 00000000..3c840861 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/module_utils/tower_legacy.py @@ -0,0 +1,117 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# Copyright (c), Wayne Witzel III <wayne@riotousliving.com> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import traceback + +TOWER_CLI_IMP_ERR = None +try: + import tower_cli.utils.exceptions as exc + from tower_cli.utils import parser + from tower_cli.api import client + + HAS_TOWER_CLI = True +except ImportError: + TOWER_CLI_IMP_ERR = traceback.format_exc() + HAS_TOWER_CLI = False + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib + + +def tower_auth_config(module): + ''' + `tower_auth_config` attempts to load the tower-cli.cfg file + specified from the `tower_config_file` parameter. If found, + if returns the contents of the file as a dictionary, else + it will attempt to fetch values from the module params and + only pass those values that have been set. + ''' + config_file = module.params.pop('tower_config_file', None) + if config_file: + if not os.path.exists(config_file): + module.fail_json(msg='file not found: %s' % config_file) + if os.path.isdir(config_file): + module.fail_json(msg='directory can not be used as config file: %s' % config_file) + + with open(config_file, 'r') as f: + return parser.string_to_dict(f.read()) + else: + auth_config = {} + host = module.params.pop('tower_host', None) + if host: + auth_config['host'] = host + username = module.params.pop('tower_username', None) + if username: + auth_config['username'] = username + password = module.params.pop('tower_password', None) + if password: + auth_config['password'] = password + module.params.pop('tower_verify_ssl', None) # pop alias if used + verify_ssl = module.params.pop('validate_certs', None) + if verify_ssl is not None: + auth_config['verify_ssl'] = verify_ssl + return auth_config + + +def tower_check_mode(module): + '''Execute check mode logic for Ansible Tower modules''' + if module.check_mode: + try: + result = client.get('/ping').json() + module.exit_json(changed=True, tower_version='{0}'.format(result['version'])) + except (exc.ServerError, exc.ConnectionError, exc.BadRequest) as excinfo: + module.fail_json(changed=False, msg='Failed check mode: {0}'.format(excinfo)) + + +class TowerLegacyModule(AnsibleModule): + def __init__(self, argument_spec, **kwargs): + args = dict( + tower_host=dict(), + tower_username=dict(), + tower_password=dict(no_log=True), + validate_certs=dict(type='bool', aliases=['tower_verify_ssl']), + tower_config_file=dict(type='path'), + ) + args.update(argument_spec) + + kwargs.setdefault('mutually_exclusive', []) + kwargs['mutually_exclusive'].extend(( + ('tower_config_file', 'tower_host'), + ('tower_config_file', 'tower_username'), + ('tower_config_file', 'tower_password'), + ('tower_config_file', 'validate_certs'), + )) + + super(TowerLegacyModule, self).__init__(argument_spec=args, **kwargs) + + if not HAS_TOWER_CLI: + self.fail_json(msg=missing_required_lib('ansible-tower-cli'), + exception=TOWER_CLI_IMP_ERR) diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/module_utils/tower_module.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/module_utils/tower_module.py new file mode 100644 index 00000000..553a3524 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/module_utils/tower_module.py @@ -0,0 +1,239 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.basic import AnsibleModule, env_fallback +from ansible.module_utils.six import PY2, string_types +from ansible.module_utils.six.moves import StringIO +from ansible.module_utils.six.moves.urllib.parse import urlparse +from ansible.module_utils.six.moves.configparser import ConfigParser, NoOptionError +from socket import gethostbyname +import re +from os.path import isfile, expanduser, split, join, exists, isdir +from os import access, R_OK, getcwd +from distutils.util import strtobool + +try: + import yaml + HAS_YAML = True +except ImportError: + HAS_YAML = False + + +class ConfigFileException(Exception): + pass + + +class ItemNotDefined(Exception): + pass + + +class TowerModule(AnsibleModule): + url = None + AUTH_ARGSPEC = dict( + tower_host=dict(required=False, fallback=(env_fallback, ['TOWER_HOST'])), + tower_username=dict(required=False, fallback=(env_fallback, ['TOWER_USERNAME'])), + tower_password=dict(no_log=True, required=False, fallback=(env_fallback, ['TOWER_PASSWORD'])), + validate_certs=dict(type='bool', aliases=['tower_verify_ssl'], required=False, fallback=(env_fallback, ['TOWER_VERIFY_SSL'])), + tower_oauthtoken=dict(type='raw', no_log=True, required=False, fallback=(env_fallback, ['TOWER_OAUTH_TOKEN'])), + tower_config_file=dict(type='path', required=False, default=None), + ) + short_params = { + 'host': 'tower_host', + 'username': 'tower_username', + 'password': 'tower_password', + 'verify_ssl': 'validate_certs', + 'oauth_token': 'tower_oauthtoken', + } + host = '127.0.0.1' + username = None + password = None + verify_ssl = True + oauth_token = None + oauth_token_id = None + authenticated = False + config_name = 'tower_cli.cfg' + ENCRYPTED_STRING = "$encrypted$" + version_checked = False + error_callback = None + warn_callback = None + + def __init__(self, argument_spec=None, direct_params=None, error_callback=None, warn_callback=None, **kwargs): + full_argspec = {} + full_argspec.update(TowerModule.AUTH_ARGSPEC) + full_argspec.update(argument_spec) + kwargs['supports_check_mode'] = True + + self.error_callback = error_callback + self.warn_callback = warn_callback + + self.json_output = {'changed': False} + + if direct_params is not None: + self.params = direct_params + else: + super(TowerModule, self).__init__(argument_spec=full_argspec, **kwargs) + + self.load_config_files() + + # Parameters specified on command line will override settings in any config + for short_param, long_param in self.short_params.items(): + direct_value = self.params.get(long_param) + if direct_value is not None: + setattr(self, short_param, direct_value) + + # Perform magic depending on whether tower_oauthtoken is a string or a dict + if self.params.get('tower_oauthtoken'): + token_param = self.params.get('tower_oauthtoken') + if type(token_param) is dict: + if 'token' in token_param: + self.oauth_token = self.params.get('tower_oauthtoken')['token'] + else: + self.fail_json(msg="The provided dict in tower_oauthtoken did not properly contain the token entry") + elif isinstance(token_param, string_types): + self.oauth_token = self.params.get('tower_oauthtoken') + else: + error_msg = "The provided tower_oauthtoken type was not valid ({0}). Valid options are str or dict.".format(type(token_param).__name__) + self.fail_json(msg=error_msg) + + # Perform some basic validation + if not re.match('^https{0,1}://', self.host): + self.host = "https://{0}".format(self.host) + + # Try to parse the hostname as a url + try: + self.url = urlparse(self.host) + except Exception as e: + self.fail_json(msg="Unable to parse tower_host as a URL ({1}): {0}".format(self.host, e)) + + # Try to resolve the hostname + hostname = self.url.netloc.split(':')[0] + try: + gethostbyname(hostname) + except Exception as e: + self.fail_json(msg="Unable to resolve tower_host ({1}): {0}".format(hostname, e)) + + def load_config_files(self): + # Load configs like TowerCLI would have from least import to most + config_files = ['/etc/tower/tower_cli.cfg', join(expanduser("~"), ".{0}".format(self.config_name))] + local_dir = getcwd() + config_files.append(join(local_dir, self.config_name)) + while split(local_dir)[1]: + local_dir = split(local_dir)[0] + config_files.insert(2, join(local_dir, ".{0}".format(self.config_name))) + + # If we have a specified tower config, load it + if self.params.get('tower_config_file'): + duplicated_params = [ + fn for fn in self.AUTH_ARGSPEC + if fn != 'tower_config_file' and self.params.get(fn) is not None + ] + if duplicated_params: + self.warn(( + 'The parameter(s) {0} were provided at the same time as tower_config_file. ' + 'Precedence may be unstable, we suggest either using config file or params.' + ).format(', '.join(duplicated_params))) + try: + # TODO: warn if there are conflicts with other params + self.load_config(self.params.get('tower_config_file')) + except ConfigFileException as cfe: + # Since we were told specifically to load this we want it to fail if we have an error + self.fail_json(msg=cfe) + else: + for config_file in config_files: + if exists(config_file) and not isdir(config_file): + # Only throw a formatting error if the file exists and is not a directory + try: + self.load_config(config_file) + except ConfigFileException: + self.fail_json(msg='The config file {0} is not properly formatted'.format(config_file)) + + def load_config(self, config_path): + # Validate the config file is an actual file + if not isfile(config_path): + raise ConfigFileException('The specified config file does not exist') + + if not access(config_path, R_OK): + raise ConfigFileException("The specified config file cannot be read") + + # Read in the file contents: + with open(config_path, 'r') as f: + config_string = f.read() + + # First try to yaml load the content (which will also load json) + try: + try_config_parsing = True + if HAS_YAML: + try: + config_data = yaml.load(config_string, Loader=yaml.SafeLoader) + # If this is an actual ini file, yaml will return the whole thing as a string instead of a dict + if type(config_data) is not dict: + raise AssertionError("The yaml config file is not properly formatted as a dict.") + try_config_parsing = False + + except(AttributeError, yaml.YAMLError, AssertionError): + try_config_parsing = True + + if try_config_parsing: + # TowerCLI used to support a config file with a missing [general] section by prepending it if missing + if '[general]' not in config_string: + config_string = '[general]\n{0}'.format(config_string) + + config = ConfigParser() + + try: + placeholder_file = StringIO(config_string) + # py2 ConfigParser has readfp, that has been deprecated in favor of read_file in py3 + # This "if" removes the deprecation warning + if hasattr(config, 'read_file'): + config.read_file(placeholder_file) + else: + config.readfp(placeholder_file) + + # If we made it here then we have values from reading the ini file, so let's pull them out into a dict + config_data = {} + for honorred_setting in self.short_params: + try: + config_data[honorred_setting] = config.get('general', honorred_setting) + except NoOptionError: + pass + + except Exception as e: + raise ConfigFileException("An unknown exception occured trying to ini load config file: {0}".format(e)) + + except Exception as e: + raise ConfigFileException("An unknown exception occured trying to load config file: {0}".format(e)) + + # If we made it here, we have a dict which has values in it from our config, any final settings logic can be performed here + for honorred_setting in self.short_params: + if honorred_setting in config_data: + # Veriffy SSL must be a boolean + if honorred_setting == 'verify_ssl': + if type(config_data[honorred_setting]) is str: + setattr(self, honorred_setting, strtobool(config_data[honorred_setting])) + else: + setattr(self, honorred_setting, bool(config_data[honorred_setting])) + else: + setattr(self, honorred_setting, config_data[honorred_setting]) + + def logout(self): + # This method is intended to be overridden + pass + + def fail_json(self, **kwargs): + # Try to log out if we are authenticated + self.logout() + if self.error_callback: + self.error_callback(**kwargs) + else: + super(TowerModule, self).fail_json(**kwargs) + + def exit_json(self, **kwargs): + # Try to log out if we are authenticated + self.logout() + super(TowerModule, self).exit_json(**kwargs) + + def warn(self, warning): + if self.warn_callback is not None: + self.warn_callback(warning) + else: + super(TowerModule, self).warn(warning) diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/__init__.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/__init__.py diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_credential.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_credential.py new file mode 100644 index 00000000..2d4cbd33 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_credential.py @@ -0,0 +1,427 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright: (c) 2017, Wayne Witzel III <wayne@riotousliving.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_credential +author: "Wayne Witzel III (@wwitzel3)" +short_description: create, update, or destroy Ansible Tower credential. +description: + - Create, update, or destroy Ansible Tower credentials. See + U(https://www.ansible.com/tower) for an overview. +options: + name: + description: + - The name to use for the credential. + required: True + type: str + new_name: + description: + - Setting this option will change the existing name (looked up via the name field. + required: False + type: str + description: + description: + - The description to use for the credential. + type: str + organization: + description: + - Organization that should own the credential. + type: str + credential_type: + description: + - Name of credential type. + - Will be preferred over kind + type: str + inputs: + description: + - >- + Credential inputs where the keys are var names used in templating. + Refer to the Ansible Tower documentation for example syntax. + - Any fields in this dict will take prescedence over any fields mentioned below (i.e. host, username, etc) + type: dict + user: + description: + - User that should own this credential. + type: str + team: + description: + - Team that should own this credential. + type: str + + kind: + description: + - Type of credential being added. + - The ssh choice refers to a Tower Machine credential. + - Deprecated, please use credential_type + required: False + type: str + choices: ["ssh", "vault", "net", "scm", "aws", "vmware", "satellite6", "cloudforms", "gce", "azure_rm", "openstack", "rhv", "insights", "tower"] + host: + description: + - Host for this credential. + - Deprecated, will be removed in a future release + type: str + username: + description: + - Username for this credential. ``access_key`` for AWS. + - Deprecated, please use inputs + type: str + password: + description: + - Password for this credential. ``secret_key`` for AWS. ``api_key`` for RAX. + - Use "ASK" and launch in Tower to be prompted. + - Deprecated, please use inputs + type: str + project: + description: + - Project that should use this credential for GCP. + - Deprecated, will be removed in a future release + type: str + ssh_key_data: + description: + - SSH private key content. To extract the content from a file path, use the lookup function (see examples). + - Deprecated, please use inputs + type: str + ssh_key_unlock: + description: + - Unlock password for ssh_key. + - Use "ASK" and launch in Tower to be prompted. + - Deprecated, please use inputs + type: str + authorize: + description: + - Should use authorize for net type. + - Deprecated, please use inputs + type: bool + default: 'no' + authorize_password: + description: + - Password for net credentials that require authorize. + - Deprecated, please use inputs + type: str + client: + description: + - Client or application ID for azure_rm type. + - Deprecated, please use inputs + type: str + security_token: + description: + - STS token for aws type. + - Deprecated, please use inputs + type: str + secret: + description: + - Secret token for azure_rm type. + - Deprecated, please use inputs + type: str + subscription: + description: + - Subscription ID for azure_rm type. + - Deprecated, please use inputs + type: str + tenant: + description: + - Tenant ID for azure_rm type. + - Deprecated, please use inputs + type: str + domain: + description: + - Domain for openstack type. + - Deprecated, please use inputs + type: str + become_method: + description: + - Become method to use for privilege escalation. + - Some examples are "None", "sudo", "su", "pbrun" + - Due to become plugins, these can be arbitrary + - Deprecated, please use inputs + type: str + become_username: + description: + - Become username. + - Use "ASK" and launch in Tower to be prompted. + - Deprecated, please use inputs + type: str + become_password: + description: + - Become password. + - Use "ASK" and launch in Tower to be prompted. + - Deprecated, please use inputs + type: str + vault_password: + description: + - Vault password. + - Use "ASK" and launch in Tower to be prompted. + - Deprecated, please use inputs + type: str + vault_id: + description: + - Vault identifier. + - This parameter is only valid if C(kind) is specified as C(vault). + - Deprecated, please use inputs + type: str + state: + description: + - Desired state of the resource. + choices: ["present", "absent"] + default: "present" + type: str + +extends_documentation_fragment: awx.awx.auth + +notes: + - Values `inputs` and the other deprecated fields (such as `tenant`) are replacements of existing values. + See the last 4 examples for details. +''' + + +EXAMPLES = ''' +- name: Add tower machine credential + tower_credential: + name: Team Name + description: Team Description + organization: test-org + credential_type: Machine + state: present + tower_config_file: "~/tower_cli.cfg" + +- name: Create a valid SCM credential from a private_key file + tower_credential: + name: SCM Credential + organization: Default + state: present + credential_type: Source Control + inputs: + username: joe + password: secret + ssh_key_data: "{{ lookup('file', '/tmp/id_rsa') }}" + ssh_key_unlock: "passphrase" + +- name: Fetch private key + slurp: + src: '$HOME/.ssh/aws-private.pem' + register: aws_ssh_key + +- name: Add Credential Into Tower + tower_credential: + name: Workshop Credential + credential_type: Machine + organization: Default + inputs: + ssh_key_data: "{{ aws_ssh_key['content'] | b64decode }}" + run_once: true + delegate_to: localhost + +- name: Add Credential with Custom Credential Type + tower_credential: + name: Workshop Credential + credential_type: MyCloudCredential + organization: Default + tower_username: admin + tower_password: ansible + tower_host: https://localhost + +- name: Create a Vaiult credential (example for notes) + tower_credential: + name: Example password + credential_type: Vault + organization: Default + inputs: + vault_password: 'hello' + vault_id: 'My ID' + +- name: Bad password update (will replace vault_id) + tower_credential: + name: Example password + credential_type: Vault + organization: Default + inputs: + vault_password: 'new_password' + +- name: Another bad password update (will replace vault_id) + tower_credential: + name: Example password + credential_type: Vault + organization: Default + vault_password: 'new_password' + +- name: A safe way to update a password and keep vault_id + tower_credential: + name: Example password + credential_type: Vault + organization: Default + inputs: + vault_password: 'new_password' + vault_id: 'My ID' + +''' + +from ..module_utils.tower_api import TowerAPIModule + +KIND_CHOICES = { + 'ssh': 'Machine', + 'vault': 'Vault', + 'net': 'Network', + 'scm': 'Source Control', + 'aws': 'Amazon Web Services', + 'vmware': 'VMware vCenter', + 'satellite6': 'Red Hat Satellite 6', + 'cloudforms': 'Red Hat CloudForms', + 'gce': 'Google Compute Engine', + 'azure_rm': 'Microsoft Azure Resource Manager', + 'openstack': 'OpenStack', + 'rhv': 'Red Hat Virtualization', + 'insights': 'Insights', + 'tower': 'Ansible Tower', +} + + +OLD_INPUT_NAMES = ( + 'authorize', 'authorize_password', 'client', + 'security_token', 'secret', 'tenant', 'subscription', + 'domain', 'become_method', 'become_username', + 'become_password', 'vault_password', 'project', 'host', + 'username', 'password', 'ssh_key_data', 'vault_id', + 'ssh_key_unlock' +) + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + name=dict(required=True), + new_name=dict(), + description=dict(), + organization=dict(), + credential_type=dict(), + inputs=dict(type='dict', no_log=True), + user=dict(), + team=dict(), + # These are for backwards compatability + kind=dict(choices=list(KIND_CHOICES.keys())), + host=dict(), + username=dict(), + password=dict(no_log=True), + project=dict(), + ssh_key_data=dict(no_log=True), + ssh_key_unlock=dict(no_log=True), + authorize=dict(type='bool'), + authorize_password=dict(no_log=True), + client=dict(), + security_token=dict(), + secret=dict(no_log=True), + subscription=dict(), + tenant=dict(), + domain=dict(), + become_method=dict(), + become_username=dict(), + become_password=dict(no_log=True), + vault_password=dict(no_log=True), + vault_id=dict(), + # End backwards compatability + state=dict(choices=['present', 'absent'], default='present'), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec, required_one_of=[['kind', 'credential_type']]) + + # Extract our parameters + name = module.params.get('name') + new_name = module.params.get('new_name') + description = module.params.get('description') + organization = module.params.get('organization') + credential_type = module.params.get('credential_type') + inputs = module.params.get('inputs') + user = module.params.get('user') + team = module.params.get('team') + # The legacy arguments are put into a hash down below + kind = module.params.get('kind') + # End backwards compatability + state = module.params.get('state') + + # Deprication warnings + for legacy_input in OLD_INPUT_NAMES: + if module.params.get(legacy_input) is not None: + module.deprecate(msg='{0} parameter has been deprecated, please use inputs instead'.format(legacy_input), version="ansible.tower:4.0.0") + if kind: + module.deprecate(msg='The kind parameter has been deprecated, please use credential_type instead', version="ansible.tower:4.0.0") + + cred_type_id = module.resolve_name_to_id('credential_types', credential_type if credential_type else KIND_CHOICES[kind]) + if organization: + org_id = module.resolve_name_to_id('organizations', organization) + + # Attempt to look up the object based on the provided name, credential type and optional organization + lookup_data = { + 'name': name, + 'credential_type': cred_type_id, + } + if organization: + lookup_data['organization'] = org_id + + credential = module.get_one('credentials', **{'data': lookup_data}) + + if state == 'absent': + # If the state was absent we can let the module delete it if needed, the module will handle exiting from this + module.delete_if_needed(credential) + + # Attempt to look up the related items the user specified (these will fail the module if not found) + if user: + user_id = module.resolve_name_to_id('users', user) + if team: + team_id = module.resolve_name_to_id('teams', team) + + # Create credential input from legacy inputs + has_inputs = False + credential_inputs = {} + for legacy_input in OLD_INPUT_NAMES: + if module.params.get(legacy_input) is not None: + has_inputs = True + credential_inputs[legacy_input] = module.params.get(legacy_input) + + if inputs: + has_inputs = True + credential_inputs.update(inputs) + + # Create the data that gets sent for create and update + credential_fields = { + 'name': new_name if new_name else name, + 'credential_type': cred_type_id, + } + if has_inputs: + credential_fields['inputs'] = credential_inputs + + if description: + credential_fields['description'] = description + if organization: + credential_fields['organization'] = org_id + + # If we don't already have a credential (and we are creating one) we can add user/team + # The API does not appear to do anything with these after creation anyway + # NOTE: We can't just add these on a modification because they are never returned from a GET so it would always cause a changed=True + if not credential: + if user: + credential_fields['user'] = user_id + if team: + credential_fields['team'] = team_id + + # If the state was present we can let the module build or update the existing group, this will return on its own + module.create_or_update_if_needed( + credential, credential_fields, endpoint='credentials', item_type='credential' + ) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_credential_input_source.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_credential_input_source.py new file mode 100644 index 00000000..cdc55cb1 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_credential_input_source.py @@ -0,0 +1,129 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright: (c) 2020, Tom Page <tpage@redhat.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_credential_input_source +author: "Tom Page (@Tompage1994)" +version_added: "2.3" +short_description: create, update, or destroy Ansible Tower credential input sources. +description: + - Create, update, or destroy Ansible Tower credential input sources. See + U(https://www.ansible.com/tower) for an overview. +options: + description: + description: + - The description to use for the credential input source. + type: str + input_field_name: + description: + - The input field the credential source will be used for + required: True + type: str + metadata: + description: + - A JSON or YAML string + required: False + type: dict + target_credential: + description: + - The credential which will have its input defined by this source + required: true + type: str + source_credential: + description: + - The credential which is the source of the credential lookup + type: str + state: + description: + - Desired state of the resource. + choices: ["present", "absent"] + default: "present" + type: str + +extends_documentation_fragment: awx.awx.auth +''' + + +EXAMPLES = ''' +- name: Use CyberArk Lookup credential as password source + tower_credential_input_source: + input_field_name: password + target_credential: new_cred + source_credential: cyberark_lookup + metadata: + object_query: "Safe=MY_SAFE;Object=awxuser" + object_query_format: "Exact" + state: present + +''' + +from ..module_utils.tower_api import TowerAPIModule + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + description=dict(default=''), + input_field_name=dict(required=True), + target_credential=dict(required=True), + source_credential=dict(default=''), + metadata=dict(type="dict"), + state=dict(choices=['present', 'absent'], default='present'), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + description = module.params.get('description') + input_field_name = module.params.get('input_field_name') + target_credential = module.params.get('target_credential') + source_credential = module.params.get('source_credential') + metadata = module.params.get('metadata') + state = module.params.get('state') + + target_credential_id = module.resolve_name_to_id('credentials', target_credential) + + # Attempt to look up the object based on the target credential and input field + lookup_data = { + 'target_credential': target_credential_id, + 'input_field_name': input_field_name, + } + credential_input_source = module.get_one('credential_input_sources', **{'data': lookup_data}) + + if state == 'absent': + module.delete_if_needed(credential_input_source) + + # Create the data that gets sent for create and update + credential_input_source_fields = { + 'target_credential': target_credential_id, + 'input_field_name': input_field_name, + } + if source_credential: + credential_input_source_fields['source_credential'] = module.resolve_name_to_id('credentials', source_credential) + if metadata: + credential_input_source_fields['metadata'] = metadata + if description: + credential_input_source_fields['description'] = description + + # If the state was present we can let the module build or update the existing group, this will return on its own + module.create_or_update_if_needed( + credential_input_source, credential_input_source_fields, endpoint='credential_input_sources', item_type='credential_input_source' + ) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_credential_type.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_credential_type.py new file mode 100644 index 00000000..53f0cc45 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_credential_type.py @@ -0,0 +1,146 @@ +#!/usr/bin/python +# coding: utf-8 -*- +# +# (c) 2018, Adrien Fleury <fleu42@gmail.com> +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'metadata_version': '1.1'} + + +DOCUMENTATION = ''' +--- +module: tower_credential_type +author: "Adrien Fleury (@fleu42)" +short_description: Create, update, or destroy custom Ansible Tower credential type. +description: + - Create, update, or destroy Ansible Tower credential type. See + U(https://www.ansible.com/tower) for an overview. +options: + name: + description: + - The name of the credential type. + required: True + type: str + description: + description: + - The description of the credential type to give more detail about it. + type: str + kind: + description: + - >- + The type of credential type being added. Note that only cloud and + net can be used for creating credential types. Refer to the Ansible + for more information. + choices: [ 'ssh', 'vault', 'net', 'scm', 'cloud', 'insights' ] + type: str + inputs: + description: + - >- + Enter inputs using either JSON or YAML syntax. Refer to the Ansible + Tower documentation for example syntax. + type: dict + injectors: + description: + - >- + Enter injectors using either JSON or YAML syntax. Refer to the + Ansible Tower documentation for example syntax. + type: dict + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] + type: str +extends_documentation_fragment: awx.awx.auth +''' + + +EXAMPLES = ''' +- tower_credential_type: + name: Nexus + description: Credentials type for Nexus + kind: cloud + inputs: "{{ lookup('file', 'tower_credential_inputs_nexus.json') }}" + injectors: {'extra_vars': {'nexus_credential': 'test' }} + state: present + validate_certs: false + +- tower_credential_type: + name: Nexus + state: absent +''' + + +RETURN = ''' # ''' + + +from ..module_utils.tower_api import TowerAPIModule + +KIND_CHOICES = { + 'ssh': 'Machine', + 'vault': 'Ansible Vault', + 'net': 'Network', + 'scm': 'Source Control', + 'cloud': 'Lots of others', + 'insights': 'Insights' +} + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + name=dict(required=True), + description=dict(), + kind=dict(choices=list(KIND_CHOICES.keys())), + inputs=dict(type='dict'), + injectors=dict(type='dict'), + state=dict(choices=['present', 'absent'], default='present'), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + name = module.params.get('name') + new_name = None + kind = module.params.get('kind') + state = module.params.get('state') + + # These will be passed into the create/updates + credential_type_params = { + 'name': new_name if new_name else name, + 'managed_by_tower': False, + } + if kind: + credential_type_params['kind'] = kind + if module.params.get('description'): + credential_type_params['description'] = module.params.get('description') + if module.params.get('inputs'): + credential_type_params['inputs'] = module.params.get('inputs') + if module.params.get('injectors'): + credential_type_params['injectors'] = module.params.get('injectors') + + # Attempt to look up credential_type based on the provided name + credential_type = module.get_one('credential_types', **{ + 'data': { + 'name': name, + } + }) + + if state == 'absent': + # If the state was absent we can let the module delete it if needed, the module will handle exiting from this + module.delete_if_needed(credential_type) + + # If the state was present and we can let the module build or update the existing credential type, this will return on its own + module.create_or_update_if_needed(credential_type, credential_type_params, endpoint='credential_types', item_type='credential type') + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_export.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_export.py new file mode 100644 index 00000000..bd951d17 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_export.py @@ -0,0 +1,166 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, John Westcott IV <john.westcott.iv@redhat.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_export +author: "John Westcott IV (@john-westcott-iv)" +version_added: "3.7" +short_description: export resources from Ansible Tower. +description: + - Export assets from Ansible Tower. +options: + all: + description: + - Export all assets + type: bool + default: 'False' + organizations: + description: + - organization name to export + type: str + users: + description: + - user name to export + type: str + teams: + description: + - team name to export + type: str + credential_types: + description: + - credential type name to export + type: str + credentials: + description: + - credential name to export + type: str + notification_templates: + description: + - notification template name to export + type: str + inventory_sources: + description: + - inventory soruce to export + type: str + inventory: + description: + - inventory name to export + type: str + projects: + description: + - project name to export + type: str + job_templates: + description: + - job template name to export + type: str + workflow_job_templates: + description: + - workflow name to export + type: str +requirements: + - "awxkit >= 9.3.0" +notes: + - Specifying a name of "all" for any asset type will export all items of that asset type. +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Export all tower assets + tower_export: + all: True +- name: Export all inventories + tower_export: + inventory: 'all' +- name: Export a job template named "My Template" and all Credentials + tower_export: + job_template: "My Template" + credential: 'all' +''' + +from os import environ +import logging +from ansible.module_utils.six.moves import StringIO +from ..module_utils.tower_awxkit import TowerAWXKitModule + +try: + from awxkit.api.pages.api import EXPORTABLE_RESOURCES + HAS_EXPORTABLE_RESOURCES = True +except ImportError: + HAS_EXPORTABLE_RESOURCES = False + + +def main(): + argument_spec = dict( + all=dict(type='bool', default=False), + ) + + # We are not going to raise an error here because the __init__ method of TowerAWXKitModule will do that for us + if HAS_EXPORTABLE_RESOURCES: + for resource in EXPORTABLE_RESOURCES: + argument_spec[resource] = dict(type='str') + + module = TowerAWXKitModule(argument_spec=argument_spec) + + if not HAS_EXPORTABLE_RESOURCES: + module.fail_json(msg="Your version of awxkit does not have import/export") + + # The export process will never change a Tower system + module.json_output['changed'] = False + + # The exporter code currently works like the following: + # Empty string == all assets of that type + # Non-Empty string = just one asset of that type (by name or ID) + # Asset type not present or None = skip asset type (unless everything is None, then export all) + # Here we are going to setup a dict of values to export + export_args = {} + for resource in EXPORTABLE_RESOURCES: + if module.params.get('all') or module.params.get(resource) == 'all': + # If we are exporting everything or we got the keyword "all" we pass in an empty string for this asset type + export_args[resource] = '' + else: + # Otherwise we take either the string or None (if the parameter was not passed) to get one or no items + export_args[resource] = module.params.get(resource) + + # Currently the import process does not return anything on error + # It simply just logs to pythons logger + # Setup a log gobbler to get error messages from import_assets + log_capture_string = StringIO() + ch = logging.StreamHandler(log_capture_string) + for logger_name in ['awxkit.api.pages.api', 'awxkit.api.pages.page']: + logger = logging.getLogger(logger_name) + logger.setLevel(logging.WARNING) + ch.setLevel(logging.WARNING) + + logger.addHandler(ch) + log_contents = '' + + # Run the import process + try: + module.json_output['assets'] = module.get_api_v2_object().export_assets(**export_args) + module.exit_json(**module.json_output) + except Exception as e: + module.fail_json(msg="Failed to export assets {0}".format(e)) + finally: + # Finally consume the logs incase there were any errors and die if there were + log_contents = log_capture_string.getvalue() + log_capture_string.close() + if log_contents != '': + module.fail_json(msg=log_contents) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_group.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_group.py new file mode 100644 index 00000000..9e2eaf4e --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_group.py @@ -0,0 +1,156 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III <wayne@riotousliving.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_group +author: "Wayne Witzel III (@wwitzel3)" +short_description: create, update, or destroy Ansible Tower group. +description: + - Create, update, or destroy Ansible Tower groups. See + U(https://www.ansible.com/tower) for an overview. +options: + name: + description: + - The name to use for the group. + required: True + type: str + description: + description: + - The description to use for the group. + type: str + inventory: + description: + - Inventory the group should be made a member of. + required: True + type: str + variables: + description: + - Variables to use for the group. + type: dict + hosts: + description: + - List of hosts that should be put in this group. + type: list + elements: str + children: + description: + - List of groups that should be nested inside in this group. + type: list + elements: str + aliases: + - groups + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] + type: str + new_name: + description: + - A new name for this group (for renaming) + type: str +extends_documentation_fragment: awx.awx.auth +''' + + +EXAMPLES = ''' +- name: Add tower group + tower_group: + name: localhost + description: "Local Host Group" + inventory: "Local Inventory" + state: present + tower_config_file: "~/tower_cli.cfg" +''' + +from ..module_utils.tower_api import TowerAPIModule +import json + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + name=dict(required=True), + new_name=dict(), + description=dict(), + inventory=dict(required=True), + variables=dict(type='dict'), + hosts=dict(type='list', elements='str'), + children=dict(type='list', elements='str', aliases=['groups']), + state=dict(choices=['present', 'absent'], default='present'), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + name = module.params.get('name') + new_name = module.params.get('new_name') + inventory = module.params.get('inventory') + description = module.params.get('description') + state = module.params.pop('state') + variables = module.params.get('variables') + + # Attempt to look up the related items the user specified (these will fail the module if not found) + inventory_id = module.resolve_name_to_id('inventories', inventory) + + # Attempt to look up the object based on the provided name and inventory ID + group = module.get_one('groups', **{ + 'data': { + 'name': name, + 'inventory': inventory_id + } + }) + + if state == 'absent': + # If the state was absent we can let the module delete it if needed, the module will handle exiting from this + module.delete_if_needed(group) + + # Create the data that gets sent for create and update + group_fields = { + 'name': new_name if new_name else name, + 'inventory': inventory_id, + } + if description is not None: + group_fields['description'] = description + if variables is not None: + group_fields['variables'] = json.dumps(variables) + + association_fields = {} + for resource, relationship in (('hosts', 'hosts'), ('groups', 'children')): + name_list = module.params.get(relationship) + if name_list is None: + continue + id_list = [] + for sub_name in name_list: + sub_obj = module.get_one(resource, **{ + 'data': {'inventory': inventory_id, 'name': sub_name} + }) + if sub_obj is None: + module.fail_json(msg='Could not find {0} with name {1}'.format(resource, sub_name)) + id_list.append(sub_obj['id']) + if id_list: + association_fields[relationship] = id_list + + # If the state was present we can let the module build or update the existing group, this will return on its own + module.create_or_update_if_needed( + group, group_fields, endpoint='groups', item_type='group', + associations=association_fields + ) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_host.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_host.py new file mode 100644 index 00000000..f6bfe554 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_host.py @@ -0,0 +1,134 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III <wayne@riotousliving.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_host +author: "Wayne Witzel III (@wwitzel3)" +short_description: create, update, or destroy Ansible Tower host. +description: + - Create, update, or destroy Ansible Tower hosts. See + U(https://www.ansible.com/tower) for an overview. +options: + name: + description: + - The name to use for the host. + required: True + type: str + new_name: + description: + - To use when changing a hosts's name. + type: str + description: + description: + - The description to use for the host. + type: str + inventory: + description: + - Inventory the host should be made a member of. + required: True + type: str + enabled: + description: + - If the host should be enabled. + type: bool + default: 'yes' + variables: + description: + - Variables to use for the host. + type: dict + state: + description: + - Desired state of the resource. + choices: ["present", "absent"] + default: "present" + type: str +extends_documentation_fragment: awx.awx.auth +''' + + +EXAMPLES = ''' +- name: Add tower host + tower_host: + name: localhost + description: "Local Host Group" + inventory: "Local Inventory" + state: present + tower_config_file: "~/tower_cli.cfg" + variables: + example_var: 123 +''' + + +from ..module_utils.tower_api import TowerAPIModule +import json + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + name=dict(required=True), + new_name=dict(), + description=dict(), + inventory=dict(required=True), + enabled=dict(type='bool', default=True), + variables=dict(type='dict'), + state=dict(choices=['present', 'absent'], default='present'), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + name = module.params.get('name') + new_name = module.params.get('new_name') + description = module.params.get('description') + inventory = module.params.get('inventory') + enabled = module.params.get('enabled') + state = module.params.get('state') + variables = module.params.get('variables') + + # Attempt to look up the related items the user specified (these will fail the module if not found) + inventory_id = module.resolve_name_to_id('inventories', inventory) + + # Attempt to look up host based on the provided name and inventory ID + host = module.get_one('hosts', **{ + 'data': { + 'name': name, + 'inventory': inventory_id + } + }) + + if state == 'absent': + # If the state was absent we can let the module delete it if needed, the module will handle exiting from this + module.delete_if_needed(host) + + # Create the data that gets sent for create and update + host_fields = { + 'name': new_name if new_name else name, + 'inventory': inventory_id, + 'enabled': enabled, + } + if description is not None: + host_fields['description'] = description + if variables is not None: + host_fields['variables'] = json.dumps(variables) + + # If the state was present and we can let the module build or update the existing host, this will return on its own + module.create_or_update_if_needed(host, host_fields, endpoint='hosts', item_type='host') + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_import.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_import.py new file mode 100644 index 00000000..a39a98a5 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_import.py @@ -0,0 +1,105 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, John Westcott IV <john.westcott.iv@redhat.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_import +author: "John Westcott (@john-westcott-iv)" +version_added: "3.7" +short_description: import resources into Ansible Tower. +description: + - Import assets into Ansible Tower. See + U(https://www.ansible.com/tower) for an overview. +options: + assets: + description: + - The assets to import. + - This can be the output of tower_export or loaded from a file + required: True + type: dict +requirements: + - "awxkit >= 9.3.0" +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Export all assets + tower_export: + all: True + registeR: export_output + +- name: Import all tower assets from our export + tower_import: + assets: "{{ export_output.assets }}" + +- name: Load data from a json file created by a command like awx export --organization Default + tower_import: + assets: "{{ lookup('file', 'org.json') | from_json() }}" +''' + +from ..module_utils.tower_awxkit import TowerAWXKitModule + +# These two lines are not needed if awxkit changes to do progamatic notifications on issues +from ansible.module_utils.six.moves import StringIO +import logging + +# In this module we don't use EXPORTABLE_RESOURCES, we just want to validate that our installed awxkit has import/export +try: + from awxkit.api.pages.api import EXPORTABLE_RESOURCES + HAS_EXPORTABLE_RESOURCES = True +except ImportError: + HAS_EXPORTABLE_RESOURCES = False + + +def main(): + argument_spec = dict( + assets=dict(type='dict', required=True) + ) + + module = TowerAWXKitModule(argument_spec=argument_spec, supports_check_mode=False) + + assets = module.params.get('assets') + + if not HAS_EXPORTABLE_RESOURCES: + module.fail_json(msg="Your version of awxkit does not appear to have import/export") + + # Currently the import process does not return anything on error + # It simply just logs to pythons logger + # Setup a log gobbler to get error messages from import_assets + logger = logging.getLogger('awxkit.api.pages.api') + logger.setLevel(logging.WARNING) + log_capture_string = StringIO() + ch = logging.StreamHandler(log_capture_string) + ch.setLevel(logging.WARNING) + logger.addHandler(ch) + log_contents = '' + + # Run the import process + try: + module.json_output['changed'] = module.get_api_v2_object().import_assets(assets) + except Exception as e: + module.fail_json(msg="Failed to import assets {0}".format(e)) + finally: + # Finally consume the logs incase there were any errors and die if there were + log_contents = log_capture_string.getvalue() + log_capture_string.close() + if log_contents != '': + module.fail_json(msg=log_contents) + + module.exit_json(**module.json_output) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_inventory.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_inventory.py new file mode 100644 index 00000000..7f03645e --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_inventory.py @@ -0,0 +1,138 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III <wayne@riotousliving.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_inventory +author: "Wayne Witzel III (@wwitzel3)" +short_description: create, update, or destroy Ansible Tower inventory. +description: + - Create, update, or destroy Ansible Tower inventories. See + U(https://www.ansible.com/tower) for an overview. +options: + name: + description: + - The name to use for the inventory. + required: True + type: str + description: + description: + - The description to use for the inventory. + type: str + organization: + description: + - Organization the inventory belongs to. + required: True + type: str + variables: + description: + - Inventory variables. + type: dict + kind: + description: + - The kind field. Cannot be modified after created. + default: "" + choices: ["", "smart"] + type: str + host_filter: + description: + - The host_filter field. Only useful when C(kind=smart). + type: str + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] + type: str +extends_documentation_fragment: awx.awx.auth +''' + + +EXAMPLES = ''' +- name: Add tower inventory + tower_inventory: + name: "Foo Inventory" + description: "Our Foo Cloud Servers" + organization: "Bar Org" + state: present + tower_config_file: "~/tower_cli.cfg" +''' + + +from ..module_utils.tower_api import TowerAPIModule +import json + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + name=dict(required=True), + description=dict(), + organization=dict(required=True), + variables=dict(type='dict'), + kind=dict(choices=['', 'smart'], default=''), + host_filter=dict(), + state=dict(choices=['present', 'absent'], default='present'), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + name = module.params.get('name') + description = module.params.get('description') + organization = module.params.get('organization') + variables = module.params.get('variables') + state = module.params.get('state') + kind = module.params.get('kind') + host_filter = module.params.get('host_filter') + + # Attempt to look up the related items the user specified (these will fail the module if not found) + org_id = module.resolve_name_to_id('organizations', organization) + + # Attempt to look up inventory based on the provided name and org ID + inventory = module.get_one('inventories', **{ + 'data': { + 'name': name, + 'organization': org_id + } + }) + + if state == 'absent': + # If the state was absent we can let the module delete it if needed, the module will handle exiting from this + module.delete_if_needed(inventory) + + # Create the data that gets sent for create and update + inventory_fields = { + 'name': name, + 'organization': org_id, + 'kind': kind, + 'host_filter': host_filter, + } + if description is not None: + inventory_fields['description'] = description + if variables is not None: + inventory_fields['variables'] = json.dumps(variables) + + # We need to perform a check to make sure you are not trying to convert a regular inventory into a smart one. + if inventory and inventory['kind'] == '' and inventory_fields['kind'] == 'smart': + module.fail_json(msg='You cannot turn a regular inventory into a "smart" inventory.') + + # If the state was present and we can let the module build or update the existing inventory, this will return on its own + module.create_or_update_if_needed(inventory, inventory_fields, endpoint='inventories', item_type='inventory') + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_inventory_source.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_inventory_source.py new file mode 100644 index 00000000..5b0e2961 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_inventory_source.py @@ -0,0 +1,277 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright: (c) 2018, Adrien Fleury <fleu42@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_inventory_source +author: "Adrien Fleury (@fleu42)" +short_description: create, update, or destroy Ansible Tower inventory source. +description: + - Create, update, or destroy Ansible Tower inventory source. See + U(https://www.ansible.com/tower) for an overview. +options: + name: + description: + - The name to use for the inventory source. + required: True + type: str + new_name: + description: + - A new name for this assets (will rename the asset) + type: str + description: + description: + - The description to use for the inventory source. + type: str + inventory: + description: + - Inventory the group should be made a member of. + required: True + type: str + source: + description: + - The source to use for this group. + choices: [ "scm", "ec2", "gce", "azure_rm", "vmware", "satellite6", "openstack", "rhv", "tower", "custom" ] + type: str + source_path: + description: + - For an SCM based inventory source, the source path points to the file within the repo to use as an inventory. + type: str + source_script: + description: + - Inventory script to be used when group type is C(custom). + type: str + source_vars: + description: + - The variables or environment fields to apply to this source type. + type: dict + credential: + description: + - Credential to use for the source. + type: str + source_regions: + description: + - Regions for cloud provider. + type: str + instance_filters: + description: + - Comma-separated list of filter expressions for matching hosts. + type: str + group_by: + description: + - Limit groups automatically created from inventory source. + type: str + overwrite: + description: + - Delete child groups and hosts not found in source. + type: bool + overwrite_vars: + description: + - Override vars in child groups and hosts with those from external source. + type: bool + custom_virtualenv: + description: + - Local absolute file path containing a custom Python virtualenv to use. + type: str + timeout: + description: The amount of time (in seconds) to run before the task is canceled. + type: int + verbosity: + description: The verbosity level to run this inventory source under. + type: int + choices: [ 0, 1, 2 ] + update_on_launch: + description: + - Refresh inventory data from its source each time a job is run. + type: bool + update_cache_timeout: + description: + - Time in seconds to consider an inventory sync to be current. + type: int + source_project: + description: + - Project to use as source with scm option + type: str + update_on_project_update: + description: Update this source when the related project updates if source is C(scm) + type: bool + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] + type: str + notification_templates_started: + description: + - list of notifications to send on start + type: list + elements: str + notification_templates_success: + description: + - list of notifications to send on success + type: list + elements: str + notification_templates_error: + description: + - list of notifications to send on error + type: list + elements: str +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Add an inventory source + tower_inventory_source: + name: "source-inventory" + description: Source for inventory + inventory: previously-created-inventory + credential: previously-created-credential + overwrite: True + update_on_launch: True + source_vars: + private: false +''' + +from ..module_utils.tower_api import TowerAPIModule +from json import dumps + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + name=dict(required=True), + new_name=dict(), + description=dict(), + inventory=dict(required=True), + # + # How do we handle manual and file? Tower does not seem to be able to activate them + # + source=dict(choices=["scm", "ec2", "gce", + "azure_rm", "vmware", "satellite6", + "openstack", "rhv", "tower", "custom"]), + source_path=dict(), + source_script=dict(), + source_vars=dict(type='dict'), + credential=dict(), + source_regions=dict(), + instance_filters=dict(), + group_by=dict(), + overwrite=dict(type='bool'), + overwrite_vars=dict(type='bool'), + custom_virtualenv=dict(), + timeout=dict(type='int'), + verbosity=dict(type='int', choices=[0, 1, 2]), + update_on_launch=dict(type='bool'), + update_cache_timeout=dict(type='int'), + source_project=dict(), + update_on_project_update=dict(type='bool'), + notification_templates_started=dict(type="list", elements='str'), + notification_templates_success=dict(type="list", elements='str'), + notification_templates_error=dict(type="list", elements='str'), + state=dict(choices=['present', 'absent'], default='present'), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + name = module.params.get('name') + new_name = module.params.get('new_name') + inventory = module.params.get('inventory') + source_script = module.params.get('source_script') + credential = module.params.get('credential') + source_project = module.params.get('source_project') + state = module.params.get('state') + + # Attempt to look up inventory source based on the provided name and inventory ID + inventory_id = module.resolve_name_to_id('inventories', inventory) + inventory_source = module.get_one('inventory_sources', **{ + 'data': { + 'name': name, + 'inventory': inventory_id, + } + }) + + if state == 'absent': + # If the state was absent we can let the module delete it if needed, the module will handle exiting from this + module.delete_if_needed(inventory_source) + + # Attempt to look up associated field items the user specified. + association_fields = {} + + notifications_start = module.params.get('notification_templates_started') + if notifications_start is not None: + association_fields['notification_templates_started'] = [] + for item in notifications_start: + association_fields['notification_templates_started'].append(module.resolve_name_to_id('notification_templates', item)) + + notifications_success = module.params.get('notification_templates_success') + if notifications_success is not None: + association_fields['notification_templates_success'] = [] + for item in notifications_success: + association_fields['notification_templates_success'].append(module.resolve_name_to_id('notification_templates', item)) + + notifications_error = module.params.get('notification_templates_error') + if notifications_error is not None: + association_fields['notification_templates_error'] = [] + for item in notifications_error: + association_fields['notification_templates_error'].append(module.resolve_name_to_id('notification_templates', item)) + + # Create the data that gets sent for create and update + inventory_source_fields = { + 'name': new_name if new_name else name, + 'inventory': inventory_id, + } + + # Attempt to look up the related items the user specified (these will fail the module if not found) + if credential is not None: + inventory_source_fields['credential'] = module.resolve_name_to_id('credentials', credential) + if source_project is not None: + inventory_source_fields['source_project'] = module.resolve_name_to_id('projects', source_project) + if source_script is not None: + inventory_source_fields['source_script'] = module.resolve_name_to_id('inventory_scripts', source_script) + + OPTIONAL_VARS = ( + 'description', 'source', 'source_path', 'source_vars', + 'source_regions', 'instance_filters', 'group_by', + 'overwrite', 'overwrite_vars', 'custom_virtualenv', + 'timeout', 'verbosity', 'update_on_launch', 'update_cache_timeout', + 'update_on_project_update' + ) + + # Layer in all remaining optional information + for field_name in OPTIONAL_VARS: + field_val = module.params.get(field_name) + if field_val is not None: + inventory_source_fields[field_name] = field_val + + # Attempt to JSON encode source vars + if inventory_source_fields.get('source_vars', None): + inventory_source_fields['source_vars'] = dumps(inventory_source_fields['source_vars']) + + # Sanity check on arguments + if state == 'present' and not inventory_source and not inventory_source_fields['source']: + module.fail_json(msg="If creating a new inventory source, the source param must be present") + + # If the state was present we can let the module build or update the existing inventory_source, this will return on its own + module.create_or_update_if_needed( + inventory_source, inventory_source_fields, + endpoint='inventory_sources', item_type='inventory source', + associations=association_fields + ) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_job_cancel.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_job_cancel.py new file mode 100644 index 00000000..7404d452 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_job_cancel.py @@ -0,0 +1,99 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III <wayne@riotousliving.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_job_cancel +author: "Wayne Witzel III (@wwitzel3)" +short_description: Cancel an Ansible Tower Job. +description: + - Cancel Ansible Tower jobs. See + U(https://www.ansible.com/tower) for an overview. +options: + job_id: + description: + - ID of the job to cancel + required: True + type: int + fail_if_not_running: + description: + - Fail loudly if the I(job_id) can not be canceled + default: False + type: bool +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Cancel job + tower_job_cancel: + job_id: job.id +''' + +RETURN = ''' +id: + description: job id requesting to cancel + returned: success + type: int + sample: 94 +''' + + +from ..module_utils.tower_api import TowerAPIModule + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + job_id=dict(type='int', required=True), + fail_if_not_running=dict(type='bool', default=False), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + job_id = module.params.get('job_id') + fail_if_not_running = module.params.get('fail_if_not_running') + + # Attempt to look up the job based on the provided name + job = module.get_one('jobs', **{ + 'data': { + 'id': job_id, + } + }) + + if job is None: + module.fail_json(msg="Unable to find job with id {0}".format(job_id)) + + cancel_page = module.get_endpoint(job['related']['cancel']) + if 'json' not in cancel_page or 'can_cancel' not in cancel_page['json']: + module.fail_json(msg="Failed to cancel job, got unexpected response from tower", **{'response': cancel_page}) + + if not cancel_page['json']['can_cancel']: + if fail_if_not_running: + module.fail_json(msg="Job is not running") + else: + module.exit_json(**{'changed': False}) + + results = module.post_endpoint(job['related']['cancel'], **{'data': {}}) + + if results['status_code'] != 202: + module.fail_json(msg="Failed to cancel job, see response for details", **{'response': results}) + + module.exit_json(**{'changed': True}) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_job_launch.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_job_launch.py new file mode 100644 index 00000000..25a1c52f --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_job_launch.py @@ -0,0 +1,227 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III <wayne@riotousliving.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_job_launch +author: "Wayne Witzel III (@wwitzel3)" +short_description: Launch an Ansible Job. +description: + - Launch an Ansible Tower jobs. See + U(https://www.ansible.com/tower) for an overview. +options: + name: + description: + - Name of the job template to use. + required: True + type: str + aliases: ['job_template'] + job_type: + description: + - Job_type to use for the job, only used if prompt for job_type is set. + choices: ["run", "check"] + type: str + inventory: + description: + - Inventory to use for the job, only used if prompt for inventory is set. + type: str + credentials: + description: + - Credential to use for job, only used if prompt for credential is set. + type: list + aliases: ['credential'] + elements: str + extra_vars: + description: + - extra_vars to use for the Job Template. + - ask_extra_vars needs to be set to True via tower_job_template module + when creating the Job Template. + type: dict + limit: + description: + - Limit to use for the I(job_template). + type: str + tags: + description: + - Specific tags to use for from playbook. + type: list + elements: str + scm_branch: + description: + - A specific of the SCM project to run the template on. + - This is only applicable if your project allows for branch override. + type: str + skip_tags: + description: + - Specific tags to skip from the playbook. + type: list + elements: str + verbosity: + description: + - Verbosity level for this job run + type: int + choices: [ 0, 1, 2, 3, 4, 5 ] + diff_mode: + description: + - Show the changes made by Ansible tasks where supported + type: bool + credential_passwords: + description: + - Passwords for credentials which are set to prompt on launch + type: dict +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Launch a job + tower_job_launch: + job_template: "My Job Template" + register: job + +- name: Launch a job template with extra_vars on remote Tower instance + tower_job_launch: + job_template: "My Job Template" + extra_vars: + var1: "My First Variable" + var2: "My Second Variable" + var3: "My Third Variable" + job_type: run + +- name: Launch a job with inventory and credential + tower_job_launch: + job_template: "My Job Template" + inventory: "My Inventory" + credential: "My Credential" + register: job +- name: Wait for job max 120s + tower_job_wait: + job_id: "{{ job.id }}" + timeout: 120 +''' + +RETURN = ''' +id: + description: job id of the newly launched job + returned: success + type: int + sample: 86 +status: + description: status of newly launched job + returned: success + type: str + sample: pending +''' + +from ..module_utils.tower_api import TowerAPIModule + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + name=dict(required=True, aliases=['job_template']), + job_type=dict(choices=['run', 'check']), + inventory=dict(default=None), + # Credentials will be a str instead of a list for backwards compatability + credentials=dict(type='list', default=None, aliases=['credential'], elements='str'), + limit=dict(), + tags=dict(type='list', elements='str'), + extra_vars=dict(type='dict'), + scm_branch=dict(), + skip_tags=dict(type='list', elements='str'), + verbosity=dict(type='int', choices=[0, 1, 2, 3, 4, 5]), + diff_mode=dict(type='bool'), + credential_passwords=dict(type='dict'), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + optional_args = {} + # Extract our parameters + name = module.params.get('name') + optional_args['job_type'] = module.params.get('job_type') + inventory = module.params.get('inventory') + credentials = module.params.get('credentials') + optional_args['limit'] = module.params.get('limit') + optional_args['tags'] = module.params.get('tags') + optional_args['extra_vars'] = module.params.get('extra_vars') + optional_args['scm_branch'] = module.params.get('scm_branch') + optional_args['skip_tags'] = module.params.get('skip_tags') + optional_args['verbosity'] = module.params.get('verbosity') + optional_args['diff_mode'] = module.params.get('diff_mode') + optional_args['credential_passwords'] = module.params.get('credential_passwords') + + # Create a datastructure to pass into our job launch + post_data = {} + for key in optional_args.keys(): + if optional_args[key]: + post_data[key] = optional_args[key] + + # Attempt to look up the related items the user specified (these will fail the module if not found) + if inventory: + post_data['inventory'] = module.resolve_name_to_id('inventories', inventory) + + if credentials: + post_data['credentials'] = [] + for credential in credentials: + post_data['credentials'].append(module.resolve_name_to_id('credentials', credential)) + + # Attempt to look up job_template based on the provided name + job_template = module.get_one('job_templates', **{ + 'data': { + 'name': name, + } + }) + + if job_template is None: + module.fail_json(msg="Unable to find job template by name {0}".format(name)) + + # The API will allow you to submit values to a jb launch that are not prompt on launch. + # Therefore, we will test to see if anything is set which is not prompt on launch and fail. + check_vars_to_prompts = { + 'scm_branch': 'ask_scm_branch_on_launch', + 'diff_mode': 'ask_diff_mode_on_launch', + 'extra_vars': 'ask_variables_on_launch', + 'limit': 'ask_limit_on_launch', + 'tags': 'ask_tags_on_launch', + 'skip_tags': 'ask_skip_tags_on_launch', + 'job_type': 'ask_job_type_on_launch', + 'verbosity': 'ask_verbosity_on_launch', + 'inventory': 'ask_inventory_on_launch', + 'credentials': 'ask_credential_on_launch', + } + + param_errors = [] + for variable_name in check_vars_to_prompts: + if module.params.get(variable_name) and not job_template[check_vars_to_prompts[variable_name]]: + param_errors.append("The field {0} was specified but the job template does not allow for it to be overridden".format(variable_name)) + if len(param_errors) > 0: + module.fail_json(msg="Parameters specified which can not be passed into job template, see errors for details", **{'errors': param_errors}) + + # Launch the job + results = module.post_endpoint(job_template['related']['launch'], **{'data': post_data}) + + if results['status_code'] != 201: + module.fail_json(msg="Failed to launch job, see response for details", **{'response': results}) + + module.exit_json(**{ + 'changed': True, + 'id': results['json']['id'], + 'status': results['json']['status'], + }) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_job_list.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_job_list.py new file mode 100644 index 00000000..642a48b0 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_job_list.py @@ -0,0 +1,126 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III <wayne@riotousliving.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_job_list +author: "Wayne Witzel III (@wwitzel3)" +short_description: List Ansible Tower jobs. +description: + - List Ansible Tower jobs. See + U(https://www.ansible.com/tower) for an overview. +options: + status: + description: + - Only list jobs with this status. + choices: ['pending', 'waiting', 'running', 'error', 'failed', 'canceled', 'successful'] + type: str + page: + description: + - Page number of the results to fetch. + type: int + all_pages: + description: + - Fetch all the pages and return a single result. + type: bool + default: 'no' + query: + description: + - Query used to further filter the list of jobs. C({"foo":"bar"}) will be passed at C(?foo=bar) + type: dict +extends_documentation_fragment: awx.awx.auth +''' + + +EXAMPLES = ''' +- name: List running jobs for the testing.yml playbook + tower_job_list: + status: running + query: {"playbook": "testing.yml"} + tower_config_file: "~/tower_cli.cfg" + register: testing_jobs +''' + +RETURN = ''' +count: + description: Total count of objects return + returned: success + type: int + sample: 51 +next: + description: next page available for the listing + returned: success + type: int + sample: 3 +previous: + description: previous page available for the listing + returned: success + type: int + sample: 1 +results: + description: a list of job objects represented as dictionaries + returned: success + type: list + sample: [{"allow_simultaneous": false, "artifacts": {}, "ask_credential_on_launch": false, + "ask_inventory_on_launch": false, "ask_job_type_on_launch": false, "failed": false, + "finished": "2017-02-22T15:09:05.633942Z", "force_handlers": false, "forks": 0, "id": 2, + "inventory": 1, "job_explanation": "", "job_tags": "", "job_template": 5, "job_type": "run"}, ...] +''' + + +from ..module_utils.tower_api import TowerAPIModule + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + status=dict(choices=['pending', 'waiting', 'running', 'error', 'failed', 'canceled', 'successful']), + page=dict(type='int'), + all_pages=dict(type='bool', default=False), + query=dict(type='dict'), + ) + + # Create a module for ourselves + module = TowerAPIModule( + argument_spec=argument_spec, + mutually_exclusive=[ + ('page', 'all_pages'), + ] + ) + + # Extract our parameters + query = module.params.get('query') + status = module.params.get('status') + page = module.params.get('page') + all_pages = module.params.get('all_pages') + + job_search_data = {} + if page: + job_search_data['page'] = page + if status: + job_search_data['status'] = status + if query: + job_search_data.update(query) + if all_pages: + job_list = module.get_all_endpoint('jobs', **{'data': job_search_data}) + else: + job_list = module.get_endpoint('jobs', **{'data': job_search_data}) + + # Attempt to look up jobs based on the status + module.exit_json(**job_list['json']) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_job_template.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_job_template.py new file mode 100644 index 00000000..1f12d5aa --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_job_template.py @@ -0,0 +1,526 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III <wayne@riotousliving.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_job_template +author: "Wayne Witzel III (@wwitzel3)" +short_description: create, update, or destroy Ansible Tower job templates. +description: + - Create, update, or destroy Ansible Tower job templates. See + U(https://www.ansible.com/tower) for an overview. +options: + name: + description: + - Name to use for the job template. + required: True + type: str + new_name: + description: + - Setting this option will change the existing name (looed up via the name field. + type: str + description: + description: + - Description to use for the job template. + type: str + job_type: + description: + - The job type to use for the job template. + choices: ["run", "check"] + type: str + inventory: + description: + - Name of the inventory to use for the job template. + type: str + organization: + description: + - Organization the job template exists in. + - Used to help lookup the object, cannot be modified using this module. + - The Organization is inferred from the associated project + - If not provided, will lookup by name only, which does not work with duplicates. + - Requires Tower Version 3.7.0 or AWX 10.0.0 IS NOT backwards compatible with earlier versions. + type: str + project: + description: + - Name of the project to use for the job template. + type: str + playbook: + description: + - Path to the playbook to use for the job template within the project provided. + type: str + credential: + description: + - Name of the credential to use for the job template. + - Deprecated, use 'credentials'. + type: str + credentials: + description: + - List of credentials to use for the job template. + type: list + elements: str + vault_credential: + description: + - Name of the vault credential to use for the job template. + - Deprecated, use 'credentials'. + type: str + forks: + description: + - The number of parallel or simultaneous processes to use while executing the playbook. + type: int + limit: + description: + - A host pattern to further constrain the list of hosts managed or affected by the playbook + type: str + verbosity: + description: + - Control the output level Ansible produces as the playbook runs. 0 - Normal, 1 - Verbose, 2 - More Verbose, 3 - Debug, 4 - Connection Debug. + choices: [0, 1, 2, 3, 4] + default: 0 + type: int + extra_vars: + description: + - Specify C(extra_vars) for the template. + type: dict + job_tags: + description: + - Comma separated list of the tags to use for the job template. + type: str + force_handlers: + description: + - Enable forcing playbook handlers to run even if a task fails. + type: bool + default: 'no' + aliases: + - force_handlers_enabled + skip_tags: + description: + - Comma separated list of the tags to skip for the job template. + type: str + start_at_task: + description: + - Start the playbook at the task matching this name. + type: str + diff_mode: + description: + - Enable diff mode for the job template. + type: bool + aliases: + - diff_mode_enabled + default: 'no' + use_fact_cache: + description: + - Enable use of fact caching for the job template. + type: bool + default: 'no' + aliases: + - fact_caching_enabled + host_config_key: + description: + - Allow provisioning callbacks using this host config key. + type: str + ask_scm_branch_on_launch: + description: + - Prompt user for (scm branch) on launch. + type: bool + default: 'False' + ask_diff_mode_on_launch: + description: + - Prompt user to enable diff mode (show changes) to files when supported by modules. + type: bool + default: 'False' + aliases: + - ask_diff_mode + ask_variables_on_launch: + description: + - Prompt user for (extra_vars) on launch. + type: bool + default: 'False' + aliases: + - ask_extra_vars + ask_limit_on_launch: + description: + - Prompt user for a limit on launch. + type: bool + default: 'False' + aliases: + - ask_limit + ask_tags_on_launch: + description: + - Prompt user for job tags on launch. + type: bool + default: 'False' + aliases: + - ask_tags + ask_skip_tags_on_launch: + description: + - Prompt user for job tags to skip on launch. + type: bool + default: 'False' + aliases: + - ask_skip_tags + ask_job_type_on_launch: + description: + - Prompt user for job type on launch. + type: bool + default: 'False' + aliases: + - ask_job_type + ask_verbosity_on_launch: + description: + - Prompt user to choose a verbosity level on launch. + type: bool + default: 'False' + aliases: + - ask_verbosity + ask_inventory_on_launch: + description: + - Prompt user for inventory on launch. + type: bool + default: 'False' + aliases: + - ask_inventory + ask_credential_on_launch: + description: + - Prompt user for credential on launch. + type: bool + default: 'False' + aliases: + - ask_credential + survey_enabled: + description: + - Enable a survey on the job template. + type: bool + default: 'no' + survey_spec: + description: + - JSON/YAML dict formatted survey definition. + type: dict + become_enabled: + description: + - Activate privilege escalation. + type: bool + default: 'no' + allow_simultaneous: + description: + - Allow simultaneous runs of the job template. + type: bool + default: 'no' + aliases: + - concurrent_jobs_enabled + timeout: + description: + - Maximum time in seconds to wait for a job to finish (server-side). + type: int + custom_virtualenv: + description: + - Local absolute file path containing a custom Python virtualenv to use. + type: str + job_slice_count: + description: + - The number of jobs to slice into at runtime. Will cause the Job Template to launch a workflow if value is greater than 1. + type: int + default: '1' + webhook_service: + description: + - Service that webhook requests will be accepted from + type: str + choices: + - '' + - 'github' + - 'gitlab' + webhook_credential: + description: + - Personal Access Token for posting back the status to the service API + type: str + scm_branch: + description: + - Branch to use in job run. Project default used if blank. Only allowed if project allow_override field is set to true. + type: str + default: '' + labels: + description: + - The labels applied to this job template + type: list + elements: str + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] + type: str + notification_templates_started: + description: + - list of notifications to send on start + type: list + elements: str + notification_templates_success: + description: + - list of notifications to send on success + type: list + elements: str + notification_templates_error: + description: + - list of notifications to send on error + type: list + elements: str + +extends_documentation_fragment: awx.awx.auth + +notes: + - JSON for survey_spec can be found in Tower API Documentation. See + U(https://docs.ansible.com/ansible-tower/latest/html/towerapi/api_ref.html#/Job_Templates/Job_Templates_job_templates_survey_spec_create) + for POST operation payload example. +''' + + +EXAMPLES = ''' +- name: Create Tower Ping job template + tower_job_template: + name: "Ping" + job_type: "run" + organization: "Default" + inventory: "Local" + project: "Demo" + playbook: "ping.yml" + credentials: + - "Local" + state: "present" + tower_config_file: "~/tower_cli.cfg" + survey_enabled: yes + survey_spec: "{{ lookup('file', 'my_survey.json') }}" + custom_virtualenv: "/var/lib/awx/venv/custom-venv/" + +- name: Add start notification to Job Template + tower_job_template: + name: "Ping" + notification_templates_started: + - Notification1 + - Notification2 + +- name: Remove Notification1 start notification from Job Template + tower_job_template: + name: "Ping" + notification_templates_started: + - Notification2 + +''' + +from ..module_utils.tower_api import TowerAPIModule +import json + + +def update_survey(module, last_request): + spec_endpoint = last_request.get('related', {}).get('survey_spec') + if module.params.get('survey_spec') == {}: + response = module.delete_endpoint(spec_endpoint) + if response['status_code'] != 200: + # Not sure how to make this actually return a non 200 to test what to dump in the respinse + module.fail_json(msg="Failed to delete survey: {0}".format(response['json'])) + else: + response = module.post_endpoint(spec_endpoint, **{'data': module.params.get('survey_spec')}) + if response['status_code'] != 200: + module.fail_json(msg="Failed to update survey: {0}".format(response['json']['error'])) + module.exit_json(**module.json_output) + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + name=dict(required=True), + new_name=dict(), + description=dict(default=''), + organization=dict(), + job_type=dict(choices=['run', 'check']), + inventory=dict(), + project=dict(), + playbook=dict(), + credential=dict(default=''), + vault_credential=dict(default=''), + custom_virtualenv=dict(), + credentials=dict(type='list', elements='str'), + forks=dict(type='int'), + limit=dict(default=''), + verbosity=dict(type='int', choices=[0, 1, 2, 3, 4], default=0), + extra_vars=dict(type='dict'), + job_tags=dict(default=''), + force_handlers=dict(type='bool', default=False, aliases=['force_handlers_enabled']), + skip_tags=dict(default=''), + start_at_task=dict(default=''), + timeout=dict(type='int', default=0), + use_fact_cache=dict(type='bool', aliases=['fact_caching_enabled']), + host_config_key=dict(), + ask_diff_mode_on_launch=dict(type='bool', aliases=['ask_diff_mode']), + ask_variables_on_launch=dict(type='bool', aliases=['ask_extra_vars']), + ask_limit_on_launch=dict(type='bool', aliases=['ask_limit']), + ask_tags_on_launch=dict(type='bool', aliases=['ask_tags']), + ask_skip_tags_on_launch=dict(type='bool', aliases=['ask_skip_tags']), + ask_job_type_on_launch=dict(type='bool', aliases=['ask_job_type']), + ask_verbosity_on_launch=dict(type='bool', aliases=['ask_verbosity']), + ask_inventory_on_launch=dict(type='bool', aliases=['ask_inventory']), + ask_credential_on_launch=dict(type='bool', aliases=['ask_credential']), + survey_enabled=dict(type='bool'), + survey_spec=dict(type="dict"), + become_enabled=dict(type='bool'), + diff_mode=dict(type='bool', aliases=['diff_mode_enabled']), + allow_simultaneous=dict(type='bool', aliases=['concurrent_jobs_enabled']), + scm_branch=dict(), + ask_scm_branch_on_launch=dict(type='bool'), + job_slice_count=dict(type='int', default='1'), + webhook_service=dict(choices=['github', 'gitlab', '']), + webhook_credential=dict(), + labels=dict(type="list", elements='str'), + notification_templates_started=dict(type="list", elements='str'), + notification_templates_success=dict(type="list", elements='str'), + notification_templates_error=dict(type="list", elements='str'), + state=dict(choices=['present', 'absent'], default='present'), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + name = module.params.get('name') + new_name = module.params.get("new_name") + state = module.params.get('state') + + # Deal with legacy credential and vault_credential + credential = module.params.get('credential') + vault_credential = module.params.get('vault_credential') + credentials = module.params.get('credentials') + if vault_credential != '': + if credentials is None: + credentials = [] + credentials.append(vault_credential) + if credential != '': + if credentials is None: + credentials = [] + credentials.append(credential) + + new_fields = {} + search_fields = {'name': name} + + # Attempt to look up the related items the user specified (these will fail the module if not found) + organization_id = None + organization = module.params.get('organization') + if organization: + organization_id = module.resolve_name_to_id('organizations', organization) + search_fields['organization'] = new_fields['organization'] = organization_id + + # Attempt to look up an existing item based on the provided data + existing_item = module.get_one('job_templates', **{'data': search_fields}) + + if state == 'absent': + # If the state was absent we can let the module delete it if needed, the module will handle exiting from this + module.delete_if_needed(existing_item) + + # Create the data that gets sent for create and update + new_fields['name'] = new_name if new_name else name + for field_name in ( + 'description', 'job_type', 'playbook', 'scm_branch', 'forks', 'limit', 'verbosity', + 'job_tags', 'force_handlers', 'skip_tags', 'start_at_task', 'timeout', 'use_fact_cache', + 'host_config_key', 'ask_scm_branch_on_launch', 'ask_diff_mode_on_launch', 'ask_variables_on_launch', + 'ask_limit_on_launch', 'ask_tags_on_launch', 'ask_skip_tags_on_launch', 'ask_job_type_on_launch', + 'ask_verbosity_on_launch', 'ask_inventory_on_launch', 'ask_credential_on_launch', 'survey_enabled', + 'become_enabled', 'diff_mode', 'allow_simultaneous', 'custom_virtualenv', 'job_slice_count', 'webhook_service', + ): + field_val = module.params.get(field_name) + if field_val: + new_fields[field_name] = field_val + + # Special treatment of extra_vars parameter + extra_vars = module.params.get('extra_vars') + if extra_vars is not None: + new_fields['extra_vars'] = json.dumps(extra_vars) + + # Attempt to look up the related items the user specified (these will fail the module if not found) + inventory = module.params.get('inventory') + project = module.params.get('project') + webhook_credential = module.params.get('webhook_credential') + + if inventory is not None: + new_fields['inventory'] = module.resolve_name_to_id('inventories', inventory) + if project is not None: + if organization_id is not None: + project_data = module.get_one('projects', **{ + 'data': { + 'name': project, + 'organization': organization_id, + } + }) + if project_data is None: + module.fail_json(msg="The project {0} in organization {1} was not found on the Tower server".format( + project, organization + )) + new_fields['project'] = project_data['id'] + else: + new_fields['project'] = module.resolve_name_to_id('projects', project) + if webhook_credential is not None: + new_fields['webhook_credential'] = module.resolve_name_to_id('credentials', webhook_credential) + + association_fields = {} + + if credentials is not None: + association_fields['credentials'] = [] + for item in credentials: + association_fields['credentials'].append(module.resolve_name_to_id('credentials', item)) + + labels = module.params.get('labels') + if labels is not None: + association_fields['labels'] = [] + for item in labels: + association_fields['labels'].append(module.resolve_name_to_id('labels', item)) + + notifications_start = module.params.get('notification_templates_started') + if notifications_start is not None: + association_fields['notification_templates_started'] = [] + for item in notifications_start: + association_fields['notification_templates_started'].append(module.resolve_name_to_id('notification_templates', item)) + + notifications_success = module.params.get('notification_templates_success') + if notifications_success is not None: + association_fields['notification_templates_success'] = [] + for item in notifications_success: + association_fields['notification_templates_success'].append(module.resolve_name_to_id('notification_templates', item)) + + notifications_error = module.params.get('notification_templates_error') + if notifications_error is not None: + association_fields['notification_templates_error'] = [] + for item in notifications_error: + association_fields['notification_templates_error'].append(module.resolve_name_to_id('notification_templates', item)) + + on_change = None + new_spec = module.params.get('survey_spec') + if new_spec is not None: + existing_spec = None + if existing_item: + spec_endpoint = existing_item.get('related', {}).get('survey_spec') + existing_spec = module.get_endpoint(spec_endpoint)['json'] + if new_spec != existing_spec: + module.json_output['changed'] = True + if existing_item and module.has_encrypted_values(existing_spec): + module._encrypted_changed_warning('survey_spec', existing_item, warning=True) + on_change = update_survey + + # If the state was present and we can let the module build or update the existing item, this will return on its own + module.create_or_update_if_needed( + existing_item, new_fields, + endpoint='job_templates', item_type='job_template', + associations=association_fields, + on_create=on_change, on_update=on_change, + ) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_job_wait.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_job_wait.py new file mode 100644 index 00000000..5e5801b2 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_job_wait.py @@ -0,0 +1,186 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III <wayne@riotousliving.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_job_wait +author: "Wayne Witzel III (@wwitzel3)" +short_description: Wait for Ansible Tower job to finish. +description: + - Wait for Ansible Tower job to finish and report success or failure. See + U(https://www.ansible.com/tower) for an overview. +options: + job_id: + description: + - ID of the job to monitor. + required: True + type: int + interval: + description: + - The interval in sections, to request an update from Tower. + - For backwards compatability if unset this will be set to the average of min and max intervals + required: False + default: 1 + type: float + min_interval: + description: + - Minimum interval in seconds, to request an update from Tower. + - deprecated, use interval instead + type: float + max_interval: + description: + - Maximum interval in seconds, to request an update from Tower. + - deprecated, use interval instead + type: float + timeout: + description: + - Maximum time in seconds to wait for a job to finish. + type: int +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Launch a job + tower_job_launch: + job_template: "My Job Template" + register: job + +- name: Wait for job max 120s + tower_job_wait: + job_id: "{{ job.id }}" + timeout: 120 +''' + +RETURN = ''' +id: + description: job id that is being waited on + returned: success + type: int + sample: 99 +elapsed: + description: total time in seconds the job took to run + returned: success + type: float + sample: 10.879 +started: + description: timestamp of when the job started running + returned: success + type: str + sample: "2017-03-01T17:03:53.200234Z" +finished: + description: timestamp of when the job finished running + returned: success + type: str + sample: "2017-03-01T17:04:04.078782Z" +status: + description: current status of job + returned: success + type: str + sample: successful +''' + + +from ..module_utils.tower_api import TowerAPIModule +import time + + +def check_job(module, job_url): + response = module.get_endpoint(job_url) + if response['status_code'] != 200: + module.fail_json(msg="Unable to read job from Tower {0}: {1}".format(response['status_code'], module.extract_errors_from_response(response))) + + # Since we were successful, extract the fields we want to return + for k in ('id', 'status', 'elapsed', 'started', 'finished'): + module.json_output[k] = response['json'].get(k) + + # And finally return the payload + return response['json'] + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + job_id=dict(type='int', required=True), + timeout=dict(type='int'), + min_interval=dict(type='float'), + max_interval=dict(type='float'), + interval=dict(type='float', default=1), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + job_id = module.params.get('job_id') + timeout = module.params.get('timeout') + min_interval = module.params.get('min_interval') + max_interval = module.params.get('max_interval') + interval = module.params.get('interval') + + if min_interval is not None or max_interval is not None: + # We can't tell if we got the default or if someone actually set this to 1. + # For now if we find 1 and had a min or max then we will do the average logic. + if interval == 1: + if not min_interval: + min_interval = 1 + if not max_interval: + max_interval = 30 + interval = abs((min_interval + max_interval) / 2) + module.deprecate( + msg="Min and max interval have been deprecated, please use interval instead; interval will be set to {0}".format(interval), + version="ansible.tower:4.0.0" + ) + + # Attempt to look up job based on the provided id + job = module.get_one('jobs', **{ + 'data': { + 'id': job_id, + } + }) + + if job is None: + module.fail_json(msg='Unable to wait on job {0}; that ID does not exist in Tower.'.format(job_id)) + + job_url = job['url'] + + # Grab our start time to compare against for the timeout + start = time.time() + + # Get the initial job status from Tower, this will exit if there are any issues with the HTTP call + result = check_job(module, job_url) + + # Loop while the job is not yet completed + while not result['finished']: + # If we are past our time out fail with a message + if timeout and timeout < time.time() - start: + module.json_output['msg'] = "Monitoring aborted due to timeout" + module.fail_json(**module.json_output) + + # Put the process to sleep for our interval + time.sleep(interval) + + # Check the job again + result = check_job(module, job_url) + + # If the job has failed, we want to raise an Exception for that so we get a non-zero response. + if result['failed']: + module.json_output['msg'] = 'Job with id {0} failed'.format(job_id) + module.fail_json(**module.json_output) + + module.exit_json(**module.json_output) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_label.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_label.py new file mode 100644 index 00000000..6a3a8288 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_label.py @@ -0,0 +1,105 @@ +#!/usr/bin/python +# coding: utf-8 -*- + + +# (c) 2017, Wayne Witzel III <wayne@riotousliving.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: tower_label +author: "Wayne Witzel III (@wwitzel3)" +short_description: create, update, or destroy Ansible Tower labels. +description: + - Create, update, or destroy Ansible Tower labels. See + U(https://www.ansible.com/tower) for an overview. + - Note, labels can only be created via the Tower API, they can not be deleted. + Once they are fully disassociated the API will clean them up on its own. +options: + name: + description: + - Name of this label. + required: True + type: str + new_name: + description: + - Setting this option will change the existing name (looked up via the name field). + type: str + organization: + description: + - Organization this label belongs to. + required: True + type: str + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present"] + type: str +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Add label to tower organization + tower_label: + name: Custom Label + organization: My Organization +''' + +from ..module_utils.tower_api import TowerAPIModule + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + name=dict(required=True), + new_name=dict(), + organization=dict(required=True), + state=dict(choices=['present'], default='present'), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + name = module.params.get('name') + new_name = module.params.get("new_name") + organization = module.params.get('organization') + + # Attempt to look up the related items the user specified (these will fail the module if not found) + organization_id = None + if organization: + organization_id = module.resolve_name_to_id('organizations', organization) + + # Attempt to look up an existing item based on the provided data + existing_item = module.get_one('labels', **{ + 'data': { + 'name': name, + 'organization': organization_id, + } + }) + + # Create the data that gets sent for create and update + new_fields = {} + new_fields['name'] = new_name if new_name else name + if organization: + new_fields['organization'] = organization_id + + module.create_or_update_if_needed( + existing_item, new_fields, + endpoint='labels', item_type='label', + associations={ + } + ) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_license.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_license.py new file mode 100644 index 00000000..25d337cc --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_license.py @@ -0,0 +1,81 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2019, John Westcott IV <john.westcott.iv@redhat.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_license +author: "John Westcott IV (@john-westcott-iv)" +short_description: Set the license for Ansible Tower +description: + - Get or Set Ansible Tower license. See + U(https://www.ansible.com/tower) for an overview. +options: + data: + description: + - The contents of the license file + required: True + type: dict + eula_accepted: + description: + - Whether or not the EULA is accepted. + required: True + type: bool +extends_documentation_fragment: awx.awx.auth +''' + +RETURN = ''' # ''' + +EXAMPLES = ''' +- name: Set the license using a file + license: + data: "{{ lookup('file', '/tmp/my_tower.license') }}" + eula_accepted: True +''' + +from ..module_utils.tower_api import TowerAPIModule + + +def main(): + + module = TowerAPIModule( + argument_spec=dict( + data=dict(type='dict', required=True), + eula_accepted=dict(type='bool', required=True), + ), + ) + + json_output = {'changed': False} + + if not module.params.get('eula_accepted'): + module.fail_json(msg='You must accept the EULA by passing in the param eula_accepted as True') + + json_output['old_license'] = module.get_endpoint('settings/system/')['json']['LICENSE'] + new_license = module.params.get('data') + + if json_output['old_license'] != new_license: + json_output['changed'] = True + + # Deal with check mode + if module.check_mode: + module.exit_json(**json_output) + + # We need to add in the EULA + new_license['eula_accepted'] = True + module.post_endpoint('config', data=new_license) + + module.exit_json(**json_output) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_meta.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_meta.py new file mode 100644 index 00000000..9455bdf0 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_meta.py @@ -0,0 +1,84 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2020, Ansible by Red Hat, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: tower_meta +author: "Alan Rominger (@alancoding)" +short_description: Returns metadata about the collection this module lives in. +description: + - Allows a user to find out what collection this module exists in. + - This takes common module parameters, but does nothing with them. +options: {} +extends_documentation_fragment: awx.awx.auth +''' + + +RETURN = ''' +prefix: + description: Collection namespace and name in the namespace.name format + returned: success + sample: awx.awx + type: str +name: + description: Collection name + returned: success + sample: awx + type: str +namespace: + description: Collection namespace + returned: success + sample: awx + type: str +version: + description: Version of the collection + returned: success + sample: 0.0.1-devel + type: str +''' + + +EXAMPLES = ''' +- tower_meta: + register: result + +- name: Show details about the collection + debug: var=result + +- name: Load the UI setting without hard-coding the collection name + debug: + msg: "{{ lookup(result.prefix + '.tower_api', 'settings/ui') }}" +''' + + +from ..module_utils.tower_api import TowerAPIModule + + +def main(): + module = TowerAPIModule(argument_spec={}) + namespace = { + 'awx': 'awx', + 'tower': 'ansible' + }.get(module._COLLECTION_TYPE, 'unknown') + namespace_name = '{0}.{1}'.format(namespace, module._COLLECTION_TYPE) + module.exit_json( + prefix=namespace_name, + name=module._COLLECTION_TYPE, + namespace=namespace, + version=module._COLLECTION_VERSION + ) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_notification.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_notification.py new file mode 100644 index 00000000..12dd28ef --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_notification.py @@ -0,0 +1,427 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2018, Samuel Carpentier <samuelcarpentier0@gmail.ca> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_notification +author: "Samuel Carpentier (@samcarpentier)" +short_description: create, update, or destroy Ansible Tower notification. +description: + - Create, update, or destroy Ansible Tower notifications. See + U(https://www.ansible.com/tower) for an overview. +options: + name: + description: + - The name of the notification. + type: str + required: True + new_name: + description: + - Setting this option will change the existing name (looked up via the name field. + type: str + description: + description: + - The description of the notification. + type: str + organization: + description: + - The organization the notification belongs to. + type: str + notification_type: + description: + - The type of notification to be sent. + choices: + - 'email' + - 'grafana' + - 'irc' + - 'mattermost' + - 'pagerduty' + - 'rocketchat' + - 'slack' + - 'twilio' + - 'webhook' + type: str + notification_configuration: + description: + - The notification configuration file. Note providing this field would disable all notification-configuration-related fields. + type: dict + messages: + description: + - Optional custom messages for notification template. + type: dict + username: + description: + - The mail server username. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: str + sender: + description: + - The sender email address. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: str + recipients: + description: + - The recipients email addresses. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: list + elements: str + use_tls: + description: + - The TLS trigger. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: bool + host: + description: + - The mail server host. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: str + use_ssl: + description: + - The SSL trigger. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: bool + password: + description: + - The mail server password. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: str + port: + description: + - The mail server port. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: int + channels: + description: + - The destination Slack channels. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: list + elements: str + token: + description: + - The access token. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: str + account_token: + description: + - The Twillio account token. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: str + from_number: + description: + - The source phone number. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: str + to_numbers: + description: + - The destination phone numbers. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: list + elements: str + account_sid: + description: + - The Twillio account SID. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: str + subdomain: + description: + - The PagerDuty subdomain. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: str + service_key: + description: + - The PagerDuty service/integration API key. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: str + client_name: + description: + - The PagerDuty client identifier. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: str + message_from: + description: + - The label to be shown with the notification. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: str + color: + description: + - The notification color. + - This parameter has been deprecated, please use 'notification_configuration' instead. + choices: ["yellow", "green", "red", "purple", "gray", "random"] + type: str + notify: + description: + - The notify channel trigger. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: bool + url: + description: + - The target URL. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: str + headers: + description: + - The HTTP headers as JSON string. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: dict + server: + description: + - The IRC server address. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: str + nickname: + description: + - The IRC nickname. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: str + targets: + description: + - The destination channels or users. + - This parameter has been deprecated, please use 'notification_configuration' instead. + type: list + elements: str + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] + type: str +extends_documentation_fragment: awx.awx.auth +''' + + +EXAMPLES = ''' +- name: Add Slack notification with custom messages + tower_notification: + name: slack notification + organization: Default + notification_type: slack + notification_configuration: + channels: + - general + token: cefda9e2be1f21d11cdd9452f5b7f97fda977f42 + messages: + started: + message: "{{ '{{ job_friendly_name }}{{ job.id }} started' }}" + success: + message: "{{ '{{ job_friendly_name }} completed in {{ job.elapsed }} seconds' }}" + error: + message: "{{ '{{ job_friendly_name }} FAILED! Please look at {{ job.url }}' }}" + state: present + tower_config_file: "~/tower_cli.cfg" + +- name: Add webhook notification + tower_notification: + name: webhook notification + notification_type: webhook + notification_configuration: + url: http://www.example.com/hook + headers: + X-Custom-Header: value123 + state: present + tower_config_file: "~/tower_cli.cfg" + +- name: Add email notification + tower_notification: + name: email notification + notification_type: email + notification_configuration: + username: user + password: s3cr3t + sender: tower@example.com + recipients: + - user1@example.com + host: smtp.example.com + port: 25 + use_tls: no + use_ssl: no + state: present + tower_config_file: "~/tower_cli.cfg" + +- name: Add twilio notification + tower_notification: + name: twilio notification + notification_type: twilio + notification_configuration: + account_token: a_token + account_sid: a_sid + from_number: '+15551112222' + to_numbers: + - '+15553334444' + state: present + tower_config_file: "~/tower_cli.cfg" + +- name: Add PagerDuty notification + tower_notification: + name: pagerduty notification + notification_type: pagerduty + notification_configuration: + token: a_token + subdomain: sub + client_name: client + service_key: a_key + state: present + tower_config_file: "~/tower_cli.cfg" + +- name: Add IRC notification + tower_notification: + name: irc notification + notification_type: irc + notification_configuration: + nickname: tower + password: s3cr3t + targets: + - user1 + port: 8080 + server: irc.example.com + use_ssl: no + state: present + tower_config_file: "~/tower_cli.cfg" + +- name: Delete notification + tower_notification: + name: old notification + state: absent + tower_config_file: "~/tower_cli.cfg" +''' + + +RETURN = ''' # ''' + + +from ..module_utils.tower_api import TowerAPIModule + +OLD_INPUT_NAMES = ( + 'username', 'sender', 'recipients', 'use_tls', + 'host', 'use_ssl', 'password', 'port', + 'channels', 'token', 'account_token', 'from_number', + 'to_numbers', 'account_sid', 'subdomain', 'service_key', + 'client_name', 'message_from', 'color', + 'notify', 'url', 'headers', 'server', + 'nickname', 'targets', +) + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + name=dict(required=True), + new_name=dict(), + description=dict(), + organization=dict(), + notification_type=dict(choices=[ + 'email', 'grafana', 'irc', 'mattermost', + 'pagerduty', 'rocketchat', 'slack', 'twilio', 'webhook' + ]), + notification_configuration=dict(type='dict'), + messages=dict(type='dict'), + username=dict(), + sender=dict(), + recipients=dict(type='list', elements='str'), + use_tls=dict(type='bool'), + host=dict(), + use_ssl=dict(type='bool'), + password=dict(no_log=True), + port=dict(type='int'), + channels=dict(type='list', elements='str'), + token=dict(no_log=True), + account_token=dict(no_log=True), + from_number=dict(), + to_numbers=dict(type='list', elements='str'), + account_sid=dict(), + subdomain=dict(), + service_key=dict(no_log=True), + client_name=dict(), + message_from=dict(), + color=dict(choices=['yellow', 'green', 'red', 'purple', 'gray', 'random']), + notify=dict(type='bool'), + url=dict(), + headers=dict(type='dict'), + server=dict(), + nickname=dict(), + targets=dict(type='list', elements='str'), + state=dict(choices=['present', 'absent'], default='present'), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + name = module.params.get('name') + new_name = module.params.get('new_name') + description = module.params.get('description') + organization = module.params.get('organization') + notification_type = module.params.get('notification_type') + notification_configuration = module.params.get('notification_configuration') + messages = module.params.get('messages') + state = module.params.get('state') + + # Deprecation warnings for all other params + for legacy_input in OLD_INPUT_NAMES: + if module.params.get(legacy_input) is not None: + module.deprecate( + msg='{0} parameter has been deprecated, please use notification_configuration instead'.format(legacy_input), + version="ansible.tower:4.0.0") + + # Attempt to look up the related items the user specified (these will fail the module if not found) + organization_id = None + if organization: + organization_id = module.resolve_name_to_id('organizations', organization) + + # Attempt to look up an existing item based on the provided data + existing_item = module.get_one('notification_templates', **{ + 'data': { + 'name': name, + 'organization': organization_id, + } + }) + + if state == 'absent': + # If the state was absent we can let the module delete it if needed, the module will handle exiting from this + module.delete_if_needed(existing_item) + + # Create notification_configuration from legacy inputs + final_notification_configuration = {} + for legacy_input in OLD_INPUT_NAMES: + if module.params.get(legacy_input) is not None: + final_notification_configuration[legacy_input] = module.params.get(legacy_input) + # Give anything in notification_configuration prescedence over the individual inputs + if notification_configuration is not None: + final_notification_configuration.update(notification_configuration) + + # Create the data that gets sent for create and update + new_fields = {} + if final_notification_configuration: + new_fields['notification_configuration'] = final_notification_configuration + new_fields['name'] = new_name if new_name else name + if description is not None: + new_fields['description'] = description + if organization is not None: + new_fields['organization'] = organization_id + if notification_type is not None: + new_fields['notification_type'] = notification_type + if messages is not None: + new_fields['messages'] = messages + + # If the state was present and we can let the module build or update the existing item, this will return on its own + module.create_or_update_if_needed( + existing_item, new_fields, + endpoint='notification_templates', item_type='notification_template', + associations={ + } + ) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_organization.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_organization.py new file mode 100644 index 00000000..16378281 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_organization.py @@ -0,0 +1,174 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III <wayne@riotousliving.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_organization +author: "Wayne Witzel III (@wwitzel3)" +short_description: create, update, or destroy Ansible Tower organizations +description: + - Create, update, or destroy Ansible Tower organizations. See + U(https://www.ansible.com/tower) for an overview. +options: + name: + description: + - Name to use for the organization. + required: True + type: str + description: + description: + - The description to use for the organization. + type: str + custom_virtualenv: + description: + - Local absolute file path containing a custom Python virtualenv to use. + type: str + default: '' + max_hosts: + description: + - The max hosts allowed in this organizations + default: "0" + type: int + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] + type: str + notification_templates_started: + description: + - list of notifications to send on start + type: list + elements: str + notification_templates_success: + description: + - list of notifications to send on success + type: list + elements: str + notification_templates_error: + description: + - list of notifications to send on error + type: list + elements: str + notification_templates_approvals: + description: + - list of notifications to send on start + type: list + elements: str +extends_documentation_fragment: awx.awx.auth +''' + + +EXAMPLES = ''' +- name: Create tower organization + tower_organization: + name: "Foo" + description: "Foo bar organization" + state: present + tower_config_file: "~/tower_cli.cfg" + +- name: Create tower organization using 'foo-venv' as default Python virtualenv + tower_organization: + name: "Foo" + description: "Foo bar organization using foo-venv" + custom_virtualenv: "/var/lib/awx/venv/foo-venv/" + state: present + tower_config_file: "~/tower_cli.cfg" +''' + +from ..module_utils.tower_api import TowerAPIModule + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + name=dict(required=True), + description=dict(), + custom_virtualenv=dict(), + max_hosts=dict(type='int', default="0"), + notification_templates_started=dict(type="list", elements='str'), + notification_templates_success=dict(type="list", elements='str'), + notification_templates_error=dict(type="list", elements='str'), + notification_templates_approvals=dict(type="list", elements='str'), + state=dict(choices=['present', 'absent'], default='present'), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + name = module.params.get('name') + description = module.params.get('description') + custom_virtualenv = module.params.get('custom_virtualenv') + max_hosts = module.params.get('max_hosts') + # instance_group_names = module.params.get('instance_groups') + state = module.params.get('state') + + # Attempt to look up organization based on the provided name + organization = module.get_one('organizations', **{ + 'data': { + 'name': name, + } + }) + + if state == 'absent': + # If the state was absent we can let the module delete it if needed, the module will handle exiting from this + module.delete_if_needed(organization) + # Attempt to look up associated field items the user specified. + association_fields = {} + + notifications_start = module.params.get('notification_templates_started') + if notifications_start is not None: + association_fields['notification_templates_started'] = [] + for item in notifications_start: + association_fields['notification_templates_started'].append(module.resolve_name_to_id('notification_templates', item)) + + notifications_success = module.params.get('notification_templates_success') + if notifications_success is not None: + association_fields['notification_templates_success'] = [] + for item in notifications_success: + association_fields['notification_templates_success'].append(module.resolve_name_to_id('notification_templates', item)) + + notifications_error = module.params.get('notification_templates_error') + if notifications_error is not None: + association_fields['notification_templates_error'] = [] + for item in notifications_error: + association_fields['notification_templates_error'].append(module.resolve_name_to_id('notification_templates', item)) + + notifications_approval = module.params.get('notification_templates_approvals') + if notifications_approval is not None: + association_fields['notification_templates_approvals'] = [] + for item in notifications_approval: + association_fields['notification_templates_approvals'].append(module.resolve_name_to_id('notification_templates', item)) + + # Create the data that gets sent for create and update + org_fields = {'name': name} + if description is not None: + org_fields['description'] = description + if custom_virtualenv is not None: + org_fields['custom_virtualenv'] = custom_virtualenv + if max_hosts is not None: + org_fields['max_hosts'] = max_hosts + + # If the state was present and we can let the module build or update the existing organization, this will return on its own + module.create_or_update_if_needed( + organization, org_fields, + endpoint='organizations', item_type='organization', + associations=association_fields, + ) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_project.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_project.py new file mode 100644 index 00000000..36a4f866 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_project.py @@ -0,0 +1,313 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III <wayne@riotousliving.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_project +author: "Wayne Witzel III (@wwitzel3)" +short_description: create, update, or destroy Ansible Tower projects +description: + - Create, update, or destroy Ansible Tower projects. See + U(https://www.ansible.com/tower) for an overview. +options: + name: + description: + - Name to use for the project. + required: True + type: str + description: + description: + - Description to use for the project. + type: str + scm_type: + description: + - Type of SCM resource. + choices: ["manual", "git", "hg", "svn", "insights"] + default: "manual" + type: str + scm_url: + description: + - URL of SCM resource. + type: str + local_path: + description: + - The server playbook directory for manual projects. + type: str + scm_branch: + description: + - The branch to use for the SCM resource. + type: str + default: '' + scm_refspec: + description: + - The refspec to use for the SCM resource. + type: str + default: '' + scm_credential: + description: + - Name of the credential to use with this SCM resource. + type: str + scm_clean: + description: + - Remove local modifications before updating. + type: bool + default: 'no' + scm_delete_on_update: + description: + - Remove the repository completely before updating. + type: bool + default: 'no' + scm_update_on_launch: + description: + - Before an update to the local repository before launching a job with this project. + type: bool + default: 'no' + scm_update_cache_timeout: + description: + - Cache Timeout to cache prior project syncs for a certain number of seconds. + Only valid if scm_update_on_launch is to True, otherwise ignored. + type: int + default: 0 + allow_override: + description: + - Allow changing the SCM branch or revision in a job template that uses this project. + type: bool + aliases: + - scm_allow_override + job_timeout: + description: + - The amount of time (in seconds) to run before the SCM Update is canceled. A value of 0 means no timeout. + default: 0 + type: int + custom_virtualenv: + description: + - Local absolute file path containing a custom Python virtualenv to use + type: str + default: '' + organization: + description: + - Name of organization for project. + type: str + required: True + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] + type: str + wait: + description: + - Provides option (True by default) to wait for completed project sync + before returning + - Can assure playbook files are populated so that job templates that rely + on the project may be successfully created + type: bool + default: True + notification_templates_started: + description: + - list of notifications to send on start + type: list + elements: str + notification_templates_success: + description: + - list of notifications to send on success + type: list + elements: str + notification_templates_error: + description: + - list of notifications to send on error + type: list + elements: str +extends_documentation_fragment: awx.awx.auth +''' + + +EXAMPLES = ''' +- name: Add tower project + tower_project: + name: "Foo" + description: "Foo bar project" + organization: "test" + state: present + tower_config_file: "~/tower_cli.cfg" + +- name: Add Tower Project with cache timeout and custom virtualenv + tower_project: + name: "Foo" + description: "Foo bar project" + organization: "test" + scm_update_on_launch: True + scm_update_cache_timeout: 60 + custom_virtualenv: "/var/lib/awx/venv/ansible-2.2" + state: present + tower_config_file: "~/tower_cli.cfg" +''' + +import time + +from ..module_utils.tower_api import TowerAPIModule + + +def wait_for_project_update(module, last_request): + # The current running job for the udpate is in last_request['summary_fields']['current_update']['id'] + + if 'current_update' in last_request['summary_fields']: + running = True + while running: + result = module.get_endpoint('/project_updates/{0}/'.format(last_request['summary_fields']['current_update']['id']))['json'] + + if module.is_job_done(result['status']): + time.sleep(1) + running = False + + if result['status'] != 'successful': + module.fail_json(msg="Project update failed") + + module.exit_json(**module.json_output) + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + name=dict(required=True), + description=dict(), + scm_type=dict(choices=['manual', 'git', 'hg', 'svn', 'insights'], default='manual'), + scm_url=dict(), + local_path=dict(), + scm_branch=dict(default=''), + scm_refspec=dict(default=''), + scm_credential=dict(), + scm_clean=dict(type='bool', default=False), + scm_delete_on_update=dict(type='bool', default=False), + scm_update_on_launch=dict(type='bool', default=False), + scm_update_cache_timeout=dict(type='int', default=0), + allow_override=dict(type='bool', aliases=['scm_allow_override']), + job_timeout=dict(type='int', default=0), + custom_virtualenv=dict(), + organization=dict(required=True), + notification_templates_started=dict(type="list", elements='str'), + notification_templates_success=dict(type="list", elements='str'), + notification_templates_error=dict(type="list", elements='str'), + state=dict(choices=['present', 'absent'], default='present'), + wait=dict(type='bool', default=True), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + name = module.params.get('name') + description = module.params.get('description') + scm_type = module.params.get('scm_type') + if scm_type == "manual": + scm_type = "" + scm_url = module.params.get('scm_url') + local_path = module.params.get('local_path') + scm_branch = module.params.get('scm_branch') + scm_refspec = module.params.get('scm_refspec') + scm_credential = module.params.get('scm_credential') + scm_clean = module.params.get('scm_clean') + scm_delete_on_update = module.params.get('scm_delete_on_update') + scm_update_on_launch = module.params.get('scm_update_on_launch') + scm_update_cache_timeout = module.params.get('scm_update_cache_timeout') + allow_override = module.params.get('allow_override') + job_timeout = module.params.get('job_timeout') + custom_virtualenv = module.params.get('custom_virtualenv') + organization = module.params.get('organization') + state = module.params.get('state') + wait = module.params.get('wait') + + # Attempt to look up the related items the user specified (these will fail the module if not found) + org_id = module.resolve_name_to_id('organizations', organization) + if scm_credential is not None: + scm_credential_id = module.resolve_name_to_id('credentials', scm_credential) + + # Attempt to look up project based on the provided name and org ID + project = module.get_one('projects', **{ + 'data': { + 'name': name, + 'organization': org_id + } + }) + + if state == 'absent': + # If the state was absent we can let the module delete it if needed, the module will handle exiting from this + module.delete_if_needed(project) + + # Attempt to look up associated field items the user specified. + association_fields = {} + + notifications_start = module.params.get('notification_templates_started') + if notifications_start is not None: + association_fields['notification_templates_started'] = [] + for item in notifications_start: + association_fields['notification_templates_started'].append(module.resolve_name_to_id('notification_templates', item)) + + notifications_success = module.params.get('notification_templates_success') + if notifications_success is not None: + association_fields['notification_templates_success'] = [] + for item in notifications_success: + association_fields['notification_templates_success'].append(module.resolve_name_to_id('notification_templates', item)) + + notifications_error = module.params.get('notification_templates_error') + if notifications_error is not None: + association_fields['notification_templates_error'] = [] + for item in notifications_error: + association_fields['notification_templates_error'].append(module.resolve_name_to_id('notification_templates', item)) + + # Create the data that gets sent for create and update + project_fields = { + 'name': name, + 'scm_type': scm_type, + 'scm_url': scm_url, + 'scm_branch': scm_branch, + 'scm_refspec': scm_refspec, + 'scm_clean': scm_clean, + 'scm_delete_on_update': scm_delete_on_update, + 'timeout': job_timeout, + 'organization': org_id, + 'scm_update_on_launch': scm_update_on_launch, + 'scm_update_cache_timeout': scm_update_cache_timeout, + 'custom_virtualenv': custom_virtualenv, + } + if description is not None: + project_fields['description'] = description + if scm_credential is not None: + project_fields['credential'] = scm_credential_id + if allow_override is not None: + project_fields['allow_override'] = allow_override + if scm_type == '': + project_fields['local_path'] = local_path + + if scm_update_cache_timeout != 0 and scm_update_on_launch is not True: + module.warn('scm_update_cache_timeout will be ignored since scm_update_on_launch was not set to true') + + # If we are doing a not manual project, register our on_change method + # An on_change function, if registered, will fire after an post_endpoint or update_if_needed completes successfully + on_change = None + if wait and scm_type != '': + on_change = wait_for_project_update + + # If the state was present and we can let the module build or update the existing project, this will return on its own + module.create_or_update_if_needed( + project, project_fields, + endpoint='projects', item_type='project', + associations=association_fields, + on_create=on_change, on_update=on_change + ) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_receive.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_receive.py new file mode 100644 index 00000000..bd086825 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_receive.py @@ -0,0 +1,199 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, John Westcott IV <john.westcott.iv@redhat.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['deprecated'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_receive +deprecated: + removed_in: "14.0.0" + why: Deprecated in favor of upcoming C(_export) module. + alternative: Once published, use M(tower_export) instead. +author: "John Westcott IV (@john-westcott-iv)" +short_description: Receive assets from Ansible Tower. +description: + - Receive assets from Ansible Tower. See + U(https://www.ansible.com/tower) for an overview. +options: + all: + description: + - Export all assets + type: bool + default: 'False' + organization: + description: + - List of organization names to export + default: [] + type: list + elements: str + user: + description: + - List of user names to export + default: [] + type: list + elements: str + team: + description: + - List of team names to export + default: [] + type: list + elements: str + credential_type: + description: + - List of credential type names to export + default: [] + type: list + elements: str + credential: + description: + - List of credential names to export + default: [] + type: list + elements: str + notification_template: + description: + - List of notification template names to export + default: [] + type: list + elements: str + inventory_script: + description: + - List of inventory script names to export + default: [] + type: list + elements: str + inventory: + description: + - List of inventory names to export + default: [] + type: list + elements: str + project: + description: + - List of project names to export + default: [] + type: list + elements: str + job_template: + description: + - List of job template names to export + default: [] + type: list + elements: str + workflow: + description: + - List of workflow names to export + default: [] + type: list + elements: str + +requirements: + - "ansible-tower-cli >= 3.3.0" + +notes: + - Specifying a name of "all" for any asset type will export all items of that asset type. + +extends_documentation_fragment: awx.awx.auth_legacy +''' + +EXAMPLES = ''' +- name: Export all tower assets + tower_receive: + all: True + tower_config_file: "~/tower_cli.cfg" + +- name: Export all inventories + tower_receive: + inventory: + - all + +- name: Export a job template named "My Template" and all Credentials + tower_receive: + job_template: + - "My Template" + credential: + - all +''' + +RETURN = ''' +assets: + description: The exported assets + returned: success + type: dict + sample: [ {}, {} ] +''' + +from ..module_utils.tower_legacy import TowerLegacyModule, tower_auth_config, HAS_TOWER_CLI + +try: + from tower_cli.cli.transfer.receive import Receiver + from tower_cli.cli.transfer.common import SEND_ORDER + from tower_cli.utils.exceptions import TowerCLIError + + from tower_cli.conf import settings + TOWER_CLI_HAS_EXPORT = True +except ImportError: + TOWER_CLI_HAS_EXPORT = False + + +def main(): + argument_spec = dict( + all=dict(type='bool', default=False), + credential=dict(type='list', default=[], elements='str'), + credential_type=dict(type='list', default=[], elements='str'), + inventory=dict(type='list', default=[], elements='str'), + inventory_script=dict(type='list', default=[], elements='str'), + job_template=dict(type='list', default=[], elements='str'), + notification_template=dict(type='list', default=[], elements='str'), + organization=dict(type='list', default=[], elements='str'), + project=dict(type='list', default=[], elements='str'), + team=dict(type='list', default=[], elements='str'), + user=dict(type='list', default=[], elements='str'), + workflow=dict(type='list', default=[], elements='str'), + ) + + module = TowerLegacyModule(argument_spec=argument_spec, supports_check_mode=False) + + module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI export command.", version="awx.awx:14.0.0") + + if not HAS_TOWER_CLI: + module.fail_json(msg='ansible-tower-cli required for this module') + + if not TOWER_CLI_HAS_EXPORT: + module.fail_json(msg='ansible-tower-cli version does not support export') + + export_all = module.params.get('all') + assets_to_export = {} + for asset_type in SEND_ORDER: + assets_to_export[asset_type] = module.params.get(asset_type) + + result = dict( + assets=None, + changed=False, + message='', + ) + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + try: + receiver = Receiver() + result['assets'] = receiver.export_assets(all=export_all, asset_input=assets_to_export) + module.exit_json(**result) + except TowerCLIError as e: + result['message'] = e.message + module.fail_json(msg='Receive Failed', **result) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_role.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_role.py new file mode 100644 index 00000000..d0d010a0 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_role.py @@ -0,0 +1,186 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III <wayne@riotousliving.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_role +author: "Wayne Witzel III (@wwitzel3)" +short_description: grant or revoke an Ansible Tower role. +description: + - Roles are used for access control, this module is for managing user access to server resources. + - Grant or revoke Ansible Tower roles to users. See U(https://www.ansible.com/tower) for an overview. +options: + user: + description: + - User that receives the permissions specified by the role. + type: str + team: + description: + - Team that receives the permissions specified by the role. + type: str + role: + description: + - The role type to grant/revoke. + required: True + choices: ["admin", "read", "member", "execute", "adhoc", "update", "use", "auditor", "project_admin", "inventory_admin", "credential_admin", + "workflow_admin", "notification_admin", "job_template_admin"] + type: str + target_team: + description: + - Team that the role acts on. + - For example, make someone a member or an admin of a team. + - Members of a team implicitly receive the permissions that the team has. + type: str + inventory: + description: + - Inventory the role acts on. + type: str + job_template: + description: + - The job template the role acts on. + type: str + workflow: + description: + - The workflow job template the role acts on. + type: str + credential: + description: + - Credential the role acts on. + type: str + organization: + description: + - Organization the role acts on. + type: str + project: + description: + - Project the role acts on. + type: str + state: + description: + - Desired state. + - State of present indicates the user should have the role. + - State of absent indicates the user should have the role taken away, if they have it. + default: "present" + choices: ["present", "absent"] + type: str + +extends_documentation_fragment: awx.awx.auth +''' + + +EXAMPLES = ''' +- name: Add jdoe to the member role of My Team + tower_role: + user: jdoe + target_team: "My Team" + role: member + state: present +''' + +from ..module_utils.tower_api import TowerAPIModule + + +def main(): + + argument_spec = dict( + user=dict(), + team=dict(), + role=dict(choices=["admin", "read", "member", "execute", "adhoc", "update", "use", "auditor", "project_admin", "inventory_admin", "credential_admin", + "workflow_admin", "notification_admin", "job_template_admin"], required=True), + target_team=dict(), + inventory=dict(), + job_template=dict(), + workflow=dict(), + credential=dict(), + organization=dict(), + project=dict(), + state=dict(choices=['present', 'absent'], default='present'), + ) + + module = TowerAPIModule(argument_spec=argument_spec) + + role_type = module.params.pop('role') + role_field = role_type + '_role' + state = module.params.pop('state') + + module.json_output['role'] = role_type + + # Lookup data for all the objects specified in params + params = module.params.copy() + resource_param_keys = ( + 'user', 'team', + 'target_team', 'inventory', 'job_template', 'workflow', 'credential', 'organization', 'project' + ) + resource_data = {} + for param in resource_param_keys: + endpoint = module.param_to_endpoint(param) + name_field = 'username' if param == 'user' else 'name' + + resource_name = params.get(param) + if resource_name: + resource = module.get_one(endpoint, **{'data': {name_field: resource_name}}) + if not resource: + module.fail_json( + msg='Failed to update role, {0} not found in {1}'.format(param, endpoint), + changed=False + ) + resource_data[param] = resource + + # separate actors from resources + actor_data = {} + for key in ('user', 'team'): + if key in resource_data: + actor_data[key] = resource_data.pop(key) + + # build association agenda + associations = {} + for actor_type, actor in actor_data.items(): + for resource_type, resource in resource_data.items(): + resource_roles = resource['summary_fields']['object_roles'] + if role_field not in resource_roles: + available_roles = ', '.join(list(resource_roles.keys())) + module.fail_json(msg='Resource {0} has no role {1}, available roles: {2}'.format( + resource['url'], role_field, available_roles + ), changed=False) + role_data = resource_roles[role_field] + endpoint = '/roles/{0}/{1}/'.format(role_data['id'], module.param_to_endpoint(actor_type)) + associations.setdefault(endpoint, []) + associations[endpoint].append(actor['id']) + + # perform associations + for association_endpoint, new_association_list in associations.items(): + response = module.get_all_endpoint(association_endpoint) + existing_associated_ids = [association['id'] for association in response['json']['results']] + + if state == 'present': + for an_id in list(set(new_association_list) - set(existing_associated_ids)): + response = module.post_endpoint(association_endpoint, **{'data': {'id': int(an_id)}}) + if response['status_code'] == 204: + module.json_output['changed'] = True + else: + module.fail_json(msg="Failed to grant role {0}".format(response['json']['detail'])) + else: + for an_id in list(set(existing_associated_ids) & set(new_association_list)): + response = module.post_endpoint(association_endpoint, **{'data': {'id': int(an_id), 'disassociate': True}}) + if response['status_code'] == 204: + module.json_output['changed'] = True + else: + module.fail_json(msg="Failed to revoke role {0}".format(response['json']['detail'])) + + module.exit_json(**module.json_output) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_schedule.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_schedule.py new file mode 100644 index 00000000..4922aaa6 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_schedule.py @@ -0,0 +1,243 @@ +#!/usr/bin/python +# coding: utf-8 -*- + + +# (c) 2020, John Westcott IV <john.westcott.iv@redhat.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: tower_schedule +author: "John Westcott IV (@john-westcott-iv)" +short_description: create, update, or destroy Ansible Tower schedules. +description: + - Create, update, or destroy Ansible Tower schedules. See + U(https://www.ansible.com/tower) for an overview. +options: + rrule: + description: + - A value representing the schedules iCal recurrence rule. + - See rrule plugin for help constructing this value + required: False + type: str + name: + description: + - Name of this schedule. + required: True + type: str + new_name: + description: + - Setting this option will change the existing name (looked up via the name field. + required: False + type: str + description: + description: + - Optional description of this schedule. + required: False + type: str + extra_data: + description: + - Specify C(extra_vars) for the template. + required: False + type: dict + default: {} + inventory: + description: + - Inventory applied as a prompt, assuming job template prompts for inventory + required: False + type: str + scm_branch: + description: + - Branch to use in job run. Project default used if blank. Only allowed if project allow_override field is set to true. + required: False + type: str + job_type: + description: + - The job type to use for the job template. + required: False + type: str + choices: + - 'run' + - 'check' + job_tags: + description: + - Comma separated list of the tags to use for the job template. + required: False + type: str + skip_tags: + description: + - Comma separated list of the tags to skip for the job template. + required: False + type: str + limit: + description: + - A host pattern to further constrain the list of hosts managed or affected by the playbook + required: False + type: str + diff_mode: + description: + - Enable diff mode for the job template. + required: False + type: bool + verbosity: + description: + - Control the output level Ansible produces as the playbook runs. 0 - Normal, 1 - Verbose, 2 - More Verbose, 3 - Debug, 4 - Connection Debug. + required: False + type: int + choices: + - 0 + - 1 + - 2 + - 3 + - 4 + - 5 + unified_job_template: + description: + - Name of unified job template to schedule. + required: False + type: str + enabled: + description: + - Enables processing of this schedule. + required: False + type: bool + state: + description: + - Desired state of the resource. + choices: ["present", "absent"] + default: "present" + type: str +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Build a schedule for Demo Job Template + tower_schedule: + name: "{{ sched1 }}" + state: present + unified_job_template: "Demo Job Template" + rrule: "DTSTART:20191219T130551Z RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=1" + register: result + +- name: Build the same schedule using the rrule plugin + tower_schedule: + name: "{{ sched1 }}" + state: present + unified_job_template: "Demo Job Template" + rrule: "{{ query('awx.awx.tower_schedule_rrule', 'week', start_date='2019-12-19 13:05:51') }}" + register: result +''' + +from ..module_utils.tower_api import TowerAPIModule + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + rrule=dict(), + name=dict(required=True), + new_name=dict(), + description=dict(), + extra_data=dict(type='dict'), + inventory=dict(), + scm_branch=dict(), + job_type=dict(choices=['run', 'check']), + job_tags=dict(), + skip_tags=dict(), + limit=dict(), + diff_mode=dict(type='bool'), + verbosity=dict(type='int', choices=[0, 1, 2, 3, 4, 5]), + unified_job_template=dict(), + enabled=dict(type='bool'), + state=dict(choices=['present', 'absent'], default='present'), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + rrule = module.params.get('rrule') + name = module.params.get('name') + new_name = module.params.get("new_name") + description = module.params.get('description') + extra_data = module.params.get('extra_data') + inventory = module.params.get('inventory') + scm_branch = module.params.get('scm_branch') + job_type = module.params.get('job_type') + job_tags = module.params.get('job_tags') + skip_tags = module.params.get('skip_tags') + limit = module.params.get('limit') + diff_mode = module.params.get('diff_mode') + verbosity = module.params.get('verbosity') + unified_job_template = module.params.get('unified_job_template') + enabled = module.params.get('enabled') + state = module.params.get('state') + + # Attempt to look up the related items the user specified (these will fail the module if not found) + inventory_id = None + if inventory: + inventory_id = module.resolve_name_to_id('inventories', inventory) + unified_job_template_id = None + if unified_job_template: + unified_job_template_id = module.resolve_name_to_id('unified_job_templates', unified_job_template) + + # Attempt to look up an existing item based on the provided data + existing_item = module.get_one('schedules', **{ + 'data': { + 'name': name, + } + }) + + # Create the data that gets sent for create and update + new_fields = {} + if rrule is not None: + new_fields['rrule'] = rrule + new_fields['name'] = new_name if new_name else name + if description is not None: + new_fields['description'] = description + if extra_data is not None: + new_fields['extra_data'] = extra_data + if inventory is not None: + new_fields['inventory'] = inventory_id + if scm_branch is not None: + new_fields['scm_branch'] = scm_branch + if job_type is not None: + new_fields['job_type'] = job_type + if job_tags is not None: + new_fields['job_tags'] = job_tags + if skip_tags is not None: + new_fields['skip_tags'] = skip_tags + if limit is not None: + new_fields['limit'] = limit + if diff_mode is not None: + new_fields['diff_mode'] = diff_mode + if verbosity is not None: + new_fields['verbosity'] = verbosity + if unified_job_template is not None: + new_fields['unified_job_template'] = unified_job_template_id + if enabled is not None: + new_fields['enabled'] = enabled + + if state == 'absent': + # If the state was absent we can let the module delete it if needed, the module will handle exiting from this + module.delete_if_needed(existing_item) + elif state == 'present': + # If the state was present and we can let the module build or update the existing item, this will return on its own + module.create_or_update_if_needed( + existing_item, new_fields, + endpoint='schedules', item_type='schedule', + associations={ + } + ) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_send.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_send.py new file mode 100644 index 00000000..772b2b67 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_send.py @@ -0,0 +1,174 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, John Westcott IV <john.westcott.iv@redhat.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['deprecated'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_send +deprecated: + removed_in: "14.0.0" + why: Deprecated in favor of upcoming C(_import) module. + alternative: Once published, use M(tower_import) instead. +author: "John Westcott IV (@john-westcott-iv)" +short_description: Send assets to Ansible Tower. +description: + - Send assets to Ansible Tower. See + U(https://www.ansible.com/tower) for an overview. +options: + assets: + description: + - The assets to import. + - This can be the output of tower_receive or loaded from a file + type: str + files: + description: + - List of files to import. + default: [] + type: list + elements: str + prevent: + description: + - A list of asset types to prevent import for + default: [] + type: list + elements: str + password_management: + description: + - The password management option to use. + - The prompt option is not supported. + default: 'default' + choices: ["default", "random"] + type: str + +notes: + - One of assets or files needs to be passed in + +requirements: + - "ansible-tower-cli >= 3.3.0" + - six.moves.StringIO + - sys + +extends_documentation_fragment: awx.awx.auth_legacy +''' + +EXAMPLES = ''' +- name: Import all tower assets + tower_send: + assets: "{{ export_output.assets }}" + tower_config_file: "~/tower_cli.cfg" +''' + +RETURN = ''' +output: + description: The import messages + returned: success, fail + type: list + sample: [ 'Message 1', 'Message 2' ] +''' + +import os +import sys + +from ansible.module_utils.six.moves import StringIO +from ..module_utils.tower_legacy import TowerLegacyModule, tower_auth_config, HAS_TOWER_CLI + +from tempfile import mkstemp + +try: + from tower_cli.cli.transfer.send import Sender + from tower_cli.utils.exceptions import TowerCLIError + + from tower_cli.conf import settings + TOWER_CLI_HAS_EXPORT = True +except ImportError: + TOWER_CLI_HAS_EXPORT = False + + +def main(): + argument_spec = dict( + assets=dict(), + files=dict(default=[], type='list', elements='str'), + prevent=dict(default=[], type='list', elements='str'), + password_management=dict(default='default', choices=['default', 'random']), + ) + + module = TowerLegacyModule(argument_spec=argument_spec, supports_check_mode=False) + + module.deprecate(msg="This module is deprecated and will be replaced by the AWX CLI import command", version="awx.awx:14.0.0") + + if not HAS_TOWER_CLI: + module.fail_json(msg='ansible-tower-cli required for this module') + + if not TOWER_CLI_HAS_EXPORT: + module.fail_json(msg='ansible-tower-cli version does not support export') + + assets = module.params.get('assets') + prevent = module.params.get('prevent') + password_management = module.params.get('password_management') + files = module.params.get('files') + + result = dict( + changed=False, + msg='', + output='', + ) + + if not assets and not files: + result['msg'] = "Assets or files must be specified" + module.fail_json(**result) + + path = None + if assets: + # We got assets so we need to dump this out to a temp file and append that to files + handle, path = mkstemp(prefix='', suffix='', dir='') + with open(path, 'w') as f: + f.write(assets) + files.append(path) + + tower_auth = tower_auth_config(module) + failed = False + with settings.runtime_values(**tower_auth): + try: + sender = Sender(no_color=False) + old_stdout = sys.stdout + sys.stdout = captured_stdout = StringIO() + try: + sender.send(files, prevent, password_management) + except TypeError: + # Newer versions of TowerCLI require 4 parameters + sender.send(files, prevent, [], password_management) + + if sender.error_messages > 0: + failed = True + result['msg'] = "Transfer Failed with %d errors" % sender.error_messages + if sender.changed_messages > 0: + result['changed'] = True + except TowerCLIError as e: + result['msg'] = e.message + failed = True + finally: + if path is not None: + os.remove(path) + result['output'] = captured_stdout.getvalue().split("\n") + sys.stdout = old_stdout + + # Return stdout so that module returns will work + if failed: + module.fail_json(**result) + else: + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_settings.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_settings.py new file mode 100644 index 00000000..c2e8ed1a --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_settings.py @@ -0,0 +1,176 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2018, Nikhil Jain <nikjain@redhat.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_settings +author: "Nikhil Jain (@jainnikhil30)" +short_description: Modify Ansible Tower settings. +description: + - Modify Ansible Tower settings. See + U(https://www.ansible.com/tower) for an overview. +options: + name: + description: + - Name of setting to modify + type: str + value: + description: + - Value to be modified for given setting. + - If given a non-string type, will make best effort to cast it to type API expects. + - For better control over types, use the C(settings) param instead. + type: str + settings: + description: + - A data structure to be sent into the settings endpoint + type: dict +requirements: + - pyyaml +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Set the value of AWX_PROOT_BASE_PATH + tower_settings: + name: AWX_PROOT_BASE_PATH + value: "/tmp" + register: testing_settings + +- name: Set the value of AWX_PROOT_SHOW_PATHS + tower_settings: + name: "AWX_PROOT_SHOW_PATHS" + value: "'/var/lib/awx/projects/', '/tmp'" + register: testing_settings + +- name: Set the LDAP Auth Bind Password + tower_settings: + name: "AUTH_LDAP_BIND_PASSWORD" + value: "Password" + no_log: true + +- name: Set all the LDAP Auth Bind Params + tower_settings: + settings: + AUTH_LDAP_BIND_PASSWORD: "password" + AUTH_LDAP_USER_ATTR_MAP: + email: "mail" + first_name: "givenName" + last_name: "surname" +''' + +from ..module_utils.tower_api import TowerAPIModule + +try: + import yaml + HAS_YAML = True +except ImportError: + HAS_YAML = False + + +def coerce_type(module, value): + # If our value is already None we can just return directly + if value is None: + return value + + yaml_ish = bool(( + value.startswith('{') and value.endswith('}') + ) or ( + value.startswith('[') and value.endswith(']')) + ) + if yaml_ish: + if not HAS_YAML: + module.fail_json(msg="yaml is not installed, try 'pip install pyyaml'") + return yaml.safe_load(value) + elif value.lower in ('true', 'false', 't', 'f'): + return {'t': True, 'f': False}[value[0].lower()] + try: + return int(value) + except ValueError: + pass + return value + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + name=dict(), + value=dict(), + settings=dict(type='dict'), + ) + + # Create a module for ourselves + module = TowerAPIModule( + argument_spec=argument_spec, + required_one_of=[['name', 'settings']], + mutually_exclusive=[['name', 'settings']], + required_if=[['name', 'present', ['value']]] + ) + + # Extract our parameters + name = module.params.get('name') + value = module.params.get('value') + new_settings = module.params.get('settings') + + # If we were given a name/value pair we will just make settings out of that and proceed normally + if new_settings is None: + new_value = coerce_type(module, value) + + new_settings = {name: new_value} + + # Load the existing settings + existing_settings = module.get_endpoint('settings/all')['json'] + + # Begin a json response + json_response = {'changed': False, 'old_values': {}} + + # Check any of the settings to see if anything needs to be updated + needs_update = False + for a_setting in new_settings: + if a_setting not in existing_settings or existing_settings[a_setting] != new_settings[a_setting]: + # At least one thing is different so we need to patch + needs_update = True + json_response['old_values'][a_setting] = existing_settings[a_setting] + + # If nothing needs an update we can simply exit with the response (as not changed) + if not needs_update: + module.exit_json(**json_response) + + # Make the call to update the settings + response = module.patch_endpoint('settings/all', **{'data': new_settings}) + + if response['status_code'] == 200: + # Set the changed response to True + json_response['changed'] = True + + # To deal with the old style values we need to return 'value' in the response + new_values = {} + for a_setting in new_settings: + new_values[a_setting] = response['json'][a_setting] + + # If we were using a name we will just add a value of a string, otherwise we will return an array in values + if name is not None: + json_response['value'] = new_values[name] + else: + json_response['values'] = new_values + + module.exit_json(**json_response) + elif 'json' in response and '__all__' in response['json']: + module.fail_json(msg=response['json']['__all__']) + else: + module.fail_json(**{'msg': "Unable to update settings, see response", 'response': response}) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_team.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_team.py new file mode 100644 index 00000000..8ed56e48 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_team.py @@ -0,0 +1,114 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III <wayne@riotousliving.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_team +author: "Wayne Witzel III (@wwitzel3)" +short_description: create, update, or destroy Ansible Tower team. +description: + - Create, update, or destroy Ansible Tower teams. See + U(https://www.ansible.com/tower) for an overview. +options: + name: + description: + - Name to use for the team. + required: True + type: str + new_name: + description: + - To use when changing a team's name. + type: str + description: + description: + - The description to use for the team. + type: str + organization: + description: + - Organization the team should be made a member of. + required: True + type: str + state: + description: + - Desired state of the resource. + choices: ["present", "absent"] + default: "present" + type: str +extends_documentation_fragment: awx.awx.auth +''' + + +EXAMPLES = ''' +- name: Create tower team + tower_team: + name: Team Name + description: Team Description + organization: test-org + state: present + tower_config_file: "~/tower_cli.cfg" +''' + +from ..module_utils.tower_api import TowerAPIModule + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + name=dict(required=True), + new_name=dict(), + description=dict(), + organization=dict(required=True), + state=dict(choices=['present', 'absent'], default='present'), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + name = module.params.get('name') + new_name = module.params.get('new_name') + description = module.params.get('description') + organization = module.params.get('organization') + state = module.params.get('state') + + # Attempt to look up the related items the user specified (these will fail the module if not found) + org_id = module.resolve_name_to_id('organizations', organization) + + # Attempt to look up team based on the provided name and org ID + team = module.get_one('teams', **{ + 'data': { + 'name': name, + 'organization': org_id + } + }) + + if state == 'absent': + # If the state was absent we can let the module delete it if needed, the module will handle exiting from this + module.delete_if_needed(team) + + # Create the data that gets sent for create and update + team_fields = { + 'name': new_name if new_name else name, + 'organization': org_id + } + if description is not None: + team_fields['description'] = description + + # If the state was present and we can let the module build or update the existing team, this will return on its own + module.create_or_update_if_needed(team, team_fields, endpoint='teams', item_type='team') + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_token.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_token.py new file mode 100644 index 00000000..ee6fd5c2 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_token.py @@ -0,0 +1,201 @@ +#!/usr/bin/python +# coding: utf-8 -*- + + +# (c) 2020, John Westcott IV <john.westcott.iv@redhat.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: tower_token +author: "John Westcott IV (@john-westcott-iv)" +version_added: "2.3" +short_description: create, update, or destroy Ansible Tower tokens. +description: + - Create or destroy Ansible Tower tokens. See + U(https://www.ansible.com/tower) for an overview. + - In addition, the module sets an Ansible fact which can be passed into other + tower_* modules as the parameter tower_oauthtoken. See examples for usage. + - Because of the sensitive nature of tokens, the created token value is only available once + through the Ansible fact. (See RETURN for details) + - Due to the nature of tokens in Tower this module is not idempotent. A second will + with the same parameters will create a new token. + - If you are creating a temporary token for use with modules you should delete the token + when you are done with it. See the example for how to do it. +options: + description: + description: + - Optional description of this access token. + required: False + type: str + default: '' + application: + description: + - The application tied to this token. + required: False + type: str + scope: + description: + - Allowed scopes, further restricts user's permissions. Must be a simple space-separated string with allowed scopes ['read', 'write']. + required: False + type: str + default: 'write' + choices: ["read", "write"] + existing_token: + description: The data structure produced from tower_token in create mode to be used with state absent. + type: dict + existing_token_id: + description: A token ID (number) which can be used to delete an arbitrary token with state absent. + type: str + state: + description: + - Desired state of the resource. + choices: ["present", "absent"] + default: "present" + type: str +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- block: + - name: Create a new token using an existing token + tower_token: + description: '{{ token_description }}' + scope: "write" + state: present + tower_oauthtoken: "{{ my_existing_token }}" + + - name: Delete this token + tower_token: + existing_token: "{{ tower_token }}" + state: absent + + - name: Create a new token using username/password + tower_token: + description: '{{ token_description }}' + scope: "write" + state: present + tower_username: "{{ my_username }}" + tower_password: "{{ my_password }}" + + - name: Use our new token to make another call + tower_job_list: + tower_oauthtoken: "{{ tower_token }}" + + always: + - name: Delete our Token with the token we created + tower_token: + existing_token: "{{ tower_token }}" + state: absent + when: tower_token is defined + +- name: Delete a token by its id + tower_token: + existing_token_id: 4 + state: absent +''' + +RETURN = ''' +tower_token: + type: dict + description: An Ansible Fact variable representing a Tower token object which can be used for auth in subsequent modules. See examples for usage. + contains: + token: + description: The token that was generated. This token can never be accessed again, make sure this value is noted before it is lost. + type: str + id: + description: The numeric ID of the token created + type: str + returned: on successful create +''' + +from ..module_utils.tower_api import TowerAPIModule + + +def return_token(module, last_response): + # A token is special because you can never get the actual token ID back from the API. + # So the default module return would give you an ID but then the token would forever be masked on you. + # This method will return the entire token object we got back so that a user has access to the token + + module.json_output['ansible_facts'] = { + 'tower_token': last_response, + } + module.exit_json(**module.json_output) + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + description=dict(), + application=dict(), + scope=dict(choices=['read', 'write'], default='write'), + existing_token=dict(type='dict'), + existing_token_id=dict(), + state=dict(choices=['present', 'absent'], default='present'), + ) + + # Create a module for ourselves + module = TowerAPIModule( + argument_spec=argument_spec, + mutually_exclusive=[ + ('existing_token', 'existing_token_id'), + ], + # If we are state absent make sure one of existing_token or existing_token_id are present + required_if=[ + ['state', 'absent', ('existing_token', 'existing_token_id'), True, ], + ], + ) + + # Extract our parameters + description = module.params.get('description') + application = module.params.get('application') + scope = module.params.get('scope') + existing_token = module.params.get('existing_token') + existing_token_id = module.params.get('existing_token_id') + state = module.params.get('state') + + if state == 'absent': + if not existing_token: + existing_token = module.get_one('tokens', **{ + 'data': { + 'id': existing_token_id, + } + }) + + # If the state was absent we can let the module delete it if needed, the module will handle exiting from this + module.delete_if_needed(existing_token) + + # Attempt to look up the related items the user specified (these will fail the module if not found) + application_id = None + if application: + application_id = module.resolve_name_to_id('applications', application) + + # Create the data that gets sent for create and update + new_fields = {} + if description is not None: + new_fields['description'] = description + if application is not None: + new_fields['application'] = application_id + if scope is not None: + new_fields['scope'] = scope + + # If the state was present and we can let the module build or update the existing item, this will return on its own + module.create_or_update_if_needed( + None, new_fields, + endpoint='tokens', item_type='token', + associations={ + }, + on_create=return_token, + ) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_user.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_user.py new file mode 100644 index 00000000..15c41cb0 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_user.py @@ -0,0 +1,169 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2020, John Westcott IV <john.westcott.iv@redhat.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_user +author: "John Westcott IV (@john-westcott-iv)" +short_description: create, update, or destroy Ansible Tower users. +description: + - Create, update, or destroy Ansible Tower users. See + U(https://www.ansible.com/tower) for an overview. +options: + username: + description: + - Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only. + required: True + type: str + first_name: + description: + - First name of the user. + type: str + last_name: + description: + - Last name of the user. + type: str + email: + description: + - Email address of the user. + type: str + is_superuser: + description: + - Designates that this user has all permissions without explicitly assigning them. + type: bool + default: False + aliases: ['superuser'] + is_system_auditor: + description: + - User is a system wide auditor. + type: bool + default: False + aliases: ['auditor'] + password: + description: + - Write-only field used to change the password. + type: str + state: + description: + - Desired state of the resource. + choices: ["present", "absent"] + default: "present" + type: str +extends_documentation_fragment: awx.awx.auth +''' + + +EXAMPLES = ''' +- name: Add tower user + tower_user: + username: jdoe + password: foobarbaz + email: jdoe@example.org + first_name: John + last_name: Doe + state: present + tower_config_file: "~/tower_cli.cfg" + +- name: Add tower user as a system administrator + tower_user: + username: jdoe + password: foobarbaz + email: jdoe@example.org + superuser: yes + state: present + tower_config_file: "~/tower_cli.cfg" + +- name: Add tower user as a system auditor + tower_user: + username: jdoe + password: foobarbaz + email: jdoe@example.org + auditor: yes + state: present + tower_config_file: "~/tower_cli.cfg" + +- name: Delete tower user + tower_user: + username: jdoe + email: jdoe@example.org + state: absent + tower_config_file: "~/tower_cli.cfg" +''' + +from ..module_utils.tower_api import TowerAPIModule + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + username=dict(required=True), + first_name=dict(), + last_name=dict(), + email=dict(), + is_superuser=dict(type='bool', default=False, aliases=['superuser']), + is_system_auditor=dict(type='bool', default=False, aliases=['auditor']), + password=dict(no_log=True), + state=dict(choices=['present', 'absent'], default='present'), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + username = module.params.get('username') + first_name = module.params.get('first_name') + last_name = module.params.get('last_name') + email = module.params.get('email') + is_superuser = module.params.get('is_superuser') + is_system_auditor = module.params.get('is_system_auditor') + password = module.params.get('password') + state = module.params.get('state') + + # Attempt to look up the related items the user specified (these will fail the module if not found) + + # Attempt to look up an existing item based on the provided data + existing_item = module.get_one('users', **{ + 'data': { + 'username': username, + } + }) + + if state == 'absent': + # If the state was absent we can let the module delete it if needed, the module will handle exiting from this + module.delete_if_needed(existing_item) + + # Create the data that gets sent for create and update + new_fields = {} + if username: + new_fields['username'] = username + if first_name: + new_fields['first_name'] = first_name + if last_name: + new_fields['last_name'] = last_name + if email: + new_fields['email'] = email + if is_superuser: + new_fields['is_superuser'] = is_superuser + if is_system_auditor: + new_fields['is_system_auditor'] = is_system_auditor + if password: + new_fields['password'] = password + + # If the state was present and we can let the module build or update the existing item, this will return on its own + module.create_or_update_if_needed(existing_item, new_fields, endpoint='users', item_type='user') + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_workflow_job_template.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_workflow_job_template.py new file mode 100644 index 00000000..8fb350b9 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_workflow_job_template.py @@ -0,0 +1,273 @@ +#!/usr/bin/python +# coding: utf-8 -*- + + +# (c) 2020, John Westcott IV <john.westcott.iv@redhat.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: tower_workflow_job_template +author: "John Westcott IV (@john-westcott-iv)" +short_description: create, update, or destroy Ansible Tower workflow job templates. +description: + - Create, update, or destroy Ansible Tower workflow job templates. + - Replaces the deprecated tower_workflow_template module. + - Use the tower_workflow_job_template_node after this to build the workflow's graph. +options: + name: + description: + - Name of this workflow job template. + required: True + type: str + new_name: + description: + - Setting this option will change the existing name. + type: str + description: + description: + - Optional description of this workflow job template. + type: str + extra_vars: + description: + - Variables which will be made available to jobs ran inside the workflow. + type: dict + organization: + description: + - Organization the workflow job template exists in. + - Used to help lookup the object, cannot be modified using this module. + - If not provided, will lookup by name only, which does not work with duplicates. + type: str + allow_simultaneous: + description: + - Allow simultaneous runs of the workflow job template. + type: bool + ask_variables_on_launch: + description: + - Prompt user for C(extra_vars) on launch. + type: bool + inventory: + description: + - Inventory applied as a prompt, assuming job template prompts for inventory + type: str + limit: + description: + - Limit applied as a prompt, assuming job template prompts for limit + type: str + scm_branch: + description: + - SCM branch applied as a prompt, assuming job template prompts for SCM branch + type: str + ask_inventory_on_launch: + description: + - Prompt user for inventory on launch of this workflow job template + type: bool + ask_scm_branch_on_launch: + description: + - Prompt user for SCM branch on launch of this workflow job template + type: bool + ask_limit_on_launch: + description: + - Prompt user for limit on launch of this workflow job template + type: bool + webhook_service: + description: + - Service that webhook requests will be accepted from + type: str + choices: + - github + - gitlab + webhook_credential: + description: + - Personal Access Token for posting back the status to the service API + type: str + survey_enabled: + description: + - Setting that variable will prompt the user for job type on the + workflow launch. + type: bool + survey: + description: + - The definition of the survey associated to the workflow. + type: dict + state: + description: + - Desired state of the resource. + choices: + - present + - absent + default: "present" + type: str + notification_templates_started: + description: + - list of notifications to send on start + type: list + elements: str + notification_templates_success: + description: + - list of notifications to send on success + type: list + elements: str + notification_templates_error: + description: + - list of notifications to send on error + type: list + elements: str + notification_templates_approvals: + description: + - list of notifications to send on start + type: list + elements: str +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Create a workflow job template + tower_workflow_job_template: + name: example-workflow + description: created by Ansible Playbook + organization: Default +''' + +from ..module_utils.tower_api import TowerAPIModule + +import json + + +def update_survey(module, last_request): + spec_endpoint = last_request.get('related', {}).get('survey_spec') + module.post_endpoint(spec_endpoint, **{'data': module.params.get('survey')}) + module.exit_json(**module.json_output) + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + name=dict(required=True), + new_name=dict(), + description=dict(), + extra_vars=dict(type='dict'), + organization=dict(), + survey=dict(type='dict'), # special handling + survey_enabled=dict(type='bool'), + allow_simultaneous=dict(type='bool'), + ask_variables_on_launch=dict(type='bool'), + inventory=dict(), + limit=dict(), + scm_branch=dict(), + ask_inventory_on_launch=dict(type='bool'), + ask_scm_branch_on_launch=dict(type='bool'), + ask_limit_on_launch=dict(type='bool'), + webhook_service=dict(choices=['github', 'gitlab']), + webhook_credential=dict(), + notification_templates_started=dict(type="list", elements='str'), + notification_templates_success=dict(type="list", elements='str'), + notification_templates_error=dict(type="list", elements='str'), + notification_templates_approvals=dict(type="list", elements='str'), + state=dict(choices=['present', 'absent'], default='present'), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + name = module.params.get('name') + new_name = module.params.get("new_name") + state = module.params.get('state') + + new_fields = {} + search_fields = {'name': name} + + # Attempt to look up the related items the user specified (these will fail the module if not found) + organization = module.params.get('organization') + if organization: + organization_id = module.resolve_name_to_id('organizations', organization) + search_fields['organization'] = new_fields['organization'] = organization_id + + # Attempt to look up an existing item based on the provided data + existing_item = module.get_one('workflow_job_templates', **{'data': search_fields}) + + if state == 'absent': + # If the state was absent we can let the module delete it if needed, the module will handle exiting from this + module.delete_if_needed(existing_item) + + inventory = module.params.get('inventory') + if inventory: + new_fields['inventory'] = module.resolve_name_to_id('inventories', inventory) + + webhook_credential = module.params.get('webhook_credential') + if webhook_credential: + new_fields['webhook_credential'] = module.resolve_name_to_id('webhook_credential', webhook_credential) + + # Create the data that gets sent for create and update + new_fields['name'] = new_name if new_name else name + for field_name in ( + 'description', 'survey_enabled', 'allow_simultaneous', + 'limit', 'scm_branch', 'extra_vars', + 'ask_inventory_on_launch', 'ask_scm_branch_on_launch', 'ask_limit_on_launch', 'ask_variables_on_launch', + 'webhook_service',): + field_val = module.params.get(field_name) + if field_val: + new_fields[field_name] = field_val + + if 'extra_vars' in new_fields: + new_fields['extra_vars'] = json.dumps(new_fields['extra_vars']) + + association_fields = {} + + notifications_start = module.params.get('notification_templates_started') + if notifications_start is not None: + association_fields['notification_templates_started'] = [] + for item in notifications_start: + association_fields['notification_templates_started'].append(module.resolve_name_to_id('notification_templates', item)) + + notifications_success = module.params.get('notification_templates_success') + if notifications_success is not None: + association_fields['notification_templates_success'] = [] + for item in notifications_success: + association_fields['notification_templates_success'].append(module.resolve_name_to_id('notification_templates', item)) + + notifications_error = module.params.get('notification_templates_error') + if notifications_error is not None: + association_fields['notification_templates_error'] = [] + for item in notifications_error: + association_fields['notification_templates_error'].append(module.resolve_name_to_id('notification_templates', item)) + + notifications_approval = module.params.get('notification_templates_approvals') + if notifications_approval is not None: + association_fields['notification_templates_approvals'] = [] + for item in notifications_approval: + association_fields['notification_templates_approvals'].append(module.resolve_name_to_id('notification_templates', item)) + + on_change = None + new_spec = module.params.get('survey') + if new_spec: + existing_spec = None + if existing_item: + spec_endpoint = existing_item.get('related', {}).get('survey_spec') + existing_spec = module.get_endpoint(spec_endpoint) + if new_spec != existing_spec: + module.json_output['changed'] = True + if existing_item and module.has_encrypted_values(existing_spec): + module._encrypted_changed_warning('survey_spec', existing_item, warning=True) + on_change = update_survey + + # If the state was present and we can let the module build or update the existing item, this will return on its own + module.create_or_update_if_needed( + existing_item, new_fields, + endpoint='workflow_job_templates', item_type='workflow_job_template', + associations=association_fields, + on_create=on_change, on_update=on_change + ) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_workflow_job_template_node.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_workflow_job_template_node.py new file mode 100644 index 00000000..7ef9e146 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_workflow_job_template_node.py @@ -0,0 +1,271 @@ +#!/usr/bin/python +# coding: utf-8 -*- + + +# (c) 2020, John Westcott IV <john.westcott.iv@redhat.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: tower_workflow_job_template_node +author: "John Westcott IV (@john-westcott-iv)" +short_description: create, update, or destroy Ansible Tower workflow job template nodes. +description: + - Create, update, or destroy Ansible Tower workflow job template nodes. + - Use this to build a graph for a workflow, which dictates what the workflow runs. + - Replaces the deprecated tower_workflow_template module schema command. + - You can create nodes first, and link them afterwards, and not worry about ordering. + For failsafe referencing of a node, specify identifier, WFJT, and organization. + With those specified, you can choose to modify or not modify any other parameter. +options: + extra_data: + description: + - Variables to apply at launch time. + - Will only be accepted if job template prompts for vars or has a survey asking for those vars. + type: dict + default: {} + inventory: + description: + - Inventory applied as a prompt, if job template prompts for inventory + type: str + scm_branch: + description: + - SCM branch applied as a prompt, if job template prompts for SCM branch + type: str + job_type: + description: + - Job type applied as a prompt, if job template prompts for job type + type: str + choices: + - 'run' + - 'check' + job_tags: + description: + - Job tags applied as a prompt, if job template prompts for job tags + type: str + skip_tags: + description: + - Tags to skip, applied as a prompt, if job tempalte prompts for job tags + type: str + limit: + description: + - Limit to act on, applied as a prompt, if job template prompts for limit + type: str + diff_mode: + description: + - Run diff mode, applied as a prompt, if job template prompts for diff mode + type: bool + verbosity: + description: + - Verbosity applied as a prompt, if job template prompts for verbosity + type: str + choices: + - '0' + - '1' + - '2' + - '3' + - '4' + - '5' + workflow_job_template: + description: + - The workflow job template the node exists in. + - Used for looking up the node, cannot be modified after creation. + required: True + type: str + aliases: + - workflow + organization: + description: + - The organization of the workflow job template the node exists in. + - Used for looking up the workflow, not a direct model field. + type: str + unified_job_template: + description: + - Name of unified job template to run in the workflow. + - Can be a job template, project, inventory source, etc. + - Omit if creating an approval node (not yet implemented). + type: str + all_parents_must_converge: + description: + - If enabled then the node will only run if all of the parent nodes have met the criteria to reach this node + type: bool + identifier: + description: + - An identifier for this node that is unique within its workflow. + - It is copied to workflow job nodes corresponding to this node. + required: True + type: str + always_nodes: + description: + - Nodes that will run after this node completes. + - List of node identifiers. + type: list + elements: str + success_nodes: + description: + - Nodes that will run after this node on success. + - List of node identifiers. + type: list + elements: str + failure_nodes: + description: + - Nodes that will run after this node on failure. + - List of node identifiers. + type: list + elements: str + credentials: + description: + - Credentials to be applied to job as launch-time prompts. + - List of credential names. + - Uniqueness is not handled rigorously. + type: list + elements: str + state: + description: + - Desired state of the resource. + choices: ["present", "absent"] + default: "present" + type: str +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Create a node, follows tower_workflow_job_template example + tower_workflow_job_template_node: + identifier: my-first-node + workflow: example-workflow + unified_job_template: jt-for-node-use + organization: Default # organization of workflow job template + extra_data: + foo_key: bar_value + +- name: Create parent node for prior node + tower_workflow_job_template_node: + identifier: my-root-node + workflow: example-workflow + unified_job_template: jt-for-node-use + organization: Default + success_nodes: + - my-first-node +''' + +from ..module_utils.tower_api import TowerAPIModule + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + identifier=dict(required=True), + workflow_job_template=dict(required=True, aliases=['workflow']), + organization=dict(), + extra_data=dict(type='dict'), + inventory=dict(), + scm_branch=dict(), + job_type=dict(choices=['run', 'check']), + job_tags=dict(), + skip_tags=dict(), + limit=dict(), + diff_mode=dict(type='bool'), + verbosity=dict(choices=['0', '1', '2', '3', '4', '5']), + unified_job_template=dict(), + all_parents_must_converge=dict(type='bool'), + success_nodes=dict(type='list', elements='str'), + always_nodes=dict(type='list', elements='str'), + failure_nodes=dict(type='list', elements='str'), + credentials=dict(type='list', elements='str'), + state=dict(choices=['present', 'absent'], default='present'), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + identifier = module.params.get('identifier') + state = module.params.get('state') + + new_fields = {} + search_fields = {'identifier': identifier} + + # Attempt to look up the related items the user specified (these will fail the module if not found) + workflow_job_template = module.params.get('workflow_job_template') + workflow_job_template_id = None + if workflow_job_template: + wfjt_search_fields = {'name': workflow_job_template} + organization = module.params.get('organization') + if organization: + organization_id = module.resolve_name_to_id('organizations', organization) + wfjt_search_fields['organization'] = organization_id + wfjt_data = module.get_one('workflow_job_templates', **{'data': wfjt_search_fields}) + if wfjt_data is None: + module.fail_json(msg="The workflow {0} in organization {1} was not found on the Tower server".format( + workflow_job_template, organization + )) + workflow_job_template_id = wfjt_data['id'] + search_fields['workflow_job_template'] = new_fields['workflow_job_template'] = workflow_job_template_id + + # Attempt to look up an existing item based on the provided data + existing_item = module.get_one('workflow_job_template_nodes', **{'data': search_fields}) + + if state == 'absent': + # If the state was absent we can let the module delete it if needed, the module will handle exiting from this + module.delete_if_needed(existing_item) + + unified_job_template = module.params.get('unified_job_template') + if unified_job_template: + new_fields['unified_job_template'] = module.resolve_name_to_id('unified_job_templates', unified_job_template) + + inventory = module.params.get('inventory') + if inventory: + new_fields['inventory'] = module.resolve_name_to_id('inventories', inventory) + + # Create the data that gets sent for create and update + for field_name in ( + 'identifier', 'extra_data', 'scm_branch', 'job_type', 'job_tags', 'skip_tags', + 'limit', 'diff_mode', 'verbosity', 'all_parents_must_converge',): + field_val = module.params.get(field_name) + if field_val: + new_fields[field_name] = field_val + + association_fields = {} + for association in ('always_nodes', 'success_nodes', 'failure_nodes', 'credentials'): + name_list = module.params.get(association) + if name_list is None: + continue + id_list = [] + for sub_name in name_list: + if association == 'credentials': + endpoint = 'credentials' + lookup_data = {'name': sub_name} + else: + endpoint = 'workflow_job_template_nodes' + lookup_data = {'identifier': sub_name} + if workflow_job_template_id: + lookup_data['workflow_job_template'] = workflow_job_template_id + sub_obj = module.get_one(endpoint, **{'data': lookup_data}) + if sub_obj is None: + module.fail_json(msg='Could not find {0} entry with name {1}'.format(association, sub_name)) + id_list.append(sub_obj['id']) + if id_list: + association_fields[association] = id_list + + # In the case of a new object, the utils need to know it is a node + new_fields['type'] = 'workflow_job_template_node' + + # If the state was present and we can let the module build or update the existing item, this will return on its own + module.create_or_update_if_needed( + existing_item, new_fields, + endpoint='workflow_job_template_nodes', item_type='workflow_job_template_node', + associations=association_fields + ) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_workflow_launch.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_workflow_launch.py new file mode 100644 index 00000000..249feeed --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_workflow_launch.py @@ -0,0 +1,206 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: tower_workflow_launch +author: "John Westcott IV (@john-westcott-iv)" +short_description: Run a workflow in Ansible Tower +description: + - Launch an Ansible Tower workflows. See + U(https://www.ansible.com/tower) for an overview. +options: + name: + description: + - The name of the workflow template to run. + required: True + type: str + aliases: + - workflow_template + organization: + description: + - Organization the workflow job template exists in. + - Used to help lookup the object, cannot be modified using this module. + - If not provided, will lookup by name only, which does not work with duplicates. + type: str + inventory: + description: + - Inventory to use for the job ran with this workflow, only used if prompt for inventory is set. + type: str + limit: + description: + - Limit to use for the I(job_template). + type: str + scm_branch: + description: + - A specific branch of the SCM project to run the template on. + - This is only applicable if your project allows for branch override. + type: str + extra_vars: + description: + - Any extra vars required to launch the job. + type: dict + wait: + description: + - Wait for the workflow to complete. + default: True + type: bool + interval: + description: + - The interval to request an update from Tower. + required: False + default: 1 + type: float + timeout: + description: + - If waiting for the workflow to complete this will abort after this + amount of seconds + type: int +extends_documentation_fragment: awx.awx.auth +''' + +RETURN = ''' +job_info: + description: dictionary containing information about the workflow executed + returned: If workflow launched + type: dict +''' + + +EXAMPLES = ''' +- name: Launch a workflow with a timeout of 10 seconds + tower_workflow_launch: + workflow_template: "Test Workflow" + timeout: 10 + +- name: Launch a Workflow with extra_vars without waiting + tower_workflow_launch: + workflow_template: "Test workflow" + extra_vars: + var1: My First Variable + var2: My Second Variable + wait: False +''' + +from ..module_utils.tower_api import TowerAPIModule +import json +import time + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + name=dict(required=True, aliases=['workflow_template']), + organization=dict(), + inventory=dict(), + limit=dict(), + scm_branch=dict(), + extra_vars=dict(type='dict'), + wait=dict(required=False, default=True, type='bool'), + interval=dict(required=False, default=1.0, type='float'), + timeout=dict(required=False, default=None, type='int'), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + optional_args = {} + # Extract our parameters + name = module.params.get('name') + organization = module.params.get('organization') + inventory = module.params.get('inventory') + optional_args['limit'] = module.params.get('limit') + wait = module.params.get('wait') + interval = module.params.get('interval') + timeout = module.params.get('timeout') + + # Special treatment of extra_vars parameter + extra_vars = module.params.get('extra_vars') + if extra_vars is not None: + optional_args['extra_vars'] = json.dumps(extra_vars) + + # Create a datastructure to pass into our job launch + post_data = {} + for key in optional_args.keys(): + if optional_args[key]: + post_data[key] = optional_args[key] + + # Attempt to look up the related items the user specified (these will fail the module if not found) + if inventory: + post_data['inventory'] = module.resolve_name_to_id('inventories', inventory) + + # Attempt to look up job_template based on the provided name + lookup_data = {'name': name} + if organization: + lookup_data['organization'] = module.resolve_name_to_id('organizations', organization) + workflow_job_template = module.get_one('workflow_job_templates', data=lookup_data) + + if workflow_job_template is None: + module.fail_json(msg="Unable to find workflow job template") + + # The API will allow you to submit values to a jb launch that are not prompt on launch. + # Therefore, we will test to see if anything is set which is not prompt on launch and fail. + check_vars_to_prompts = { + 'inventory': 'ask_inventory_on_launch', + 'limit': 'ask_limit_on_launch', + 'scm_branch': 'ask_scm_branch_on_launch', + 'extra_vars': 'ask_variables_on_launch', + } + + param_errors = [] + for variable_name in check_vars_to_prompts: + if variable_name in post_data and not workflow_job_template[check_vars_to_prompts[variable_name]]: + param_errors.append("The field {0} was specified but the workflow job template does not allow for it to be overridden".format(variable_name)) + if len(param_errors) > 0: + module.fail_json(msg="Parameters specified which can not be passed into wotkflow job template, see errors for details", errors=param_errors) + + # Launch the job + result = module.post_endpoint(workflow_job_template['related']['launch'], data=post_data) + + if result['status_code'] != 201: + module.fail_json(msg="Failed to launch workflow, see response for details", response=result) + + module.json_output['changed'] = True + module.json_output['id'] = result['json']['id'] + module.json_output['status'] = result['json']['status'] + # This is for backwards compatability + module.json_output['job_info'] = {'id': result['json']['id']} + + if not wait: + module.exit_json(**module.json_output) + + # Grab our start time to compare against for the timeout + start = time.time() + + job_url = result['json']['url'] + while not result['json']['finished']: + # If we are past our time out fail with a message + if timeout and timeout < time.time() - start: + module.json_output['msg'] = "Monitoring aborted due to timeout" + module.fail_json(**module.json_output) + + # Put the process to sleep for our interval + time.sleep(interval) + + result = module.get_endpoint(job_url) + module.json_output['status'] = result['json']['status'] + + # If the job has failed, we want to raise a task failure for that so we get a non-zero response. + if result['json']['failed']: + module.json_output['msg'] = 'The workflow "{0}" failed'.format(name) + module.fail_json(**module.json_output) + + module.exit_json(**module.json_output) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_workflow_template.py b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_workflow_template.py new file mode 100644 index 00000000..a8557b2a --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/plugins/modules/tower_workflow_template.py @@ -0,0 +1,230 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright: (c) 2018, Adrien Fleury <fleu42@gmail.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'status': ['deprecated'], + 'supported_by': 'community', + 'metadata_version': '1.1'} + + +DOCUMENTATION = ''' +--- +module: tower_workflow_template +deprecated: + removed_in: "14.0.0" + why: Deprecated in favor of C(_workflow_job_template) and C(_workflow_job_template_node) modules. + alternative: Use M(tower_workflow_job_template) and M(_workflow_job_template_node) instead. +author: "Adrien Fleury (@fleu42)" +short_description: create, update, or destroy Ansible Tower workflow template. +description: + - A tower-cli based module for CRUD actions on workflow job templates. + - Enables use of the old schema functionality. + - Not updated for new features, convert to the modules for + workflow_job_template and workflow_job_template node instead. +options: + allow_simultaneous: + description: + - If enabled, simultaneous runs of this job template will be allowed. + type: bool + ask_extra_vars: + description: + - Prompt user for (extra_vars) on launch. + type: bool + ask_inventory: + description: + - Prompt user for inventory on launch. + type: bool + description: + description: + - The description to use for the workflow. + type: str + extra_vars: + description: + - Extra variables used by Ansible in YAML or key=value format. + type: dict + inventory: + description: + - Name of the inventory to use for the job template. + type: str + name: + description: + - The name to use for the workflow. + required: True + type: str + organization: + description: + - The organization the workflow is linked to. + type: str + schema: + description: + - > + The schema is a JSON- or YAML-formatted string defining the + hierarchy structure that connects the nodes. Refer to Tower + documentation for more information. + type: list + elements: dict + survey_enabled: + description: + - Setting that variable will prompt the user for job type on the + workflow launch. + type: bool + survey: + description: + - The definition of the survey associated to the workflow. + type: dict + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] + type: str + +requirements: +- ansible-tower-cli >= 3.0.2 + +extends_documentation_fragment: awx.awx.auth_legacy +''' + + +EXAMPLES = ''' +- tower_workflow_template: + name: Workflow Template + description: My very first Workflow Template + organization: My optional Organization + schema: "{{ lookup('file', 'my_workflow.json') }}" + +- tower_workflow_template: + name: Workflow Template + state: absent +''' + + +RETURN = ''' # ''' + + +from ..module_utils.tower_legacy import ( + TowerLegacyModule, + tower_auth_config, + tower_check_mode +) + +import json + +try: + import tower_cli + import tower_cli.exceptions as exc + from tower_cli.conf import settings +except ImportError: + pass + + +def main(): + argument_spec = dict( + name=dict(required=True), + description=dict(), + extra_vars=dict(type='dict'), + organization=dict(), + allow_simultaneous=dict(type='bool'), + schema=dict(type='list', elements='dict'), + survey=dict(type='dict'), + survey_enabled=dict(type='bool'), + inventory=dict(), + ask_inventory=dict(type='bool'), + ask_extra_vars=dict(type='bool'), + state=dict(choices=['present', 'absent'], default='present'), + ) + + module = TowerLegacyModule( + argument_spec=argument_spec, + supports_check_mode=False + ) + + module.deprecate(msg=( + "This module is replaced by the combination of tower_workflow_job_template and " + "tower_workflow_job_template_node. This uses the old tower-cli and wll be " + "removed in 2022." + ), version='awx.awx:14.0.0') + + name = module.params.get('name') + state = module.params.get('state') + + schema = None + if module.params.get('schema'): + schema = module.params.get('schema') + + if schema and state == 'absent': + module.fail_json( + msg='Setting schema when state is absent is not allowed', + changed=False + ) + + json_output = {'workflow_template': name, 'state': state} + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + tower_check_mode(module) + wfjt_res = tower_cli.get_resource('workflow') + params = {} + params['name'] = name + + if module.params.get('description'): + params['description'] = module.params.get('description') + + if module.params.get('organization'): + organization_res = tower_cli.get_resource('organization') + try: + organization = organization_res.get( + name=module.params.get('organization')) + params['organization'] = organization['id'] + except exc.NotFound as excinfo: + module.fail_json( + msg='Failed to update organization source,' + 'organization not found: {0}'.format(excinfo), + changed=False + ) + + if module.params.get('survey'): + params['survey_spec'] = module.params.get('survey') + + if module.params.get('ask_extra_vars'): + params['ask_variables_on_launch'] = module.params.get('ask_extra_vars') + + if module.params.get('ask_inventory'): + params['ask_inventory_on_launch'] = module.params.get('ask_inventory') + + for key in ('allow_simultaneous', 'inventory', + 'survey_enabled', 'description'): + if module.params.get(key): + params[key] = module.params.get(key) + + # Special treatment for tower-cli extra_vars + extra_vars = module.params.get('extra_vars') + if extra_vars: + params['extra_vars'] = [json.dumps(extra_vars)] + + try: + if state == 'present': + params['create_on_missing'] = True + result = wfjt_res.modify(**params) + json_output['id'] = result['id'] + if schema: + wfjt_res.schema(result['id'], json.dumps(schema)) + elif state == 'absent': + params['fail_on_missing'] = False + result = wfjt_res.delete(**params) + except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo: + module.fail_json(msg='Failed to update workflow template: \ + {0}'.format(excinfo), changed=False) + + json_output['changed'] = result['changed'] + module.exit_json(**json_output) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/requirements.txt b/collections-debian-merged/ansible_collections/awx/awx/requirements.txt new file mode 100644 index 00000000..37d1ffa1 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/requirements.txt @@ -0,0 +1,3 @@ +pytz # for tower_schedule_rrule lookup plugin +python-dateutil>=2.7.0 # tower_schedule_rrule +awxkit # For import and export modules
\ No newline at end of file diff --git a/collections-debian-merged/ansible_collections/awx/awx/setup.cfg b/collections-debian-merged/ansible_collections/awx/awx/setup.cfg new file mode 100644 index 00000000..fdfea44c --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/setup.cfg @@ -0,0 +1,3 @@ +[flake8] +max-line-length=160 +ignore=E402
\ No newline at end of file diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/conftest.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/conftest.py new file mode 100644 index 00000000..5db00d63 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/conftest.py @@ -0,0 +1,289 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import io +import os +import json +import datetime +import importlib +from contextlib import redirect_stdout, suppress +from unittest import mock +import logging + +from requests.models import Response, PreparedRequest + +import pytest + +from awx.main.tests.functional.conftest import _request +from awx.main.models import Organization, Project, Inventory, JobTemplate, Credential, CredentialType + +try: + import tower_cli # noqa + HAS_TOWER_CLI = True +except ImportError: + HAS_TOWER_CLI = False + +try: + # Because awxkit will be a directory at the root of this makefile and we are using python3, import awxkit will work even if its not installed. + # However, awxkit will not contain api whih causes a stack failure down on line 170 when we try to mock it. + # So here we are importing awxkit.api to prevent that. Then you only get an error on tests for awxkit functionality. + import awxkit.api + HAS_AWX_KIT = True +except ImportError: + HAS_AWX_KIT = False + +logger = logging.getLogger('awx.main.tests') + + +def sanitize_dict(din): + '''Sanitize Django response data to purge it of internal types + so it may be used to cast a requests response object + ''' + if isinstance(din, (int, str, type(None), bool)): + return din # native JSON types, no problem + elif isinstance(din, datetime.datetime): + return din.isoformat() + elif isinstance(din, list): + for i in range(len(din)): + din[i] = sanitize_dict(din[i]) + return din + elif isinstance(din, dict): + for k in din.copy().keys(): + din[k] = sanitize_dict(din[k]) + return din + else: + return str(din) # translation proxies often not string but stringlike + + +@pytest.fixture(autouse=True) +def collection_path_set(monkeypatch): + """Monkey patch sys.path, insert the root of the collection folder + so that content can be imported without being fully packaged + """ + base_folder = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.pardir, os.pardir) + ) + monkeypatch.syspath_prepend(base_folder) + + +@pytest.fixture +def collection_import(): + """These tests run assuming that the awx_collection folder is inserted + into the PATH before-hand by collection_path_set. + But all imports internally to the collection + go through this fixture so that can be changed if needed. + For instance, we could switch to fully-qualified import paths. + """ + def rf(path): + return importlib.import_module(path) + return rf + + +@pytest.fixture +def run_module(request, collection_import): + def rf(module_name, module_params, request_user): + + def new_request(self, method, url, **kwargs): + kwargs_copy = kwargs.copy() + if 'data' in kwargs: + if isinstance(kwargs['data'], dict): + kwargs_copy['data'] = kwargs['data'] + elif kwargs['data'] is None: + pass + elif isinstance(kwargs['data'], str): + kwargs_copy['data'] = json.loads(kwargs['data']) + else: + raise RuntimeError('Expected data to be dict or str, got {0}, data: {1}'.format( + type(kwargs['data']), kwargs['data'])) + if 'params' in kwargs and method == 'GET': + # query params for GET are handled a bit differently by + # tower-cli and python requests as opposed to REST framework APIRequestFactory + if not kwargs_copy.get('data'): + kwargs_copy['data'] = {} + if isinstance(kwargs['params'], dict): + kwargs_copy['data'].update(kwargs['params']) + elif isinstance(kwargs['params'], list): + for k, v in kwargs['params']: + kwargs_copy['data'][k] = v + + # make request + rf = _request(method.lower()) + django_response = rf(url, user=request_user, expect=None, **kwargs_copy) + + # requests library response object is different from the Django response, but they are the same concept + # this converts the Django response object into a requests response object for consumption + resp = Response() + py_data = django_response.data + sanitize_dict(py_data) + resp._content = bytes(json.dumps(django_response.data), encoding='utf8') + resp.status_code = django_response.status_code + resp.headers = {'X-API-Product-Name': 'AWX', 'X-API-Product-Version': '0.0.1-devel'} + + if request.config.getoption('verbose') > 0: + logger.info( + '%s %s by %s, code:%s', + method, '/api/' + url.split('/api/')[1], + request_user.username, resp.status_code + ) + + resp.request = PreparedRequest() + resp.request.prepare(method=method, url=url) + return resp + + def new_open(self, method, url, **kwargs): + r = new_request(self, method, url, **kwargs) + m = mock.MagicMock(read=mock.MagicMock(return_value=r._content), + status=r.status_code, + getheader=mock.MagicMock(side_effect=r.headers.get) + ) + return m + + stdout_buffer = io.StringIO() + # Requies specific PYTHONPATH, see docs + # Note that a proper Ansiballz explosion of the modules will have an import path like: + # ansible_collections.awx.awx.plugins.modules.{} + # We should consider supporting that in the future + resource_module = collection_import('plugins.modules.{0}'.format(module_name)) + + if not isinstance(module_params, dict): + raise RuntimeError('Module params must be dict, got {0}'.format(type(module_params))) + + # Ansible params can be passed as an invocation argument or over stdin + # this short circuits within the AnsibleModule interface + def mock_load_params(self): + self.params = module_params + + if getattr(resource_module, 'TowerAWXKitModule', None): + resource_class = resource_module.TowerAWXKitModule + elif getattr(resource_module, 'TowerAPIModule', None): + resource_class = resource_module.TowerAPIModule + elif getattr(resource_module, 'TowerLegacyModule', None): + resource_class = resource_module.TowerLegacyModule + else: + raise("The module has neither a TowerLegacyModule, TowerAWXKitModule or a TowerAPIModule") + + with mock.patch.object(resource_class, '_load_params', new=mock_load_params): + # Call the test utility (like a mock server) instead of issuing HTTP requests + with mock.patch('ansible.module_utils.urls.Request.open', new=new_open): + if HAS_TOWER_CLI: + tower_cli_mgr = mock.patch('tower_cli.api.Session.request', new=new_request) + elif HAS_AWX_KIT: + tower_cli_mgr = mock.patch('awxkit.api.client.requests.Session.request', new=new_request) + else: + tower_cli_mgr = suppress() + with tower_cli_mgr: + try: + # Ansible modules return data to the mothership over stdout + with redirect_stdout(stdout_buffer): + resource_module.main() + except SystemExit: + pass # A system exit indicates successful execution + except Exception: + # dump the stdout back to console for debugging + print(stdout_buffer.getvalue()) + raise + + module_stdout = stdout_buffer.getvalue().strip() + try: + result = json.loads(module_stdout) + except Exception as e: + raise Exception('Module did not write valid JSON, error: {0}, stdout:\n{1}'.format(str(e), module_stdout)) + # A module exception should never be a test expectation + if 'exception' in result: + if "ModuleNotFoundError: No module named 'tower_cli'" in result['exception']: + pytest.skip('The tower-cli library is needed to run this test, module no longer supported.') + raise Exception('Module encountered error:\n{0}'.format(result['exception'])) + return result + + return rf + + +@pytest.fixture +def survey_spec(): + return { + "spec": [ + { + "index": 0, + "question_name": "my question?", + "default": "mydef", + "variable": "myvar", + "type": "text", + "required": False + } + ], + "description": "test", + "name": "test" + } + + +@pytest.fixture +def organization(): + return Organization.objects.create(name='Default') + + +@pytest.fixture +def project(organization): + return Project.objects.create( + name="test-proj", + description="test-proj-desc", + organization=organization, + playbook_files=['helloworld.yml'], + local_path='_92__test_proj', + scm_revision='1234567890123456789012345678901234567890', + scm_url='localhost', + scm_type='git' + ) + + +@pytest.fixture +def inventory(organization): + return Inventory.objects.create( + name='test-inv', + organization=organization + ) + + +@pytest.fixture +def job_template(project, inventory): + return JobTemplate.objects.create( + name='test-jt', + project=project, + inventory=inventory, + playbook='helloworld.yml' + ) + + +@pytest.fixture +def machine_credential(organization): + ssh_type = CredentialType.defaults['ssh']() + ssh_type.save() + return Credential.objects.create( + credential_type=ssh_type, name='machine-cred', + inputs={'username': 'test_user', 'password': 'pas4word'} + ) + + +@pytest.fixture +def vault_credential(organization): + ct = CredentialType.defaults['vault']() + ct.save() + return Credential.objects.create( + credential_type=ct, name='vault-cred', + inputs={'vault_id': 'foo', 'vault_password': 'pas4word'} + ) + + +@pytest.fixture +def silence_deprecation(): + """The deprecation warnings are stored in a global variable + they will create cross-test interference. Use this to turn them off. + """ + with mock.patch('ansible.module_utils.basic.AnsibleModule.deprecate') as this_mock: + yield this_mock + + +@pytest.fixture(autouse=True) +def silence_warning(): + """Warnings use global variable, same as deprecations.""" + with mock.patch('ansible.module_utils.basic.AnsibleModule.warn') as this_mock: + yield this_mock diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_credential.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_credential.py new file mode 100644 index 00000000..754133de --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_credential.py @@ -0,0 +1,178 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.main.models import Credential, CredentialType, Organization + + +@pytest.fixture +def cred_type(): + # Make a credential type which will be used by the credential + ct = CredentialType.objects.create( + name='Ansible Galaxy Token', + inputs={ + "fields": [ + { + "id": "token", + "type": "string", + "secret": True, + "label": "Ansible Galaxy Secret Token Value" + } + ], + "required": ["token"] + }, + injectors={ + "extra_vars": { + "galaxy_token": "{{token}}", + } + } + ) + return ct + + +@pytest.mark.django_db +def test_create_machine_credential(run_module, admin_user, organization, silence_deprecation): + Organization.objects.create(name='test-org') + # create the ssh credential type + ct = CredentialType.defaults['ssh']() + ct.save() + # Example from docs + result = run_module('tower_credential', dict( + name='Test Machine Credential', + organization=organization.name, + kind='ssh', + state='present' + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result + + cred = Credential.objects.get(name='Test Machine Credential') + assert cred.credential_type == ct + + assert result['name'] == "Test Machine Credential" + assert result['id'] == cred.pk + + +@pytest.mark.django_db +def test_create_vault_credential(run_module, admin_user, organization, silence_deprecation): + # https://github.com/ansible/ansible/issues/61324 + Organization.objects.create(name='test-org') + ct = CredentialType.defaults['vault']() + ct.save() + + result = run_module('tower_credential', dict( + name='Test Vault Credential', + organization=organization.name, + kind='vault', + vault_id='bar', + vault_password='foobar', + state='present' + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result + + cred = Credential.objects.get(name='Test Vault Credential') + assert cred.credential_type == ct + assert 'vault_id' in cred.inputs + assert 'vault_password' in cred.inputs + + assert result['name'] == "Test Vault Credential" + assert result['id'] == cred.pk + + +@pytest.mark.django_db +def test_ct_precedence_over_kind(run_module, admin_user, organization, cred_type, silence_deprecation): + result = run_module('tower_credential', dict( + name='A credential', + organization=organization.name, + kind='ssh', + credential_type=cred_type.name, + state='present' + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + + cred = Credential.objects.get(name='A credential') + + assert cred.credential_type == cred_type + + +@pytest.mark.django_db +def test_input_overrides_old_fields(run_module, admin_user, organization, silence_deprecation): + # create the vault credential type + ct = CredentialType.defaults['vault']() + ct.save() + result = run_module('tower_credential', dict( + name='A Vault credential', + organization=organization.name, + kind='vault', + vault_id='1234', + inputs={'vault_id': 'asdf'}, + state='present', + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + + cred = Credential.objects.get(name='A Vault credential') + + assert cred.inputs['vault_id'] == 'asdf' + + +@pytest.mark.django_db +def test_missing_credential_type(run_module, admin_user, organization): + Organization.objects.create(name='test-org') + result = run_module('tower_credential', dict( + name='A credential', + organization=organization.name, + credential_type='foobar', + state='present' + ), admin_user) + assert result.get('failed', False), result + assert 'foobar was not found on the Tower server' in result['msg'] + + +@pytest.mark.django_db +def test_make_use_of_custom_credential_type(run_module, organization, admin_user, cred_type): + result = run_module('tower_credential', dict( + name='Galaxy Token for Steve', + organization=organization.name, + credential_type=cred_type.name, + inputs={'token': '7rEZK38DJl58A7RxA6EC7lLvUHbBQ1'} + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed', False), result + + cred = Credential.objects.get(name='Galaxy Token for Steve') + assert cred.credential_type_id == cred_type.id + assert list(cred.inputs.keys()) == ['token'] + assert cred.inputs['token'].startswith('$encrypted$') + assert len(cred.inputs['token']) >= len('$encrypted$') + len('7rEZK38DJl58A7RxA6EC7lLvUHbBQ1') + + assert result['name'] == "Galaxy Token for Steve" + assert result['id'] == cred.pk + + +@pytest.mark.django_db +def test_secret_field_write_twice(run_module, organization, admin_user, cred_type): + val1 = '7rEZK38DJl58A7RxA6EC7lLvUHbBQ1' + result = run_module('tower_credential', dict( + name='Galaxy Token for Steve', + organization=organization.name, + credential_type=cred_type.name, + inputs={'token': val1} + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + + Credential.objects.get(id=result['id']).inputs['token'] == val1 + + val2 = '7rEZ238DJl5837rxA6xxxlLvUHbBQ1' + + result = run_module('tower_credential', dict( + name='Galaxy Token for Steve', + organization=organization.name, + credential_type=cred_type.name, + inputs={'token': val2} + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + + Credential.objects.get(id=result['id']).inputs['token'] == val2 + assert result.get('changed'), result diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_credential_input_source.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_credential_input_source.py new file mode 100644 index 00000000..a676ab15 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_credential_input_source.py @@ -0,0 +1,333 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.main.models import CredentialInputSource, Credential, CredentialType, Organization + + +@pytest.fixture +def aim_cred_type(): + ct = CredentialType.defaults['aim']() + ct.save() + return ct + + +# Test CyberArk AIM credential source +@pytest.fixture +def source_cred_aim(aim_cred_type): + return Credential.objects.create( + name='CyberArk AIM Cred', + credential_type=aim_cred_type, + inputs={ + "url": "https://cyberark.example.com", + "app_id": "myAppID", + "verify": "false" + } + ) + + +@pytest.mark.django_db +def test_aim_credential_source(run_module, admin_user, organization, source_cred_aim, silence_deprecation): + ct = CredentialType.defaults['ssh']() + ct.save() + tgt_cred = Credential.objects.create( + name='Test Machine Credential', + organization=organization, + credential_type=ct, + inputs={'username': 'bob'} + ) + + result = run_module('tower_credential_input_source', dict( + source_credential=source_cred_aim.name, + target_credential=tgt_cred.name, + input_field_name='password', + metadata={"object_query": "Safe=SUPERSAFE;Object=MyAccount"}, + state='present' + ), admin_user) + + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result + + assert CredentialInputSource.objects.count() == 1 + cis = CredentialInputSource.objects.first() + + assert cis.metadata['object_query'] == "Safe=SUPERSAFE;Object=MyAccount" + assert cis.source_credential.name == source_cred_aim.name + assert cis.target_credential.name == tgt_cred.name + assert cis.input_field_name == 'password' + assert result['id'] == cis.pk + + +# Test CyberArk Conjur credential source +@pytest.fixture +def source_cred_conjur(organization): + # Make a credential type which will be used by the credential + ct = CredentialType.defaults['conjur']() + ct.save() + return Credential.objects.create( + name='CyberArk CONJUR Cred', + credential_type=ct, + inputs={ + "url": "https://cyberark.example.com", + "api_key": "myApiKey", + "account": "account", + "username": "username" + } + ) + + +@pytest.mark.django_db +def test_conjur_credential_source(run_module, admin_user, organization, source_cred_conjur, silence_deprecation): + ct = CredentialType.defaults['ssh']() + ct.save() + tgt_cred = Credential.objects.create( + name='Test Machine Credential', + organization=organization, + credential_type=ct, + inputs={'username': 'bob'} + ) + + result = run_module('tower_credential_input_source', dict( + source_credential=source_cred_conjur.name, + target_credential=tgt_cred.name, + input_field_name='password', + metadata={"secret_path": "/path/to/secret"}, + state='present' + ), admin_user) + + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result + + assert CredentialInputSource.objects.count() == 1 + cis = CredentialInputSource.objects.first() + + assert cis.metadata['secret_path'] == "/path/to/secret" + assert cis.source_credential.name == source_cred_conjur.name + assert cis.target_credential.name == tgt_cred.name + assert cis.input_field_name == 'password' + assert result['id'] == cis.pk + + +# Test Hashicorp Vault secret credential source +@pytest.fixture +def source_cred_hashi_secret(organization): + # Make a credential type which will be used by the credential + ct = CredentialType.defaults['hashivault_kv']() + ct.save() + return Credential.objects.create( + name='HashiCorp secret Cred', + credential_type=ct, + inputs={ + "url": "https://secret.hash.example.com", + "token": "myApiKey", + "role_id": "role", + "secret_id": "secret" + } + ) + + +@pytest.mark.django_db +def test_hashi_secret_credential_source(run_module, admin_user, organization, source_cred_hashi_secret, silence_deprecation): + ct = CredentialType.defaults['ssh']() + ct.save() + tgt_cred = Credential.objects.create( + name='Test Machine Credential', + organization=organization, + credential_type=ct, + inputs={'username': 'bob'} + ) + + result = run_module('tower_credential_input_source', dict( + source_credential=source_cred_hashi_secret.name, + target_credential=tgt_cred.name, + input_field_name='password', + metadata={"secret_path": "/path/to/secret", "auth_path": "/path/to/auth", "secret_backend": "backend", "secret_key": "a_key"}, + state='present' + ), admin_user) + + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result + + assert CredentialInputSource.objects.count() == 1 + cis = CredentialInputSource.objects.first() + + assert cis.metadata['secret_path'] == "/path/to/secret" + assert cis.metadata['auth_path'] == "/path/to/auth" + assert cis.metadata['secret_backend'] == "backend" + assert cis.metadata['secret_key'] == "a_key" + assert cis.source_credential.name == source_cred_hashi_secret.name + assert cis.target_credential.name == tgt_cred.name + assert cis.input_field_name == 'password' + assert result['id'] == cis.pk + + +# Test Hashicorp Vault signed ssh credential source +@pytest.fixture +def source_cred_hashi_ssh(organization): + # Make a credential type which will be used by the credential + ct = CredentialType.defaults['hashivault_ssh']() + ct.save() + return Credential.objects.create( + name='HashiCorp ssh Cred', + credential_type=ct, + inputs={ + "url": "https://ssh.hash.example.com", + "token": "myApiKey", + "role_id": "role", + "secret_id": "secret" + } + ) + + +@pytest.mark.django_db +def test_hashi_ssh_credential_source(run_module, admin_user, organization, source_cred_hashi_ssh, silence_deprecation): + ct = CredentialType.defaults['ssh']() + ct.save() + tgt_cred = Credential.objects.create( + name='Test Machine Credential', + organization=organization, + credential_type=ct, + inputs={'username': 'bob'} + ) + + result = run_module('tower_credential_input_source', dict( + source_credential=source_cred_hashi_ssh.name, + target_credential=tgt_cred.name, + input_field_name='password', + metadata={"secret_path": "/path/to/secret", "auth_path": "/path/to/auth", "role": "role", "public_key": "a_key", "valid_principals": "some_value"}, + state='present' + ), admin_user) + + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result + + assert CredentialInputSource.objects.count() == 1 + cis = CredentialInputSource.objects.first() + + assert cis.metadata['secret_path'] == "/path/to/secret" + assert cis.metadata['auth_path'] == "/path/to/auth" + assert cis.metadata['role'] == "role" + assert cis.metadata['public_key'] == "a_key" + assert cis.metadata['valid_principals'] == "some_value" + assert cis.source_credential.name == source_cred_hashi_ssh.name + assert cis.target_credential.name == tgt_cred.name + assert cis.input_field_name == 'password' + assert result['id'] == cis.pk + + +# Test Azure Key Vault credential source +@pytest.fixture +def source_cred_azure_kv(organization): + # Make a credential type which will be used by the credential + ct = CredentialType.defaults['azure_kv']() + ct.save() + return Credential.objects.create( + name='Azure KV Cred', + credential_type=ct, + inputs={ + "url": "https://key.azure.example.com", + "client": "client", + "secret": "secret", + "tenant": "tenant", + "cloud_name": "the_cloud", + } + ) + + +@pytest.mark.django_db +def test_azure_kv_credential_source(run_module, admin_user, organization, source_cred_azure_kv, silence_deprecation): + ct = CredentialType.defaults['ssh']() + ct.save() + tgt_cred = Credential.objects.create( + name='Test Machine Credential', + organization=organization, + credential_type=ct, + inputs={'username': 'bob'} + ) + + result = run_module('tower_credential_input_source', dict( + source_credential=source_cred_azure_kv.name, + target_credential=tgt_cred.name, + input_field_name='password', + metadata={"secret_field": "my_pass"}, + state='present' + ), admin_user) + + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result + + assert CredentialInputSource.objects.count() == 1 + cis = CredentialInputSource.objects.first() + + assert cis.metadata['secret_field'] == "my_pass" + assert cis.source_credential.name == source_cred_azure_kv.name + assert cis.target_credential.name == tgt_cred.name + assert cis.input_field_name == 'password' + assert result['id'] == cis.pk + + +# Test Changing Credential Source +@pytest.fixture +def source_cred_aim_alt(aim_cred_type): + return Credential.objects.create( + name='Alternate CyberArk AIM Cred', + credential_type=aim_cred_type, + inputs={ + "url": "https://cyberark-alt.example.com", + "app_id": "myAltID", + "verify": "false" + } + ) + + +@pytest.mark.django_db +def test_aim_credential_source(run_module, admin_user, organization, source_cred_aim, source_cred_aim_alt, silence_deprecation): + ct = CredentialType.defaults['ssh']() + ct.save() + tgt_cred = Credential.objects.create( + name='Test Machine Credential', + organization=organization, + credential_type=ct, + inputs={'username': 'bob'} + ) + + result = run_module('tower_credential_input_source', dict( + source_credential=source_cred_aim.name, + target_credential=tgt_cred.name, + input_field_name='password', + metadata={"object_query": "Safe=SUPERSAFE;Object=MyAccount"}, + state='present' + ), admin_user) + + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result + + unchangedResult = run_module('tower_credential_input_source', dict( + source_credential=source_cred_aim.name, + target_credential=tgt_cred.name, + input_field_name='password', + metadata={"object_query": "Safe=SUPERSAFE;Object=MyAccount"}, + state='present' + ), admin_user) + + assert not unchangedResult.get('failed', False), result.get('msg', result) + assert not unchangedResult.get('changed'), result + + changedResult = run_module('tower_credential_input_source', dict( + source_credential=source_cred_aim_alt.name, + target_credential=tgt_cred.name, + input_field_name='password', + state='present' + ), admin_user) + + assert not changedResult.get('failed', False), changedResult.get('msg', result) + assert changedResult.get('changed'), result + + assert CredentialInputSource.objects.count() == 1 + cis = CredentialInputSource.objects.first() + + assert cis.metadata['object_query'] == "Safe=SUPERSAFE;Object=MyAccount" + assert cis.source_credential.name == source_cred_aim_alt.name + assert cis.target_credential.name == tgt_cred.name + assert cis.input_field_name == 'password' diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_credential_type.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_credential_type.py new file mode 100644 index 00000000..29f4869d --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_credential_type.py @@ -0,0 +1,49 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.main.models import CredentialType + + +@pytest.mark.django_db +def test_create_custom_credential_type(run_module, admin_user, silence_deprecation): + # Example from docs + result = run_module('tower_credential_type', dict( + name='Nexus', + description='Credentials type for Nexus', + kind='cloud', + inputs={"fields": [{"id": "server", "type": "string", "default": "", "label": ""}], "required": []}, + injectors={'extra_vars': {'nexus_credential': 'test'}}, + state='present', + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result + + ct = CredentialType.objects.get(name='Nexus') + + assert result['name'] == 'Nexus' + assert result['id'] == ct.pk + + assert ct.inputs == {"fields": [{"id": "server", "type": "string", "default": "", "label": ""}], "required": []} + assert ct.injectors == {'extra_vars': {'nexus_credential': 'test'}} + + +@pytest.mark.django_db +def test_changed_false_with_api_changes(run_module, admin_user): + result = run_module('tower_credential_type', dict( + name='foo', + kind='cloud', + inputs={"fields": [{"id": "env_value", "label": "foo", "default": "foo"}]}, + injectors={'env': {'TEST_ENV_VAR': '{{ env_value }}'}}, + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result + + result = run_module('tower_credential_type', dict( + name='foo', + inputs={"fields": [{"id": "env_value", "label": "foo", "default": "foo"}]}, + injectors={'env': {'TEST_ENV_VAR': '{{ env_value }}'}}, + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert not result.get('changed'), result diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_group.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_group.py new file mode 100644 index 00000000..3e5bcc6b --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_group.py @@ -0,0 +1,117 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.main.models import Organization, Inventory, Group, Host + + +@pytest.mark.django_db +def test_create_group(run_module, admin_user): + org = Organization.objects.create(name='test-org') + inv = Inventory.objects.create(name='test-inv', organization=org) + variables = {"ansible_network_os": "iosxr"} + + result = run_module('tower_group', dict( + name='Test Group', + inventory='test-inv', + variables=variables, + state='present' + ), admin_user) + assert result.get('changed'), result + + group = Group.objects.get(name='Test Group') + assert group.inventory == inv + assert group.variables == '{"ansible_network_os": "iosxr"}' + + result.pop('invocation') + assert result == { + 'id': group.id, + 'name': 'Test Group', + 'changed': True, + } + + +@pytest.mark.django_db +def test_associate_hosts_and_children(run_module, admin_user, organization): + inv = Inventory.objects.create(name='test-inv', organization=organization) + group = Group.objects.create(name='Test Group', inventory=inv) + + inv_hosts = [Host.objects.create(inventory=inv, name='foo{0}'.format(i)) for i in range(3)] + group.hosts.add(inv_hosts[0], inv_hosts[1]) + + child = Group.objects.create(inventory=inv, name='child_group') + + result = run_module('tower_group', dict( + name='Test Group', + inventory='test-inv', + hosts=[inv_hosts[1].name, inv_hosts[2].name], + children=[child.name], + state='present' + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result['changed'] is True + + assert set(group.hosts.all()) == set([inv_hosts[1], inv_hosts[2]]) + assert set(group.children.all()) == set([child]) + + +@pytest.mark.django_db +def test_associate_on_create(run_module, admin_user, organization): + inv = Inventory.objects.create(name='test-inv', organization=organization) + child = Group.objects.create(name='test-child', inventory=inv) + host = Host.objects.create(name='test-host', inventory=inv) + + result = run_module('tower_group', dict( + name='Test Group', + inventory='test-inv', + hosts=[host.name], + groups=[child.name], + state='present' + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result['changed'] is True + + group = Group.objects.get(pk=result['id']) + assert set(group.hosts.all()) == set([host]) + assert set(group.children.all()) == set([child]) + + +@pytest.mark.django_db +def test_children_alias_of_groups(run_module, admin_user, organization): + inv = Inventory.objects.create(name='test-inv', organization=organization) + group = Group.objects.create(name='Test Group', inventory=inv) + child = Group.objects.create(inventory=inv, name='child_group') + result = run_module('tower_group', dict( + name='Test Group', + inventory='test-inv', + groups=[child.name], + state='present' + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result['changed'] is True + + assert set(group.children.all()) == set([child]) + + +@pytest.mark.django_db +def test_tower_group_idempotent(run_module, admin_user): + # https://github.com/ansible/ansible/issues/46803 + org = Organization.objects.create(name='test-org') + inv = Inventory.objects.create(name='test-inv', organization=org) + group = Group.objects.create( + name='Test Group', + inventory=inv, + ) + + result = run_module('tower_group', dict( + name='Test Group', + inventory='test-inv', + state='present' + ), admin_user) + + result.pop('invocation') + assert result == { + 'id': group.id, + 'changed': False, # idempotency assertion + } diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_inventory.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_inventory.py new file mode 100644 index 00000000..2ba52ac0 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_inventory.py @@ -0,0 +1,60 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.main.models import Inventory + + +@pytest.mark.django_db +def test_inventory_create(run_module, admin_user, organization): + result = run_module('tower_inventory', { + 'name': 'foo-inventory', + 'organization': organization.name, + 'variables': {'foo': 'bar', 'another-foo': {'barz': 'bar2'}}, + 'state': 'present' + }, admin_user) + + inv = Inventory.objects.get(name='foo-inventory') + assert inv.variables == '{"foo": "bar", "another-foo": {"barz": "bar2"}}' + + result.pop('module_args', None) + result.pop('invocation', None) + assert result == { + "name": "foo-inventory", + "id": inv.id, + "changed": True + } + + assert inv.organization_id == organization.id + + +@pytest.mark.django_db +def test_invalid_smart_inventory_create(run_module, admin_user, organization): + result = run_module('tower_inventory', { + 'name': 'foo-inventory', + 'organization': organization.name, + 'kind': 'smart', + 'host_filter': 'ansible', + 'state': 'present' + }, admin_user) + assert result.get('failed', False), result + + assert 'Invalid query ansible' in result['msg'] + + +@pytest.mark.django_db +def test_valid_smart_inventory_create(run_module, admin_user, organization): + result = run_module('tower_inventory', { + 'name': 'foo-inventory', + 'organization': organization.name, + 'kind': 'smart', + 'host_filter': 'name=my_host', + 'state': 'present' + }, admin_user) + assert not result.get('failed', False), result + + inv = Inventory.objects.get(name='foo-inventory') + assert inv.host_filter == 'name=my_host' + assert inv.kind == 'smart' + assert inv.organization_id == organization.id diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_inventory_source.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_inventory_source.py new file mode 100644 index 00000000..ab029668 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_inventory_source.py @@ -0,0 +1,256 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.main.models import Organization, Inventory, InventorySource, Project + + +@pytest.fixture +def base_inventory(): + org = Organization.objects.create(name='test-org') + inv = Inventory.objects.create(name='test-inv', organization=org) + return inv + + +@pytest.fixture +def project(base_inventory): + return Project.objects.create( + name='test-proj', + organization=base_inventory.organization, + scm_type='git', + scm_url='https://github.com/ansible/test-playbooks.git', + ) + + +@pytest.mark.django_db +def test_inventory_source_create(run_module, admin_user, base_inventory, project): + source_path = '/var/lib/awx/example_source_path/' + result = run_module('tower_inventory_source', dict( + name='foo', + inventory=base_inventory.name, + state='present', + source='scm', + source_path=source_path, + source_project=project.name + ), admin_user) + assert result.pop('changed', None), result + + inv_src = InventorySource.objects.get(name='foo') + assert inv_src.inventory == base_inventory + result.pop('invocation') + assert result == { + 'id': inv_src.id, + 'name': 'foo', + } + + +@pytest.mark.django_db +def test_create_inventory_source_implied_org(run_module, admin_user): + org = Organization.objects.create(name='test-org') + inv = Inventory.objects.create(name='test-inv', organization=org) + + # Credential is not required for ec2 source, because of IAM roles + result = run_module('tower_inventory_source', dict( + name='Test Inventory Source', + inventory='test-inv', + source='ec2', + state='present' + ), admin_user) + assert result.pop('changed', None), result + + inv_src = InventorySource.objects.get(name='Test Inventory Source') + assert inv_src.inventory == inv + + result.pop('invocation') + assert result == { + "name": "Test Inventory Source", + "id": inv_src.id, + } + + +@pytest.mark.django_db +def test_create_inventory_source_multiple_orgs(run_module, admin_user): + org = Organization.objects.create(name='test-org') + Inventory.objects.create(name='test-inv', organization=org) + + # make another inventory by same name in another org + org2 = Organization.objects.create(name='test-org-number-two') + inv2 = Inventory.objects.create(name='test-inv', organization=org2) + + result = run_module('tower_inventory_source', dict( + name='Test Inventory Source', + inventory=inv2.id, + source='ec2', + state='present' + ), admin_user) + assert result.pop('changed', None), result + + inv_src = InventorySource.objects.get(name='Test Inventory Source') + assert inv_src.inventory == inv2 + + result.pop('invocation') + assert result == { + "name": "Test Inventory Source", + "id": inv_src.id, + } + + +@pytest.mark.django_db +def test_create_inventory_source_with_venv(run_module, admin_user, base_inventory, mocker, project): + path = '/var/lib/awx/venv/custom-venv/foobar13489435/' + source_path = '/var/lib/awx/example_source_path/' + with mocker.patch('awx.main.models.mixins.get_custom_venv_choices', return_value=[path]): + result = run_module('tower_inventory_source', dict( + name='foo', + inventory=base_inventory.name, + state='present', + source='scm', + source_project=project.name, + custom_virtualenv=path, + source_path=source_path + ), admin_user) + assert result.pop('changed'), result + + inv_src = InventorySource.objects.get(name='foo') + assert inv_src.inventory == base_inventory + result.pop('invocation') + + assert inv_src.custom_virtualenv == path + + +@pytest.mark.django_db +def test_custom_venv_no_op(run_module, admin_user, base_inventory, mocker, project): + """If the inventory source is modified, then it should not blank fields + unrelated to the params that the user passed. + This enforces assumptions about the behavior of the AnsibleModule + default argument_spec behavior. + """ + source_path = '/var/lib/awx/example_source_path/' + inv_src = InventorySource.objects.create( + name='foo', + inventory=base_inventory, + source_project=project, + source='scm', + custom_virtualenv='/venv/foobar/' + ) + # mock needed due to API behavior, not incorrect client behavior + with mocker.patch('awx.main.models.mixins.get_custom_venv_choices', return_value=['/venv/foobar/']): + result = run_module('tower_inventory_source', dict( + name='foo', + description='this is the changed description', + inventory=base_inventory.name, + source='scm', # is required, but behavior is arguable + state='present', + source_project=project.name, + source_path=source_path + ), admin_user) + assert result.pop('changed', None), result + inv_src.refresh_from_db() + assert inv_src.custom_virtualenv == '/venv/foobar/' + assert inv_src.description == 'this is the changed description' + + +@pytest.mark.django_db +def test_falsy_value(run_module, admin_user, base_inventory): + result = run_module('tower_inventory_source', dict( + name='falsy-test', + inventory=base_inventory.name, + source='ec2', + update_on_launch=True + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed', None), result + + inv_src = InventorySource.objects.get(name='falsy-test') + assert inv_src.update_on_launch is True + + result = run_module('tower_inventory_source', dict( + name='falsy-test', + inventory=base_inventory.name, + # source='ec2', + update_on_launch=False + ), admin_user) + + inv_src.refresh_from_db() + assert inv_src.update_on_launch is False + + +# Tests related to source-specific parameters +# +# We want to let the API return issues with "this doesn't support that", etc. +# +# GUI OPTIONS: +# - - - - - - - manual: file: scm: ec2: gce azure_rm vmware sat cloudforms openstack rhv tower custom +# credential ? ? o o r r r r r r r r o +# source_project ? ? r - - - - - - - - - - +# source_path ? ? r - - - - - - - - - - +# verbosity ? ? o o o o o o o o o o o +# overwrite ? ? o o o o o o o o o o o +# overwrite_vars ? ? o o o o o o o o o o o +# update_on_launch ? ? o o o o o o o o o o o +# UoPL ? ? o - - - - - - - - - - +# source_regions ? ? - o o o - - - - - - - +# instance_filters ? ? - o - - o - - - - o - +# group_by ? ? - o - - o - - - - - - +# source_vars* ? ? - o - o o o o o - - - +# environmet vars* ? ? o - - - - - - - - - o +# source_script ? ? - - - - - - - - - - r +# +# UoPL - update_on_project_launch +# * - source_vars are labeled environment_vars on project and custom sources + + +@pytest.mark.django_db +def test_missing_required_credential(run_module, admin_user, base_inventory): + result = run_module('tower_inventory_source', dict( + name='Test Azure Source', + inventory=base_inventory.name, + source='azure_rm', + state='present' + ), admin_user) + assert result.pop('failed', None) is True, result + + assert 'Credential is required for a cloud source' in result.get('msg', '') + + +@pytest.mark.django_db +def test_source_project_not_for_cloud(run_module, admin_user, base_inventory, project): + result = run_module('tower_inventory_source', dict( + name='Test ec2 Inventory Source', + inventory=base_inventory.name, + source='ec2', + state='present', + source_project=project.name + ), admin_user) + assert result.pop('failed', None) is True, result + + assert 'Cannot set source_project if not SCM type' in result.get('msg', '') + + +@pytest.mark.django_db +def test_source_path_not_for_cloud(run_module, admin_user, base_inventory): + result = run_module('tower_inventory_source', dict( + name='Test ec2 Inventory Source', + inventory=base_inventory.name, + source='ec2', + state='present', + source_path='where/am/I' + ), admin_user) + assert result.pop('failed', None) is True, result + + assert 'Cannot set source_path if not SCM type' in result.get('msg', '') + + +@pytest.mark.django_db +def test_scm_source_needs_project(run_module, admin_user, base_inventory): + result = run_module('tower_inventory_source', dict( + name='SCM inventory without project', + inventory=base_inventory.name, + state='present', + source='scm', + source_path='/var/lib/awx/example_source_path/' + ), admin_user) + assert result.pop('failed', None), result + + assert 'Project required for scm type sources' in result.get('msg', '') diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_job.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_job.py new file mode 100644 index 00000000..5e478d96 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_job.py @@ -0,0 +1,55 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest +from django.utils.timezone import now + +from awx.main.models import Job + + +@pytest.mark.django_db +def test_job_wait_successful(run_module, admin_user): + job = Job.objects.create(status='successful', started=now(), finished=now()) + result = run_module('tower_job_wait', dict( + job_id=job.id + ), admin_user) + result.pop('invocation', None) + assert result.pop('finished', '')[:10] == str(job.finished)[:10] + assert result.pop('started', '')[:10] == str(job.started)[:10] + assert result == { + "status": "successful", + "changed": False, + "elapsed": str(job.elapsed), + "id": job.id + } + + +@pytest.mark.django_db +def test_job_wait_failed(run_module, admin_user): + job = Job.objects.create(status='failed', started=now(), finished=now()) + result = run_module('tower_job_wait', dict( + job_id=job.id + ), admin_user) + result.pop('invocation', None) + assert result.pop('finished', '')[:10] == str(job.finished)[:10] + assert result.pop('started', '')[:10] == str(job.started)[:10] + assert result == { + "status": "failed", + "failed": True, + "changed": False, + "elapsed": str(job.elapsed), + "id": job.id, + "msg": "Job with id 1 failed" + } + + +@pytest.mark.django_db +def test_job_wait_not_found(run_module, admin_user): + result = run_module('tower_job_wait', dict( + job_id=42 + ), admin_user) + result.pop('invocation', None) + assert result == { + "failed": True, + "msg": "Unable to wait on job 42; that ID does not exist in Tower." + } diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_job_template.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_job_template.py new file mode 100644 index 00000000..4b480ed4 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_job_template.py @@ -0,0 +1,217 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.main.models import ActivityStream, JobTemplate, Job, NotificationTemplate + + +@pytest.mark.django_db +def test_create_job_template(run_module, admin_user, project, inventory): + + module_args = { + 'name': 'foo', 'playbook': 'helloworld.yml', + 'project': project.name, 'inventory': inventory.name, + 'extra_vars': {'foo': 'bar'}, + 'job_type': 'run', + 'state': 'present' + } + + result = run_module('tower_job_template', module_args, admin_user) + + jt = JobTemplate.objects.get(name='foo') + assert jt.extra_vars == '{"foo": "bar"}' + + assert result == { + "name": "foo", + "id": jt.id, + "changed": True, + "invocation": { + "module_args": module_args + } + } + + assert jt.project_id == project.id + assert jt.inventory_id == inventory.id + + +@pytest.mark.django_db +def test_job_launch_with_prompting(run_module, admin_user, project, inventory, machine_credential): + JobTemplate.objects.create( + name='foo', + project=project, + playbook='helloworld.yml', + ask_variables_on_launch=True, + ask_inventory_on_launch=True, + ask_credential_on_launch=True + ) + result = run_module('tower_job_launch', dict( + job_template='foo', + inventory=inventory.name, + credential=machine_credential.name, + extra_vars={"var1": "My First Variable", + "var2": "My Second Variable", + "var3": "My Third Variable" + } + ), admin_user) + assert result.pop('changed', None), result + + job = Job.objects.get(id=result['id']) + assert job.extra_vars == '{"var1": "My First Variable", "var2": "My Second Variable", "var3": "My Third Variable"}' + assert job.inventory == inventory + assert [cred.id for cred in job.credentials.all()] == [machine_credential.id] + + +@pytest.mark.django_db +def test_job_template_with_new_credentials( + run_module, admin_user, project, inventory, + machine_credential, vault_credential): + result = run_module('tower_job_template', dict( + name='foo', + playbook='helloworld.yml', + project=project.name, + inventory=inventory.name, + credentials=[machine_credential.name, vault_credential.name] + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed', False), result + jt = JobTemplate.objects.get(pk=result['id']) + + assert set([machine_credential.id, vault_credential.id]) == set([ + cred.pk for cred in jt.credentials.all()]) + + prior_ct = ActivityStream.objects.count() + result = run_module('tower_job_template', dict( + name='foo', + playbook='helloworld.yml', + project=project.name, + inventory=inventory.name, + credentials=[machine_credential.name, vault_credential.name] + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert not result.get('changed', True), result + jt.refresh_from_db() + assert result['id'] == jt.id + + assert set([machine_credential.id, vault_credential.id]) == set([ + cred.pk for cred in jt.credentials.all()]) + assert ActivityStream.objects.count() == prior_ct + + +@pytest.mark.django_db +def test_job_template_with_survey_spec(run_module, admin_user, project, inventory, survey_spec): + result = run_module('tower_job_template', dict( + name='foo', + playbook='helloworld.yml', + project=project.name, + inventory=inventory.name, + survey_spec=survey_spec, + survey_enabled=True + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed', False), result + jt = JobTemplate.objects.get(pk=result['id']) + + assert jt.survey_spec == survey_spec + + prior_ct = ActivityStream.objects.count() + result = run_module('tower_job_template', dict( + name='foo', + playbook='helloworld.yml', + project=project.name, + inventory=inventory.name, + survey_spec=survey_spec, + survey_enabled=True + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert not result.get('changed', True), result + jt.refresh_from_db() + assert result['id'] == jt.id + + assert jt.survey_spec == survey_spec + assert ActivityStream.objects.count() == prior_ct + + +@pytest.mark.django_db +def test_job_template_with_survey_encrypted_default(run_module, admin_user, project, inventory, silence_warning): + spec = { + "spec": [ + { + "index": 0, + "question_name": "my question?", + "default": "very_secret_value", + "variable": "myvar", + "type": "password", + "required": False + } + ], + "description": "test", + "name": "test" + } + for i in range(2): + result = run_module('tower_job_template', dict( + name='foo', + playbook='helloworld.yml', + project=project.name, + inventory=inventory.name, + survey_spec=spec, + survey_enabled=True + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + + assert result.get('changed', False), result # not actually desired, but assert for sanity + + silence_warning.assert_called_once_with( + "The field survey_spec of job_template {0} has encrypted data and " + "may inaccurately report task is changed.".format(result['id'])) + + +@pytest.mark.django_db +def test_associate_only_on_success(run_module, admin_user, organization, project): + jt = JobTemplate.objects.create( + name='foo', + project=project, + playbook='helloworld.yml', + ask_inventory_on_launch=True, + ) + create_kwargs = dict( + notification_configuration={ + 'url': 'http://www.example.com/hook', + 'headers': { + 'X-Custom-Header': 'value123' + }, + 'password': 'bar' + }, + notification_type='webhook', + organization=organization + ) + nt1 = NotificationTemplate.objects.create(name='nt1', **create_kwargs) + nt2 = NotificationTemplate.objects.create(name='nt2', **create_kwargs) + + jt.notification_templates_error.add(nt1) + + # test preservation of error NTs when success NTs are added + result = run_module('tower_job_template', dict( + name='foo', + playbook='helloworld.yml', + project=project.name, + notification_templates_success=['nt2'] + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed', True), result + + assert list(jt.notification_templates_success.values_list('id', flat=True)) == [nt2.id] + assert list(jt.notification_templates_error.values_list('id', flat=True)) == [nt1.id] + + # test removal to empty list + result = run_module('tower_job_template', dict( + name='foo', + playbook='helloworld.yml', + project=project.name, + notification_templates_success=[] + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed', True), result + + assert list(jt.notification_templates_success.values_list('id', flat=True)) == [] + assert list(jt.notification_templates_error.values_list('id', flat=True)) == [nt1.id] diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_label.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_label.py new file mode 100644 index 00000000..9ede40f3 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_label.py @@ -0,0 +1,47 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.main.models import Label + + +@pytest.mark.django_db +def test_create_label(run_module, admin_user, organization): + result = run_module('tower_label', dict( + name='test-label', + organization=organization.name + ), admin_user) + assert not result.get('failed'), result.get('msg', result) + assert result.get('changed', False) + + assert Label.objects.get(name='test-label').organization == organization + + +@pytest.mark.django_db +def test_create_label_using_org_id(run_module, admin_user, organization): + result = run_module('tower_label', dict( + name='test-label', + organization=organization.id + ), admin_user) + assert not result.get('failed'), result.get('msg', result) + assert result.get('changed', False) + + assert Label.objects.get(name='test-label').organization == organization + + +@pytest.mark.django_db +def test_modify_label(run_module, admin_user, organization): + label = Label.objects.create(name='test-label', organization=organization) + + result = run_module('tower_label', dict( + name='test-label', + new_name='renamed-label', + organization=organization.name + ), admin_user) + assert not result.get('failed'), result.get('msg', result) + assert result.get('changed', False) + + label.refresh_from_db() + assert label.organization == organization + assert label.name == 'renamed-label' diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_module_utils.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_module_utils.py new file mode 100644 index 00000000..e93b6ee9 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_module_utils.py @@ -0,0 +1,104 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import json +import sys + +from requests.models import Response +from unittest import mock + + +def getheader(self, header_name, default): + mock_headers = {'X-API-Product-Name': 'not-junk', 'X-API-Product-Version': '1.2.3'} + return mock_headers.get(header_name, default) + + +def read(self): + return json.dumps({}) + + +def status(self): + return 200 + + +def mock_ping_response(self, method, url, **kwargs): + r = Response() + r.getheader = getheader.__get__(r) + r.read = read.__get__(r) + r.status = status.__get__(r) + return r + + +def test_version_warning(collection_import, silence_warning): + TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule + cli_data = {'ANSIBLE_MODULE_ARGS': {}} + testargs = ['module_file2.py', json.dumps(cli_data)] + with mock.patch.object(sys, 'argv', testargs): + with mock.patch('ansible.module_utils.urls.Request.open', new=mock_ping_response): + my_module = TowerAPIModule(argument_spec=dict()) + my_module._COLLECTION_VERSION = "1.0.0" + my_module._COLLECTION_TYPE = "not-junk" + my_module.collection_to_version['not-junk'] = 'not-junk' + my_module.get_endpoint('ping') + silence_warning.assert_called_once_with( + 'You are running collection version 1.0.0 but connecting to tower version 1.2.3' + ) + + +def test_type_warning(collection_import, silence_warning): + TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule + cli_data = {'ANSIBLE_MODULE_ARGS': {}} + testargs = ['module_file2.py', json.dumps(cli_data)] + with mock.patch.object(sys, 'argv', testargs): + with mock.patch('ansible.module_utils.urls.Request.open', new=mock_ping_response): + my_module = TowerAPIModule(argument_spec={}) + my_module._COLLECTION_VERSION = "1.2.3" + my_module._COLLECTION_TYPE = "junk" + my_module.collection_to_version['junk'] = 'junk' + my_module.get_endpoint('ping') + silence_warning.assert_called_once_with( + 'You are using the junk version of this collection but connecting to not-junk' + ) + + +def test_duplicate_config(collection_import, silence_warning): + # imports done here because of PATH issues unique to this test suite + TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule + data = { + 'name': 'zigzoom', + 'zig': 'zoom', + 'tower_username': 'bob', + 'tower_config_file': 'my_config' + } + + with mock.patch.object(TowerAPIModule, 'load_config') as mock_load: + argument_spec = dict( + name=dict(required=True), + zig=dict(type='str'), + ) + TowerAPIModule(argument_spec=argument_spec, direct_params=data) + assert mock_load.mock_calls[-1] == mock.call('my_config') + + silence_warning.assert_called_once_with( + 'The parameter(s) tower_username were provided at the same time as ' + 'tower_config_file. Precedence may be unstable, ' + 'we suggest either using config file or params.' + ) + + +def test_no_templated_values(collection_import): + """This test corresponds to replacements done by + awx_collection/tools/roles/template_galaxy/tasks/main.yml + Those replacements should happen at build time, so they should not be + checked into source. + """ + TowerAPIModule = collection_import('plugins.module_utils.tower_api').TowerAPIModule + assert TowerAPIModule._COLLECTION_VERSION == "0.0.1-devel", ( + 'The collection version is templated when the collection is built ' + 'and the code should retain the placeholder of "0.0.1-devel".' + ) + InventoryModule = collection_import('plugins.inventory.tower').InventoryModule + assert InventoryModule.NAME == 'awx.awx.tower', ( + 'The inventory plugin FQCN is templated when the collection is built ' + 'and the code should retain the default of awx.awx.' + ) diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_notification.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_notification.py new file mode 100644 index 00000000..9d916d1d --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_notification.py @@ -0,0 +1,111 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.main.models import NotificationTemplate + + +def compare_with_encrypted(model_config, param_config): + '''Given a model_config from the database, assure that this is consistent + with the config given in the notification_configuration parameter + this requires handling of password fields + ''' + for key, model_val in model_config.items(): + param_val = param_config.get(key, 'missing') + if isinstance(model_val, str) and (model_val.startswith('$encrypted$') or param_val.startswith('$encrypted$')): + assert model_val.startswith('$encrypted$') # must be saved as encrypted + assert len(model_val) > len('$encrypted$') + else: + assert model_val == param_val, 'Config key {0} did not match, (model: {1}, input: {2})'.format( + key, model_val, param_val + ) + + +@pytest.mark.django_db +def test_create_modify_notification_template(run_module, admin_user, organization): + nt_config = { + 'username': 'user', + 'password': 'password', + 'sender': 'foo@invalid.com', + 'recipients': ['foo2@invalid.com'], + 'host': 'smtp.example.com', + 'port': 25, + 'use_tls': False, 'use_ssl': False, + 'timeout': 4 + } + result = run_module('tower_notification', dict( + name='foo-notification-template', + organization=organization.name, + notification_type='email', + notification_configuration=nt_config, + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.pop('changed', None), result + + nt = NotificationTemplate.objects.get(id=result['id']) + compare_with_encrypted(nt.notification_configuration, nt_config) + assert nt.organization == organization + + # Test no-op, this is impossible if the notification_configuration is given + # because we cannot determine if password fields changed + result = run_module('tower_notification', dict( + name='foo-notification-template', + organization=organization.name, + notification_type='email', + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert not result.pop('changed', None), result + + # Test a change in the configuration + nt_config['timeout'] = 12 + result = run_module('tower_notification', dict( + name='foo-notification-template', + organization=organization.name, + notification_type='email', + notification_configuration=nt_config, + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.pop('changed', None), result + + nt.refresh_from_db() + compare_with_encrypted(nt.notification_configuration, nt_config) + + +@pytest.mark.django_db +def test_invalid_notification_configuration(run_module, admin_user, organization): + result = run_module('tower_notification', dict( + name='foo-notification-template', + organization=organization.name, + notification_type='email', + notification_configuration={}, + ), admin_user) + assert result.get('failed', False), result.get('msg', result) + assert 'Missing required fields for Notification Configuration' in result['msg'] + + +@pytest.mark.django_db +def test_deprecated_to_modern_no_op(run_module, admin_user, organization): + nt_config = { + 'url': 'http://www.example.com/hook', + 'headers': { + 'X-Custom-Header': 'value123' + } + } + result = run_module('tower_notification', dict( + name='foo-notification-template', + organization=organization.name, + notification_type='webhook', + notification_configuration=nt_config, + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.pop('changed', None), result + + result = run_module('tower_notification', dict( + name='foo-notification-template', + organization=organization.name, + notification_type='webhook', + notification_configuration=nt_config, + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert not result.pop('changed', None), result diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_organization.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_organization.py new file mode 100644 index 00000000..8f4872c3 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_organization.py @@ -0,0 +1,60 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.main.models import Organization + + +@pytest.mark.django_db +def test_create_organization(run_module, admin_user): + + module_args = { + 'name': 'foo', + 'description': 'barfoo', + 'state': 'present', + 'max_hosts': '0', + 'tower_host': None, + 'tower_username': None, + 'tower_password': None, + 'validate_certs': None, + 'tower_oauthtoken': None, + 'tower_config_file': None, + 'custom_virtualenv': None + } + + result = run_module('tower_organization', module_args, admin_user) + assert result.get('changed'), result + + org = Organization.objects.get(name='foo') + assert result == { + "name": "foo", + "changed": True, + "id": org.id, + "invocation": { + "module_args": module_args + } + } + + assert org.description == 'barfoo' + + +@pytest.mark.django_db +def test_create_organization_with_venv(run_module, admin_user, mocker): + path = '/var/lib/awx/venv/custom-venv/foobar13489435/' + with mocker.patch('awx.main.models.mixins.get_custom_venv_choices', return_value=[path]): + result = run_module('tower_organization', { + 'name': 'foo', + 'custom_virtualenv': path, + 'state': 'present' + }, admin_user) + assert result.pop('changed'), result + + org = Organization.objects.get(name='foo') + result.pop('invocation') + assert result == { + "name": "foo", + "id": org.id + } + + assert org.custom_virtualenv == path diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_project.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_project.py new file mode 100644 index 00000000..9ef1596d --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_project.py @@ -0,0 +1,33 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.main.models import Project + + +@pytest.mark.django_db +def test_create_project(run_module, admin_user, organization, silence_warning): + result = run_module('tower_project', dict( + name='foo', + organization=organization.name, + scm_type='git', + scm_url='https://foo.invalid', + wait=False, + scm_update_cache_timeout=5 + ), admin_user) + silence_warning.assert_called_once_with( + 'scm_update_cache_timeout will be ignored since scm_update_on_launch ' + 'was not set to true') + + assert result.pop('changed', None), result + + proj = Project.objects.get(name='foo') + assert proj.scm_url == 'https://foo.invalid' + assert proj.organization == organization + + result.pop('invocation') + assert result == { + 'name': 'foo', + 'id': proj.id + } diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_role.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_role.py new file mode 100644 index 00000000..a97bdb76 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_role.py @@ -0,0 +1,64 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.main.models import WorkflowJobTemplate, User + + +@pytest.mark.django_db +@pytest.mark.parametrize('state', ('present', 'absent')) +def test_grant_organization_permission(run_module, admin_user, organization, state): + rando = User.objects.create(username='rando') + if state == 'absent': + organization.admin_role.members.add(rando) + + result = run_module('tower_role', { + 'user': rando.username, + 'organization': organization.name, + 'role': 'admin', + 'state': state + }, admin_user) + assert not result.get('failed', False), result.get('msg', result) + + if state == 'present': + assert rando in organization.execute_role + else: + assert rando not in organization.execute_role + + +@pytest.mark.django_db +@pytest.mark.parametrize('state', ('present', 'absent')) +def test_grant_workflow_permission(run_module, admin_user, organization, state): + wfjt = WorkflowJobTemplate.objects.create(organization=organization, name='foo-workflow') + rando = User.objects.create(username='rando') + if state == 'absent': + wfjt.execute_role.members.add(rando) + + result = run_module('tower_role', { + 'user': rando.username, + 'workflow': wfjt.name, + 'role': 'execute', + 'state': state + }, admin_user) + assert not result.get('failed', False), result.get('msg', result) + + if state == 'present': + assert rando in wfjt.execute_role + else: + assert rando not in wfjt.execute_role + + +@pytest.mark.django_db +def test_invalid_role(run_module, admin_user, project): + rando = User.objects.create(username='rando') + result = run_module('tower_role', { + 'user': rando.username, + 'project': project.name, + 'role': 'adhoc', + 'state': 'present' + }, admin_user) + assert result.get('failed', False) + msg = result.get('msg') + assert 'has no role adhoc_role' in msg + assert 'available roles: admin_role, use_role, update_role, read_role' in msg diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_schedule.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_schedule.py new file mode 100644 index 00000000..7a58892d --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_schedule.py @@ -0,0 +1,107 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from ansible.errors import AnsibleError + +from awx.main.models import Schedule +from awx.api.serializers import SchedulePreviewSerializer + + +@pytest.mark.django_db +def test_create_schedule(run_module, job_template, admin_user): + my_rrule = 'DTSTART;TZID=Zulu:20200416T034507 RRULE:FREQ=MONTHLY;INTERVAL=1' + result = run_module('tower_schedule', { + 'name': 'foo_schedule', + 'unified_job_template': job_template.name, + 'rrule': my_rrule + }, admin_user) + assert not result.get('failed', False), result.get('msg', result) + + schedule = Schedule.objects.filter(name='foo_schedule').first() + + assert result['id'] == schedule.id + assert result['changed'] + + assert schedule.rrule == my_rrule + + +@pytest.mark.parametrize("freq, kwargs, expect", [ + # Test with a valid start date (no time) (also tests none frequency and count) + ('none', {'start_date': '2020-04-16'}, 'DTSTART;TZID=America/New_York:20200416T000000 RRULE:FREQ=DAILY;COUNT=1;INTERVAL=1'), + # Test with a valid start date and time + ('none', {'start_date': '2020-04-16 03:45:07'}, 'DTSTART;TZID=America/New_York:20200416T034507 RRULE:FREQ=DAILY;COUNT=1;INTERVAL=1'), + # Test end_on as count (also integration test) + ('minute', {'start_date': '2020-4-16 03:45:07', 'end_on': '2'}, 'DTSTART;TZID=America/New_York:20200416T034507 RRULE:FREQ=MINUTELY;COUNT=2;INTERVAL=1'), + # Test end_on as date + ('minute', {'start_date': '2020-4-16 03:45:07', 'end_on': '2020-4-17 03:45:07'}, + 'DTSTART;TZID=America/New_York:20200416T034507 RRULE:FREQ=MINUTELY;UNTIL=20200417T034507;INTERVAL=1'), + # Test on_days as a single day + ('week', {'start_date': '2020-4-16 03:45:07', 'on_days': 'saturday'}, + 'DTSTART;TZID=America/New_York:20200416T034507 RRULE:FREQ=WEEKLY;BYDAY=SA;INTERVAL=1'), + # Test on_days as multiple days (with some whitespaces) + ('week', {'start_date': '2020-4-16 03:45:07', 'on_days': 'saturday,monday , friday'}, + 'DTSTART;TZID=America/New_York:20200416T034507 RRULE:FREQ=WEEKLY;BYDAY=MO,FR,SA;INTERVAL=1'), + # Test valid month_day_number + ('month', {'start_date': '2020-4-16 03:45:07', 'month_day_number': '18'}, + 'DTSTART;TZID=America/New_York:20200416T034507 RRULE:FREQ=MONTHLY;BYMONTHDAY=18;INTERVAL=1'), + # Test a valid on_the + ('month', {'start_date': '2020-4-16 03:45:07', 'on_the': 'second sunday'}, + 'DTSTART;TZID=America/New_York:20200416T034507 RRULE:FREQ=MONTHLY;BYSETPOS=2;BYDAY=SU;INTERVAL=1'), + # Test an valid timezone + ('month', {'start_date': '2020-4-16 03:45:07', 'timezone': 'Zulu'}, + 'DTSTART;TZID=Zulu:20200416T034507 RRULE:FREQ=MONTHLY;INTERVAL=1'), +]) +def test_rrule_lookup_plugin(collection_import, freq, kwargs, expect): + LookupModule = collection_import('plugins.lookup.tower_schedule_rrule').LookupModule + generated_rule = LookupModule.get_rrule(freq, kwargs) + assert generated_rule == expect + rrule_checker = SchedulePreviewSerializer() + # Try to run our generated rrule through the awx validator + # This will raise its own exception on failure + rrule_checker.validate_rrule(generated_rule) + + +@pytest.mark.parametrize("freq", ('none', 'minute', 'hour', 'day', 'week', 'month')) +def test_empty_schedule_rrule(collection_import, freq): + LookupModule = collection_import('plugins.lookup.tower_schedule_rrule').LookupModule + if freq == 'day': + pfreq = 'DAILY' + elif freq == 'none': + pfreq = 'DAILY;COUNT=1' + else: + pfreq = freq.upper() + 'LY' + assert LookupModule.get_rrule(freq, {}).endswith(' RRULE:FREQ={0};INTERVAL=1'.format(pfreq)) + + +@pytest.mark.parametrize("freq, kwargs, msg", [ + # Test end_on as junk + ('minute', {'start_date': '2020-4-16 03:45:07', 'end_on': 'junk'}, + 'Parameter end_on must either be an integer or in the format YYYY-MM-DD'), + # Test on_days as junk + ('week', {'start_date': '2020-4-16 03:45:07', 'on_days': 'junk'}, + 'Parameter on_days must only contain values monday, tuesday, wednesday, thursday, friday, saturday, sunday'), + # Test combo of both month_day_number and on_the + ('month', dict(start_date='2020-4-16 03:45:07', on_the='something', month_day_number='else'), + "Month based frequencies can have month_day_number or on_the but not both"), + # Test month_day_number as not an integer + ('month', dict(start_date='2020-4-16 03:45:07', month_day_number='junk'), "month_day_number must be between 1 and 31"), + # Test month_day_number < 1 + ('month', dict(start_date='2020-4-16 03:45:07', month_day_number='0'), "month_day_number must be between 1 and 31"), + # Test month_day_number > 31 + ('month', dict(start_date='2020-4-16 03:45:07', month_day_number='32'), "month_day_number must be between 1 and 31"), + # Test on_the as junk + ('month', dict(start_date='2020-4-16 03:45:07', on_the='junk'), "on_the parameter must be two words separated by a space"), + # Test on_the with invalid occurance + ('month', dict(start_date='2020-4-16 03:45:07', on_the='junk wednesday'), "The first string of the on_the parameter is not valid"), + # Test on_the with invalid weekday + ('month', dict(start_date='2020-4-16 03:45:07', on_the='second junk'), "Weekday portion of on_the parameter is not valid"), + # Test an invalid timezone + ('month', dict(start_date='2020-4-16 03:45:07', timezone='junk'), 'Timezone parameter is not valid'), +]) +def test_rrule_lookup_plugin_failure(collection_import, freq, kwargs, msg): + LookupModule = collection_import('plugins.lookup.tower_schedule_rrule').LookupModule + with pytest.raises(AnsibleError) as e: + assert LookupModule.get_rrule(freq, kwargs) + assert msg in str(e.value) diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_send_receive.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_send_receive.py new file mode 100644 index 00000000..14f3c894 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_send_receive.py @@ -0,0 +1,76 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest +import json + +from awx.main.models import ( + Organization, + Project, + Inventory, + Host, + CredentialType, + Credential, + JobTemplate +) + + +# warns based on password_management param, but not security issue +@pytest.mark.django_db +def test_receive_send_jt(run_module, admin_user, mocker, silence_deprecation): + org = Organization.objects.create(name='SRtest') + proj = Project.objects.create( + name='SRtest', + playbook_files=['debug.yml'], + scm_type='git', + scm_url='https://github.com/ansible/test-playbooks.git', + organization=org, + allow_override=True # so we do not require playbooks populated + ) + inv = Inventory.objects.create(name='SRtest', organization=org) + Host.objects.create(name='SRtest', inventory=inv) + ct = CredentialType.defaults['ssh']() + ct.save() + cred = Credential.objects.create( + name='SRtest', + credential_type=ct, + organization=org + ) + jt = JobTemplate.objects.create( + name='SRtest', + project=proj, + inventory=inv, + playbook='helloworld.yml' + ) + jt.credentials.add(cred) + jt.admin_role.members.add(admin_user) # work around send/receive bug + + # receive everything + result = run_module('tower_receive', dict(all=True), admin_user) + + assert 'assets' in result, result + assets = result['assets'] + assert not result.get('changed', True) + assert set(a['asset_type'] for a in assets) == set(( + 'organization', 'inventory', 'job_template', 'credential', 'project', + 'user' + )) + + # delete everything + for obj in (jt, inv, proj, cred, org): + obj.delete() + + def fake_wait(self, pk, parent_pk=None, **kwargs): + return {"changed": True} + + # recreate everything + with mocker.patch('sys.stdin.isatty', return_value=True): + with mocker.patch('tower_cli.models.base.MonitorableResource.wait'): + result = run_module('tower_send', dict(assets=json.dumps(assets)), admin_user) + + assert not result.get('failed'), result + + new = JobTemplate.objects.get(name='SRtest') + assert new.project.name == 'SRtest' + assert new.inventory.name == 'SRtest' + assert [cred.name for cred in new.credentials.all()] == ['SRtest'] diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_settings.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_settings.py new file mode 100644 index 00000000..e39d7eaa --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_settings.py @@ -0,0 +1,67 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.conf.models import Setting + + +@pytest.mark.django_db +def test_setting_flat_value(run_module, admin_user): + the_value = 'CN=service_account,OU=ServiceAccounts,DC=domain,DC=company,DC=org' + result = run_module('tower_settings', dict( + name='AUTH_LDAP_BIND_DN', + value=the_value + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result + + assert Setting.objects.get(key='AUTH_LDAP_BIND_DN').value == the_value + + +@pytest.mark.django_db +def test_setting_dict_value(run_module, admin_user): + the_value = { + 'email': 'mail', + 'first_name': 'givenName', + 'last_name': 'surname' + } + result = run_module('tower_settings', dict( + name='AUTH_LDAP_USER_ATTR_MAP', + value=the_value + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result + + assert Setting.objects.get(key='AUTH_LDAP_USER_ATTR_MAP').value == the_value + + +@pytest.mark.django_db +def test_setting_nested_type(run_module, admin_user): + the_value = { + 'email': 'mail', + 'first_name': 'givenName', + 'last_name': 'surname' + } + result = run_module('tower_settings', dict( + settings={ + 'AUTH_LDAP_USER_ATTR_MAP': the_value + } + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result + + assert Setting.objects.get(key='AUTH_LDAP_USER_ATTR_MAP').value == the_value + + +@pytest.mark.django_db +def test_setting_bool_value(run_module, admin_user): + for the_value in (True, False): + result = run_module('tower_settings', dict( + name='ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC', + value=the_value + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result + + assert Setting.objects.get(key='ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC').value is the_value diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_team.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_team.py new file mode 100644 index 00000000..ccc164dc --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_team.py @@ -0,0 +1,66 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.main.models import Organization, Team + + +@pytest.mark.django_db +def test_create_team(run_module, admin_user): + org = Organization.objects.create(name='foo') + + result = run_module('tower_team', { + 'name': 'foo_team', + 'description': 'fooin around', + 'state': 'present', + 'organization': 'foo' + }, admin_user) + + team = Team.objects.filter(name='foo_team').first() + + result.pop('invocation') + assert result == { + "changed": True, + "name": "foo_team", + "id": team.id if team else None, + } + team = Team.objects.get(name='foo_team') + assert team.description == 'fooin around' + assert team.organization_id == org.id + + +@pytest.mark.django_db +def test_modify_team(run_module, admin_user): + org = Organization.objects.create(name='foo') + team = Team.objects.create( + name='foo_team', + organization=org, + description='flat foo' + ) + assert team.description == 'flat foo' + + result = run_module('tower_team', { + 'name': 'foo_team', + 'description': 'fooin around', + 'organization': 'foo' + }, admin_user) + team.refresh_from_db() + result.pop('invocation') + assert result == { + "changed": True, + "id": team.id, + } + assert team.description == 'fooin around' + + # 2nd modification, should cause no change + result = run_module('tower_team', { + 'name': 'foo_team', + 'description': 'fooin around', + 'organization': 'foo' + }, admin_user) + result.pop('invocation') + assert result == { + "id": team.id, + "changed": False + } diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_token.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_token.py new file mode 100644 index 00000000..442fa2e9 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_token.py @@ -0,0 +1,29 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.main.models import OAuth2AccessToken + + +@pytest.mark.django_db +def test_create_token(run_module, admin_user): + + module_args = { + 'description': 'barfoo', + 'state': 'present', + 'scope': 'read', + 'tower_host': None, + 'tower_username': None, + 'tower_password': None, + 'validate_certs': None, + 'tower_oauthtoken': None, + 'tower_config_file': None, + } + + result = run_module('tower_token', module_args, admin_user) + assert result.get('changed'), result + + tokens = OAuth2AccessToken.objects.filter(description='barfoo') + assert len(tokens) == 1, 'Tokens with description of barfoo != 0: {0}'.format(len(tokens)) + assert tokens[0].scope == 'read', 'Token was not given read access' diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_user.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_user.py new file mode 100644 index 00000000..db705bd5 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_user.py @@ -0,0 +1,46 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from unittest import mock + +from awx.main.models import User + + +@pytest.fixture +def mock_auth_stuff(): + """Some really specific session-related stuff is done for changing or setting + passwords, so we will just avoid that here. + """ + with mock.patch('awx.api.serializers.update_session_auth_hash'): + yield + + +@pytest.mark.django_db +def test_create_user(run_module, admin_user, mock_auth_stuff): + result = run_module('tower_user', dict( + username='Bob', + password='pass4word' + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result + + user = User.objects.get(id=result['id']) + assert user.username == 'Bob' + + +@pytest.mark.django_db +def test_password_no_op_warning(run_module, admin_user, mock_auth_stuff, silence_warning): + for i in range(2): + result = run_module('tower_user', dict( + username='Bob', + password='pass4word' + ), admin_user) + assert not result.get('failed', False), result.get('msg', result) + + assert result.get('changed') # not actually desired, but assert for sanity + + silence_warning.assert_called_once_with( + "The field password of user {0} has encrypted data and " + "may inaccurately report task is changed.".format(result['id'])) diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_workflow_job_template.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_workflow_job_template.py new file mode 100644 index 00000000..bc2a44b1 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_workflow_job_template.py @@ -0,0 +1,146 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.main.models import WorkflowJobTemplate, NotificationTemplate + + +@pytest.mark.django_db +def test_create_workflow_job_template(run_module, admin_user, organization, survey_spec): + result = run_module('tower_workflow_job_template', { + 'name': 'foo-workflow', + 'organization': organization.name, + 'extra_vars': {'foo': 'bar', 'another-foo': {'barz': 'bar2'}}, + 'survey': survey_spec, + 'survey_enabled': True, + 'state': 'present' + }, admin_user) + assert not result.get('failed', False), result.get('msg', result) + + wfjt = WorkflowJobTemplate.objects.get(name='foo-workflow') + assert wfjt.extra_vars == '{"foo": "bar", "another-foo": {"barz": "bar2"}}' + + result.pop('invocation', None) + assert result == {"name": "foo-workflow", "id": wfjt.id, "changed": True} + + assert wfjt.organization_id == organization.id + assert wfjt.survey_spec == survey_spec + + +@pytest.mark.django_db +def test_create_modify_no_survey(run_module, admin_user, organization, survey_spec): + result = run_module('tower_workflow_job_template', { + 'name': 'foo-workflow', + 'organization': organization.name + }, admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed', False), result + + wfjt = WorkflowJobTemplate.objects.get(name='foo-workflow') + assert wfjt.organization_id == organization.id + assert wfjt.survey_spec == {} + result.pop('invocation', None) + assert result == {"name": "foo-workflow", "id": wfjt.id, "changed": True} + + result = run_module('tower_workflow_job_template', { + 'name': 'foo-workflow', + 'organization': organization.name + }, admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert not result.get('changed', True), result + + +@pytest.mark.django_db +def test_survey_spec_only_changed(run_module, admin_user, organization, survey_spec): + wfjt = WorkflowJobTemplate.objects.create( + organization=organization, name='foo-workflow', + survey_enabled=True, survey_spec=survey_spec + ) + result = run_module('tower_workflow_job_template', { + 'name': 'foo-workflow', + 'organization': organization.name, + 'state': 'present' + }, admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert not result.get('changed', True), result + wfjt.refresh_from_db() + assert wfjt.survey_spec == survey_spec + + survey_spec['description'] = 'changed description' + + result = run_module('tower_workflow_job_template', { + 'name': 'foo-workflow', + 'organization': organization.name, + 'survey': survey_spec, + 'state': 'present' + }, admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed', True), result + wfjt.refresh_from_db() + assert wfjt.survey_spec == survey_spec + + +@pytest.mark.django_db +def test_associate_only_on_success(run_module, admin_user, organization, project): + wfjt = WorkflowJobTemplate.objects.create( + organization=organization, name='foo-workflow', + # survey_enabled=True, survey_spec=survey_spec + ) + create_kwargs = dict( + notification_configuration={ + 'url': 'http://www.example.com/hook', + 'headers': { + 'X-Custom-Header': 'value123' + }, + 'password': 'bar' + }, + notification_type='webhook', + organization=organization + ) + nt1 = NotificationTemplate.objects.create(name='nt1', **create_kwargs) + nt2 = NotificationTemplate.objects.create(name='nt2', **create_kwargs) + + wfjt.notification_templates_error.add(nt1) + + # test preservation of error NTs when success NTs are added + result = run_module('tower_workflow_job_template', { + 'name': 'foo-workflow', + 'organization': organization.name, + 'notification_templates_success': ['nt2'] + }, admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed', True), result + + assert list(wfjt.notification_templates_success.values_list('id', flat=True)) == [nt2.id] + assert list(wfjt.notification_templates_error.values_list('id', flat=True)) == [nt1.id] + + # test removal to empty list + result = run_module('tower_workflow_job_template', { + 'name': 'foo-workflow', + 'organization': organization.name, + 'notification_templates_success': [] + }, admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed', True), result + + assert list(wfjt.notification_templates_success.values_list('id', flat=True)) == [] + assert list(wfjt.notification_templates_error.values_list('id', flat=True)) == [nt1.id] + + +@pytest.mark.django_db +def test_delete_with_spec(run_module, admin_user, organization, survey_spec): + WorkflowJobTemplate.objects.create( + organization=organization, name='foo-workflow', + survey_enabled=True, survey_spec=survey_spec + ) + result = run_module('tower_workflow_job_template', { + 'name': 'foo-workflow', + 'organization': organization.name, + 'state': 'absent' + }, admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed', True), result + + assert WorkflowJobTemplate.objects.filter( + name='foo-workflow', organization=organization).count() == 0 diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_workflow_job_template_node.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_workflow_job_template_node.py new file mode 100644 index 00000000..935b0154 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_workflow_job_template_node.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.main.models import WorkflowJobTemplateNode, WorkflowJobTemplate, JobTemplate + + +@pytest.fixture +def job_template(project, inventory): + return JobTemplate.objects.create( + project=project, + inventory=inventory, + playbook='helloworld.yml', + name='foo-jt', + ask_variables_on_launch=True, + ask_credential_on_launch=True, + ask_limit_on_launch=True + ) + + +@pytest.fixture +def wfjt(organization): + WorkflowJobTemplate.objects.create(organization=None, name='foo-workflow') # to test org scoping + return WorkflowJobTemplate.objects.create(organization=organization, name='foo-workflow') + + +@pytest.mark.django_db +def test_create_workflow_job_template_node(run_module, admin_user, wfjt, job_template): + this_identifier = '42🐉' + result = run_module('tower_workflow_job_template_node', { + 'identifier': this_identifier, + 'workflow_job_template': 'foo-workflow', + 'organization': wfjt.organization.name, + 'unified_job_template': 'foo-jt', + 'state': 'present' + }, admin_user) + assert not result.get('failed', False), result.get('msg', result) + + node = WorkflowJobTemplateNode.objects.get(identifier=this_identifier) + + result.pop('invocation', None) + assert result == { + "name": this_identifier, # FIXME: should this be identifier instead + "id": node.id, + "changed": True + } + + assert node.identifier == this_identifier + assert node.workflow_job_template_id == wfjt.id + assert node.unified_job_template_id == job_template.id + + +@pytest.mark.django_db +def test_create_workflow_job_template_node_no_template(run_module, admin_user, wfjt, job_template): + """This is a part of the API contract for creating approval nodes + and at some point in the future, tha feature will be supported by the collection + """ + this_identifier = '42🐉' + result = run_module('tower_workflow_job_template_node', { + 'identifier': this_identifier, + 'workflow_job_template': wfjt.name, + 'organization': wfjt.organization.name, + }, admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed', False), result + + node = WorkflowJobTemplateNode.objects.get(pk=result['id']) + # node = WorkflowJobTemplateNode.objects.first() + + assert result['id'] == node.id + + assert node.identifier == this_identifier + assert node.workflow_job_template_id == wfjt.id + assert node.unified_job_template_id is None + + +@pytest.mark.django_db +def test_make_use_of_prompts(run_module, admin_user, wfjt, job_template, machine_credential, vault_credential): + result = run_module('tower_workflow_job_template_node', { + 'identifier': '42', + 'workflow_job_template': 'foo-workflow', + 'organization': wfjt.organization.name, + 'unified_job_template': 'foo-jt', + 'extra_data': {'foo': 'bar', 'another-foo': {'barz': 'bar2'}}, + 'limit': 'foo_hosts', + 'credentials': [machine_credential.name, vault_credential.name], + 'state': 'present' + }, admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed', False) + + node = WorkflowJobTemplateNode.objects.get(identifier='42') + + assert node.limit == 'foo_hosts' + assert node.extra_data == {'foo': 'bar', 'another-foo': {'barz': 'bar2'}} + assert set(node.credentials.all()) == set([machine_credential, vault_credential]) + + +@pytest.mark.django_db +def test_create_with_edges(run_module, admin_user, wfjt, job_template): + next_nodes = [ + WorkflowJobTemplateNode.objects.create( + identifier='foo{0}'.format(i), + workflow_job_template=wfjt, + unified_job_template=job_template + ) for i in range(3) + ] + + result = run_module('tower_workflow_job_template_node', { + 'identifier': '42', + 'workflow_job_template': 'foo-workflow', + 'organization': wfjt.organization.name, + 'unified_job_template': 'foo-jt', + 'success_nodes': ['foo0'], + 'always_nodes': ['foo1'], + 'failure_nodes': ['foo2'], + 'state': 'present' + }, admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed', False) + + node = WorkflowJobTemplateNode.objects.get(identifier='42') + + assert list(node.success_nodes.all()) == [next_nodes[0]] + assert list(node.always_nodes.all()) == [next_nodes[1]] + assert list(node.failure_nodes.all()) == [next_nodes[2]] diff --git a/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_workflow_template.py b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_workflow_template.py new file mode 100644 index 00000000..c8b401ae --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/test/awx/test_workflow_template.py @@ -0,0 +1,130 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.main.models import ( + WorkflowJobTemplate, JobTemplate, Project, InventorySource, + Inventory, WorkflowJobTemplateNode +) + + +@pytest.mark.django_db +def test_create_workflow_job_template(run_module, admin_user, organization, survey_spec, silence_deprecation): + result = run_module('tower_workflow_template', { + 'name': 'foo-workflow', + 'organization': organization.name, + 'extra_vars': {'foo': 'bar', 'another-foo': {'barz': 'bar2'}}, + 'survey': survey_spec, + 'survey_enabled': True, + 'state': 'present' + }, admin_user) + + wfjt = WorkflowJobTemplate.objects.get(name='foo-workflow') + assert wfjt.extra_vars == '{"foo": "bar", "another-foo": {"barz": "bar2"}}' + + result.pop('invocation', None) + assert result == { + "workflow_template": "foo-workflow", # TODO: remove after refactor + "state": "present", + "id": wfjt.id, + "changed": True + } + + assert wfjt.organization_id == organization.id + assert wfjt.survey_spec == survey_spec + + +@pytest.mark.django_db +def test_with_nested_workflow(run_module, admin_user, organization, silence_deprecation): + wfjt1 = WorkflowJobTemplate.objects.create(name='first', organization=organization) + + result = run_module('tower_workflow_template', { + 'name': 'foo-workflow', + 'organization': organization.name, + 'schema': [ + {'workflow': wfjt1.name} + ], + 'state': 'present' + }, admin_user) + assert not result.get('failed', False), result.get('msg', result) + + wfjt = WorkflowJobTemplate.objects.get(name='foo-workflow') + node = wfjt.workflow_nodes.first() + assert node is not None + assert node.unified_job_template == wfjt1 + + +@pytest.mark.django_db +def test_schema_with_branches(run_module, admin_user, organization, silence_deprecation): + + proj = Project.objects.create(organization=organization, name='Ansible Examples') + inv = Inventory.objects.create(organization=organization, name='test-inv') + jt = JobTemplate.objects.create( + project=proj, + playbook='helloworld.yml', + inventory=inv, + name='Hello world' + ) + inv_src = InventorySource.objects.create( + inventory=inv, + name='AWS servers', + source='ec2' + ) + + result = run_module('tower_workflow_template', { + 'name': 'foo-workflow', + 'organization': organization.name, + 'schema': [ + { + 'job_template': 'Hello world', + 'failure': [ + { + 'inventory_source': 'AWS servers', + 'success': [ + { + 'project': 'Ansible Examples', + 'always': [ + { + 'job_template': "Hello world" + } + ] + } + ] + } + ] + } + ], + 'state': 'present' + }, admin_user) + assert not result.get('failed', False), result.get('msg', result) + + wfjt = WorkflowJobTemplate.objects.get(name='foo-workflow') + root_nodes = wfjt.workflow_nodes.filter(**{ + '%ss_success__isnull' % WorkflowJobTemplateNode.__name__.lower(): True, + '%ss_failure__isnull' % WorkflowJobTemplateNode.__name__.lower(): True, + '%ss_always__isnull' % WorkflowJobTemplateNode.__name__.lower(): True, + }) + assert len(root_nodes) == 1 + node = root_nodes[0] + assert node.unified_job_template == jt + second = node.failure_nodes.first() + assert second.unified_job_template == inv_src + third = second.success_nodes.first() + assert third.unified_job_template == proj + fourth = third.always_nodes.first() + assert fourth.unified_job_template == jt + + +@pytest.mark.django_db +def test_with_missing_ujt(run_module, admin_user, organization, silence_deprecation): + result = run_module('tower_workflow_template', { + 'name': 'foo-workflow', + 'organization': organization.name, + 'schema': [ + {'foo': 'bar'} + ], + 'state': 'present' + }, admin_user) + assert result.get('failed', False), result + assert 'You should provide exactly one of the attributes job_template,' in result['msg'] diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/demo_data/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/demo_data/tasks/main.yml new file mode 100644 index 00000000..800afda5 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/demo_data/tasks/main.yml @@ -0,0 +1,33 @@ +--- +- name: Assure that default organization exists + tower_organization: + name: Default + +- name: Assure that demo project exists + tower_project: + name: "Demo Project" + scm_type: 'git' + scm_url: 'https://github.com/ansible/ansible-tower-samples' + scm_update_on_launch: true + organization: Default + +- name: Assure that demo inventory exists + tower_inventory: + name: "Demo Inventory" + organization: Default + +- name: Create a Host + tower_host: + name: "localhost" + inventory: "Demo Inventory" + state: present + variables: + ansible_connection: local + register: result + +- name: Assure that demo job template exists + tower_job_template: + name: "Demo Job Template" + project: "Demo Project" + inventory: "Demo Inventory" + playbook: "hello_world.yml" diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_credential/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_credential/tasks/main.yml new file mode 100644 index 00000000..7c0f4b08 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_credential/tasks/main.yml @@ -0,0 +1,754 @@ +--- +- name: Generate a random string for test + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + +- name: Generate names + set_fact: + ssh_cred_name1: "AWX-Collection-tests-tower_credential-ssh-cred1-{{ test_id }}" + ssh_cred_name2: "AWX-Collection-tests-tower_credential-ssh-cred2-{{ test_id }}" + ssh_cred_name3: "AWX-Collection-tests-tower_credential-ssh-cred-lookup-source-{{ test_id }}" + ssh_cred_name4: "AWX-Collection-tests-tower_credential-ssh-cred-file-source-{{ test_id }}" + vault_cred_name1: "AWX-Collection-tests-tower_credential-vault-cred1-{{ test_id }}" + vault_cred_name2: "AWX-Collection-tests-tower_credential-vault-ssh-cred1-{{ test_id }}" + net_cred_name1: "AWX-Collection-tests-tower_credential-net-cred1-{{ test_id }}" + scm_cred_name1: "AWX-Collection-tests-tower_credential-scm-cred1-{{ test_id }}" + aws_cred_name1: "AWX-Collection-tests-tower_credential-aws-cred1-{{ test_id }}" + vmware_cred_name1: "AWX-Collection-tests-tower_credential-vmware-cred1-{{ test_id }}" + sat6_cred_name1: "AWX-Collection-tests-tower_credential-sat6-cred1-{{ test_id }}" + cf_cred_name1: "AWX-Collection-tests-tower_credential-cf-cred1-{{ test_id }}" + gce_cred_name1: "AWX-Collection-tests-tower_credential-gce-cred1-{{ test_id }}" + azurerm_cred_name1: "AWX-Collection-tests-tower_credential-azurerm-cred1-{{ test_id }}" + openstack_cred_name1: "AWX-Collection-tests-tower_credential-openstack-cred1-{{ test_id }}" + rhv_cred_name1: "AWX-Collection-tests-tower_credential-rhv-cred1-{{ test_id }}" + insights_cred_name1: "AWX-Collection-tests-tower_credential-insights-cred1-{{ test_id }}" + tower_cred_name1: "AWX-Collection-tests-tower_credential-tower-cred1-{{ test_id }}" + +- name: create a tempdir for an SSH key + local_action: shell mktemp -d + register: tempdir + +- name: Generate a local SSH key + local_action: "shell ssh-keygen -b 2048 -t rsa -f {{ tempdir.stdout }}/id_rsa -q -N 'passphrase'" + +- name: Read the generated key + set_fact: + ssh_key_data: "{{ lookup('file', tempdir.stdout + '/id_rsa') }}" + +- name: Test deprecation warnings + tower_credential: + name: "{{ ssh_cred_name1 }}" + organization: Default + kind: ssh + authorize: false + authorize_password: 'test' + client: 'test' + security_token: 'test' + secret: 'test' + tenant: 'test' + subscription: 'test' + domain: 'test' + become_method: 'test' + become_username: 'test' + become_password: 'test' + vault_password: 'test' + project: 'test' + host: 'test' + username: 'test' + password: 'test' + ssh_key_data: 'test' + vault_id: 'test' + ssh_key_unlock: 'test' + state: absent + ignore_errors: true + register: result + +- assert: + that: + - "'deprecations' in result" + # The 20 comes from the length of OLD_INPUT_NAMES + 1 for kind + - result['deprecations'] | length() == 20 + +- name: Create an Org-specific credential (old school) + tower_credential: + name: "{{ ssh_cred_name1 }}" + organization: Default + state: present + kind: ssh + register: result + +- assert: + that: + - "result is changed" + +- name: Re-create the Org-specific credential (new school) + tower_credential: + name: "{{ ssh_cred_name1 }}" + organization: Default + credential_type: 'Machine' + state: present + register: result + +- assert: + that: + - "result is not changed" + +- name: Delete a Org-specific credential + tower_credential: + name: "{{ ssh_cred_name1 }}" + organization: Default + state: absent + kind: ssh + register: result + +- assert: + that: + - "result is changed" + +- name: Create the User-specific credential + tower_credential: + name: "{{ ssh_cred_name1 }}" + user: admin + credential_type: 'Machine' + state: present + register: result + +- assert: + that: + - "result is changed" + +- name: Delete a User-specific credential + tower_credential: + name: "{{ ssh_cred_name1 }}" + user: admin + state: absent + kind: ssh + register: result + +- assert: + that: + - "result is changed" + +- name: Create a valid SSH credential (old school) + tower_credential: + name: "{{ ssh_cred_name2 }}" + organization: Default + state: present + kind: ssh + description: An example SSH credential + username: joe + password: secret + become_method: sudo + become_username: superuser + become_password: supersecret + ssh_key_data: "{{ ssh_key_data }}" + ssh_key_unlock: "passphrase" + register: result + +- assert: + that: + - "result is changed" + +- name: Create a valid SSH credential (new school) + tower_credential: + name: "{{ ssh_cred_name2 }}" + organization: Default + state: present + credential_type: Machine + description: An example SSH credential + inputs: + username: joe + password: secret + become_method: sudo + become_username: superuser + become_password: supersecret + ssh_key_data: "{{ ssh_key_data }}" + ssh_key_unlock: "passphrase" + register: result + +# This will be changed because we are setting ssh_key_data and ssh_key_unlock. +# These will come out as $encrypted$ which will always compare false to the values. +- assert: + that: + - result is changed + +- name: Create a valid SSH credential (new school) + tower_credential: + name: "{{ ssh_cred_name2 }}" + organization: Default + state: present + credential_type: Machine + description: An example SSH credential + inputs: + username: joe + become_method: sudo + become_username: superuser + register: result + +# This shows as "changed" because these listed inputs replace the existing inputs from the previous task +- assert: + that: + - result is changed + +- name: Check for inputs idempotency (when "inputs" is blank) + tower_credential: + name: "{{ ssh_cred_name2 }}" + organization: Default + state: present + credential_type: Machine + description: An example SSH credential + register: result + +- assert: + that: + - result is not changed + +- name: Create a valid SSH credential from lookup source (old school) + tower_credential: + name: "{{ ssh_cred_name3 }}" + organization: Default + state: present + kind: ssh + description: An example SSH credential from lookup source + username: joe + password: secret + become_method: sudo + become_username: superuser + become_password: supersecret + ssh_key_data: "{{ lookup('file', tempdir.stdout + '/id_rsa') }}" + ssh_key_unlock: "passphrase" + register: result + +- assert: + that: + - "result is changed" + +- name: Create a valid SSH credential from lookup source (new school) + tower_credential: + name: "{{ ssh_cred_name3 }}" + organization: Default + state: present + credential_type: Machine + description: An example SSH credential from lookup source + inputs: + username: joe + password: secret + become_method: sudo + become_username: superuser + become_password: supersecret + ssh_key_data: "{{ lookup('file', tempdir.stdout + '/id_rsa') }}" + ssh_key_unlock: "passphrase" + register: result + +# This will be changed because we are passing in ssh_key_data and password +- assert: + that: + - result is changed + +- name: Fail to create an SSH credential from a file source (old school format) + tower_credential: + name: "{{ ssh_cred_name4 }}" + organization: Default + state: present + kind: ssh + description: An example SSH credential from file source + username: joe + password: secret + become_method: sudo + become_username: superuser + become_password: supersecret + ssh_key_data: "{{ tempdir.stdout }}/id_rsa" + ssh_key_unlock: "passphrase" + register: result + ignore_errors: true + +- assert: + that: + - result is failed + - "'Unable to create credential {{ ssh_cred_name4 }}' in result.msg" + - "'Invalid certificate or key' in result.msg" + +- name: Create an invalid SSH credential (passphrase required) + tower_credential: + name: SSH Credential + organization: Default + state: present + kind: ssh + username: joe + ssh_key_data: "{{ ssh_key_data }}" + ignore_errors: true + register: result + +- assert: + that: + - "result is failed" + - "'must be set when SSH key is encrypted' in result.msg" + +- name: Create an invalid SSH credential (Organization not found) + tower_credential: + name: SSH Credential + organization: Missing Organization + state: present + kind: ssh + username: joe + ignore_errors: true + register: result + +- assert: + that: + - "result is failed" + - "'The organizations Missing Organization was not found on the Tower server' in result.msg" + +- name: Delete an SSH credential + tower_credential: + name: "{{ ssh_cred_name2 }}" + organization: Default + state: absent + kind: ssh + register: result + +- assert: + that: + - "result is changed" + +- name: Delete an SSH credential + tower_credential: + name: "{{ ssh_cred_name3 }}" + organization: Default + state: absent + kind: ssh + register: result + +- assert: + that: + - "result is changed" + +- name: Delete an SSH credential + tower_credential: + name: "{{ ssh_cred_name4 }}" + organization: Default + state: absent + kind: ssh + register: result + +# This one was never really created so it shouldn't be deleted +- assert: + that: + - "result is not changed" + +- name: Create a valid Vault credential + tower_credential: + name: "{{ vault_cred_name1 }}" + organization: Default + state: present + kind: vault + description: An example Vault credential + vault_password: secret-vault + register: result + +- assert: + that: + - "result is changed" + +# We should decide when to delete this test +- name: Create a valid Vault credential w/ kind=ssh (deprecated, will now fail) + tower_credential: + name: "{{ vault_cred_name2 }}" + organization: Default + state: present + kind: ssh + description: An example Vault credential + vault_password: secret-vault + register: result + ignore_errors: true + +- assert: + that: + - result is failed + - "'Unable to create credential {{ vault_cred_name2 }}' in result.msg" + - "'Additional properties are not allowed' in result.msg" + - "'\\'vault_password\\' was unexpected' in result.msg" + +- name: Delete a Vault credential + tower_credential: + name: "{{ vault_cred_name1 }}" + organization: Default + state: absent + kind: vault + register: result + +- assert: + that: + - "result is changed" + +- name: Delete a Vault credential + tower_credential: + name: "{{ vault_cred_name2 }}" + organization: Default + state: absent + kind: vault + register: result + +# The creation of vault_cred_name2 never worked so we shouldn't actually need to delete it +- assert: + that: + - "result is not changed" + +- name: Create a valid Network credential + tower_credential: + name: "{{ net_cred_name1 }}" + organization: Default + state: present + kind: net + username: joe + password: secret + authorize: true + authorize_password: authorize-me + register: result + +- assert: + that: + - "result is changed" + +- name: Delete a Network credential + tower_credential: + name: "{{ net_cred_name1 }}" + organization: Default + state: absent + kind: net + register: result + +- assert: + that: + - "result is changed" + +- name: Create a valid SCM credential + tower_credential: + name: "{{ scm_cred_name1 }}" + organization: Default + state: present + kind: scm + username: joe + password: secret + ssh_key_data: "{{ ssh_key_data }}" + ssh_key_unlock: "passphrase" + register: result + +- assert: + that: + - "result is changed" + +- name: Delete an SCM credential + tower_credential: + name: "{{ scm_cred_name1 }}" + organization: Default + state: absent + kind: scm + register: result + +- assert: + that: + - "result is changed" + +- name: Create a valid AWS credential + tower_credential: + name: "{{ aws_cred_name1 }}" + organization: Default + state: present + kind: aws + username: joe + password: secret + security_token: aws-token + register: result + +- assert: + that: + - "result is changed" + +- name: Delete an AWS credential + tower_credential: + name: "{{ aws_cred_name1 }}" + organization: Default + state: absent + kind: aws + register: result + +- assert: + that: + - "result is changed" + +- name: Create a valid VMWare credential + tower_credential: + name: "{{ vmware_cred_name1 }}" + organization: Default + state: present + kind: vmware + host: https://example.org + username: joe + password: secret + register: result + +- assert: + that: + - "result is changed" + +- name: Delete an VMWare credential + tower_credential: + name: "{{ vmware_cred_name1 }}" + organization: Default + state: absent + kind: vmware + register: result + +- assert: + that: + - "result is changed" + +- name: Create a valid Satellite6 credential + tower_credential: + name: "{{ sat6_cred_name1 }}" + organization: Default + state: present + kind: satellite6 + host: https://example.org + username: joe + password: secret + register: result + +- assert: + that: + - "result is changed" + +- name: Delete a Satellite6 credential + tower_credential: + name: "{{ sat6_cred_name1 }}" + organization: Default + state: absent + kind: satellite6 + register: result + +- assert: + that: + - "result is changed" + +- name: Create a valid CloudForms credential + tower_credential: + name: "{{ cf_cred_name1 }}" + organization: Default + state: present + kind: cloudforms + host: https://example.org + username: joe + password: secret + register: result + +- assert: + that: + - "result is changed" + +- name: Delete a CloudForms credential + tower_credential: + name: "{{ cf_cred_name1 }}" + organization: Default + state: absent + kind: cloudforms + register: result + +- assert: + that: + - "result is changed" + +- name: Create a valid GCE credential + tower_credential: + name: "{{ gce_cred_name1 }}" + organization: Default + state: present + kind: gce + username: joe + project: ABC123 + ssh_key_data: "{{ ssh_key_data }}" + register: result + +- assert: + that: + - "result is changed" + +- name: Delete a GCE credential + tower_credential: + name: "{{ gce_cred_name1 }}" + organization: Default + state: absent + kind: gce + register: result + +- assert: + that: + - "result is changed" + +- name: Create a valid AzureRM credential + tower_credential: + name: "{{ azurerm_cred_name1 }}" + organization: Default + state: present + kind: azure_rm + username: joe + password: secret + subscription: some-subscription + register: result + +- assert: + that: + - "result is changed" + +- name: Create a valid AzureRM credential with a tenant + tower_credential: + name: "{{ azurerm_cred_name1 }}" + organization: Default + state: present + kind: azure_rm + client: some-client + secret: some-secret + tenant: some-tenant + subscription: some-subscription + register: result + +- assert: + that: + - "result is changed" + +- name: Delete an AzureRM credential + tower_credential: + name: "{{ azurerm_cred_name1 }}" + organization: Default + state: absent + kind: azure_rm + register: result + +- assert: + that: + - "result is changed" + +- name: Create a valid OpenStack credential + tower_credential: + name: "{{ openstack_cred_name1 }}" + organization: Default + state: present + kind: openstack + host: https://keystone.example.org + username: joe + password: secret + project: tenant123 + domain: some-domain + register: result + +- assert: + that: + - "result is changed" + +- name: Delete a OpenStack credential + tower_credential: + name: "{{ openstack_cred_name1 }}" + organization: Default + state: absent + kind: openstack + register: result + +- assert: + that: + - "result is changed" + +- name: Create a valid RHV credential + tower_credential: + name: "{{ rhv_cred_name1 }}" + organization: Default + state: present + kind: rhv + host: https://example.org + username: joe + password: secret + register: result + +- assert: + that: + - "result is changed" + +- name: Delete an RHV credential + tower_credential: + name: "{{ rhv_cred_name1 }}" + organization: Default + state: absent + kind: rhv + register: result + +- assert: + that: + - "result is changed" + +- name: Create a valid Insights credential + tower_credential: + name: "{{ insights_cred_name1 }}" + organization: Default + state: present + kind: insights + username: joe + password: secret + register: result + +- assert: + that: + - "result is changed" + +- name: Delete an Insights credential + tower_credential: + name: "{{ insights_cred_name1 }}" + organization: Default + state: absent + kind: insights + register: result + +- assert: + that: + - "result is changed" + +- name: Create a valid Tower-to-Tower credential + tower_credential: + name: "{{ tower_cred_name1 }}" + organization: Default + state: present + kind: tower + host: https://tower.example.org + username: joe + password: secret + register: result + +- assert: + that: + - "result is changed" + +- name: Delete a Tower-to-Tower credential + tower_credential: + name: "{{ tower_cred_name1 }}" + organization: Default + state: absent + kind: tower + register: result + +- assert: + that: + - "result is changed" + +- name: Check module fails with correct msg + tower_credential: + name: test-credential + description: Credential Description + kind: ssh + organization: test-non-existing-org + state: present + register: result + ignore_errors: true + +- assert: + that: + - result is failed + - "result.msg =='The organizations test-non-existing-org was not found on the Tower server'" diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_credential_input_source/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_credential_input_source/tasks/main.yml new file mode 100644 index 00000000..45be47dd --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_credential_input_source/tasks/main.yml @@ -0,0 +1,105 @@ +--- +- name: Generate names + set_fact: + src_cred_name: src_cred + target_cred_name: target_cred + +- name: Add Tower credential Lookup + tower_credential: + description: Credential for Testing Source + name: "{{ src_cred_name }}" + credential_type: CyberArk AIM Central Credential Provider Lookup + inputs: + url: "https://cyberark.example.com" + app_id: "My-App-ID" + organization: Default + register: result + +- assert: + that: + - "result is changed" + +- name: Add Tower credential Target + tower_credential: + description: Credential for Testing Target + name: "{{ target_cred_name }}" + credential_type: Machine + inputs: + username: user + organization: Default + register: result + +- assert: + that: + - "result is changed" + +- name: Add credential Input Source + tower_credential_input_source: + input_field_name: password + target_credential: "{{ target_cred_name }}" + source_credential: "{{ src_cred_name }}" + metadata: + object_query: "Safe=MY_SAFE;Object=AWX-user" + object_query_format: "Exact" + state: present + +- assert: + that: + - "result is changed" + +- name: Add Second Tower credential Lookup + tower_credential: + description: Credential for Testing Source Change + name: "{{ src_cred_name }}-2" + credential_type: CyberArk AIM Central Credential Provider Lookup + inputs: + url: "https://cyberark-prod.example.com" + app_id: "My-App-ID" + organization: Default + register: result + +- name: Change credential Input Source + tower_credential_input_source: + input_field_name: password + target_credential: "{{ target_cred_name }}" + source_credential: "{{ src_cred_name }}-2" + state: present + +- assert: + that: + - "result is changed" + +- name: Remove a Tower credential source + tower_credential_input_source: + input_field_name: password + target_credential: "{{ target_cred_name }}" + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Remove Tower credential Lookup + tower_credential: + name: "{{ src_cred_name }}" + organization: Default + credential_type: CyberArk AIM Central Credential Provider Lookup + state: absent + register: result + +- name: Remove Alt Tower credential Lookup + tower_credential: + name: "{{ src_cred_name }}-2" + organization: Default + credential_type: CyberArk AIM Central Credential Provider Lookup + state: absent + register: result + +- name: Remove Tower credential + tower_credential: + name: "{{ target_cred_name }}" + organization: Default + credential_type: Machine + state: absent + register: result diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_credential_type/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_credential_type/tasks/main.yml new file mode 100644 index 00000000..a15cd267 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_credential_type/tasks/main.yml @@ -0,0 +1,27 @@ +--- +- name: Generate names + set_fact: + cred_type_name: "AWX-Collection-tests-tower_credential_type-cred-type-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: Add Tower credential type + tower_credential_type: + description: Credential type for Test + name: "{{ cred_type_name }}" + kind: cloud + inputs: {"fields": [{"type": "string", "id": "username", "label": "Username"}, {"secret": true, "type": "string", "id": "password", "label": "Password"}], "required": ["username", "password"]} + injectors: {"extra_vars": {"test": "foo"}} + register: result + +- assert: + that: + - "result is changed" + +- name: Remove a Tower credential type + tower_credential_type: + name: "{{ cred_type_name }}" + state: absent + register: result + +- assert: + that: + - "result is changed" diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_export/aliases b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_export/aliases new file mode 100644 index 00000000..527d07c3 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_export/aliases @@ -0,0 +1 @@ +skip/python2 diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_export/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_export/tasks/main.yml new file mode 100644 index 00000000..7ffbc158 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_export/tasks/main.yml @@ -0,0 +1,77 @@ +--- +- name: Generate a random string for test + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + +- name: Generate names + set_fact: + org_name1: "AWX-Collection-tests-tower_export-organization-{{ test_id }}" + org_name2: "AWX-Collection-tests-tower_export-organization2-{{ test_id }}" + inventory_name1: "AWX-Collection-tests-tower_export-inv1-{{ test_id }}" + +- block: + - name: Create some organizations + tower_organization: + name: "{{ item }}" + loop: + - "{{ org_name1 }}" + - "{{ org_name2 }}" + + - name: Create an inventory + tower_inventory: + name: "{{ inventory_name1 }}" + organization: "{{ org_name1 }}" + + - name: Export all tower assets + tower_export: + all: true + register: all_assets + + - assert: + that: + - all_assets is not changed + - all_assets is successful + - all_assets['assets']['organizations'] | length() >= 2 + + - name: Export all inventories + tower_export: + inventory: 'all' + register: inventory_export + + - assert: + that: + - inventory_export is successful + - inventory_export is not changed + - inventory_export['assets']['inventory'] | length() >= 1 + - "'organizations' not in inventory_export['assets']" + + # This mimics the example in the module + - name: Export an all and a specific + tower_export: + inventory: 'all' + organizations: "{{ org_name1 }}" + register: mixed_export + + - assert: + that: + - mixed_export is successful + - mixed_export is not changed + - mixed_export['assets']['inventory'] | length() >= 1 + - mixed_export['assets']['organizations'] | length() == 1 + - "'workflow_job_templates' not in mixed_export['assets']" + + always: + - name: Remove our inventory + tower_inventory: + name: "{{ inventory_name1 }}" + organization: "{{ org_name1 }}" + state: absent + + - name: Remove test organizations + tower_organization: + name: "{{ item }}" + state: absent + loop: + - "{{ org_name1 }}" + - "{{ org_name2 }}" diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_group/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_group/tasks/main.yml new file mode 100644 index 00000000..38e76719 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_group/tasks/main.yml @@ -0,0 +1,98 @@ +--- +- name: Generate names + set_fact: + group_name1: "AWX-Collection-tests-tower_group-group-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + group_name2: "AWX-Collection-tests-tower_group-group-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + group_name3: "AWX-Collection-tests-tower_group-group-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + inv_name: "AWX-Collection-test-tower_group-inv-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + host_name1: "AWX-Collection-test-tower_group-host-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + host_name2: "AWX-Collection-test-tower_group-host-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + host_name3: "AWX-Collection-test-tower_group-host-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: Create an Inventory + tower_inventory: + name: "{{ inv_name }}" + organization: Default + state: present + +- name: Create a Group + tower_group: + name: "{{ group_name1 }}" + inventory: "{{ inv_name }}" + state: present + variables: + foo: bar + register: result + +- assert: + that: + - "result is changed" + +- name: Delete a Group + tower_group: + name: "{{ group_name1 }}" + inventory: "{{ inv_name }}" + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Check module fails with correct msg + tower_group: + name: test-group + description: Group Description + inventory: test-non-existing-inventory + state: present + register: result + ignore_errors: true + +- assert: + that: + - "result.msg =='Failed to update the group, inventory not found: The requested object could not be found.' or + result.msg =='The inventories test-non-existing-inventory was not found on the Tower server'" + +- name: add hosts + tower_host: + name: "{{ item }}" + inventory: "{{ inv_name }}" + loop: + - "{{ host_name1 }}" + - "{{ host_name2 }}" + - "{{ host_name3 }}" + +- name: add mid level group + tower_group: + name: "{{ group_name2 }}" + inventory: "{{ inv_name }}" + hosts: + - "{{ host_name3 }}" + +- name: add top group + tower_group: + name: "{{ group_name3 }}" + inventory: "{{ inv_name }}" + hosts: + - "{{ host_name1 }}" + - "{{ host_name2 }}" + children: + - "{{ group_name2 }}" + +- name: Delete the parent group + tower_group: + name: "{{ group_name3 }}" + inventory: "{{ inv_name }}" + state: absent + +- name: Delete the child group + tower_group: + name: "{{ group_name2 }}" + inventory: "{{ inv_name }}" + state: absent + +- name: Delete an Inventory + tower_inventory: + name: "{{ inv_name }}" + organization: Default + state: absent diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_host/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_host/tasks/main.yml new file mode 100644 index 00000000..597d6ef7 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_host/tasks/main.yml @@ -0,0 +1,49 @@ +--- +- name: Generate names + set_fact: + host_name: "AWX-Collection-tests-tower_host-host-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + inv_name: "AWX-Collection-tests-tower_host-inv-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: Create an Inventory + tower_inventory: + name: "{{ inv_name }}" + organization: Default + state: present + +- name: Create a Host + tower_host: + name: "{{ host_name }}" + inventory: "{{ inv_name }}" + state: present + variables: + foo: bar + register: result + +- assert: + that: + - "result is changed" + +- name: Delete a Host + tower_host: + name: "{{ host_name }}" + inventory: "{{ inv_name }}" + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Check module fails with correct msg + tower_host: + name: test-host + description: Host Description + inventory: test-non-existing-inventory + state: present + register: result + ignore_errors: true + +- assert: + that: + - "result.msg =='The inventories test-non-existing-inventory was not found on the Tower server' or + result.msg =='Failed to update host, inventory not found: The requested object could not be found.'" diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_import/aliases b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_import/aliases new file mode 100644 index 00000000..527d07c3 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_import/aliases @@ -0,0 +1 @@ +skip/python2 diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_import/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_import/tasks/main.yml new file mode 100644 index 00000000..9835ff89 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_import/tasks/main.yml @@ -0,0 +1,108 @@ +--- +- name: Generate a random string for test + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + +- name: Generate names + set_fact: + org_name1: "AWX-Collection-tests-tower_import-organization-{{ test_id }}" + org_name2: "AWX-Collection-tests-tower_import-organization2-{{ test_id }}" + +- block: + - name: "Import something" + tower_import: + assets: + organizations: + - name: "{{ org_name1 }}" + description: "" + max_hosts: 0 + custom_virtualenv: null + related: + notification_templates: [] + notification_templates_started: [] + notification_templates_success: [] + notification_templates_error: [] + notification_templates_approvals: [] + natural_key: + name: "Default" + type: "organization" + register: import_output + + - assert: + that: + - import_output is changed + + - name: "Import something again (awxkit is not idempotent, this tests a failure)" + tower_import: + assets: + organizations: + - name: "{{ org_name1 }}" + description: "" + max_hosts: 0 + custom_virtualenv: null + related: + notification_templates: [] + notification_templates_started: [] + notification_templates_success: [] + notification_templates_error: [] + notification_templates_approvals: [] + natural_key: + name: "Default" + type: "organization" + register: import_output + ignore_errors: true + + - assert: + that: + - import_output is failed + - "'Organization with this Name already exists' in import_output.msg" + + - name: "Write out a json file" + copy: + content: | + { + "organizations": [ + { + "name": "{{ org_name2 }}", + "description": "", + "max_hosts": 0, + "custom_virtualenv": null, + "related": { + "notification_templates": [], + "notification_templates_started": [], + "notification_templates_success": [], + "notification_templates_error": [], + "notification_templates_approvals": [] + }, + "natural_key": { + "name": "Default", + "type": "organization" + } + } + ] + } + dest: ./org.json + + - name: "Load assets from a file" + tower_import: + assets: "{{ lookup('file', 'org.json') | from_json() }}" + register: import_output + + - assert: + that: + - import_output is changed + + always: + - name: Remove organizations + tower_organization: + name: "{{ item }}" + state: absent + loop: + - "{{ org_name1 }}" + - "{{ org_name2 }}" + + - name: Delete org.json + file: + path: ./org.json + state: absent diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_inventory/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_inventory/tasks/main.yml new file mode 100644 index 00000000..a4e3424e --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_inventory/tasks/main.yml @@ -0,0 +1,101 @@ +--- +- name: Generate names + set_fact: + inv_name1: "AWX-Collection-tests-tower_inventory-inv1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + inv_name2: "AWX-Collection-tests-tower_inventory-inv2-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: Create an Inventory + tower_inventory: + name: "{{ inv_name1 }}" + organization: Default + state: present + register: result + +- assert: + that: + - "result is changed" + +- name: Test Inventory module idempotency + tower_inventory: + name: "{{ inv_name1 }}" + organization: Default + state: present + register: result + +- assert: + that: + - "result is not changed" + +- name: Fail Change Regular to Smart + tower_inventory: + name: "{{ inv_name1 }}" + organization: Default + kind: smart + register: result + ignore_errors: true + +- assert: + that: + - "result is failed" + +- name: Create a smart inventory + tower_inventory: + name: "{{ inv_name2 }}" + organization: Default + kind: smart + host_filter: name=foo + register: result + +- assert: + that: + - "result is changed" + +- name: Delete a smart inventory + tower_inventory: + name: "{{ inv_name2 }}" + organization: Default + kind: smart + host_filter: name=foo + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Delete an Inventory + tower_inventory: + name: "{{ inv_name1 }}" + organization: Default + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Delete a Non-Existent Inventory + tower_inventory: + name: "{{ inv_name1 }}" + organization: Default + state: absent + register: result + +- assert: + that: + - "result is not changed" + +- name: Check module fails with correct msg + tower_inventory: + name: test-inventory + description: Inventory Description + organization: test-non-existing-org + state: present + register: result + ignore_errors: true + +- assert: + that: + - "result is not changed" + - "result.msg =='Failed to update inventory, organization not found: The requested object could not be found.' + or result.msg =='The organizations test-non-existing-org was not found on the Tower server'" diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_inventory_source/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_inventory_source/tasks/main.yml new file mode 100644 index 00000000..fb5f33c3 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_inventory_source/tasks/main.yml @@ -0,0 +1,82 @@ +--- +- name: Generate names + set_fact: + openstack_cred: "AWX-Collection-tests-tower_inventory_source-cred-openstack-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + openstack_inv: "AWX-Collection-tests-tower_inventory_source-inv-openstack-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + openstack_inv_source: "AWX-Collection-tests-tower_inventory_source-inv-source-openstack-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: Add a Tower credential + tower_credential: + description: Credentials for Openstack Test project + name: "{{ openstack_cred }}" + kind: openstack + organization: Default + project: Test + username: admin + host: https://example.org:5000 + password: passw0rd + domain: test + +- name: Add a Tower inventory + tower_inventory: + description: Test inventory + organization: Default + name: "{{ openstack_inv }}" + +- name: Create a source inventory + tower_inventory_source: + name: "{{ openstack_inv_source }}" + description: Source for Test inventory + inventory: "{{ openstack_inv }}" + credential: "{{ openstack_cred }}" + overwrite: true + update_on_launch: true + source_vars: + private: false + source: openstack + register: result + +- assert: + that: + - "result is changed" + +- name: Delete the inventory source with an invalid cred, source_project, sourece_script specified + tower_inventory_source: + name: "{{ openstack_inv_source }}" + inventory: "{{ openstack_inv }}" + credential: "Does Not Exit" + source_project: "Does Not Exist" + source_script: "Does Not Exist" + state: absent + +- assert: + that: + - "result is changed" + +- name: Delete the credential + tower_credential: + description: Credentials for Openstack Test project + name: "{{ openstack_cred }}" + kind: openstack + organization: Default + project: Test + username: admin + host: https://example.org:5000 + password: passw0rd + domain: test + state: absent + +- assert: + that: + - "result is changed" + +- name: Delete the inventory + tower_inventory: + description: Test inventory + organization: Default + name: "{{ openstack_inv }}" + state: absent + +- assert: + that: + - "result is changed" diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_job_cancel/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_job_cancel/tasks/main.yml new file mode 100644 index 00000000..d949610d --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_job_cancel/tasks/main.yml @@ -0,0 +1,40 @@ +--- +- name: Launch a Job Template + tower_job_launch: + job_template: "Demo Job Template" + register: job + +- assert: + that: + - "job is changed" + +- name: Cancel the job + tower_job_cancel: + job_id: "{{ job.id }}" + register: results + +- assert: + that: + - results is changed + +- name: Cancel an already canceled job (assert failure) + tower_job_cancel: + job_id: "{{ job.id }}" + fail_if_not_running: true + register: results + ignore_errors: true + +- assert: + that: + - results is failed + +- name: Check module fails with correct msg + tower_job_cancel: + job_id: 9999999999 + register: result + ignore_errors: true + +- assert: + that: + - "result.msg =='Unable to cancel job_id/9999999999: The requested object could not be found.' + or result.msg =='Unable to find job with id 9999999999'" diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_job_launch/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_job_launch/tasks/main.yml new file mode 100644 index 00000000..86eb196a --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_job_launch/tasks/main.yml @@ -0,0 +1,136 @@ +--- +- name: Generate names + set_fact: + jt_name1: "AWX-Collection-tests-tower_job_launch-jt1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + jt_name2: "AWX-Collection-tests-tower_job_launch-jt2-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + proj_name: "AWX-Collection-tests-tower_job_launch-project-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: Launch a Job Template + tower_job_launch: + job_template: "Demo Job Template" + register: result + +- assert: + that: + - "result is changed" + - "result.status == 'pending'" + +- name: Wait for a job template to complete + tower_job_wait: + job_id: "{{ result.id }}" + max_interval: 10 + timeout: 120 + register: result + +- assert: + that: + - "result is not changed" + - "result.status == 'successful'" + +- name: Check module fails with correct msg + tower_job_launch: + job_template: "Non Existing Job Template" + inventory: "Test Inventory" + credential: "Test Credential" + register: result + ignore_errors: true + +- assert: + that: + - "result.msg =='Unable to launch job, job_template/Non Existing Job Template was not found: The requested object could not be found.' + or result.msg == 'The inventories Test Inventory was not found on the Tower server'" + +- name: Create a Job Template for testing prompt on launch + tower_job_template: + name: "{{ jt_name1 }}" + project: Demo Project + playbook: hello_world.yml + job_type: run + ask_credential: true + ask_inventory: true + state: present + register: result + +- name: Launch job template with inventory and credential for prompt on launch + tower_job_launch: + job_template: "{{ jt_name1 }}" + inventory: "Demo Inventory" + credential: "Demo Credential" + register: result + +- assert: + that: + - "result is changed" + - "result.status == 'pending'" + +- name: Create a project for testing extra_vars + tower_project: + name: "{{ proj_name }}" + organization: Default + scm_type: git + scm_url: https://github.com/ansible/test-playbooks + +- name: Create a Job Template for testing extra_vars + tower_job_template: + name: "{{ jt_name2 }}" + project: "{{ proj_name }}" + playbook: debug.yml + job_type: run + state: present + inventory: "Demo Inventory" + extra_vars: + foo: bar + register: result + +- name: Launch job template with inventory and credential for prompt on launch + tower_job_launch: + job_template: "{{ jt_name2 }}" + register: result + +- assert: + that: + - "result is changed" + +- name: Get the job + tower_job_list: + query: {"id": "{{result.id}}"} + register: result + +- assert: + that: + - '{"foo": "bar"} | to_json in result.results[0].extra_vars' + +- name: Delete the first jt + tower_job_template: + name: "{{ jt_name1 }}" + project: Demo Project + playbook: hello_world.yml + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Delete the second jt + tower_job_template: + name: "{{ jt_name2 }}" + project: "{{ proj_name }}" + playbook: debug.yml + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Delete the extra_vars project + tower_project: + name: "{{ proj_name }}" + organization: Default + state: absent + register: result + +- assert: + that: + - "result is changed" diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_job_list/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_job_list/tasks/main.yml new file mode 100644 index 00000000..a883f28d --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_job_list/tasks/main.yml @@ -0,0 +1,38 @@ +--- +- name: Launch a Job Template + tower_job_launch: + job_template: "Demo Job Template" + register: job + +- assert: + that: + - "job is changed" + - "job.status == 'pending'" + +- name: List jobs w/ a matching primary key + tower_job_list: + query: {"id": "{{ job.id }}"} + register: matching_jobs + +- assert: + that: + - "{{ matching_jobs.count }} == 1" + +- name: List failed jobs (which don't exist) + tower_job_list: + status: failed + query: {"id": "{{ job.id }}"} + register: successful_jobs + +- assert: + that: + - "{{ successful_jobs.count }} == 0" + +- name: Get ALL result pages! + tower_job_list: + all_pages: true + register: all_page_query + +- assert: + that: + - 'not all_page_query.next' diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_job_template/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_job_template/tasks/main.yml new file mode 100644 index 00000000..72711451 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_job_template/tasks/main.yml @@ -0,0 +1,378 @@ +--- +- name: Generate a random string for test + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: generate random string for project + set_fact: + cred1: "AWX-Collection-tests-tower_job_template-cred1-{{ test_id }}" + cred2: "AWX-Collection-tests-tower_job_template-cred2-{{ test_id }}" + cred3: "AWX-Collection-tests-tower_job_template-cred3-{{ test_id }}" + proj1: "AWX-Collection-tests-tower_job_template-proj-{{ test_id }}" + jt1: "AWX-Collection-tests-tower_job_template-jt1-{{ test_id }}" + jt2: "AWX-Collection-tests-tower_job_template-jt2-{{ test_id }}" + lab1: "AWX-Collection-tests-tower_job_template-lab1-{{ test_id }}" + email_not: "AWX-Collection-tests-tower_job_template-email-not-{{ test_id }}" + webhook_not: "AWX-Collection-tests-tower_notification-wehbook-not-{{ test_id }}" + +- name: Create a Demo Project + tower_project: + name: "{{ proj1 }}" + organization: Default + state: present + scm_type: git + scm_url: https://github.com/ansible/ansible-tower-samples.git + + register: result + +- name: Create Credential1 + tower_credential: + name: "{{ cred1 }}" + organization: Default + kind: tower + +- name: Create Credential2 + tower_credential: + name: "{{ cred2 }}" + organization: Default + kind: ssh + +- name: Create Credential3 + tower_credential: + name: "{{ cred3 }}" + organization: Default + kind: ssh + +- name: Create Label + tower_label: + name: "{{ lab1 }}" + organization: Default + +- name: Add email notification + tower_notification: + name: "{{ email_not }}" + organization: Default + notification_type: email + username: user + password: s3cr3t + sender: tower@example.com + recipients: + - user1@example.com + host: smtp.example.com + port: 25 + use_tls: false + use_ssl: false + state: present + +- name: Add webhook notification + tower_notification: + name: "{{ webhook_not }}" + organization: Default + notification_type: webhook + url: http://www.example.com/hook + headers: + X-Custom-Header: value123 + state: present + register: result + +- name: Create Job Template 1 + tower_job_template: + name: "{{ jt1 }}" + project: "{{ proj1 }}" + inventory: Demo Inventory + playbook: hello_world.yml + credentials: ["{{ cred1 }}", "{{ cred2 }}"] + job_type: run + state: present + register: result + +- assert: + that: + - "result is changed" + +- name: Add a credential to this JT + tower_job_template: + name: "{{ jt1 }}" + project: "{{ proj1 }}" + playbook: hello_world.yml + credentials: + - "{{ cred1 }}" + register: result + +- assert: + that: + - "result is changed" + +- name: Try to add the same credential to this JT + tower_job_template: + name: "{{ jt1 }}" + project: "{{ proj1 }}" + playbook: hello_world.yml + credentials: + - "{{ cred1 }}" + register: result + +- assert: + that: + - "result is not changed" + +- name: Add another credential to this JT + tower_job_template: + name: "{{ jt1 }}" + project: "{{ proj1 }}" + playbook: hello_world.yml + credentials: + - "{{ cred1 }}" + - "{{ cred2 }}" + register: result + +- assert: + that: + - "result is changed" + +- name: Remove a credential for this JT + tower_job_template: + name: "{{ jt1 }}" + project: "{{ proj1 }}" + playbook: hello_world.yml + credentials: + - "{{ cred1 }}" + register: result + +- assert: + that: + - "result is changed" + +- name: Remove all credentials from this JT + tower_job_template: + name: "{{ jt1 }}" + project: "{{ proj1 }}" + playbook: hello_world.yml + credentials: [] + register: result + +- assert: + that: + - "result is changed" + +# This doesnt work if you include the credentials parameter +- name: Delete Job Template 1 + tower_job_template: + name: "{{ jt1 }}" + playbook: hello_world.yml + job_type: run + project: "Does Not Exist" + inventory: "Does Not Exist" + webhook_credential: "Does Not Exist" + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Create Job Template 2 + tower_job_template: + name: "{{ jt2 }}" + project: "{{ proj1 }}" + inventory: Demo Inventory + playbook: hello_world.yml + credential: "{{ cred3 }}" + job_type: run + labels: + - "{{ lab1 }}" + state: present + register: result + +- assert: + that: + - "result is changed" + +- name: Add survey to Job Template 2 + tower_job_template: + name: "{{ jt2 }}" + survey_enabled: true + survey_spec: + name: "" + description: "" + spec: + - question_name: "Q1" + question_description: "The first question" + required: true + type: "text" + variable: "q1" + min: 5 + max: 15 + default: "hello" + register: result + +- assert: + that: + - "result is changed" + +- name: Re Add survey to Job Template 2 + tower_job_template: + name: "{{ jt2 }}" + survey_enabled: true + survey_spec: + name: "" + description: "" + spec: + - question_name: "Q1" + question_description: "The first question" + required: true + type: "text" + variable: "q1" + min: 5 + max: 15 + default: "hello" + register: result + +- assert: + that: + - "result is not changed" + +- name: Add question to survey to Job Template 2 + tower_job_template: + name: "{{ jt2 }}" + survey_enabled: true + survey_spec: + name: "" + description: "" + spec: + - question_name: "Q1" + question_description: "The first question" + required: true + type: "text" + variable: "q1" + min: 5 + max: 15 + default: "hello" + choices: "" + - question_name: "Q2" + type: "text" + variable: "q2" + required: false + register: result + +- assert: + that: + - "result is changed" + +- name: Remove survey from Job Template 2 + tower_job_template: + name: "{{ jt2 }}" + survey_enabled: false + survey_spec: {} + register: result + +- assert: + that: + - "result is changed" + +- name: Add started notifications to Job Template 2 + tower_job_template: + name: "{{ jt2 }}" + notification_templates_started: + - "{{ email_not }}" + - "{{ webhook_not }}" + register: result + +- assert: + that: + - "result is changed" + +- name: Re Add started notifications to Job Template 2 + tower_job_template: + name: "{{ jt2 }}" + notification_templates_started: + - "{{ email_not }}" + - "{{ webhook_not }}" + register: result + +- assert: + that: + - "result is not changed" + +- name: Add success notifications to Job Template 2 + tower_job_template: + name: "{{ jt2 }}" + notification_templates_success: + - "{{ email_not }}" + - "{{ webhook_not }}" + register: result + +- assert: + that: + - "result is changed" + +- name: Remove "on start" webhook notification from Job Template 2 + tower_job_template: + name: "{{ jt2 }}" + notification_templates_started: + - "{{ email_not }}" + register: result + +- assert: + that: + - "result is changed" + + +- name: Delete Job Template 2 + tower_job_template: + name: "{{ jt2 }}" + project: "{{ proj1 }}" + inventory: Demo Inventory + playbook: hello_world.yml + credential: "{{ cred3 }}" + job_type: run + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Delete the Demo Project + tower_project: + name: "{{ proj1 }}" + organization: Default + state: absent + scm_type: git + scm_url: https://github.com/ansible/ansible-tower-samples.git + register: result + +- name: Delete Credential1 + tower_credential: + name: "{{ cred1 }}" + organization: Default + kind: tower + state: absent + +- name: Delete Credential2 + tower_credential: + name: "{{ cred2 }}" + organization: Default + kind: ssh + state: absent + +- name: Delete Credential3 + tower_credential: + name: "{{ cred3 }}" + organization: Default + kind: ssh + state: absent + +# You can't delete a label directly so no cleanup needed + +- name: Delete email notification + tower_notification: + name: "{{ email_not }}" + organization: Default + state: absent + +- name: Delete webhook notification + tower_notification: + name: "{{ webhook_not }}" + organization: Default + state: absent diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_job_wait/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_job_wait/tasks/main.yml new file mode 100644 index 00000000..b04fa62f --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_job_wait/tasks/main.yml @@ -0,0 +1,137 @@ +--- +- name: Generate random string for template and project + set_fact: + jt_name: "AWX-Collection-tests-tower_job_wait-long_running-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + proj_name: "AWX-Collection-tests-tower_job_wait-long_running-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: Assure that the demo project exists + tower_project: + name: "{{ proj_name }}" + scm_type: 'git' + scm_url: 'https://github.com/ansible/test-playbooks.git' + scm_update_on_launch: true + organization: Default + +- name: Create a job template + tower_job_template: + name: "{{ jt_name }}" + playbook: "sleep.yml" + job_type: run + project: "{{ proj_name }}" + inventory: "Demo Inventory" + extra_vars: + sleep_interval: 300 + +- name: Check deprecation warnings + tower_job_wait: + min_interval: 10 + max_interval: 20 + job_id: "99999999" + register: result + ignore_errors: true + +- assert: + that: + - "'Min and max interval have been deprecated, please use interval instead; interval will be set to 15'" + +- name: Validate that interval superceeds min/max + tower_job_wait: + min_interval: 10 + max_interval: 20 + interval: 12 + job_id: "99999999" + register: result + ignore_errors: true + +- assert: + that: + - "result.msg =='Unable to wait on job 99999999; that ID does not exist in Tower.' or + 'min and max interval have been depricated, please use interval instead, interval will be set to 12'" + +- name: Check module fails with correct msg + tower_job_wait: + job_id: "99999999" + register: result + ignore_errors: true + +- assert: + that: + - result is failed + - "result.msg =='Unable to wait, no job_id 99999999 found: The requested object could not be found.' or + 'Unable to wait on job 99999999; that ID does not exist in Tower.'" + +- name: Launch Demo Job Template (take happy path) + tower_job_launch: + job_template: "Demo Job Template" + register: job + +- assert: + that: + - job is changed + +- name: Wait for the Job to finish + tower_job_wait: + job_id: "{{ job.id }}" + register: wait_results + +# Make sure it worked and that we have some data in our results +- assert: + that: + - wait_results is successful + - "'elapsed' in wait_results" + - "'id' in wait_results" + +- name: Launch a long running job + tower_job_launch: + job_template: "{{ jt_name }}" + register: job + +- assert: + that: + - job is changed + +- name: Timeout waiting for the job to complete + tower_job_wait: + job_id: "{{ job.id }}" + timeout: 5 + ignore_errors: true + register: wait_results + +# Make sure that we failed and that we have some data in our results +- assert: + that: + - "wait_results.msg == 'Monitoring aborted due to timeout' or 'Timeout waiting for job to finish.'" + - "'id' in wait_results" + +- name: Async cancel the long running job + tower_job_cancel: + job_id: "{{ job.id }}" + async: 3600 + poll: 0 + +- name: Wait for the job to exit on cancel + tower_job_wait: + job_id: "{{ job.id }}" + register: wait_results + ignore_errors: true + +- assert: + that: + - wait_results is failed + - 'wait_results.status == "canceled"' + - "wait_results.msg == 'Job with id {{ job.id }} failed' or 'Job with id={{ job.id }} failed, error: Job failed.'" + +- name: Delete the job template + tower_job_template: + name: "{{ jt_name }}" + playbook: "sleep.yml" + job_type: run + project: "{{ proj_name }}" + inventory: "Demo Inventory" + state: absent + +- name: Delete the project + tower_project: + name: "{{ proj_name }}" + organization: Default + state: absent diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_label/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_label/tasks/main.yml new file mode 100644 index 00000000..d354376c --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_label/tasks/main.yml @@ -0,0 +1,24 @@ +--- +- name: Generate names + set_fact: + label_name: "AWX-Collection-tests-tower_label-label-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: Create a Label + tower_label: + name: "{{ label_name }}" + organization: Default + state: present + +- name: Check module fails with correct msg + tower_label: + name: "Test Label" + organization: "Non existing org" + state: present + register: result + ignore_errors: true + +- assert: + that: + - "'Non existing org was not found on the Tower server' in result.msg" + +# TODO: Deleting labels doesn't seem to work currently diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_lookup_api_plugin/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_lookup_api_plugin/tasks/main.yml new file mode 100644 index 00000000..9f29b0d9 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_lookup_api_plugin/tasks/main.yml @@ -0,0 +1,238 @@ +--- +- name: Generate a random string for test + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + +- name: Generate usernames + set_fact: + usernames: + - "AWX-Collection-tests-tower_api_lookup-user1-{{ test_id }}" + - "AWX-Collection-tests-tower_api_lookup-user2-{{ test_id }}" + - "AWX-Collection-tests-tower_api_lookup-user3-{{ test_id }}" + hosts: + - "AWX-Collection-tests-tower_api_lookup-host1-{{ test_id }}" + - "AWX-Collection-tests-tower_api_lookup-host2-{{ test_id }}" + group_name: "AWX-Collection-tests-tower_api_lookup-group1-{{ test_id }}" + +- name: Get our collection package + tower_meta: + register: tower_meta + +- name: Generate the name of our plugin + set_fact: + plugin_name: "{{ tower_meta.prefix }}.tower_api" + +- name: Create all of our users + tower_user: + username: "{{ item }}" + is_superuser: true + password: "{{ test_id }}" + loop: "{{ usernames }}" + register: user_creation_results + +- block: + - name: Create our hosts + tower_host: + name: "{{ item }}" + inventory: "Demo Inventory" + loop: "{{ hosts }}" + + - name: Test too many params (failure from validation of terms) + set_fact: + junk: "{{ query(plugin_name, 'users', 'teams', query_params={}, ) }}" + ignore_errors: true + register: result + + - assert: + that: + - result is failed + - "'You must pass exactly one endpoint to query' in result.msg" + + - name: Try to load invalid endpoint + set_fact: + junk: "{{ query(plugin_name, 'john', query_params={}, ) }}" + ignore_errors: true + register: result + + - assert: + that: + - result is failed + - "'The requested object could not be found at' in result.msg" + + - name: Load user of a specific name without promoting objects + set_fact: + users_list: "{{ lookup(plugin_name, 'users', query_params={ 'username' : user_creation_results['results'][0]['item'] }, return_objects=False) }}" + + - assert: + that: + - users_list['results'] | length() == 1 + - users_list['count'] == 1 + - users_list['results'][0]['id'] == user_creation_results['results'][0]['id'] + + - name: Load user of a specific name with promoting objects + set_fact: + user_objects: "{{ query(plugin_name, 'users', query_params={ 'username' : user_creation_results['results'][0]['item'] }, return_objects=True ) }}" + + - assert: + that: + - user_objects | length() == 1 + - users_list['results'][0]['id'] == user_objects[0]['id'] + + - name: Loop over one user with the loop syntax + assert: + that: + - item['id'] == user_creation_results['results'][0]['id'] + loop: "{{ query(plugin_name, 'users', query_params={ 'username' : user_creation_results['results'][0]['item'] } ) }}" + loop_control: + label: "{{ item.id }}" + + - name: Get a page of users as just ids + set_fact: + users: "{{ query(plugin_name, 'users', query_params={ 'username__endswith': test_id, 'page_size': 2 }, return_ids=True ) }}" + + - name: Assert that user list has 2 ids only and that they are strings, not ints + assert: + that: + - users | length() == 2 + - user_creation_results['results'][0]['id'] not in users + - user_creation_results['results'][0]['id'] | string in users + + - name: Get all users of a system through next attribute + set_fact: + users: "{{ query(plugin_name, 'users', query_params={ 'username__endswith': test_id, 'page_size': 1 }, return_all=true ) }}" + + - assert: + that: + - users | length() >= 3 + + - name: Get all of the users created with a max_objects of 1 + set_fact: + users: "{{ lookup(plugin_name, 'users', query_params={ 'username__endswith': test_id, 'page_size': 1 }, return_all=true, max_objects=1 ) }}" + ignore_errors: true + register: max_user_errors + + - assert: + that: + - max_user_errors is failed + - "'List view at users returned 3 objects, which is more than the maximum allowed by max_objects' in max_user_errors.msg" + + - name: Get the ID of the first user created and verify that it is correct + assert: + that: "{{ query(plugin_name, 'users', query_params={ 'username' : user_creation_results['results'][0]['item'] }, return_ids=True)[0] }} == {{ user_creation_results['results'][0]['id'] }}" + + - name: Try to get an ID of someone who does not exist + set_fact: + failed_user_id: "{{ query(plugin_name, 'users', query_params={ 'username': 'john jacob jingleheimer schmidt' }, expect_one=True) }}" + register: result + ignore_errors: true + + - assert: + that: + - result is failed + - "'Expected one object from endpoint users' in result['msg']" + + - name: Lookup too many users + set_fact: + too_many_user_ids: " {{ query(plugin_name, 'users', query_params={ 'username__endswith': test_id }, expect_one=True) }}" + register: results + ignore_errors: true + + - assert: + that: + - results is failed + - "'Expected one object from endpoint users, but obtained 3' in results['msg']" + + - name: Get the ping page + set_fact: + ping_data: "{{ lookup(plugin_name, 'ping' ) }}" + register: results + + - assert: + that: + - results is succeeded + - "'active_node' in ping_data" + + - name: "Make sure that expect_objects fails on an API page" + set_fact: + my_var: "{{ lookup(plugin_name, 'settings/ui', expect_objects=True) }}" + ignore_errors: true + register: results + + - assert: + that: + - results is failed + - "'Did not obtain a list or detail view at settings/ui, and expect_objects or expect_one is set to True' in results.msg" + + # DOCS Example Tests + - name: Load the UI settings + set_fact: + tower_settings: "{{ lookup('awx.awx.tower_api', 'settings/ui') }}" + + - assert: + that: + - "'CUSTOM_LOGO' in tower_settings" + + - name: Display the usernames of all admin users + debug: + msg: "Admin users: {{ query('awx.awx.tower_api', 'users', query_params={ 'is_superuser': true }) | map(attribute='username') | join(', ') }}" + register: results + + - assert: + that: + - "'admin' in results.msg" + + - name: debug all organizations in a loop # use query to return a list + debug: + msg: "Organization description={{ item['description'] }} id={{ item['id'] }}" + loop: "{{ query('awx.awx.tower_api', 'organizations') }}" + loop_control: + label: "{{ item['name'] }}" + + - name: Make sure user 'john' is an org admin of the default org if the user exists + tower_role: + organization: Default + role: admin + user: "{{ usernames[0] }}" + state: absent + register: tower_role_revoke + when: "query('awx.awx.tower_api', 'users', query_params={ 'username': 'DNE_TESTING' }) | length == 1" + + - assert: + that: + - tower_role_revoke is skipped + + - name: Create an inventory group with all 'foo' hosts + tower_group: + name: "{{ group_name }}" + inventory: "Demo Inventory" + hosts: >- + {{ query( + 'awx.awx.tower_api', + 'hosts', + query_params={ 'name__endswith' : test_id, }, + ) | map(attribute='name') | list }} + register: group_creation + + - assert: + that: group_creation is changed + + always: + - name: Cleanup group + tower_group: + name: "{{ group_name }}" + inventory: "Demo Inventory" + state: absent + + - name: Cleanup hosts + tower_host: + name: "{{ item }}" + inventory: "Demo Inventory" + state: absent + loop: "{{ hosts }}" + + - name: Cleanup users + tower_user: + username: "{{ item }}" + state: absent + loop: "{{ usernames }}" diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_notification/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_notification/tasks/main.yml new file mode 100644 index 00000000..ea3e96b9 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_notification/tasks/main.yml @@ -0,0 +1,230 @@ +--- +- name: Generate names + set_fact: + slack_not: "AWX-Collection-tests-tower_notification-slack-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + webhook_not: "AWX-Collection-tests-tower_notification-wehbook-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + email_not: "AWX-Collection-tests-tower_notification-email-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + twillo_not: "AWX-Collection-tests-tower_notification-twillo-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + pd_not: "AWX-Collection-tests-tower_notification-pd-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + irc_not: "AWX-Collection-tests-tower_notification-irc-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: Test deprecation warnings + tower_notification: + name: "{{ slack_not }}" + organization: Default + notification_type: slack + username: maw + sender: maw + recipients: + - everyone + use_tls: true + host: all + use_ssl: false + password: password + port: 12 + channels: + - general + token: chunkecheese + account_token: asdf1234 + from_number: "1 (888) 733-4281" + to_numbers: + - 867-5309 + account_sid: vicious + subdomain: 'redhat.com' + service_key: skeleton + client_name: Bill + message_from: me + color: green + notify: true + url: ansible.com + headers: + X-Custom-Header: value123 + server: littimer.somewhere.com + nickname: chalk + targets: + - zombie + state: absent + register: result + ignore_errors: true + +- assert: + that: + - "'deprecations' in result" + # The 25 can be count from the size of the OLD_INPUT_NAMES list in the module + - result['deprecations'] | length() == 25 + +- name: Create Slack notification with custom messages + tower_notification: + name: "{{ slack_not }}" + organization: Default + notification_type: slack + token: a_token + channels: + - general + messages: + started: + message: "{{ '{{' }} job_friendly_name {{' }}' }} {{ '{{' }} job.id {{' }}' }} started" + success: + message: "{{ '{{' }} job_friendly_name {{ '}}' }} completed in {{ '{{' }} job.elapsed {{ '}}' }} seconds" + error: + message: "{{ '{{' }} job_friendly_name {{ '}}' }} FAILED! Please look at {{ '{{' }} job.url {{ '}}' }}" + state: present + register: result + +- assert: + that: + - result is changed + +- name: Delete Slack notification + tower_notification: + name: "{{ slack_not }}" + organization: Default + state: absent + register: result + +- assert: + that: + - result is changed + +- name: Add webhook notification + tower_notification: + name: "{{ webhook_not }}" + organization: Default + notification_type: webhook + url: http://www.example.com/hook + headers: + X-Custom-Header: value123 + state: present + register: result + +- assert: + that: + - result is changed + +- name: Delete webhook notification + tower_notification: + name: "{{ webhook_not }}" + organization: Default + state: absent + register: result + +- assert: + that: + - result is changed + +- name: Add email notification + tower_notification: + name: "{{ email_not }}" + organization: Default + notification_type: email + username: user + password: s3cr3t + sender: tower@example.com + recipients: + - user1@example.com + host: smtp.example.com + port: 25 + use_tls: false + use_ssl: false + state: present + register: result + +- assert: + that: + - result is changed + +- name: Delete email notification + tower_notification: + name: "{{ email_not }}" + organization: Default + state: absent + register: result + +- assert: + that: + - result is changed + +- name: Add twilio notification + tower_notification: + name: "{{ twillo_not }}" + organization: Default + notification_type: twilio + account_token: a_token + account_sid: a_sid + from_number: '+15551112222' + to_numbers: + - '+15553334444' + state: present + register: result + +- assert: + that: + - result is changed + +- name: Delete twilio notification + tower_notification: + name: "{{ twillo_not }}" + organization: Default + state: absent + register: result + +- assert: + that: + - result is changed + +- name: Add PagerDuty notification + tower_notification: + name: "{{ pd_not }}" + organization: Default + notification_type: pagerduty + token: a_token + subdomain: sub + client_name: client + service_key: a_key + state: present + register: result + +- assert: + that: + - result is changed + +- name: Delete PagerDuty notification + tower_notification: + name: "{{ pd_not }}" + organization: Default + state: absent + register: result + +- assert: + that: + - result is changed + +- name: Add IRC notification + tower_notification: + name: "{{ irc_not }}" + organization: Default + notification_type: irc + nickname: tower + password: s3cr3t + targets: + - user1 + port: 8080 + server: irc.example.com + use_ssl: false + state: present + register: result + +- assert: + that: + - result is changed + +- name: Delete IRC notification + tower_notification: + name: "{{ irc_not }}" + organization: Default + state: absent + register: result + +- assert: + that: + - result is changed diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_organization/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_organization/tasks/main.yml new file mode 100644 index 00000000..cc40ed59 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_organization/tasks/main.yml @@ -0,0 +1,98 @@ +--- +- name: Generate an org name + set_fact: + org_name: "AWX-Collection-tests-tower_organization-org-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: Make sure {{ org_name }} is not there + tower_organization: + name: "{{ org_name }}" + state: absent + register: result + +- name: "Create a new organization" + tower_organization: + name: "{{ org_name }}" + register: result + +- assert: + that: "result is changed" + +- name: "Make sure making the same org is not a change" + tower_organization: + name: "{{ org_name }}" + register: result + +- assert: + that: + - "result is not changed" + +- name: "Try adding a bad custom_virtualenv" + tower_organization: + name: "{{ org_name }}" + custom_virtualenv: "/does/not/exit" + register: result + ignore_errors: true + +- assert: + that: + - "result is failed" + +- name: "Pass in all parameters" + tower_organization: + name: "{{ org_name }}" + description: "A description" + custom_virtualenv: "" + register: result + +- assert: + that: + - "result is changed" + +- name: "Change the description" + tower_organization: + name: "{{ org_name }}" + description: "A new description" + custom_virtualenv: "" + register: result + +- assert: + that: + - "result is changed" + +- name: "Remove the organization" + tower_organization: + name: "{{ org_name }}" + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: "Remove a missing organization" + tower_organization: + name: "{{ org_name }}" + state: absent + register: result + +- assert: + that: + - "result is not changed" + +# Test behaviour common to all tower modules +- name: Check that SSL is available and verify_ssl is enabled (task must fail) + tower_organization: + name: Default + validate_certs: true + ignore_errors: true + register: check_ssl_is_used + +- name: Check that connection failed + assert: + that: + - "'CERTIFICATE_VERIFY_FAILED' in check_ssl_is_used['msg']" + +- name: Check that verify_ssl is disabled (task must not fail) + tower_organization: + name: Default + validate_certs: false diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_project/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_project/tasks/main.yml new file mode 100644 index 00000000..cece78ac --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_project/tasks/main.yml @@ -0,0 +1,225 @@ +--- +- name: Generate names + set_fact: + project_name1: "AWX-Collection-tests-tower_project-project1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + project_name2: "AWX-Collection-tests-tower_project-project2-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + project_name3: "AWX-Collection-tests-tower_project-project3-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + jt1: "AWX-Collection-tests-tower_project-jt1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + scm_cred_name: "AWX-Collection-tests-tower_project-scm-cred-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + org_name: "AWX-Collection-tests-tower_project-org-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + cred_name: "AWX-Collection-tests-tower_project-cred-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: Create an SCM Credential + tower_credential: + name: "{{ scm_cred_name }}" + organization: Default + kind: scm + register: result + +- assert: + that: + - result is changed + +- name: Create a git project without credentials without waiting + tower_project: + name: "{{ project_name1 }}" + organization: Default + scm_type: git + scm_url: https://github.com/ansible/test-playbooks + wait: false + register: result + +- assert: + that: + - result is changed + +- name: Recreate the project to validate not changed + tower_project: + name: "{{ project_name1 }}" + organization: Default + scm_type: git + scm_url: https://github.com/ansible/test-playbooks + wait: true + register: result + +- assert: + that: + - result is not changed + +- name: Create organizations + tower_organization: + name: "{{ org_name }}" + register: result + +- assert: + that: + - result is changed + +- name: Create credential + tower_credential: + kind: scm + name: "{{ cred_name }}" + organization: "{{ org_name }}" + register: result + +- assert: + that: + - result is changed + +- name: Create a new test project in check_mode + tower_project: + name: "{{ project_name2 }}" + organization: "{{ org_name }}" + scm_type: git + scm_url: https://github.com/ansible/test-playbooks + scm_credential: "{{ cred_name }}" + check_mode: true + +- name: Create a new test project + tower_project: + name: "{{ project_name2 }}" + organization: "{{ org_name }}" + scm_type: git + scm_url: https://github.com/ansible/test-playbooks + scm_credential: "{{ cred_name }}" + register: result + +# If this fails it may be because the check_mode task actually already created +# the project, or it could be because the module actually failed somehow +- assert: + that: + - "result is changed" + +- name: Check module fails with correct msg when given non-existing org as param + tower_project: + name: "{{ project_name2 }}" + organization: Non Existing Org + scm_type: git + scm_url: https://github.com/ansible/test-playbooks + scm_credential: "{{ cred_name }}" + register: result + ignore_errors: true + +- assert: + that: + - "result.msg == 'The organizations Non Existing Org was not found on the Tower server' or + result.msg == 'Failed to update project, organization not found: Non Existing Org'" + +- name: Check module fails with correct msg when given non-existing credential as param + tower_project: + name: "{{ project_name2 }}" + organization: "{{ org_name }}" + scm_type: git + scm_url: https://github.com/ansible/test-playbooks + scm_credential: Non Existing Credential + register: result + ignore_errors: true + +- assert: + that: + - "result.msg =='The credentials Non Existing Credential was not found on the Tower server' or + result.msg =='Failed to update project, credential not found: Non Existing Credential'" + +- name: Create a git project without credentials without waiting + tower_project: + name: "{{ project_name3 }}" + organization: Default + scm_type: git + scm_branch: empty_branch + scm_url: https://github.com/ansible/test-playbooks + allow_override: true + register: result + +- assert: + that: + - result is changed + +- name: Create a job template that overrides the project scm_branch + tower_job_template: + name: "{{ jt1 }}" + project: "{{ project_name3 }}" + inventory: "Demo Inventory" + scm_branch: master + playbook: debug.yml + +- name: Launch "{{ jt1 }}" + tower_job_launch: + job_template: "{{ jt1 }}" + register: result + +- assert: + that: + - result is changed + +- name: "wait for job {{ result.id }}" + tower_job_wait: + job_id: "{{ result.id }}" + register: job + +- assert: + that: + - job is successful + +- name: Delete the test job_template + tower_job_template: + name: "{{ jt1 }}" + project: "{{ project_name3 }}" + inventory: "Demo Inventory" + state: absent + +- name: Delete the test project 3 + tower_project: + name: "{{ project_name3 }}" + organization: Default + state: absent + +- name: Delete the test project 2 + tower_project: + name: "{{ project_name2 }}" + organization: "{{ org_name }}" + state: absent + +- name: Delete the SCM Credential + tower_credential: + name: "{{ scm_cred_name }}" + organization: Default + kind: scm + state: absent + register: result + +- assert: + that: + - result is changed + +- name: Delete the test project 1 + tower_project: + name: "{{ project_name1 }}" + organization: Default + state: absent + register: result + +- assert: + that: + - result is changed + +- name: Delete credential + tower_credential: + kind: scm + name: "{{ cred_name }}" + organization: "{{ org_name }}" + state: absent + register: result + +- assert: + that: + - result is changed + +- name: Delete the organization + tower_organization: + name: "{{ org_name }}" + state: absent + register: result + +- assert: + that: + - result is changed diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_project_manual/tasks/create_project_dir.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_project_manual/tasks/create_project_dir.yml new file mode 100644 index 00000000..7f5b3b49 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_project_manual/tasks/create_project_dir.yml @@ -0,0 +1,58 @@ +--- +- name: get tower host variable + shell: tower-cli config host | cut -d ' ' -f2 + register: host + +- name: get tower username variable + shell: tower-cli config username | cut -d ' ' -f2 + register: username + +- name: get tower password variable + shell: tower-cli config password | cut -d ' ' -f2 + register: password + +- name: Fetch project_base_dir + uri: + url: "{{ host.stdout }}/api/v2/config/" + user: "{{ username.stdout }}" + password: "{{ password.stdout }}" + validate_certs: false + return_content: true + force_basic_auth: true + register: awx_config + +- tower_inventory: + name: localhost + organization: Default + +- tower_host: + name: localhost + inventory: localhost + variables: + ansible_connection: local + +- name: create an unused SSH / Machine credential + tower_credential: + name: dummy + kind: ssh + ssh_key_data: | + -----BEGIN EC PRIVATE KEY----- + MHcCAQEEIIUl6R1xgzR6siIUArz4XBPtGZ09aetma2eWf1v3uYymoAoGCCqGSM49 + AwEHoUQDQgAENJNjgeZDAh/+BY860s0yqrLDprXJflY0GvHIr7lX3ieCtrzOMCVU + QWzw35pc5tvuP34SSi0ZE1E+7cVMDDOF3w== + -----END EC PRIVATE KEY----- + organization: Default + +- name: Disable bubblewrap + command: tower-cli setting modify AWX_PROOT_ENABLED false + +- block: + - name: Create a directory for manual project + vars: + project_base_dir: "{{ awx_config.json.project_base_dir }}" + command: tower-cli ad_hoc launch --wait --inventory localhost + --credential dummy --module-name command + --module-args "mkdir -p {{ project_base_dir }}/{{ project_dir_name }}" + always: + - name: enable bubblewrap + command: tower-cli setting modify AWX_PROOT_ENABLED true diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_project_manual/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_project_manual/tasks/main.yml new file mode 100644 index 00000000..8718219e --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_project_manual/tasks/main.yml @@ -0,0 +1,37 @@ +--- +- name: generate random string for project + set_fact: + rand_string: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" +- name: Generate manual project dir name + set_fact: + project_name: "manual project {{ rand_string }}" + +- name: Generate manual project dir name + set_fact: + project_dir_name: "proj_{{ rand_string }}" + +- name: create a project directory for manual project + import_tasks: create_project_dir.yml + +- name: Create a manual project + tower_project: + name: "{{ project_name }}" + organization: Default + scm_type: manual + local_path: "{{ project_dir_name }}" + register: result + +- assert: + that: + - "result is changed" + +- name: Delete a manual project + tower_project: + name: "{{ project_name }}" + organization: Default + state: absent + register: result + +- assert: + that: + - "result is changed" diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_role/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_role/tasks/main.yml new file mode 100644 index 00000000..f0c26a7e --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_role/tasks/main.yml @@ -0,0 +1,74 @@ +--- +- name: Generate names + set_fact: + username: "AWX-Collection-tests-tower_role-user-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: Create a User + tower_user: + first_name: Joe + last_name: User + username: "{{ username }}" + password: "{{ 65535 | random | to_uuid }}" + email: joe@example.org + state: present + register: result + +- assert: + that: + - "result is changed" + +- name: Add Joe to the update role of the default Project + tower_role: + user: "{{ username }}" + role: update + project: Demo Project + state: "{{ item }}" + register: result + with_items: + - "present" + - "absent" + +- assert: + that: + - "result is changed" + +- name: Create a workflow + tower_workflow_job_template: + name: test-role-workflow + organization: Default + state: present + +- name: Add Joe to workflow execute role + tower_role: + user: "{{ username }}" + role: execute + workflow: test-role-workflow + state: present + register: result + +- assert: + that: + - "result is changed" + +- name: Add Joe to workflow execute role, no-op + tower_role: + user: "{{ username }}" + role: execute + workflow: test-role-workflow + state: present + register: result + +- assert: + that: + - "result is not changed" + +- name: Delete a User + tower_user: + username: "{{ username }}" + email: joe@example.org + state: absent + register: result + +- assert: + that: + - "result is changed" diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_schedule/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_schedule/tasks/main.yml new file mode 100644 index 00000000..9a2caa0c --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_schedule/tasks/main.yml @@ -0,0 +1,88 @@ +--- +- name: Generate a random string for test + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: generate random string for project + set_fact: + sched1: "AWX-Collection-tests-tower_schedule-sched1-{{ test_id }}" + +- name: Try to create without an rrule + tower_schedule: + name: "{{ sched1 }}" + state: present + unified_job_template: "Demo Job Template" + enabled: true + register: result + ignore_errors: true + +- assert: + that: + - result is failed + - "'Unable to create schedule {{ sched1 }}' in result.msg" + +- name: Create with options that the JT does not support + tower_schedule: + name: "{{ sched1 }}" + state: present + unified_job_template: "Demo Job Template" + rrule: "DTSTART:20191219T130551Z RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=1" + description: "This hopefully will not work" + extra_data: + some: var + inventory: Demo Inventory + scm_branch: asdf1234 + job_type: run + job_tags: other_tags + skip_tags: some_tags + limit: node1 + diff_mode: true + verbosity: 4 + enabled: true + register: result + ignore_errors: true + +- assert: + that: + - result is failed + - "'Unable to create schedule {{ sched1 }}' in result.msg" + +- name: Build a real schedule + tower_schedule: + name: "{{ sched1 }}" + state: present + unified_job_template: "Demo Job Template" + rrule: "DTSTART:20191219T130551Z RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=1" + register: result + +- assert: + that: + - result is changed + +- name: Rebuild the same schedule + tower_schedule: + name: "{{ sched1 }}" + state: present + unified_job_template: "Demo Job Template" + rrule: "DTSTART:20191219T130551Z RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=1" + register: result + +- assert: + that: + - result is not changed + +- name: Disable a schedule + tower_schedule: + name: "{{ sched1 }}" + state: present + enabled: "false" + register: result + +- assert: + that: + - result is changed + +- name: Delete the schedule + tower_schedule: + name: "{{ sched1 }}" + state: absent diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_schedule_rrule/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_schedule_rrule/tasks/main.yml new file mode 100644 index 00000000..837821ba --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_schedule_rrule/tasks/main.yml @@ -0,0 +1,50 @@ +--- +- name: Get our collection package + tower_meta: + register: tower_meta + +- name: Generate the name of our plugin + set_fact: + plugin_name: "{{ tower_meta.prefix }}.tower_schedule_rrule" + +- name: Test too many params (failure from validation of terms) + debug: + msg: "{{ query(plugin_name, 'none', 'weekly', start_date='2020-4-16 03:45:07') }}" + ignore_errors: true + register: result + +- assert: + that: + - result is failed + - "'You may only pass one schedule type in at a time' in result.msg" + +- name: Test invalid frequency (failure from validation of term) + debug: + msg: "{{ query(plugin_name, 'john', start_date='2020-4-16 03:45:07') }}" + ignore_errors: true + register: result + +- assert: + that: + - result is failed + - "'Frequency of john is invalid' in result.msg" + +- name: Test an invalid start date (generic failure case from get_rrule) + debug: + msg: "{{ query(plugin_name, 'none', start_date='invalid') }}" + ignore_errors: true + register: result + +- assert: + that: + - result is failed + - "'Parameter start_date must be in the format YYYY-MM-DD' in result.msg" + +- name: Test end_on as count (generic success case) + debug: + msg: "{{ query(plugin_name, 'minute', start_date='2020-4-16 03:45:07', end_on='2') }}" + register: result + +- assert: + that: + - result.msg == 'DTSTART;TZID=America/New_York:20200416T034507 RRULE:FREQ=MINUTELY;COUNT=2;INTERVAL=1' diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_settings/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_settings/tasks/main.yml new file mode 100644 index 00000000..8a42f576 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_settings/tasks/main.yml @@ -0,0 +1,87 @@ +--- +- name: Set the value of AWX_PROOT_SHOW_PATHS to a baseline + tower_settings: + name: AWX_PROOT_SHOW_PATHS + value: '["/var/lib/awx/projects/"]' + +- name: Set the value of AWX_PROOT_SHOW_PATHS to get an error back from Tower + tower_settings: + settings: + AWX_PROOT_SHOW_PATHS: + 'not': 'a valid' + 'tower': 'setting' + register: result + ignore_errors: true + +- assert: + that: + - "result is failed" + +- name: Set the value of AWX_PROOT_SHOW_PATHS + tower_settings: + name: AWX_PROOT_SHOW_PATHS + value: '["/var/lib/awx/projects/", "/tmp"]' + register: result + +- assert: + that: + - "result is changed" + +- name: Attempt to set the value of AWX_PROOT_BASE_PATH to what it already is + tower_settings: + name: AWX_PROOT_BASE_PATH + value: /tmp + register: result + +- debug: + msg: "{{ result }}" + +- assert: + that: + - "result is not changed" + +- name: Apply a single setting via settings + tower_settings: + name: AWX_PROOT_SHOW_PATHS + value: '["/var/lib/awx/projects/", "/var/tmp"]' + register: result + +- assert: + that: + - "result is changed" + +- name: Apply multiple setting via settings with no change + tower_settings: + settings: + AWX_PROOT_BASE_PATH: /tmp + AWX_PROOT_SHOW_PATHS: ["/var/lib/awx/projects/", "/var/tmp"] + register: result + +- debug: + msg: "{{ result }}" + +- assert: + that: + - "result is not changed" + +- name: Apply multiple setting via settings with change + tower_settings: + settings: + AWX_PROOT_BASE_PATH: /tmp + AWX_PROOT_SHOW_PATHS: [] + register: result + +- assert: + that: + - "result is changed" + +- name: Handle an omit value + tower_settings: + name: AWX_PROOT_BASE_PATH + value: '{{ junk_var | default(omit) }}' + register: result + ignore_errors: true + +- assert: + that: + - "'Unable to update settings' in result.msg" diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_team/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_team/tasks/main.yml new file mode 100644 index 00000000..0bb9047f --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_team/tasks/main.yml @@ -0,0 +1,53 @@ +--- +- name: Generate names + set_fact: + team_name: "AWX-Collection-tests-tower_team-team-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: Attempt to add a Tower team to a non-existant Organization + tower_team: + name: Test Team + organization: Missing Organization + state: present + register: result + ignore_errors: true + +- name: Assert a meaningful error was provided for the failed Tower team creation + assert: + that: + - result is failed + - "result.msg =='Failed to update team, organization not found: The requested object could not be found.' or + result.msg =='The organizations Missing Organization was not found on the Tower server'" + +- name: Create a Tower team + tower_team: + name: "{{ team_name }}" + organization: Default + register: result + +- assert: + that: + - "result is changed" + +- name: Delete a Tower team + tower_team: + name: "{{ team_name }}" + organization: Default + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Check module fails with correct msg + tower_team: + name: "{{ team_name }}" + organization: Non Existing Org + state: present + register: result + ignore_errors: true + +- assert: + that: + - "result.msg =='Failed to update team, organization not found: The requested object could not be found.' or + result.msg =='The organizations Non Existing Org was not found on the Tower server'" diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_token/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_token/tasks/main.yml new file mode 100644 index 00000000..355d5dd0 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_token/tasks/main.yml @@ -0,0 +1,110 @@ +--- +- name: Generate names + set_fact: + token_description: "AWX-Collection-tests-tower_token-description-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: Try to use a token as a dict which is missing the token parameter + tower_job_list: + tower_oauthtoken: + not_token: "This has no token entry" + register: results + ignore_errors: true + +- assert: + that: + - results is failed + - '"The provided dict in tower_oauthtoken did not properly contain the token entry" == results.msg' + +- name: Try to use a token as a list + tower_job_list: + tower_oauthtoken: + - dummy_token + register: results + ignore_errors: true + +- assert: + that: + - results is failed + - '"The provided tower_oauthtoken type was not valid (list). Valid options are str or dict." == results.msg' + +- name: Try to delete a token with no existing_token or existing_token_id + tower_token: + state: absent + register: results + ignore_errors: true + +- assert: + that: + - results is failed + # We don't assert a message here because it handled by ansible + +- name: Try to delete a token with both existing_token or existing_token_id + tower_token: + existing_token: + id: 1234 + existing_token_id: 1234 + state: absent + register: results + ignore_errors: true + +- assert: + that: + - results is failed + # We don't assert a message here because it handled by ansible + + +- block: + - name: Create a Token + tower_token: + description: '{{ token_description }}' + scope: "write" + state: present + register: new_token + + - name: Validate our token works by token + tower_job_list: + tower_oauthtoken: "{{ tower_token.token }}" + register: job_list + + - name: Validate out token works by object + tower_job_list: + tower_oauthtoken: "{{ tower_token }}" + register: job_list + + always: + - name: Delete our Token with our own token + tower_token: + existing_token: "{{ tower_token }}" + tower_oauthtoken: "{{ tower_token }}" + state: absent + when: tower_token is defined + register: results + + - assert: + that: + - results is changed or results is skipped + +- block: + - name: Create a second token + tower_token: + description: '{{ token_description }}' + scope: "write" + state: present + register: results + + - assert: + that: + - results is changed + + always: + - name: Delete the second Token with our own token + tower_token: + existing_token_id: "{{ tower_token['id'] }}" + tower_oauthtoken: "{{ tower_token }}" + state: absent + when: tower_token is defined + register: results + + - assert: + that: + - results is changed or resuslts is skipped diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_user/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_user/tasks/main.yml new file mode 100644 index 00000000..f04f1a83 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_user/tasks/main.yml @@ -0,0 +1,120 @@ +--- +- name: Generate names + set_fact: + username: "AWX-Collection-tests-tower_user-user-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: Create a User + tower_user: + username: "{{ username }}" + first_name: Joe + password: "{{ 65535 | random | to_uuid }}" + state: present + register: result + +- assert: + that: + - "result is changed" + +- name: Change a User + tower_user: + username: "{{ username }}" + last_name: User + email: joe@example.org + state: present + register: result + +- assert: + that: + - "result is changed" + +- name: Check idempotency + tower_user: + username: "{{ username }}" + first_name: Joe + last_name: User + register: result + +- assert: + that: + - "result is not changed" + +- name: Delete a User + tower_user: + username: "{{ username }}" + email: joe@example.org + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Create an Auditor + tower_user: + first_name: Joe + last_name: Auditor + username: "{{ username }}" + password: "{{ 65535 | random | to_uuid }}" + email: joe@example.org + state: present + auditor: true + register: result + +- assert: + that: + - "result is changed" + +- name: Delete an Auditor + tower_user: + username: "{{ username }}" + email: joe@example.org + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Create a Superuser + tower_user: + first_name: Joe + last_name: Super + username: "{{ username }}" + password: "{{ 65535 | random | to_uuid }}" + email: joe@example.org + state: present + superuser: true + register: result + +- assert: + that: + - "result is changed" + +- name: Delete a Superuser + tower_user: + username: "{{ username }}" + email: joe@example.org + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Test tower SSL parameter + tower_user: + first_name: Joe + last_name: User + username: "{{ username }}" + password: "{{ 65535 | random | to_uuid }}" + email: joe@example.org + state: present + validate_certs: true + tower_host: http://foo.invalid + ignore_errors: true + register: result + +- assert: + that: + - "'Unable to resolve tower_host' in result.msg or + 'Can not verify ssl with non-https protocol' in result.exception" diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_workflow_job_template/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_workflow_job_template/tasks/main.yml new file mode 100644 index 00000000..8a7a9771 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_workflow_job_template/tasks/main.yml @@ -0,0 +1,276 @@ +--- +- name: Generate a random string for names + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + +- name: Generate random names for test objects + set_fact: + scm_cred_name: "AWX-Collection-tests-tower_workflow_job_template-scm-cred-{{ test_id }}" + demo_project_name: "AWX-Collection-tests-tower_workflow_job_template-proj-{{ test_id }}" + jt1_name: "AWX-Collection-tests-tower_workflow_job_template-jt1-{{ test_id }}" + jt2_name: "AWX-Collection-tests-tower_workflow_job_template-jt2-{{ test_id }}" + wfjt_name: "AWX-Collection-tests-tower_workflow_job_template-wfjt-{{ test_id }}" + email_not: "AWX-Collection-tests-tower_job_template-email-not-{{ test_id }}" + webhook_not: "AWX-Collection-tests-tower_notification-wehbook-not-{{ test_id }}" + +- name: Create an SCM Credential + tower_credential: + name: "{{ scm_cred_name }}" + organization: Default + kind: scm + register: result + +- assert: + that: + - "result is changed" + +- name: Add email notification + tower_notification: + name: "{{ email_not }}" + organization: Default + notification_type: email + username: user + password: s3cr3t + sender: tower@example.com + recipients: + - user1@example.com + host: smtp.example.com + port: 25 + use_tls: false + use_ssl: false + state: present + +- name: Add webhook notification + tower_notification: + name: "{{ webhook_not }}" + organization: Default + notification_type: webhook + url: http://www.example.com/hook + headers: + X-Custom-Header: value123 + state: present + register: result + +- name: Create a Demo Project + tower_project: + name: "{{ demo_project_name }}" + organization: Default + state: present + scm_type: git + scm_url: https://github.com/ansible/ansible-tower-samples.git + scm_credential: "{{ scm_cred_name }}" + register: result + +- assert: + that: + - "result is changed" + +- name: Create a Job Template + tower_job_template: + name: "{{ jt1_name }}" + project: "{{ demo_project_name }}" + inventory: Demo Inventory + playbook: hello_world.yml + credential: Demo Credential + job_type: run + state: present + register: result + +- assert: + that: + - "result is changed" + +- name: Create a second Job Template + tower_job_template: + name: "{{ jt2_name }}" + project: "{{ demo_project_name }}" + inventory: Demo Inventory + playbook: hello_world.yml + credential: Demo Credential + job_type: run + state: present + register: result + +- assert: + that: + - "result is changed" + +- name: Add a Survey to second Job Template + tower_job_template: + name: "{{ jt2_name }}" + project: "{{ demo_project_name }}" + inventory: Demo Inventory + playbook: hello_world.yml + credential: Demo Credential + job_type: run + state: present + survey_enabled: true + survey_spec: '{"spec": [{"index": 0, "question_name": "my question?", "default": "mydef", "variable": "myvar", "type": "text", "required": false}], "description": "test", "name": "test"}' + register: result + +- assert: + that: + - "result is changed" + +- name: Create a workflow job template + tower_workflow_job_template: + name: "{{ wfjt_name }}" + inventory: Demo Inventory + extra_vars: {'foo': 'bar', 'another-foo': {'barz': 'bar2'}} + register: result + +- assert: + that: + - "result is changed" + +# Node actions do what this schema command used to do +# schema: [{"success": [{"job_template": "{{ jt1_name }}"}], "job_template": "{{ jt2_name }}"}] +- name: Create leaf node + tower_workflow_job_template_node: + identifier: leaf + unified_job_template: "{{ jt2_name }}" + workflow: "{{ wfjt_name }}" + +- name: Create root node + tower_workflow_job_template_node: + identifier: root + unified_job_template: "{{ jt1_name }}" + workflow: "{{ wfjt_name }}" + +- name: Add started notifications to workflow job template + tower_workflow_job_template: + name: "{{ wfjt_name }}" + notification_templates_started: + - "{{ email_not }}" + - "{{ webhook_not }}" + register: result + +- assert: + that: + - "result is changed" + +- name: Re Add started notifications to workflow job template + tower_workflow_job_template: + name: "{{ wfjt_name }}" + notification_templates_started: + - "{{ email_not }}" + - "{{ webhook_not }}" + register: result + +- assert: + that: + - "result is not changed" + +- name: Add success notifications to workflow job template + tower_workflow_job_template: + name: "{{ wfjt_name }}" + notification_templates_success: + - "{{ email_not }}" + - "{{ webhook_not }}" + register: result + +- assert: + that: + - "result is changed" + +- name: Remove "on start" webhook notification from workflow job template + tower_workflow_job_template: + name: "{{ wfjt_name }}" + notification_templates_started: + - "{{ email_not }}" + register: result + +- assert: + that: + - "result is changed" + +- name: Delete a workflow job template with an invalid inventory and webook_credential + tower_workflow_job_template: + name: "{{ wfjt_name }}" + inventory: "Does Not Exist" + webhook_credential: "Does Not Exist" + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Check module fails with correct msg + tower_workflow_job_template: + name: "{{ wfjt_name }}" + organization: Non Existing Organization + register: result + ignore_errors: true + +- assert: + that: + - "'The organizations Non Existing Organization was not found' in result.msg" + +- name: Delete the Job Template + tower_job_template: + name: "{{ jt1_name }}" + project: "{{ demo_project_name }}" + inventory: Demo Inventory + playbook: hello_world.yml + credential: Demo Credential + job_type: run + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Delete the second Job Template + tower_job_template: + name: "{{ jt2_name }}" + project: "{{ demo_project_name }}" + inventory: Demo Inventory + playbook: hello_world.yml + credential: Demo Credential + job_type: run + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Delete the Demo Project + tower_project: + name: "{{ demo_project_name }}" + organization: Default + scm_type: git + scm_url: https://github.com/ansible/ansible-tower-samples.git + scm_credential: "{{ scm_cred_name }}" + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Delete the SCM Credential + tower_credential: + name: "{{ scm_cred_name }}" + organization: Default + kind: scm + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Delete email notification + tower_notification: + name: "{{ email_not }}" + organization: Default + state: absent + +- name: Delete webhook notification + tower_notification: + name: "{{ webhook_not }}" + organization: Default + state: absent diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_workflow_launch/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_workflow_launch/tasks/main.yml new file mode 100644 index 00000000..bf88aecf --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/integration/targets/tower_workflow_launch/tasks/main.yml @@ -0,0 +1,91 @@ +--- +- name: Generate a random string for test + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + +- name: Generate names + set_fact: + wfjt_name1: "AWX-Collection-tests-tower_workflow_launch--wfjt1-{{ test_id }}" + +- name: Create our workflow + tower_workflow_job_template: + name: "{{ wfjt_name1 }}" + state: present + +- name: Add a node + tower_workflow_job_template_node: + workflow_job_template: "{{ wfjt_name1 }}" + unified_job_template: "Demo Job Template" + identifier: leaf + register: new_node + +- name: Connect to Tower server but request an invalid workflow + tower_workflow_launch: + workflow_template: "Does Not Exist" + ignore_errors: true + register: result + +- assert: + that: + - result is failed + - "'Unable to find workflow job template' in result.msg" + +- name: Run the workflow without waiting (this should just give us back a job ID) + tower_workflow_launch: + workflow_template: "{{ wfjt_name1 }}" + wait: false + ignore_errors: true + register: result + +- assert: + that: + - result is not failed + - "'id' in result['job_info']" + +- name: Kick off a workflow and wait for it, but only for a second + tower_workflow_launch: + workflow_template: "{{ wfjt_name1 }}" + timeout: 1 + ignore_errors: true + register: result + +- assert: + that: + - result is failed + - "'Monitoring aborted due to timeout' in result.msg" + +- name: Kick off a workflow and wait for it + tower_workflow_launch: + workflow_template: "{{ wfjt_name1 }}" + ignore_errors: true + register: result + +- assert: + that: + - result is not failed + - "'id' in result['job_info']" + +- name: Prompt the workflow's extra_vars on launch + tower_workflow_job_template: + name: "{{ wfjt_name1 }}" + state: present + ask_variables_on_launch: true + +- name: Kick off a workflow with extra_vars + tower_workflow_launch: + workflow_template: "{{ wfjt_name1 }}" + extra_vars: + var1: My First Variable + var2: My Second Variable + ignore_errors: true + register: result + +- assert: + that: + - result is not failed + +- name: Clean up test workflow + tower_workflow_job_template: + name: "{{ wfjt_name1 }}" + state: absent diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/sanity/ignore-2.10.txt b/collections-debian-merged/ansible_collections/awx/awx/tests/sanity/ignore-2.10.txt new file mode 100644 index 00000000..76f35a9a --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/sanity/ignore-2.10.txt @@ -0,0 +1,6 @@ +plugins/modules/tower_receive.py validate-modules:deprecation-mismatch +plugins/modules/tower_send.py validate-modules:deprecation-mismatch +plugins/modules/tower_workflow_template.py validate-modules:deprecation-mismatch +plugins/modules/tower_credential.py pylint:wrong-collection-deprecated-version-tag +plugins/modules/tower_job_wait.py pylint:wrong-collection-deprecated-version-tag +plugins/modules/tower_notification.py pylint:wrong-collection-deprecated-version-tag diff --git a/collections-debian-merged/ansible_collections/awx/awx/tests/sanity/ignore-2.9.txt b/collections-debian-merged/ansible_collections/awx/awx/tests/sanity/ignore-2.9.txt new file mode 100644 index 00000000..9242eefc --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tests/sanity/ignore-2.9.txt @@ -0,0 +1,6 @@ +plugins/modules/tower_receive.py validate-modules:deprecation-mismatch +plugins/modules/tower_receive.py validate-modules:invalid-documentation +plugins/modules/tower_send.py validate-modules:deprecation-mismatch +plugins/modules/tower_send.py validate-modules:invalid-documentation +plugins/modules/tower_workflow_template.py validate-modules:deprecation-mismatch +plugins/modules/tower_workflow_template.py validate-modules:invalid-documentation diff --git a/collections-debian-merged/ansible_collections/awx/awx/tools/generate.yml b/collections-debian-merged/ansible_collections/awx/awx/tools/generate.yml new file mode 100644 index 00000000..4a592ae8 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tools/generate.yml @@ -0,0 +1,21 @@ +--- +- name: Generate the awx.awx collection + hosts: localhost + connection: local + gather_facts: false + vars: + api_url: "{{ lookup('env', 'TOWER_HOST') }}" + vars_files: + - vars/generate_for.yml + - vars/aliases.yml + - vars/associations.yml + - vars/resolution.yml + - vars/examples.yml + module_defaults: + uri: + validate_certs: false + force_basic_auth: true + url_username: "{{ lookup('env', 'TOWER_USERNAME') }}" + url_password: "{{ lookup('env', 'TOWER_PASSWORD') }}" + roles: + - generate diff --git a/collections-debian-merged/ansible_collections/awx/awx/tools/roles/generate/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tools/roles/generate/tasks/main.yml new file mode 100644 index 00000000..6a7b4cf6 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tools/roles/generate/tasks/main.yml @@ -0,0 +1,64 @@ +--- +- name: Get date time data + setup: + gather_subset: min + +- name: Create module directory + file: + state: directory + name: "modules" + +- name: Load api/v2 + uri: + method: GET + url: "{{ api_url }}/api/v2/" + register: endpoints + +- name: Load endpoint options + uri: + method: "OPTIONS" + url: "{{ api_url }}{{ item.value }}" + loop: "{{ endpoints['json'] | dict2items }}" + loop_control: + label: "{{ item.key }}" + register: end_point_options + when: "generate_for is not defined or item.key in generate_for" + +- name: Scan POST options for different things + set_fact: + all_options: "{{ all_options | default({}) | combine(options[0]) }}" + loop: "{{ end_point_options.results }}" + vars: + options: "{{ item | json_query('json.actions.POST.[*]') }}" + loop_control: + label: "{{ item['item']['key'] }}" + when: + - item is not skipped + - options is defined + +- name: Process endpoint + template: + src: "templates/tower_module.j2" + dest: "{{ playbook_dir | dirname }}/plugins/modules/{{ file_name }}" + loop: "{{ end_point_options['results'] }}" + loop_control: + label: "{{ item['item']['key'] }}" + when: "'json' in item and 'actions' in item['json'] and 'POST' in item['json']['actions']" + vars: + item_type: "{{ item['item']['key'] }}" + human_readable: "{{ item_type | replace('_', ' ') }}" + singular_item_type: "{{ item['item']['key'] | regex_replace('ies$', 'y') | regex_replace('s$', '') }}" + file_name: "tower_{% if item['item']['key'] in ['settings'] %}{{ item['item']['key'] }}{% else %}{{ singular_item_type }}{% endif %}.py" + type_map: + bool: 'bool' + boolean: 'bool' + choice: 'str' + datetime: 'str' + id: 'str' + int: 'int' + integer: 'int' + json: 'dict' + list: 'list' + object: 'dict' + password: 'str' + string: 'str' diff --git a/collections-debian-merged/ansible_collections/awx/awx/tools/roles/generate/templates/tower_module.j2 b/collections-debian-merged/ansible_collections/awx/awx/tools/roles/generate/templates/tower_module.j2 new file mode 100644 index 00000000..3606cff5 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tools/roles/generate/templates/tower_module.j2 @@ -0,0 +1,222 @@ +#!/usr/bin/env python +# coding: utf-8 -*- + +{# The following is set by the generate.yml file: + # item_type: the type of item i.e. 'teams' + # human_readable: the type with _ replaced with spaces i.e. worflow job template + # singular_item_type: the type of an item replace singularized i.e. team + # type_map: a mapping of things like string to str + #} +{% set name_option = 'username' if item_type == 'users' else 'name' %} + +# (c) {{ ansible_date_time['year'] }}, John Westcott IV <john.westcott.iv@redhat.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: tower_{{ singular_item_type }} +author: "John Westcott IV (@john-westcott-iv)" +version_added: "4.0" +short_description: create, update, or destroy Ansible Tower {{ human_readable }}. +description: + - Create, update, or destroy Ansible Tower {{ human_readable }}. See + U(https://www.ansible.com/tower) for an overview. +options: +{% for option in item['json']['actions']['POST'] %} +{# to do: sort documentation options #} + {{ option }}: + description: +{% if 'help_text' in item['json']['actions']['POST'][option] %} + - {{ item['json']['actions']['POST'][option]['help_text'] }} +{% else %} + - NO DESCRIPTION GIVEN IN THE TOWER API +{% endif %} + required: {{ item['json']['actions']['POST'][option]['required'] }} + type: {{ type_map[ item['json']['actions']['POST'][option]['type'] ] }} +{% if 'default' in item['json']['actions']['POST'][option] %} +{# for tower_job_template/extra vars, its type is dict but its default is '', so we want to make that {} #} +{% if item['json']['actions']['POST'][option]['default'] == '' and type_map[ item['json']['actions']['POST'][option]['type'] ] == 'dict' %} + default: {} +{% else %} + default: '{{ item['json']['actions']['POST'][option]['default'] }}' +{% endif %} +{% endif %} +{% if 'choices' in item['json']['actions']['POST'][option] %} + choices: +{% for choice in item['json']['actions']['POST'][option]['choices'] %} + - '{{ choice[0] }}' +{% endfor %} +{%endif %} +{% if aliases[item_type][option] | default(False) %} + aliases: +{% for alias_name in aliases[item_type][option] %} + - {{ alias_name }} +{% endfor %} +{% endif %} +{% if option == name_option %} + new_{{ name_option }}: + description: + - Setting this option will change the existing name (looked up via the {{ name_option }} field. + required: True + type: str +{% endif %} +{% endfor %} +{% for association in associations[item_type] | default([]) %} + {{ association['related_item'] }}: + description: + - {{ association['description'] }} + required: {{ association['required'] }} + type: list +{% endfor %} + state: + description: + - Desired state of the resource. + choices: ["present", "absent"] + default: "present" + type: str + tower_oauthtoken: + description: + - The Tower OAuth token to use. + required: False + type: str +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +{% if examples[item_type] | default(False) %} +{{ examples[item_type] }} +{% endif %} +''' + +from ..module_utils.tower_api import TowerAPIModule + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( +{% for option in item['json']['actions']['POST'] %} +{% set option_data = [] %} +{{ option_data.append('required={}'.format(item['json']['actions']['POST'][option]['required'])) -}} +{{ option_data.append('type=\'{}\''.format(type_map[item['json']['actions']['POST'][option]['type']])) -}} +{% if item['json']['actions']['POST'][option]['type'] == 'password' %} +{{ option_data.append('no_log=True') -}} +{% endif %} +{% if 'choices' in item['json']['actions']['POST'][option] %} +{% set all_choices = [] %} +{% for choice in item['json']['actions']['POST'][option]['choices'] %} +{{ all_choices.append("'{}'".format(choice[0])) -}} +{% endfor %} +{{ option_data.append('choices=[{}]'.format(all_choices | join(', '))) -}} +{% endif %} +{% if item['json']['actions']['POST'][option].get('default', '') != '' %} +{% set default_value = item['json']['actions']['POST'][option]['default'] %} +{% if item['json']['actions']['POST'][option]['default'] == '' and type_map[ item['json']['actions']['POST'][option]['type'] ] == 'dict' %} +{% set default_value = '{}' %} +{% endif %} +{{ option_data.append("default='{}'".format(default_value)) -}} +{% endif %} +{% if aliases[item_type][option] | default(False) %} +{% set alias_list = [] %} +{% for alias_name in aliases[item_type][option] %} +{{ alias_list.append("'{}'".format(alias_name)) -}} +{% endfor %} +{{ option_data.append('aliases=[{}]'.format(alias_list | join(', '))) -}} +{% endif %} + {{ option }}=dict({{ option_data | join(', ') }}), +{% if option == name_option %} + new_{{ name_option }}=dict(required=False, type='str'), +{% endif %} +{% endfor %} +{% for association_option in associations[item_type] | default([]) %} + {{ association_option['related_item'] }}=dict(required={{ association_option['required'] }}, type="list", default=None), +{% endfor %} + state=dict(choices=['present', 'absent'], default='present'), + ) + + # Create a module for ourselves + module = TowerAPIModule(argument_spec=argument_spec) + + # Extract our parameters +{% for option in item['json']['actions']['POST'] %} + {{ option }} = module.params.get('{{ option }}') +{% if option == name_option %} + new_{{ name_option }} = module.params.get("new_{{ name_option }}") +{% endif %} +{% endfor %} +{% for association_option in associations[item_type] | default([]) %} + {{ association_option['related_item'] }} = module.params.get('{{ association_option['related_item'] }}') +{% endfor %} + state = module.params.get('state') + +{% if item['json']['actions']['POST'] | length() > 0 %} + # Attempt to look up the related items the user specified (these will fail the module if not found) +{% for option in item['json']['actions']['POST'] %} +{% if item['json']['actions']['POST'][option]['type'] == 'id' %} + {{ option }}_id = None + if {{ option }}: + {{ option }}_id = module.resolve_name_to_id('{{ name_to_id_endpoint_resolution[option] }}', {{ option }}) +{% endif %} +{% endfor %} +{% endif %} +{% for association in associations[item_type] | default([]) %} + {{ association['related_item'] }}_ids = None + if {{ association['related_item'] }} is not None: + {{ association['related_item'] }}_ids = [] + for item in {{ association['related_item'] }}: + {{ association['related_item'] }}_ids.append( module.resolve_name_to_id('{{ association['related_item'] }}', item) ) +{% endfor %} + + # Attempt to look up an existing item based on the provided data + existing_item = module.get_one('{{ item_type }}', **{ + 'data': { + '{{ name_option }}': {{ name_option }}, +{% if 'organization' in item['json']['actions']['POST'] and item['json']['actions']['POST']['organization']['type'] == 'id' %} + 'organization': org_id, +{% endif %} +{% if item_type in ['hosts', 'groups', 'inventory_sources'] %} + 'inventory': inventory_id, +{% endif %} + } + }) + + if state is 'absent': + # If the state was absent we can let the module delete it if needed, the module will handle exiting from this + module.delete_if_needed(existing_item) + + # Create the data that gets sent for create and update + new_fields = {} +{% for option in item['json']['actions']['POST'] %} +{% if option == name_option %} + new_fields['{{ name_option }}'] = new_{{ name_option }} if new_{{ name_option }} else {{ name_option }} +{% else %} + if {{ option }} is not None: +{% if item['json']['actions']['POST'][option]['type'] == 'id' %} + new_fields['{{ option }}'] = {{ option }}_id +{% else %} + new_fields['{{ option }}'] = {{ option }} +{% endif %} +{% endif %} +{% endfor %} + + # If the state was present and we can let the module build or update the existing item, this will return on its own + module.create_or_update_if_needed( + existing_item, new_fields, + endpoint='{{ item_type }}', item_type='{{ singular_item_type }}', + associations={ +{% for association in associations[item_type] | default([]) %} + '{{ association['endpoint'] }}': {{ association['related_item'] }}_ids, +{% endfor %} + } + ) + + +if __name__ == '__main__': + main() diff --git a/collections-debian-merged/ansible_collections/awx/awx/tools/roles/template_galaxy/tasks/main.yml b/collections-debian-merged/ansible_collections/awx/awx/tools/roles/template_galaxy/tasks/main.yml new file mode 100644 index 00000000..96eb2641 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tools/roles/template_galaxy/tasks/main.yml @@ -0,0 +1,56 @@ +--- +- name: Set the collection version in the tower_api.py file + replace: + path: "{{ collection_path }}/plugins/module_utils/tower_api.py" + regexp: '^ _COLLECTION_VERSION = "0.0.1-devel"' + replace: ' _COLLECTION_VERSION = "{{ collection_version }}"' + when: + - "awx_template_version | default(True)" + +- name: Set the collection type in the tower_api.py file + replace: + path: "{{ collection_path }}/plugins/module_utils/tower_api.py" + regexp: '^ _COLLECTION_TYPE = "awx"' + replace: ' _COLLECTION_TYPE = "{{ collection_package }}"' + +- name: Do file content replacements for non-default namespace or package name + block: + + - name: Change module doc_fragments to support desired namespace and package names + replace: + path: "{{ item }}" + regexp: '^extends_documentation_fragment: awx.awx.auth([a-zA-Z0-9_]*)$' + replace: 'extends_documentation_fragment: {{ collection_namespace }}.{{ collection_package }}.auth\1' + with_fileglob: + - "{{ collection_path }}/plugins/inventory/*.py" + - "{{ collection_path }}/plugins/lookup/*.py" + - "{{ collection_path }}/plugins/modules/tower_*.py" + loop_control: + label: "{{ item | basename }}" + + - name: Change inventory file to support desired namespace and package names + replace: + path: "{{ collection_path }}/plugins/inventory/tower.py" + regexp: "^ NAME = 'awx.awx.tower' # REPLACE$" + replace: " NAME = '{{ collection_namespace }}.{{ collection_package }}.tower' # REPLACE" + + - name: Get sanity tests to work with non-default name + lineinfile: + path: "{{ collection_path }}/tests/sanity/ignore-2.10.txt" + state: absent + regexp: ' pylint:wrong-collection-deprecated-version-tag$' + + when: + - (collection_package != 'awx') or (collection_namespace != 'awx') + +- name: Template the galaxy.yml file + template: + src: "{{ collection_path }}/tools/roles/template_galaxy/templates/galaxy.yml.j2" + dest: "{{ collection_path }}/galaxy.yml" + force: true + +- name: Template the README.md file + template: + src: "{{ collection_path }}/tools/roles/template_galaxy/templates/README.md.j2" + dest: "{{ collection_path }}/README.md" + force: true diff --git a/collections-debian-merged/ansible_collections/awx/awx/tools/roles/template_galaxy/templates/README.md.j2 b/collections-debian-merged/ansible_collections/awx/awx/tools/roles/template_galaxy/templates/README.md.j2 new file mode 100644 index 00000000..53cc8bd0 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tools/roles/template_galaxy/templates/README.md.j2 @@ -0,0 +1,146 @@ +# {% if collection_package | lower() == 'awx' %}AWX{% else %}Tower{% endif %} Ansible Collection + +[comment]: # (*******************************************************) +[comment]: # (* *) +[comment]: # (* WARNING *) +[comment]: # (* *) +[comment]: # (* This file is templated and not to be *) +[comment]: # (* edited directly! Instead modify: *) +[comment]: # (* tools/roles/template_galaxy/templates/README.md.j2 *) +[comment]: # (* *) +[comment]: # (* Changes to the base README.md file are refreshed *) +[comment]: # (* upon build of the collection *) +[comment]: # (*******************************************************) + +This Ansible collection allows for easy interaction with an {% if collection_package | lower() == 'awx' %}AWX{% else %}Ansible Tower{% endif %} server via Ansible playbooks. + +This source for this collection lives in the `awx_collection` folder inside of the +AWX source. +The previous home for this collection was inside the folder [lib/ansible/modules/web_infrastructure/ansible_tower](https://github.com/ansible/ansible/tree/stable-2.9/lib/ansible/modules/web_infrastructure/ansible_tower) in the Ansible repo, +as well as other places for the inventory plugin, module utils, and +doc fragment. + +## Building and Installing + +{% if collection_package | lower() == 'awx' %} +This collection templates the `galaxy.yml` file it uses. +Run `make build_collection` from the root folder of the AWX source tree. +This will create the `tar.gz` file inside the `awx_collection` folder +with the current AWX version, for example: `awx_collection/awx-awx-9.2.0.tar.gz`. + +Installing the `tar.gz` involves no special instructions. + +{% else %} +This collection should be installed from [Content Hub][https://cloud.redhat.com/ansible/automation-hub/ansible/tower/] + +{% endif %} +## Running + +Non-deprecated modules in this collection have no Python requirements, but +may require the official [AWX CLI](https://docs.ansible.com/ansible-tower/latest/html/towercli/index.html) +in the future. The `DOCUMENTATION` for each module will report this. + +You can specify authentication by a combination of either: + + - host, username, password + - host, OAuth2 token + +The OAuth2 token is the preferred method. You can obtain a token via the +AWX CLI [login](https://docs.ansible.com/ansible-tower/latest/html/towercli/reference.html#awx-login) +command. + +These can be specified via (from highest to lowest precedence): + + - direct module parameters + - environment variables (most useful when running against localhost) + - a config file path specified by the `tower_config_file` parameter + - a config file at `~/.tower_cli.cfg` + - a config file at `/etc/tower/tower_cli.cfg` + +Config file syntax looks like this: + +``` +[general] +host = https://localhost:8043 +verify_ssl = true +oauth_token = LEdCpKVKc4znzffcpQL5vLG8oyeku6 +``` + +## Release and Upgrade Notes + +Notable releases of the `{{ collection_namespace }}.{{ collection_package }}` collection: + +{% if collection_package | lower() == "awx" %} + - 7.0.0 is intended to be identical to the content prior to the migration, aside from changes necessary to function as a collection. + - 11.0.0 has no non-deprecated modules that depend on the deprecated `tower-cli` [PyPI](https://pypi.org/project/ansible-tower-cli/). + - 0.0.1-devel is the version you should see if installing from source, which is intended for development and expected to be unstable. +{% else %} + - 3.7.0 initial release +{% endif %} + +The following notes are changes that may require changes to playbooks: + + - When a project is created, it will wait for the update/sync to finish by default; this can be turned off with the `wait` parameter, if desired. + - Creating a "scan" type job template is no longer supported. + - Specifying a custom certificate via the `TOWER_CERTIFICATE` environment variable no longer works. + - Type changes of variable fields: + + - `extra_vars` in the `tower_job_launch` module worked with a `list` previously, but now only works with a `dict` type + - `extra_vars` in the `tower_workflow_job_template` module worked with a `string` previously but now expects a `dict` + - When the `extra_vars` parameter is used with the `tower_job_launch` module, the launch will fail unless `ask_extra_vars` or `survey_enabled` is explicitly set to `True` on the Job Template + - The `variables` parameter in the `tower_group`, `tower_host` and `tower_inventory` modules now expects a `dict` type and no longer supports the use of `@` syntax for a file + + + - Type changes of other types of fields: + + - `inputs` or `injectors` in the `tower_credential_type` module worked with a string previously but now expects a `dict` + - `schema` in the `tower_workflow_job_template` module worked with a `string` previously but not expects a `list` of `dict`s + + - `tower_group` used to also service inventory sources, but this functionality has been removed from this module; use `tower_inventory_source` instead. + - Specified `tower_config` file used to handle `k=v` pairs on a single line; this is no longer supported. Please use a file formatted as `yaml`, `json` or `ini` only. + - Some return values (e.g., `credential_type`) have been removed. Use of `id` is recommended. + - `tower_job_template` no longer supports the deprecated `extra_vars_path` parameter, please use `extra_vars` with the lookup plugin to replace this functionality. + - The `notification_configuration` parameter of `tower_notification` has changed from a string to a dict. Please use the `lookup` plugin to read an existing file into a dict. + - `tower_credential` no longer supports passing a file name to ssh_key_data. + - The HipChat `notification_type` has been removed and can no longer be created using the `tower_notification` module. + +{% if collection_package | lower() == "awx" %} +## Running Unit Tests + +Tests to verify compatibility with the most recent AWX code are in `awx_collection/test/awx`. +These can be ran by `make test_collection` in the development container. + +To run outside of the development container, or to run against +Ansible source, set up a working environment: + +``` +mkvirtualenv my_new_venv +# may need to replace psycopg2 with psycopg2-binary in requirements/requirements.txt +pip install -r requirements/requirements.txt -r requirements/requirements_dev.txt -r requirements/requirements_git.txt +make clean-api +pip install -e <path to your Ansible> +pip install -e . +pip install -e awxkit +py.test awx_collection/test/awx/ +``` + +## Running Integration Tests + +The integration tests require a virtualenv with `ansible` >= 2.9 and `tower_cli`. +The collection must first be installed, which can be done using `make install_collection`. +You also need a configuration file, as described in the running section. + +Run the tests: + +``` +# ansible-test must be run from the directory in which the collection is installed +cd ~/.ansible/collections/ansible_collections/awx/awx/ +ansible-test integration +``` +{% endif %} + +## Licensing + +All content in this folder is licensed under the same license as Ansible, +which is the same as license that applied before the split into an +independent collection. diff --git a/collections-debian-merged/ansible_collections/awx/awx/tools/roles/template_galaxy/templates/galaxy.yml.j2 b/collections-debian-merged/ansible_collections/awx/awx/tools/roles/template_galaxy/templates/galaxy.yml.j2 new file mode 100644 index 00000000..d6e6a272 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tools/roles/template_galaxy/templates/galaxy.yml.j2 @@ -0,0 +1,38 @@ +# (********************************************************) +# (* *) +# (* WARNING *) +# (* *) +# (* This file is managed by Ansible and not to be *) +# (* edited directly! Instead modify: *) +# (* tools/roles/template_galaxy/templates/galaxy.yml.j2 *) +# (* *) +# (* Changes to the base galaxy.yml file are refreshed *) +# (* upon build of the collection *) +# (********************************************************) +--- +authors: + - AWX Project Contributors <awx-project@googlegroups.com> +dependencies: {} +description: Ansible content that interacts with the AWX or Ansible Tower API. +documentation: https://github.com/ansible/awx/blob/devel/awx_collection/README.md +homepage: https://www.ansible.com/ +issues: https://github.com/ansible/awx/issues?q=is%3Aissue+label%3Acomponent%3Aawx_collection +license: + - GPL-3.0-only +name: {{ collection_package }} +namespace: {{ collection_namespace }} +readme: README.md +repository: https://github.com/ansible/awx +tags: + - cloud + - infrastructure + - awx + - ansible + - automation +version: {{ collection_version_override | default(collection_version) }} +build_ignore: + - tools + - setup.cfg + - galaxy.yml.j2 + - template_galaxy.yml + - '*.tar.gz' diff --git a/collections-debian-merged/ansible_collections/awx/awx/tools/template_galaxy.yml b/collections-debian-merged/ansible_collections/awx/awx/tools/template_galaxy.yml new file mode 100644 index 00000000..d2e88fac --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tools/template_galaxy.yml @@ -0,0 +1,40 @@ +--- +- name: Template the collection galaxy.yml + hosts: localhost + gather_facts: false + connection: local + vars: + collection_package: awx + collection_namespace: awx + collection_version: 0.0.1 # not for updating, pass in extra_vars + collection_source: "{{ playbook_dir }}/../" + collection_path: "{{ playbook_dir }}/../../awx_collection_build" + pre_tasks: + - file: + path: "{{ collection_path }}" + state: absent + + - copy: + src: "{{ collection_source }}" + dest: "{{ collection_path }}" + remote_src: true + + roles: + - template_galaxy + + tasks: + - name: Make substitutions in source to sync with templates + set_fact: + collection_version_override: 0.0.1-devel + + - name: Template the galaxy.yml source file (should be commited with your changes) + template: + src: "{{ collection_source }}/tools/roles/template_galaxy/templates/galaxy.yml.j2" + dest: "{{ collection_source }}/galaxy.yml" + force: true + + - name: Template the README.md source file (should be commited with your changes) + template: + src: "{{ collection_source }}/tools/roles/template_galaxy/templates/README.md.j2" + dest: "{{ collection_source }}/README.md" + force: true diff --git a/collections-debian-merged/ansible_collections/awx/awx/tools/vars/aliases.yml b/collections-debian-merged/ansible_collections/awx/awx/tools/vars/aliases.yml new file mode 100644 index 00000000..03c7735c --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tools/vars/aliases.yml @@ -0,0 +1,29 @@ +--- +aliases: + job_templates: + ask_tags_on_launch: + - ask_tags + ask_verbosity_on_launch: + - ask_verbosity + ask_diff_mode_on_launch: + - ask_diff_mode + allow_simultaneous: + - concurrent_jobs_enabled + diff_mode: + - diff_mode_enabled + ask_inventory_on_launch: + - ask_inventory + limit: + - ask_limit + force_handlers: + - force_handlers_enabled + ask_job_type_on_launch: + - ask_job_type + ask_skip_tags_on_launch: + - ask_skip_tags + use_fact_cache: + - fact_caching_enabled + extra_vars: + - ask_extra_vars + ask_credential_on_launch: + - ask_credential diff --git a/collections-debian-merged/ansible_collections/awx/awx/tools/vars/associations.yml b/collections-debian-merged/ansible_collections/awx/awx/tools/vars/associations.yml new file mode 100644 index 00000000..9d95bd66 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tools/vars/associations.yml @@ -0,0 +1,13 @@ +--- +associations: + job_templates: + - related_item: credentials + endpoint: credentials + description: "The credentials used by this job template" + groups: + - related_item: hosts + endpoint: hosts + description: "The hosts associated with this group" + - related_item: groups + endpoint: children + description: "The hosts associated with this group" diff --git a/collections-debian-merged/ansible_collections/awx/awx/tools/vars/examples.yml b/collections-debian-merged/ansible_collections/awx/awx/tools/vars/examples.yml new file mode 100644 index 00000000..8a8d1328 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tools/vars/examples.yml @@ -0,0 +1,52 @@ +--- +examples: + users: | + - name: Add tower user + tower_user: + username: jdoe + password: foobarbaz + email: jdoe@example.org + first_name: John + last_name: Doe + state: present + tower_config_file: "~/tower_cli.cfg" + + - name: Add tower user as a system administrator + tower_user: + username: jdoe + password: foobarbaz + email: jdoe@example.org + superuser: yes + state: present + tower_config_file: "~/tower_cli.cfg" + + - name: Add tower user as a system auditor + tower_user: + username: jdoe + password: foobarbaz + email: jdoe@example.org + auditor: yes + state: present + tower_config_file: "~/tower_cli.cfg" + + - name: Delete tower user + tower_user: + username: jdoe + email: jdoe@example.org + state: absent + tower_config_file: "~/tower_cli.cfg" + + job_templates: | + - name: Create tower Ping job template + tower_job_template: + name: "Ping" + job_type: "run" + inventory: "Local" + project: "Demo" + playbook: "ping.yml" + credential: "Local" + state: "present" + tower_config_file: "~/tower_cli.cfg" + survey_enabled: yes + survey_spec: "{{ '{{' }} lookup('file', 'my_survey.json') {{ '}}' }}" + custom_virtualenv: "/var/lib/awx/venv/custom-venv/" diff --git a/collections-debian-merged/ansible_collections/awx/awx/tools/vars/generate_for.yml b/collections-debian-merged/ansible_collections/awx/awx/tools/vars/generate_for.yml new file mode 100644 index 00000000..029024d8 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tools/vars/generate_for.yml @@ -0,0 +1,16 @@ +--- +generate_for: + # - credentials + # - credential_types + # - groups + # - hosts + # - inventorues + # - inventory_sources + # - job_templates + # - labels + # - notification_templates + # - organizations + # - projects + - schedules + # - teams + # - users diff --git a/collections-debian-merged/ansible_collections/awx/awx/tools/vars/resolution.yml b/collections-debian-merged/ansible_collections/awx/awx/tools/vars/resolution.yml new file mode 100644 index 00000000..fd6ec096 --- /dev/null +++ b/collections-debian-merged/ansible_collections/awx/awx/tools/vars/resolution.yml @@ -0,0 +1,6 @@ +--- +name_to_id_endpoint_resolution: + webhook_credential: credentials + project: projects + inventory: inventories + organization: organizations |