diff --git a/.gitignore b/.gitignore index 778ea02004..085bd24776 100644 --- a/.gitignore +++ b/.gitignore @@ -134,3 +134,8 @@ venv/* use_dev_supervisor.txt .idea/* + +# Ansible module tests +awx_collection_test_venv/ +awx_collection/*.tar.gz +awx_collection/galaxy.yml diff --git a/Makefile b/Makefile index 7e8c6362df..981dea2484 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,7 @@ COMPOSE_TAG ?= $(GIT_BRANCH) COMPOSE_HOST ?= $(shell hostname) VENV_BASE ?= /venv +COLLECTION_VENV ?= /awx_devel/awx_collection_test_venv SCL_PREFIX ?= CELERY_SCHEDULE_FILE ?= /var/lib/awx/beat.db @@ -375,6 +376,31 @@ test: cd awxkit && $(VENV_BASE)/awx/bin/tox -re py2,py3 awx-manage check_migrations --dry-run --check -n 'vNNN_missing_migration_file' +prepare_collection_venv: + rm -rf $(COLLECTION_VENV) + mkdir $(COLLECTION_VENV) + ln -s /usr/lib/python2.7/site-packages/ansible $(COLLECTION_VENV)/ansible + $(VENV_BASE)/awx/bin/pip install --target=$(COLLECTION_VENV) git+https://github.com/ansible/tower-cli.git + +COLLECTION_TEST_DIRS ?= awx_collection/test/awx +COLLECTION_PACKAGE ?= awx +COLLECTION_NAMESPACE ?= awx + +test_collection: + @if [ "$(VENV_BASE)" ]; then \ + . $(VENV_BASE)/awx/bin/activate; \ + fi; \ + PYTHONPATH=$(COLLECTION_VENV):/awx_devel/awx_collection:$PYTHONPATH py.test $(COLLECTION_TEST_DIRS) + +flake8_collection: + flake8 awx_collection/ # Different settings, in main exclude list + +test_collection_all: prepare_collection_venv test_collection flake8_collection + +build_collection: + ansible-playbook -i localhost, awx_collection/template_galaxy.yml -e collection_package=$(COLLECTION_PACKAGE) -e namespace_name=$(COLLECTION_NAMESPACE) -e package_version=$(VERSION) + ansible-galaxy collection build awx_collection --output-path=awx_collection + test_unit: @if [ "$(VENV_BASE)" ]; then \ . $(VENV_BASE)/awx/bin/activate; \ diff --git a/awx_collection/COPYING b/awx_collection/COPYING new file mode 100644 index 0000000000..b743e04edb --- /dev/null +++ b/awx_collection/COPYING @@ -0,0 +1,674 @@ +GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. +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. + + +Copyright (C) + +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 . + +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: + + Copyright (C) +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 +. + +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 +. diff --git a/awx_collection/README.md b/awx_collection/README.md new file mode 100644 index 0000000000..fe65d0d013 --- /dev/null +++ b/awx_collection/README.md @@ -0,0 +1,58 @@ +# AWX Ansible Collection + +This Ansible collection allows for easy interaction with an AWX or Ansible Tower +server in Ansible playbooks. + +The previous home for this collection was in https://github.com/ansible/ansible +inside the folder `lib/ansible/modules/web_infrastructure/ansible_tower` +as well as other folders for the inventory plugin, module utils, and +doc fragment. + +## Running + +To use this collection, the "old" tower-cli needs to be installed +in the virtual environment where the collection runs. +You can install it from either: + + - https://github.com/ansible/tower-cli/ + - https://pypi.org/project/ansible-tower-cli/ + +To use this collection in AWX, you should create a custom virtual environment +to install the requirement into. NOTE: running locally, you will also need +to set the job template extra_vars to include `ansible_python_interpreter` +to be the python in that virtual environment. + +## Running Tests + +Tests to verify compatibility with the most recent AWX code are +in `awx_collection/test/awx`. These tests require that python packages +are available for all of `awx`, `ansible`, `tower_cli`, and the collection +itself. + +The target `make prepare_collection_venv` will prepare some requirements +in the `awx_collection_test_venv` folder so that `make test_collection` can +be ran to actually run the tests. A single test can be ran via: + +``` +make test_collection MODULE_TEST_DIRS=awx_collection/test/awx/test_organization.py +``` + +## Building + +The build target `make build_collection` will template out a `galaxy.yml` file +with automatic detection of the current AWX version. Then it builds the +collection with the `ansible-galaxy` CLI. + +## Roadmap + +Major future development items on the agenda include: + + - Removing tower-cli as a dependency + - Renaming the modules, for example `tower_organization` to just `organization` + and giving a deprecation period for the switch + +## 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/awx_collection/galaxy.yml.j2 b/awx_collection/galaxy.yml.j2 new file mode 100644 index 0000000000..c573edc709 --- /dev/null +++ b/awx_collection/galaxy.yml.j2 @@ -0,0 +1,16 @@ +authors: +- Wayne Witzel III (@wwitzel3) +- Chris Meyers +dependencies: {} +description: Ansible modules that interact with the AWX API. +documentation: https://docs.ansible.com/ansible/latest/modules/list_of_web_infrastructure_modules.html#ansible-tower +homepage: https://ansible.com +issues: https://github.com/ansible/awx/issues +license: +- GPL-3.0-only +name: {{ collection_package }} +namespace: {{ collection_namespace }} +readme: README.md +repository: https://github.com/ansible/awx +tags: [] +version: {{ collection_version }} diff --git a/awx_collection/plugins/doc_fragments/auth.py b/awx_collection/plugins/doc_fragments/auth.py new file mode 100644 index 0000000000..5583710201 --- /dev/null +++ b/awx_collection/plugins/doc_fragments/auth.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Wayne Witzel III +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +class ModuleDocFragment(object): + + # Ansible Tower documentation fragment + DOCUMENTATION = r''' +options: + tower_host: + description: + - URL to your Tower or AWX instance. + type: str + tower_username: + description: + - Username for your Tower or AWX instance. + type: str + tower_password: + description: + - Password for your Tower or AWX instance. + 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. + type: bool + aliases: [ tower_verify_ssl ] + tower_config_file: + description: + - Path to the Tower or AWX config file. + type: path + +requirements: +- ansible-tower-cli >= 3.0.2 + +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/awx_collection/plugins/inventory/tower.py b/awx_collection/plugins/inventory/tower.py new file mode 100644 index 0000000000..f4f20fe509 --- /dev/null +++ b/awx_collection/plugins/inventory/tower.py @@ -0,0 +1,221 @@ +# 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. + version_added: "2.7" + 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. + options: + plugin: + description: the name of this plugin, it should always be set to 'tower' + for this plugin to recognize it as it's own. + env: + - name: ANSIBLE_INVENTORY_ENABLED + required: True + choices: ['tower'] + host: + description: The network address of your Ansible Tower host. + type: string + env: + - name: TOWER_HOST + required: True + username: + description: The user that you plan to use to access inventories on Ansible Tower. + type: string + env: + - name: TOWER_USERNAME + required: True + password: + description: The password for your Ansible Tower user. + type: string + env: + - name: TOWER_PASSWORD + required: True + 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 + validate_certs: + description: Specify whether Ansible should verify the SSL certificate of Ansible Tower host. + type: bool + default: True + env: + - name: TOWER_VERIFY_SSL + required: False + aliases: [ verify_ssl ] + include_metadata: + description: Make extra requests to provide all group vars with metadata about the source Ansible Tower host. + type: bool + default: False + version_added: "2.8" +''' + +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: 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 re +import os +import json +from ansible.module_utils import six +from ansible.module_utils.urls import Request, urllib_error, ConnectionError, socket, httplib +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 + +# Python 2/3 Compatibility +try: + from urlparse import urljoin +except ImportError: + from urllib.parse import urljoin + + +class InventoryModule(BaseInventoryPlugin): + NAME = 'tower' + # 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 make_request(self, request_handler, tower_url): + """Makes the request to given URL, handles errors, returns JSON + """ + try: + response = request_handler.get(tower_url) + except (ConnectionError, urllib_error.URLError, socket.error, httplib.HTTPException) as e: + n_error_msg = 'Connection to remote host failed: {err}'.format(err=to_native(e)) + # If Tower gives a readable error message, display that message to the user. + if callable(getattr(e, 'read', None)): + n_error_msg += ' with message: {err_msg}'.format(err_msg=to_native(e.read())) + raise AnsibleParserError(n_error_msg) + + # Attempt to parse JSON. + try: + return json.loads(response.read()) + except (ValueError, TypeError) as e: + # If the JSON parse fails, print the ValueError + raise AnsibleParserError('Failed to parse json from host: {err}'.format(err=to_native(e))) + + 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 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) + # Read inventory from tower server. + # Note the environment variables will be handled automatically by InventoryManager. + tower_host = self.get_option('host') + if not re.match('(?:http|https)://', tower_host): + tower_host = 'https://{tower_host}'.format(tower_host=tower_host) + + request_handler = Request(url_username=self.get_option('username'), + url_password=self.get_option('password'), + force_basic_auth=True, + validate_certs=self.get_option('validate_certs')) + + # 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/?hostvars=1&towervars=1&all=1'.format(inv_id=inventory_id) + inventory_url = urljoin(tower_host, inventory_url) + + inventory = self.make_request(request_handler, inventory_url) + # 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_url = urljoin(tower_host, '/api/v2/config/') + config_data = self.make_request(request_handler, config_url) + 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/awx_collection/plugins/module_utils/ansible_tower.py b/awx_collection/plugins/module_utils/ansible_tower.py new file mode 100644 index 0000000000..ef687a669c --- /dev/null +++ b/awx_collection/plugins/module_utils/ansible_tower.py @@ -0,0 +1,113 @@ +# 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 +# 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. + +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, 'rb') 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 TowerModule(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) + + mutually_exclusive = kwargs.get('mutually_exclusive', []) + kwargs['mutually_exclusive'] = mutually_exclusive.extend(( + ('tower_config_file', 'tower_host'), + ('tower_config_file', 'tower_username'), + ('tower_config_file', 'tower_password'), + ('tower_config_file', 'validate_certs'), + )) + + super(TowerModule, 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/awx_collection/plugins/modules/__init__.py b/awx_collection/plugins/modules/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/awx_collection/plugins/modules/tower_credential.py b/awx_collection/plugins/modules/tower_credential.py new file mode 100644 index 0000000000..7ac08b28d6 --- /dev/null +++ b/awx_collection/plugins/modules/tower_credential.py @@ -0,0 +1,362 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright: (c) 2017, Wayne Witzel III +# 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)" +version_added: "2.3" +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 + description: + description: + - The description to use for the credential. + type: str + user: + description: + - User that should own this credential. + type: str + team: + description: + - Team that should own this credential. + type: str + project: + description: + - Project that should for this credential. + type: str + organization: + description: + - Organization that should own the credential. + required: True + type: str + kind: + description: + - Type of credential being added. + - The ssh choice refers to a Tower Machine credential. + required: True + type: str + choices: ["ssh", "vault", "net", "scm", "aws", "vmware", "satellite6", "cloudforms", "gce", "azure_rm", "openstack", "rhv", "insights", "tower"] + host: + description: + - Host for this credential. + type: str + username: + description: + - Username for this credential. ``access_key`` for AWS. + 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. + type: str + ssh_key_data: + description: + - SSH private key content. To extract the content from a file path, use the lookup function (see examples). + required: False + type: str + ssh_key_unlock: + description: + - Unlock password for ssh_key. + - Use "ASK" and launch in Tower to be prompted. + type: str + authorize: + description: + - Should use authorize for net type. + type: bool + default: 'no' + authorize_password: + description: + - Password for net credentials that require authorize. + type: str + client: + description: + - Client or application ID for azure_rm type. + type: str + security_token: + description: + - STS token for aws type. + version_added: "2.6" + type: str + secret: + description: + - Secret token for azure_rm type. + type: str + subscription: + description: + - Subscription ID for azure_rm type. + type: str + tenant: + description: + - Tenant ID for azure_rm type. + type: str + domain: + description: + - Domain for openstack type. + type: str + become_method: + description: + - Become method to use for privilege escalation. + choices: ["None", "sudo", "su", "pbrun", "pfexec", "pmrun"] + type: str + become_username: + description: + - Become username. + - Use "ASK" and launch in Tower to be prompted. + type: str + become_password: + description: + - Become password. + - Use "ASK" and launch in Tower to be prompted. + type: str + vault_password: + description: + - Vault password. + - Use "ASK" and launch in Tower to be prompted. + type: str + vault_id: + description: + - Vault identifier. + - This parameter is only valid if C(kind) is specified as C(vault). + type: str + version_added: "2.8" + state: + description: + - Desired state of the resource. + choices: ["present", "absent"] + default: "present" + type: str +extends_documentation_fragment: awx.awx.auth +''' + + +EXAMPLES = ''' +- name: Add tower credential + tower_credential: + name: Team Name + description: Team Description + organization: test-org + kind: ssh + 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 + kind: scm + 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 + ssh_key_data: "{{ aws_ssh_key['content'] | b64decode }}" + kind: ssh + organization: Default + tower_username: admin + tower_password: ansible + tower_host: https://localhost + run_once: true + delegate_to: localhost +''' + +import os + +from ansible.module_utils._text import to_text +from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode + +try: + import tower_cli + import tower_cli.exceptions as exc + + from tower_cli.conf import settings +except ImportError: + pass + + +KIND_CHOICES = { + 'ssh': 'Machine', + 'vault': 'Ansible 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', +} + + +def credential_type_for_v1_kind(params, module): + credential_type_res = tower_cli.get_resource('credential_type') + kind = params.pop('kind') + arguments = {'managed_by_tower': True} + if kind == 'ssh': + if params.get('vault_password'): + arguments['kind'] = 'vault' + else: + arguments['kind'] = 'ssh' + elif kind in ('net', 'scm', 'insights', 'vault'): + arguments['kind'] = kind + elif kind in KIND_CHOICES: + arguments.update(dict( + kind='cloud', + name=KIND_CHOICES[kind] + )) + return credential_type_res.get(**arguments) + + +def main(): + + argument_spec = dict( + name=dict(required=True), + user=dict(), + team=dict(), + kind=dict(required=True, + choices=KIND_CHOICES.keys()), + host=dict(), + username=dict(), + password=dict(no_log=True), + ssh_key_data=dict(no_log=True, type='str'), + ssh_key_unlock=dict(no_log=True), + authorize=dict(type='bool', default=False), + authorize_password=dict(no_log=True), + client=dict(), + security_token=dict(), + secret=dict(), + tenant=dict(), + subscription=dict(), + domain=dict(), + become_method=dict(), + become_username=dict(), + become_password=dict(no_log=True), + vault_password=dict(no_log=True), + description=dict(), + organization=dict(required=True), + project=dict(), + state=dict(choices=['present', 'absent'], default='present'), + vault_id=dict(), + ) + + module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) + + name = module.params.get('name') + organization = module.params.get('organization') + state = module.params.get('state') + + json_output = {'credential': name, 'state': state} + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + tower_check_mode(module) + credential = tower_cli.get_resource('credential') + try: + params = {} + params['create_on_missing'] = True + params['name'] = name + + if organization: + org_res = tower_cli.get_resource('organization') + org = org_res.get(name=organization) + params['organization'] = org['id'] + + try: + tower_cli.get_resource('credential_type') + except (ImportError, AttributeError): + # /api/v1/ backwards compat + # older versions of tower-cli don't *have* a credential_type + # resource + params['kind'] = module.params['kind'] + else: + credential_type = credential_type_for_v1_kind(module.params, module) + params['credential_type'] = credential_type['id'] + + if module.params.get('description'): + params['description'] = module.params.get('description') + + if module.params.get('user'): + user_res = tower_cli.get_resource('user') + user = user_res.get(username=module.params.get('user')) + params['user'] = user['id'] + + if module.params.get('team'): + team_res = tower_cli.get_resource('team') + team = team_res.get(name=module.params.get('team')) + params['team'] = team['id'] + + if module.params.get('ssh_key_data'): + data = module.params.get('ssh_key_data') + if os.path.exists(data): + module.deprecate( + msg='ssh_key_data should be a string, not a path to a file.', + version="2.12" + ) + if os.path.isdir(data): + module.fail_json(msg='attempted to read contents of directory: %s' % data) + with open(data, 'rb') as f: + module.params['ssh_key_data'] = to_text(f.read()) + else: + module.params['ssh_key_data'] = data + + if module.params.get('vault_id', None) and module.params.get('kind') != 'vault': + module.fail_json(msg="Parameter 'vault_id' is only valid if parameter 'kind' is specified as 'vault'") + + for key in ('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'): + if 'kind' in params: + params[key] = module.params.get(key) + elif module.params.get(key): + params.setdefault('inputs', {})[key] = module.params.get(key) + + if state == 'present': + result = credential.modify(**params) + json_output['id'] = result['id'] + elif state == 'absent': + result = credential.delete(**params) + except (exc.NotFound) as excinfo: + module.fail_json(msg='Failed to update credential, organization not found: {0}'.format(excinfo), changed=False) + except (exc.ConnectionError, exc.BadRequest, exc.NotFound, exc.AuthError) as excinfo: + module.fail_json(msg='Failed to update credential: {0}'.format(excinfo), changed=False) + + json_output['changed'] = result['changed'] + module.exit_json(**json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_credential_type.py b/awx_collection/plugins/modules/tower_credential_type.py new file mode 100644 index 0000000000..0fd6ebd046 --- /dev/null +++ b/awx_collection/plugins/modules/tower_credential_type.py @@ -0,0 +1,174 @@ +#!/usr/bin/python +# coding: utf-8 -*- +# +# (c) 2018, Adrien Fleury +# 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)" +version_added: "2.7" +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 + description: + description: + - The description of the credential type to give more detail about it. + required: False + 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' ] + required: False + inputs: + description: + - >- + Enter inputs using either JSON or YAML syntax. Refer to the Ansible + Tower documentation for example syntax. + required: False + injectors: + description: + - >- + Enter injectors using either JSON or YAML syntax. Refer to the + Ansible Tower documentation for example syntax. + required: False + state: + description: + - Desired state of the resource. + required: False + default: "present" + choices: ["present", "absent"] + validate_certs: + description: + - Tower option to avoid certificates check. + required: False + type: bool + aliases: [ tower_verify_ssl ] +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.ansible_tower import ( + TowerModule, + tower_auth_config, + tower_check_mode +) + +try: + import tower_cli + import tower_cli.exceptions as exc + from tower_cli.conf import settings +except ImportError: + pass + + +KIND_CHOICES = { + 'ssh': 'Machine', + 'vault': 'Ansible Vault', + 'net': 'Network', + 'scm': 'Source Control', + 'cloud': 'Lots of others', + 'insights': 'Insights' +} + + +def main(): + argument_spec = dict( + name=dict(required=True), + description=dict(required=False), + kind=dict(required=False, choices=KIND_CHOICES.keys()), + inputs=dict(type='dict', required=False), + injectors=dict(type='dict', required=False), + state=dict(choices=['present', 'absent'], default='present'), + ) + + module = TowerModule( + argument_spec=argument_spec, + supports_check_mode=False + ) + + name = module.params.get('name') + kind = module.params.get('kind') + state = module.params.get('state') + + json_output = {'credential_type': name, 'state': state} + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + tower_check_mode(module) + credential_type_res = tower_cli.get_resource('credential_type') + + params = {} + params['name'] = name + params['kind'] = kind + params['managed_by_tower'] = False + + if module.params.get('description'): + params['description'] = module.params.get('description') + + if module.params.get('inputs'): + params['inputs'] = module.params.get('inputs') + + if module.params.get('injectors'): + params['injectors'] = module.params.get('injectors') + + try: + if state == 'present': + params['create_on_missing'] = True + result = credential_type_res.modify(**params) + json_output['id'] = result['id'] + elif state == 'absent': + params['fail_on_missing'] = False + result = credential_type_res.delete(**params) + + except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo: + module.fail_json( + msg='Failed to update credential type: {0}'.format(excinfo), + changed=False + ) + + json_output['changed'] = result['changed'] + module.exit_json(**json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_group.py b/awx_collection/plugins/modules/tower_group.py new file mode 100644 index 0000000000..7fe717666a --- /dev/null +++ b/awx_collection/plugins/modules/tower_group.py @@ -0,0 +1,177 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III +# 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)" +version_added: "2.3" +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 + description: + description: + - The description to use for the group. + inventory: + description: + - Inventory the group should be made a member of. + required: True + variables: + description: + - Variables to use for the group, use C(@) for a file. + credential: + description: + - Credential to use for the group. + source: + description: + - The source to use for this group. + choices: ["manual", "file", "ec2", "rax", "vmware", "gce", "azure", "azure_rm", "openstack", "satellite6" , "cloudforms", "custom"] + source_regions: + description: + - Regions for cloud provider. + source_vars: + description: + - Override variables from source with variables from this field. + instance_filters: + description: + - Comma-separated list of filter expressions for matching hosts. + group_by: + description: + - Limit groups automatically created from inventory source. + source_script: + description: + - Inventory script to be used when group type is C(custom). + overwrite: + description: + - Delete child groups and hosts not found in source. + type: bool + default: 'no' + overwrite_vars: + description: + - Override vars in child groups and hosts with those from external source. + update_on_launch: + description: + - Refresh inventory data from its source each time a job is run. + type: bool + default: 'no' + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] +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" +''' + +import os + +from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode + +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(), + inventory=dict(required=True), + variables=dict(), + credential=dict(), + source=dict(choices=["manual", "file", "ec2", "rax", "vmware", + "gce", "azure", "azure_rm", "openstack", + "satellite6", "cloudforms", "custom"], default="manual"), + source_regions=dict(), + source_vars=dict(), + instance_filters=dict(), + group_by=dict(), + source_script=dict(), + overwrite=dict(type='bool', default=False), + overwrite_vars=dict(), + update_on_launch=dict(type='bool', default=False), + state=dict(choices=['present', 'absent'], default='present'), + ) + + module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) + + name = module.params.get('name') + inventory = module.params.get('inventory') + credential = module.params.get('credential') + state = module.params.pop('state') + + variables = module.params.get('variables') + if variables: + if variables.startswith('@'): + filename = os.path.expanduser(variables[1:]) + with open(filename, 'r') as f: + variables = f.read() + + json_output = {'group': name, 'state': state} + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + tower_check_mode(module) + group = tower_cli.get_resource('group') + try: + params = module.params.copy() + params['create_on_missing'] = True + params['variables'] = variables + + inv_res = tower_cli.get_resource('inventory') + inv = inv_res.get(name=inventory) + params['inventory'] = inv['id'] + + if credential: + cred_res = tower_cli.get_resource('credential') + cred = cred_res.get(name=credential) + params['credential'] = cred['id'] + + if state == 'present': + result = group.modify(**params) + json_output['id'] = result['id'] + elif state == 'absent': + result = group.delete(**params) + except (exc.NotFound) as excinfo: + module.fail_json(msg='Failed to update the group, inventory not found: {0}'.format(excinfo), changed=False) + except (exc.ConnectionError, exc.BadRequest, exc.NotFound, exc.AuthError) as excinfo: + module.fail_json(msg='Failed to update the group: {0}'.format(excinfo), changed=False) + + json_output['changed'] = result['changed'] + module.exit_json(**json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_host.py b/awx_collection/plugins/modules/tower_host.py new file mode 100644 index 0000000000..02946cf4d7 --- /dev/null +++ b/awx_collection/plugins/modules/tower_host.py @@ -0,0 +1,131 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III +# 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 +version_added: "2.3" +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 + description: + description: + - The description to use for the host. + inventory: + description: + - Inventory the host should be made a member of. + required: True + enabled: + description: + - If the host should be enabled. + type: bool + default: 'yes' + variables: + description: + - Variables to use for the host. Use C(@) for a file. + state: + description: + - Desired state of the resource. + choices: ["present", "absent"] + default: "present" +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 +''' + + +import os + +from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode + +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(), + inventory=dict(required=True), + enabled=dict(type='bool', default=True), + variables=dict(), + state=dict(choices=['present', 'absent'], default='present'), + ) + module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) + + name = module.params.get('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') + if variables: + if variables.startswith('@'): + filename = os.path.expanduser(variables[1:]) + with open(filename, 'r') as f: + variables = f.read() + + json_output = {'host': name, 'state': state} + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + tower_check_mode(module) + host = tower_cli.get_resource('host') + + try: + inv_res = tower_cli.get_resource('inventory') + inv = inv_res.get(name=inventory) + + if state == 'present': + result = host.modify(name=name, inventory=inv['id'], enabled=enabled, + variables=variables, description=description, create_on_missing=True) + json_output['id'] = result['id'] + elif state == 'absent': + result = host.delete(name=name, inventory=inv['id']) + except (exc.NotFound) as excinfo: + module.fail_json(msg='Failed to update host, inventory not found: {0}'.format(excinfo), changed=False) + except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo: + module.fail_json(msg='Failed to update host: {0}'.format(excinfo), changed=False) + + json_output['changed'] = result['changed'] + module.exit_json(**json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_inventory.py b/awx_collection/plugins/modules/tower_inventory.py new file mode 100644 index 0000000000..811430a723 --- /dev/null +++ b/awx_collection/plugins/modules/tower_inventory.py @@ -0,0 +1,130 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III +# 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 +version_added: "2.3" +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 + description: + description: + - The description to use for the inventory. + organization: + description: + - Organization the inventory belongs to. + required: True + variables: + description: + - Inventory variables. Use C(@) to get from file. + kind: + description: + - The kind field. Cannot be modified after created. + default: "" + choices: ["", "smart"] + version_added: "2.7" + host_filter: + description: + - The host_filter field. Only useful when C(kind=smart). + version_added: "2.7" + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] +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.ansible_tower import TowerModule, tower_auth_config, tower_check_mode + +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(), + organization=dict(required=True), + variables=dict(), + kind=dict(choices=['', 'smart'], default=''), + host_filter=dict(), + state=dict(choices=['present', 'absent'], default='present'), + ) + + module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) + + 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') + + json_output = {'inventory': name, 'state': state} + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + tower_check_mode(module) + inventory = tower_cli.get_resource('inventory') + + try: + org_res = tower_cli.get_resource('organization') + org = org_res.get(name=organization) + + if state == 'present': + result = inventory.modify(name=name, organization=org['id'], variables=variables, + description=description, kind=kind, host_filter=host_filter, + create_on_missing=True) + json_output['id'] = result['id'] + elif state == 'absent': + result = inventory.delete(name=name, organization=org['id']) + except (exc.NotFound) as excinfo: + module.fail_json(msg='Failed to update inventory, organization not found: {0}'.format(excinfo), changed=False) + except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo: + module.fail_json(msg='Failed to update inventory: {0}'.format(excinfo), changed=False) + + json_output['changed'] = result['changed'] + module.exit_json(**json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_inventory_source.py b/awx_collection/plugins/modules/tower_inventory_source.py new file mode 100644 index 0000000000..ede0896d64 --- /dev/null +++ b/awx_collection/plugins/modules/tower_inventory_source.py @@ -0,0 +1,315 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright: (c) 2018, Adrien Fleury +# 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_inventory_source +author: "Adrien Fleury (@fleu42)" +version_added: "2.7" +short_description: create, update, or destroy Ansible Tower inventory source. +description: + - Create, update, or destroy Ansible Tower inventories source. See + U(https://www.ansible.com/tower) for an overview. +options: + name: + description: + - The name to use for the inventory source. + required: True + description: + description: + - The description to use for the inventory source. + inventory: + description: + - The inventory the source is linked to. + required: True + source: + description: + - Types of inventory source. + choices: + - file + - scm + - ec2 + - gce + - azure + - azure_rm + - vmware + - satellite6 + - cloudforms + - openstack + - rhv + - tower + - custom + required: True + credential: + description: + - Credential to use to retrieve the inventory from. + source_vars: + description: + - >- + The source_vars allow to Override variables found in the source config + file. For example with Openstack, specifying *private: false* would + change the output of the openstack.py script. It has to be YAML or + JSON. + timeout: + description: + - Number in seconds after which the Tower API methods will time out. + source_project: + description: + - Use a *project* as a source for the *inventory*. + source_path: + description: + - Path to the file to use as a source in the selected *project*. + update_on_project_update: + description: + - >- + That parameter will sync the inventory when the project is synced. It + can only be used with a SCM source. + type: bool + source_regions: + description: + - >- + List of regions for your cloud provider. You can include multiple all + regions. Only Hosts associated with the selected regions will be + updated. Refer to Ansible Tower documentation for more detail. + instance_filters: + description: + - >- + Provide a comma-separated list of filter expressions. Hosts are + imported when all of the filters match. Refer to Ansible Tower + documentation for more detail. + group_by: + description: + - >- + Specify which groups to create automatically. Group names will be + created similar to the options selected. If blank, all groups above + are created. Refer to Ansible Tower documentation for more detail. + source_script: + description: + - >- + The source custom script to use to build the inventory. It needs to + exist. + overwrite: + description: + - >- + If set, any hosts and groups that were previously present on the + external source but are now removed will be removed from the Tower + inventory. Hosts and groups that were not managed by the inventory + source will be promoted to the next manually created group or if + there is no manually created group to promote them into, they will be + left in the "all" default group for the inventory. When not checked, + local child hosts and groups not found on the external source will + remain untouched by the inventory update process. + type: bool + overwrite_vars: + description: + - >- + If set, all variables for child groups and hosts will be removed + and replaced by those found on the external source. When not checked, + a merge will be performed, combining local variables with those found + on the external source. + type: bool + update_on_launch: + description: + - >- + Each time a job runs using this inventory, refresh the inventory from + the selected source before executing job tasks. + type: bool + update_cache_timeout: + description: + - >- + Time in seconds to consider an inventory sync to be current. During + job runs and callbacks the task system will evaluate the timestamp of + the latest sync. If it is older than Cache Timeout, it is not + considered current, and a new inventory sync will be performed. + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] + validate_certs: + description: + - Tower option to avoid certificates check. + type: bool + aliases: [ tower_verify_ssl ] +extends_documentation_fragment: awx.awx.auth +''' + + +EXAMPLES = ''' +- name: Add tower inventory source + tower_inventory_source: + name: Inventory source + description: My Inventory source + inventory: My inventory + credential: Devstack_credential + source: openstack + update_on_launch: true + overwrite: true + source_vars: '{ private: false }' + state: present + validate_certs: false +''' + + +RETURN = ''' # ''' + + +from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode + +try: + import tower_cli + import tower_cli.exceptions as exc + from tower_cli.conf import settings +except ImportError: + pass + + +SOURCE_CHOICES = { + 'file': 'Directory or Script', + 'scm': 'Sourced from a Project', + 'ec2': 'Amazon EC2', + 'gce': 'Google Compute Engine', + 'azure': 'Microsoft Azure', + 'azure_rm': 'Microsoft Azure Resource Manager', + 'vmware': 'VMware vCenter', + 'satellite6': 'Red Hat Satellite 6', + 'cloudforms': 'Red Hat CloudForms', + 'openstack': 'OpenStack', + 'rhv': 'Red Hat Virtualization', + 'tower': 'Ansible Tower', + 'custom': 'Custom Script', +} + + +def main(): + argument_spec = dict( + name=dict(required=True), + description=dict(required=False), + inventory=dict(required=True), + source=dict(required=True, + choices=SOURCE_CHOICES.keys()), + credential=dict(required=False), + source_vars=dict(required=False), + timeout=dict(type='int', required=False), + source_project=dict(required=False), + source_path=dict(required=False), + update_on_project_update=dict(type='bool', required=False), + source_regions=dict(required=False), + instance_filters=dict(required=False), + group_by=dict(required=False), + source_script=dict(required=False), + overwrite=dict(type='bool', required=False), + overwrite_vars=dict(type='bool', required=False), + update_on_launch=dict(type='bool', required=False), + update_cache_timeout=dict(type='int', required=False), + state=dict(choices=['present', 'absent'], default='present'), + ) + + module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) + + name = module.params.get('name') + inventory = module.params.get('inventory') + source = module.params.get('source') + state = module.params.get('state') + + json_output = {'inventory_source': name, 'state': state} + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + tower_check_mode(module) + inventory_source = tower_cli.get_resource('inventory_source') + try: + params = {} + params['name'] = name + params['source'] = source + + if module.params.get('description'): + params['description'] = module.params.get('description') + + if module.params.get('credential'): + credential_res = tower_cli.get_resource('credential') + try: + credential = credential_res.get( + name=module.params.get('credential')) + params['credential'] = credential['id'] + except (exc.NotFound) as excinfo: + module.fail_json( + msg='Failed to update credential source,' + 'credential not found: {0}'.format(excinfo), + changed=False + ) + + if module.params.get('source_project'): + source_project_res = tower_cli.get_resource('project') + try: + source_project = source_project_res.get( + name=module.params.get('source_project')) + params['source_project'] = source_project['id'] + except (exc.NotFound) as excinfo: + module.fail_json( + msg='Failed to update source project,' + 'project not found: {0}'.format(excinfo), + changed=False + ) + + if module.params.get('source_script'): + source_script_res = tower_cli.get_resource('inventory_script') + try: + script = source_script_res.get( + name=module.params.get('source_script')) + params['source_script'] = script['id'] + except (exc.NotFound) as excinfo: + module.fail_json( + msg='Failed to update source script,' + 'script not found: {0}'.format(excinfo), + changed=False + ) + + try: + inventory_res = tower_cli.get_resource('inventory') + params['inventory'] = inventory_res.get(name=inventory)['id'] + except (exc.NotFound) as excinfo: + module.fail_json( + msg='Failed to update inventory source, ' + 'inventory not found: {0}'.format(excinfo), + changed=False + ) + + for key in ('source_vars', 'timeout', 'source_path', + 'update_on_project_update', 'source_regions', + 'instance_filters', 'group_by', 'overwrite', + 'overwrite_vars', 'update_on_launch', + 'update_cache_timeout'): + if module.params.get(key) is not None: + params[key] = module.params.get(key) + + if state == 'present': + params['create_on_missing'] = True + result = inventory_source.modify(**params) + json_output['id'] = result['id'] + elif state == 'absent': + params['fail_on_missing'] = False + result = inventory_source.delete(**params) + + except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo: + module.fail_json(msg='Failed to update inventory source: \ + {0}'.format(excinfo), changed=False) + + json_output['changed'] = result['changed'] + module.exit_json(**json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_job_cancel.py b/awx_collection/plugins/modules/tower_job_cancel.py new file mode 100644 index 0000000000..f7a80b9f71 --- /dev/null +++ b/awx_collection/plugins/modules/tower_job_cancel.py @@ -0,0 +1,101 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III +# 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)" +version_added: "2.3" +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 + fail_if_not_running: + description: + - Fail loudly if the I(job_id) does not reference a running job. + 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 +status: + description: status of the cancel request + returned: success + type: str + sample: canceled +''' + + +from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode + +try: + import tower_cli + import tower_cli.exceptions as exc + + from tower_cli.conf import settings +except ImportError: + pass + + +def main(): + argument_spec = dict( + job_id=dict(type='int', required=True), + fail_if_not_running=dict(type='bool', default=False), + ) + + module = TowerModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + job_id = module.params.get('job_id') + json_output = {} + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + tower_check_mode(module) + job = tower_cli.get_resource('job') + params = module.params.copy() + + try: + result = job.cancel(job_id, **params) + json_output['id'] = job_id + except (exc.ConnectionError, exc.BadRequest, exc.TowerCLIError, exc.AuthError) as excinfo: + module.fail_json(msg='Unable to cancel job_id/{0}: {1}'.format(job_id, excinfo), changed=False) + + json_output['changed'] = result['changed'] + json_output['status'] = result['status'] + module.exit_json(**json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_job_launch.py b/awx_collection/plugins/modules/tower_job_launch.py new file mode 100644 index 0000000000..792632fbb4 --- /dev/null +++ b/awx_collection/plugins/modules/tower_job_launch.py @@ -0,0 +1,159 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III +# 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)" +version_added: "2.3" +short_description: Launch an Ansible Job. +description: + - Launch an Ansible Tower jobs. See + U(https://www.ansible.com/tower) for an overview. +options: + job_template: + description: + - Name of the job template to use. + required: True + job_explanation: + description: + - Job explanation field. + job_type: + description: + - Job_type to use for the job, only used if prompt for job_type is set. + choices: ["run", "check", "scan"] + inventory: + description: + - Inventory to use for the job, only used if prompt for inventory is set. + credential: + description: + - Credential to use for job, only used if prompt for credential is set. + extra_vars: + description: + - Extra_vars to use for the job_template. Prepend C(@) if a file. + limit: + description: + - Limit to use for the I(job_template). + tags: + description: + - Specific tags to use for from playbook. + use_job_endpoint: + description: + - Disable launching jobs from job template. + type: bool + default: 'no' +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +# Launch a job template +- 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 + +# Launch job template with inventory and credential for prompt on launch +- 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.ansible_tower import TowerModule, tower_auth_config, tower_check_mode + +try: + import tower_cli + import tower_cli.exceptions as exc + + from tower_cli.conf import settings +except ImportError: + pass + + +def main(): + argument_spec = dict( + job_template=dict(required=True, type='str'), + job_type=dict(choices=['run', 'check', 'scan']), + inventory=dict(type='str', default=None), + credential=dict(type='str', default=None), + limit=dict(), + tags=dict(type='list'), + extra_vars=dict(type='list'), + ) + + module = TowerModule( + argument_spec, + supports_check_mode=True + ) + + json_output = {} + tags = module.params.get('tags') + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + tower_check_mode(module) + try: + params = module.params.copy() + if isinstance(tags, list): + params['tags'] = ','.join(tags) + job = tower_cli.get_resource('job') + + lookup_fields = ('job_template', 'inventory', 'credential') + for field in lookup_fields: + try: + name = params.pop(field) + if name: + result = tower_cli.get_resource(field).get(name=name) + params[field] = result['id'] + except exc.NotFound as excinfo: + module.fail_json(msg='Unable to launch job, {0}/{1} was not found: {2}'.format(field, name, excinfo), changed=False) + + result = job.launch(no_input=True, **params) + json_output['id'] = result['id'] + json_output['status'] = result['status'] + except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo: + module.fail_json(msg='Unable to launch job: {0}'.format(excinfo), changed=False) + + json_output['changed'] = result['changed'] + module.exit_json(**json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_job_list.py b/awx_collection/plugins/modules/tower_job_list.py new file mode 100644 index 0000000000..ef8d8b3b15 --- /dev/null +++ b/awx_collection/plugins/modules/tower_job_list.py @@ -0,0 +1,128 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III +# 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)" +version_added: "2.3" +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'] + page: + description: + - Page number of the results to fetch. + 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) +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.ansible_tower import TowerModule, tower_auth_config, tower_check_mode + +try: + import tower_cli + import tower_cli.exceptions as exc + + from tower_cli.conf import settings +except ImportError: + pass + + +def main(): + 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'), + ) + + module = TowerModule( + argument_spec=argument_spec, + supports_check_mode=True + ) + + json_output = {} + + query = module.params.get('query') + status = module.params.get('status') + page = module.params.get('page') + all_pages = module.params.get('all_pages') + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + tower_check_mode(module) + try: + job = tower_cli.get_resource('job') + params = {'status': status, 'page': page, 'all_pages': all_pages} + if query: + params['query'] = query.items() + json_output = job.list(**params) + except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo: + module.fail_json(msg='Failed to list jobs: {0}'.format(excinfo), changed=False) + + module.exit_json(**json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_job_template.py b/awx_collection/plugins/modules/tower_job_template.py new file mode 100644 index 0000000000..2600e8884e --- /dev/null +++ b/awx_collection/plugins/modules/tower_job_template.py @@ -0,0 +1,337 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III +# 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)" +version_added: "2.3" +short_description: create, update, or destroy Ansible Tower job template. +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 + description: + description: + - Description to use for the job template. + job_type: + description: + - The job type to use for the job template. + required: True + choices: ["run", "check", "scan"] + inventory: + description: + - Name of the inventory to use for the job template. + project: + description: + - Name of the project to use for the job template. + required: True + playbook: + description: + - Path to the playbook to use for the job template within the project provided. + required: True + credential: + description: + - Name of the credential to use for the job template. + version_added: 2.7 + vault_credential: + description: + - Name of the vault credential to use for the job template. + version_added: 2.7 + forks: + description: + - The number of parallel or simultaneous processes to use while executing the playbook. + limit: + description: + - A host pattern to further constrain the list of hosts managed or affected by the playbook + 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 + extra_vars_path: + description: + - Path to the C(extra_vars) YAML file. + job_tags: + description: + - Comma separated list of the tags to use for the job template. + force_handlers_enabled: + description: + - Enable forcing playbook handlers to run even if a task fails. + version_added: 2.7 + type: bool + default: 'no' + skip_tags: + description: + - Comma separated list of the tags to skip for the job template. + start_at_task: + description: + - Start the playbook at the task matching this name. + version_added: 2.7 + diff_mode_enabled: + description: + - Enable diff mode for the job template. + version_added: 2.7 + type: bool + default: 'no' + fact_caching_enabled: + description: + - Enable use of fact caching for the job template. + version_added: 2.7 + type: bool + default: 'no' + host_config_key: + description: + - Allow provisioning callbacks using this host config key. + ask_diff_mode: + description: + - Prompt user to enable diff mode (show changes) to files when supported by modules. + version_added: 2.7 + type: bool + default: 'no' + ask_extra_vars: + description: + - Prompt user for (extra_vars) on launch. + type: bool + default: 'no' + ask_limit: + description: + - Prompt user for a limit on launch. + version_added: 2.7 + type: bool + default: 'no' + ask_tags: + description: + - Prompt user for job tags on launch. + type: bool + default: 'no' + ask_skip_tags: + description: + - Prompt user for job tags to skip on launch. + version_added: 2.7 + type: bool + default: 'no' + ask_job_type: + description: + - Prompt user for job type on launch. + type: bool + default: 'no' + ask_verbosity: + description: + - Prompt user to choose a verbosity level on launch. + version_added: 2.7 + type: bool + default: 'no' + ask_inventory: + description: + - Prompt user for inventory on launch. + type: bool + default: 'no' + ask_credential: + description: + - Prompt user for credential on launch. + type: bool + default: 'no' + survey_enabled: + description: + - Enable a survey on the job template. + version_added: 2.7 + type: bool + default: 'no' + survey_spec: + description: + - JSON/YAML dict formatted survey definition. + version_added: 2.8 + type: dict + required: False + become_enabled: + description: + - Activate privilege escalation. + type: bool + default: 'no' + concurrent_jobs_enabled: + description: + - Allow simultaneous runs of the job template. + version_added: 2.7 + type: bool + default: 'no' + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] +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" + 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') }}" +''' + +from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode + +try: + import tower_cli + import tower_cli.exceptions as exc + + from tower_cli.conf import settings +except ImportError: + pass + + +def update_fields(p): + '''This updates the module field names + to match the field names tower-cli expects to make + calling of the modify/delete methods easier. + ''' + params = p.copy() + field_map = { + 'fact_caching_enabled': 'use_fact_cache', + 'ask_diff_mode': 'ask_diff_mode_on_launch', + 'ask_extra_vars': 'ask_variables_on_launch', + 'ask_limit': 'ask_limit_on_launch', + 'ask_tags': 'ask_tags_on_launch', + 'ask_skip_tags': 'ask_skip_tags_on_launch', + 'ask_verbosity': 'ask_verbosity_on_launch', + 'ask_inventory': 'ask_inventory_on_launch', + 'ask_credential': 'ask_credential_on_launch', + 'ask_job_type': 'ask_job_type_on_launch', + 'diff_mode_enabled': 'diff_mode', + 'concurrent_jobs_enabled': 'allow_simultaneous', + 'force_handlers_enabled': 'force_handlers', + } + + params_update = {} + for old_k, new_k in field_map.items(): + v = params.pop(old_k) + params_update[new_k] = v + + extra_vars = params.get('extra_vars_path') + if extra_vars is not None: + params_update['extra_vars'] = ['@' + extra_vars] + + params.update(params_update) + return params + + +def update_resources(module, p): + params = p.copy() + identity_map = { + 'project': 'name', + 'inventory': 'name', + 'credential': 'name', + 'vault_credential': 'name', + } + for k, v in identity_map.items(): + try: + if params[k]: + key = 'credential' if '_credential' in k else k + result = tower_cli.get_resource(key).get(**{v: params[k]}) + params[k] = result['id'] + elif k in params: + # unset empty parameters to avoid ValueError: invalid literal for int() with base 10: '' + del(params[k]) + except (exc.NotFound) as excinfo: + module.fail_json(msg='Failed to update job template: {0}'.format(excinfo), changed=False) + return params + + +def main(): + argument_spec = dict( + name=dict(required=True), + description=dict(default=''), + job_type=dict(choices=['run', 'check', 'scan'], required=True), + inventory=dict(default=''), + project=dict(required=True), + playbook=dict(required=True), + credential=dict(default=''), + vault_credential=dict(default=''), + forks=dict(type='int'), + limit=dict(default=''), + verbosity=dict(type='int', choices=[0, 1, 2, 3, 4], default=0), + extra_vars_path=dict(type='path', required=False), + job_tags=dict(default=''), + force_handlers_enabled=dict(type='bool', default=False), + skip_tags=dict(default=''), + start_at_task=dict(default=''), + timeout=dict(type='int', default=0), + fact_caching_enabled=dict(type='bool', default=False), + host_config_key=dict(default=''), + ask_diff_mode=dict(type='bool', default=False), + ask_extra_vars=dict(type='bool', default=False), + ask_limit=dict(type='bool', default=False), + ask_tags=dict(type='bool', default=False), + ask_skip_tags=dict(type='bool', default=False), + ask_job_type=dict(type='bool', default=False), + ask_verbosity=dict(type='bool', default=False), + ask_inventory=dict(type='bool', default=False), + ask_credential=dict(type='bool', default=False), + survey_enabled=dict(type='bool', default=False), + survey_spec=dict(type='dict', required=False), + become_enabled=dict(type='bool', default=False), + diff_mode_enabled=dict(type='bool', default=False), + concurrent_jobs_enabled=dict(type='bool', default=False), + state=dict(choices=['present', 'absent'], default='present'), + ) + + module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) + + name = module.params.get('name') + state = module.params.pop('state') + json_output = {'job_template': name, 'state': state} + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + tower_check_mode(module) + jt = tower_cli.get_resource('job_template') + + params = update_resources(module, module.params) + params = update_fields(params) + params['create_on_missing'] = True + + try: + if state == 'present': + result = jt.modify(**params) + json_output['id'] = result['id'] + elif state == 'absent': + result = jt.delete(**params) + except (exc.ConnectionError, exc.BadRequest, exc.NotFound, exc.AuthError) as excinfo: + module.fail_json(msg='Failed to update job template: {0}'.format(excinfo), changed=False) + + json_output['changed'] = result['changed'] + module.exit_json(**json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_job_wait.py b/awx_collection/plugins/modules/tower_job_wait.py new file mode 100644 index 0000000000..0f0ab806db --- /dev/null +++ b/awx_collection/plugins/modules/tower_job_wait.py @@ -0,0 +1,149 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III +# 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 +version_added: "2.3" +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 + min_interval: + description: + - Minimum interval in seconds, to request an update from Tower. + default: 1 + max_interval: + description: + - Maximum interval in seconds, to request an update from Tower. + default: 30 + timeout: + description: + - Maximum time in seconds to wait for a job to finish. +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.ansible_tower import TowerModule, tower_auth_config, tower_check_mode +from ansible.module_utils.six.moves import cStringIO as StringIO + + +try: + import tower_cli + import tower_cli.exceptions as exc + + from tower_cli.conf import settings +except ImportError: + pass + + +def main(): + argument_spec = dict( + job_id=dict(type='int', required=True), + timeout=dict(type='int'), + min_interval=dict(type='float', default=1), + max_interval=dict(type='float', default=30), + ) + + module = TowerModule( + argument_spec, + supports_check_mode=True + ) + + json_output = {} + fail_json = None + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + tower_check_mode(module) + job = tower_cli.get_resource('job') + params = module.params.copy() + + # tower-cli gets very noisy when monitoring. + # We pass in our our outfile to suppress the out during our monitor call. + outfile = StringIO() + params['outfile'] = outfile + + job_id = params.get('job_id') + try: + result = job.monitor(job_id, **params) + except exc.Timeout: + result = job.status(job_id) + result['id'] = job_id + json_output['msg'] = 'Timeout waiting for job to finish.' + json_output['timeout'] = True + except exc.NotFound as excinfo: + fail_json = dict(msg='Unable to wait, no job_id {0} found: {1}'.format(job_id, excinfo), changed=False) + except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo: + fail_json = dict(msg='Unable to wait for job: {0}'.format(excinfo), changed=False) + + if fail_json is not None: + module.fail_json(**fail_json) + + json_output['success'] = True + for k in ('id', 'status', 'elapsed', 'started', 'finished'): + json_output[k] = result.get(k) + + module.exit_json(**json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_label.py b/awx_collection/plugins/modules/tower_label.py new file mode 100644 index 0000000000..09b3b4b6dd --- /dev/null +++ b/awx_collection/plugins/modules/tower_label.py @@ -0,0 +1,102 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III +# 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)" +version_added: "2.3" +short_description: create, update, or destroy Ansible Tower label. +description: + - Create, update, or destroy Ansible Tower labels. See + U(https://www.ansible.com/tower) for an overview. +options: + name: + description: + - Name to use for the label. + required: True + organization: + description: + - Organization the label should be applied to. + required: True + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] +extends_documentation_fragment: awx.awx.auth +''' + + +EXAMPLES = ''' +- name: Add label to tower organization + tower_label: + name: Custom Label + organization: My Organization + state: present + tower_config_file: "~/tower_cli.cfg" +''' + +from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode + +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), + organization=dict(required=True), + state=dict(choices=['present', 'absent'], default='present'), + ) + + module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) + + name = module.params.get('name') + organization = module.params.get('organization') + state = module.params.get('state') + + json_output = {'label': name, 'state': state} + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + tower_check_mode(module) + label = tower_cli.get_resource('label') + + try: + org_res = tower_cli.get_resource('organization') + org = org_res.get(name=organization) + + if state == 'present': + result = label.modify(name=name, organization=org['id'], create_on_missing=True) + json_output['id'] = result['id'] + elif state == 'absent': + result = label.delete(name=name, organization=org['id']) + except (exc.NotFound) as excinfo: + module.fail_json(msg='Failed to update label, organization not found: {0}'.format(excinfo), changed=False) + except (exc.ConnectionError, exc.BadRequest, exc.NotFound, exc.AuthError) as excinfo: + module.fail_json(msg='Failed to update label: {0}'.format(excinfo), changed=False) + + json_output['changed'] = result['changed'] + module.exit_json(**json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_notification.py b/awx_collection/plugins/modules/tower_notification.py new file mode 100644 index 0000000000..eee30a8cd4 --- /dev/null +++ b/awx_collection/plugins/modules/tower_notification.py @@ -0,0 +1,393 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2018, Samuel Carpentier +# 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)" +version_added: "2.8" +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. + required: True + description: + description: + - The description of the notification. + required: False + organization: + description: + - The organization the notification belongs to. + required: False + notification_type: + description: + - The type of notification to be sent. + required: True + choices: ["email", "slack", "twilio", "pagerduty", "hipchat", "webhook", "irc"] + notification_configuration: + description: + - The notification configuration file. Note providing this field would disable all notification-configuration-related fields. + required: False + username: + description: + - The mail server username. Required if I(notification_type=email). + required: False + sender: + description: + - The sender email address. Required if I(notification_type=email). + required: False + recipients: + description: + - The recipients email addresses. Required if I(notification_type=email). + required: False + use_tls: + description: + - The TLS trigger. Required if I(notification_type=email). + required: False + type: bool + host: + description: + - The mail server host. Required if I(notification_type=email). + required: False + use_ssl: + description: + - The SSL trigger. Required if I(notification_type=email) or if I(notification_type=irc). + required: False + type: bool + password: + description: + - The mail server password. Required if I(notification_type=email) or if I(notification_type=irc). + required: False + port: + description: + - The mail server port. Required if I(notification_type=email) or if I(notification_type=irc). + required: False + channels: + description: + - The destination Slack channels. Required if I(notification_type=slack). + required: False + type: list + token: + description: + - The access token. Required if I(notification_type=slack), if I(notification_type=pagerduty) or if I(notification_type=hipchat). + required: False + account_token: + description: + - The Twillio account token. Required if I(notification_type=twillio). + required: False + from_number: + description: + - The source phone number. Required if I(notification_type=twillio). + required: False + to_numbers: + description: + - The destination phone numbers. Required if I(notification_type=twillio). + required: False + account_sid: + description: + - The Twillio account SID. Required if I(notification_type=twillio). + required: False + subdomain: + description: + - The PagerDuty subdomain. Required if I(notification_type=pagerduty). + required: False + service_key: + description: + - The PagerDuty service/integration API key. Required if I(notification_type=pagerduty). + required: False + client_name: + description: + - The PagerDuty client identifier. Required if I(notification_type=pagerduty). + required: False + message_from: + description: + - The label to be shown with the notification. Required if I(notification_type=hipchat). + required: False + api_url: + description: + - The HipChat API URL. Required if I(notification_type=hipchat). + required: False + color: + description: + - The notification color. Required if I(notification_type=hipchat). + required: False + choices: ["yellow", "green", "red", "purple", "gray", "random"] + rooms: + description: + - HipChat rooms to send the notification to. Required if I(notification_type=hipchat). + required: False + type: list + notify: + description: + - The notify channel trigger. Required if I(notification_type=hipchat). + required: False + type: bool + url: + description: + - The target URL. Required if I(notification_type=webhook). + required: False + headers: + description: + - The HTTP headers as JSON string. Required if I(notification_type=webhook). + required: False + server: + description: + - The IRC server address. Required if I(notification_type=irc). + required: False + nickname: + description: + - The IRC nickname. Required if I(notification_type=irc). + required: False + targets: + description: + - The destination channels or users. Required if I(notification_type=irc). + required: False + type: list + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] +extends_documentation_fragment: awx.awx.auth +''' + + +EXAMPLES = ''' +- name: Add Slack notification + tower_notification: + name: slack notification + notification_type: slack + channels: + - general + token: cefda9e2be1f21d11cdd9452f5b7f97fda977f42 + state: present + tower_config_file: "~/tower_cli.cfg" + +- name: Add webhook notification + tower_notification: + name: webhook notification + notification_type: webhook + 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 + 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 + 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 + token: a_token + subdomain: sub + client_name: client + service_key: a_key + state: present + tower_config_file: "~/tower_cli.cfg" + +- name: Add HipChat notification + tower_notification: + name: hipchat notification + notification_type: hipchat + token: a_token + message_from: user1 + api_url: https://hipchat.example.com + color: red + rooms: + - room-A + notify: yes + state: present + tower_config_file: "~/tower_cli.cfg" + +- name: Add IRC notification + tower_notification: + name: irc notification + notification_type: irc + 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 + notification_type: email + state: absent + tower_config_file: "~/tower_cli.cfg" +''' + + +RETURN = ''' # ''' + + +from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode + +try: + import tower_cli + import tower_cli.utils.exceptions as exc + + from tower_cli.conf import settings +except ImportError: + pass + + +def main(): + argument_spec = dict( + name=dict(required=True), + description=dict(required=False), + organization=dict(required=False), + notification_type=dict(required=True, choices=['email', 'slack', 'twilio', 'pagerduty', 'hipchat', 'webhook', 'irc']), + notification_configuration=dict(required=False), + username=dict(required=False), + sender=dict(required=False), + recipients=dict(required=False, type='list'), + use_tls=dict(required=False, type='bool'), + host=dict(required=False), + use_ssl=dict(required=False, type='bool'), + password=dict(required=False, no_log=True), + port=dict(required=False, type='int'), + channels=dict(required=False, type='list'), + token=dict(required=False, no_log=True), + account_token=dict(required=False, no_log=True), + from_number=dict(required=False), + to_numbers=dict(required=False, type='list'), + account_sid=dict(required=False), + subdomain=dict(required=False), + service_key=dict(required=False, no_log=True), + client_name=dict(required=False), + message_from=dict(required=False), + api_url=dict(required=False), + color=dict(required=False, choices=['yellow', 'green', 'red', 'purple', 'gray', 'random']), + rooms=dict(required=False, type='list'), + notify=dict(required=False, type='bool'), + url=dict(required=False), + headers=dict(required=False, type='dict', default={}), + server=dict(required=False), + nickname=dict(required=False), + targets=dict(required=False, type='list'), + state=dict(choices=['present', 'absent'], default='present'), + ) + + module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) + + name = module.params.get('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') + username = module.params.get('username') + sender = module.params.get('sender') + recipients = module.params.get('recipients') + use_tls = module.params.get('use_tls') + host = module.params.get('host') + use_ssl = module.params.get('use_ssl') + password = module.params.get('password') + port = module.params.get('port') + channels = module.params.get('channels') + token = module.params.get('token') + account_token = module.params.get('account_token') + from_number = module.params.get('from_number') + to_numbers = module.params.get('to_numbers') + account_sid = module.params.get('account_sid') + subdomain = module.params.get('subdomain') + service_key = module.params.get('service_key') + client_name = module.params.get('client_name') + message_from = module.params.get('message_from') + api_url = module.params.get('api_url') + color = module.params.get('color') + rooms = module.params.get('rooms') + notify = module.params.get('notify') + url = module.params.get('url') + headers = module.params.get('headers') + server = module.params.get('server') + nickname = module.params.get('nickname') + targets = module.params.get('targets') + state = module.params.get('state') + + json_output = {'notification': name, 'state': state} + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + tower_check_mode(module) + notification_template = tower_cli.get_resource('notification_template') + + try: + org_res = tower_cli.get_resource('organization') + org = org_res.get(name=organization) + + if state == 'present': + result = notification_template.modify(name=name, description=description, organization=org['id'], + notification_type=notification_type, + notification_configuration=notification_configuration, + username=username, sender=sender, recipients=recipients, + use_tls=use_tls, host=host, use_ssl=use_ssl, password=password, + port=port, channels=channels, token=token, + account_token=account_token, from_number=from_number, + to_numbers=to_numbers, account_sid=account_sid, + subdomain=subdomain, service_key=service_key, + client_name=client_name, message_from=message_from, + api_url=api_url, color=color, rooms=rooms, notify=notify, + url=url, headers=headers, server=server, nickname=nickname, + targets=targets, create_on_missing=True) + json_output['id'] = result['id'] + elif state == 'absent': + result = notification_template.delete(name=name) + except (exc.NotFound) as excinfo: + module.fail_json(msg='Failed to update notification template, organization not found: {0}'.format(excinfo), changed=False) + except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo: + module.fail_json(msg='Failed to update notification template: {0}'.format(excinfo), changed=False) + + json_output['changed'] = result['changed'] + module.exit_json(**json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_organization.py b/awx_collection/plugins/modules/tower_organization.py new file mode 100644 index 0000000000..7f261e2f1c --- /dev/null +++ b/awx_collection/plugins/modules/tower_organization.py @@ -0,0 +1,95 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III +# 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 +version_added: "2.3" +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 + description: + description: + - The description to use for the organization. + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] +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" +''' + +from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode + +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(), + state=dict(choices=['present', 'absent'], default='present'), + ) + + module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) + + name = module.params.get('name') + description = module.params.get('description') + state = module.params.get('state') + + json_output = {'organization': name, 'state': state} + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + tower_check_mode(module) + organization = tower_cli.get_resource('organization') + try: + if state == 'present': + result = organization.modify(name=name, description=description, create_on_missing=True) + json_output['id'] = result['id'] + elif state == 'absent': + result = organization.delete(name=name) + except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo: + module.fail_json(msg='Failed to update the organization: {0}'.format(excinfo), changed=False) + + json_output['changed'] = result['changed'] + module.exit_json(**json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_project.py b/awx_collection/plugins/modules/tower_project.py new file mode 100644 index 0000000000..423f63bb7f --- /dev/null +++ b/awx_collection/plugins/modules/tower_project.py @@ -0,0 +1,213 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III +# 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)" +version_added: "2.3" +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 + description: + description: + - Description to use for the project. + scm_type: + description: + - Type of SCM resource. + choices: ["manual", "git", "hg", "svn"] + default: "manual" + scm_url: + description: + - URL of SCM resource. + local_path: + description: + - The server playbook directory for manual projects. + scm_branch: + description: + - The branch to use for the SCM resource. + scm_credential: + description: + - Name of the credential to use with this SCM resource. + 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: + version_added: "2.8" + 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. + default: 0 + job_timeout: + version_added: "2.8" + description: + - The amount of time (in seconds) to run before the SCM Update is canceled. A value of 0 means no timeout. + default: 0 + custom_virtualenv: + version_added: "2.8" + description: + - Local absolute file path containing a custom Python virtualenv to use + organization: + description: + - Primary key of organization for project. + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] +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" +''' + +from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode + +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(), + description=dict(), + organization=dict(), + scm_type=dict(choices=['manual', 'git', 'hg', 'svn'], default='manual'), + scm_url=dict(), + scm_branch=dict(), + 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), + job_timeout=dict(type='int', default=0), + custom_virtualenv=dict(), + local_path=dict(), + state=dict(choices=['present', 'absent'], default='present'), + ) + + module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) + + name = module.params.get('name') + description = module.params.get('description') + organization = module.params.get('organization') + 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_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') + job_timeout = module.params.get('job_timeout') + custom_virtualenv = module.params.get('custom_virtualenv') + state = module.params.get('state') + + json_output = {'project': name, 'state': state} + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + tower_check_mode(module) + project = tower_cli.get_resource('project') + try: + if state == 'present': + try: + org_res = tower_cli.get_resource('organization') + org = org_res.get(name=organization) + except exc.NotFound: + module.fail_json(msg='Failed to update project, organization not found: {0}'.format(organization), changed=False) + + if scm_credential: + try: + cred_res = tower_cli.get_resource('credential') + try: + cred = cred_res.get(name=scm_credential) + except tower_cli.exceptions.MultipleResults: + module.warn('Multiple credentials found for {0}, falling back looking in project organization'.format(scm_credential)) + cred = cred_res.get(name=scm_credential, organization=org['id']) + scm_credential = cred['id'] + except exc.NotFound: + module.fail_json(msg='Failed to update project, credential not found: {0}'.format(scm_credential), changed=False) + + if (scm_update_cache_timeout is not None) 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') + + result = project.modify(name=name, description=description, + organization=org['id'], + scm_type=scm_type, scm_url=scm_url, local_path=local_path, + scm_branch=scm_branch, scm_clean=scm_clean, credential=scm_credential, + scm_delete_on_update=scm_delete_on_update, + scm_update_on_launch=scm_update_on_launch, + scm_update_cache_timeout=scm_update_cache_timeout, + job_timeout=job_timeout, + custom_virtualenv=custom_virtualenv, + create_on_missing=True) + json_output['id'] = result['id'] + elif state == 'absent': + result = project.delete(name=name) + except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo: + module.fail_json(msg='Failed to update project: {0}'.format(excinfo), changed=False) + + json_output['changed'] = result['changed'] + module.exit_json(**json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_receive.py b/awx_collection/plugins/modules/tower_receive.py new file mode 100644 index 0000000000..66d45da58f --- /dev/null +++ b/awx_collection/plugins/modules/tower_receive.py @@ -0,0 +1,172 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, John Westcott IV +# 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_receive +author: "John Westcott IV (@john-westcott-iv)" +version_added: "2.8" +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: [] + user: + description: + - List of user names to export + default: [] + team: + description: + - List of team names to export + default: [] + credential_type: + description: + - List of credential type names to export + default: [] + credential: + description: + - List of credential names to export + default: [] + notification_template: + description: + - List of notification template names to export + default: [] + inventory_script: + description: + - List of inventory script names to export + default: [] + inventory: + description: + - List of inventory names to export + default: [] + project: + description: + - List of project names to export + default: [] + job_template: + description: + - List of job template names to export + default: [] + workflow: + description: + - List of workflow names to export + default: [] + +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 +''' + +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.ansible_tower import TowerModule, 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=[]), + credential_type=dict(type='list', default=[]), + inventory=dict(type='list', default=[]), + inventory_script=dict(type='list', default=[]), + job_template=dict(type='list', default=[]), + notification_template=dict(type='list', default=[]), + organization=dict(type='list', default=[]), + project=dict(type='list', default=[]), + team=dict(type='list', default=[]), + user=dict(type='list', default=[]), + workflow=dict(type='list', default=[]), + ) + + module = TowerModule(argument_spec=argument_spec, supports_check_mode=False) + + 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/awx_collection/plugins/modules/tower_role.py b/awx_collection/plugins/modules/tower_role.py new file mode 100644 index 0000000000..9352827d8b --- /dev/null +++ b/awx_collection/plugins/modules/tower_role.py @@ -0,0 +1,157 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III +# 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 +version_added: "2.3" +author: "Wayne Witzel III (@wwitzel3)" +short_description: create, update, or destroy Ansible Tower role. +description: + - Create, update, or destroy Ansible Tower roles. See + U(https://www.ansible.com/tower) for an overview. +options: + user: + description: + - User that receives the permissions specified by the role. + team: + description: + - Team that receives the permissions specified by the role. + 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"] + target_team: + description: + - Team that the role acts on. + inventory: + description: + - Inventory the role acts on. + job_template: + description: + - The job template the role acts on. + credential: + description: + - Credential the role acts on. + organization: + description: + - Organization the role acts on. + project: + description: + - Project the role acts on. + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] +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 + tower_config_file: "~/tower_cli.cfg" +''' + +from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode + +try: + import tower_cli + import tower_cli.exceptions as exc + + from tower_cli.conf import settings +except ImportError: + pass + + +def update_resources(module, p): + '''update_resources attempts to fetch any of the resources given + by name using their unique field (identity) + ''' + params = p.copy() + identity_map = { + 'user': 'username', + 'team': 'name', + 'target_team': 'name', + 'inventory': 'name', + 'job_template': 'name', + 'credential': 'name', + 'organization': 'name', + 'project': 'name', + } + for k, v in identity_map.items(): + try: + if params[k]: + key = 'team' if k == 'target_team' else k + result = tower_cli.get_resource(key).get(**{v: params[k]}) + params[k] = result['id'] + except (exc.NotFound) as excinfo: + module.fail_json(msg='Failed to update role, {0} not found: {1}'.format(k, excinfo), changed=False) + return params + + +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"]), + target_team=dict(), + inventory=dict(), + job_template=dict(), + credential=dict(), + organization=dict(), + project=dict(), + state=dict(choices=['present', 'absent'], default='present'), + ) + + module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) + + role_type = module.params.pop('role') + state = module.params.pop('state') + + json_output = {'role': role_type, 'state': state} + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + tower_check_mode(module) + role = tower_cli.get_resource('role') + + params = update_resources(module, module.params) + params['type'] = role_type + + try: + if state == 'present': + result = role.grant(**params) + json_output['id'] = result['id'] + elif state == 'absent': + result = role.revoke(**params) + except (exc.ConnectionError, exc.BadRequest, exc.NotFound, exc.AuthError) as excinfo: + module.fail_json(msg='Failed to update role: {0}'.format(excinfo), changed=False) + + json_output['changed'] = result['changed'] + module.exit_json(**json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_send.py b/awx_collection/plugins/modules/tower_send.py new file mode 100644 index 0000000000..eaa5b9d69f --- /dev/null +++ b/awx_collection/plugins/modules/tower_send.py @@ -0,0 +1,167 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, John Westcott IV +# 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_send +author: "John Westcott IV (@john-westcott-iv)" +version_added: "2.8" +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 + required: False + files: + description: + - List of files to import. + required: False + default: [] + prevent: + description: + - A list of asset types to prevent import for + required: false + default: [] + password_management: + description: + - The password management option to use. + - The prompt option is not supported. + required: false + default: 'default' + choices: ["default", "random"] + +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 +''' + +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.ansible_tower import TowerModule, 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(required=False), + files=dict(required=False, default=[], type='list'), + prevent=dict(required=False, default=[], type='list'), + password_management=dict(required=False, default='default', choices=['default', 'random']), + ) + + module = TowerModule(argument_spec=argument_spec, supports_check_mode=False) + + 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/awx_collection/plugins/modules/tower_settings.py b/awx_collection/plugins/modules/tower_settings.py new file mode 100644 index 0000000000..8de8c2e6f9 --- /dev/null +++ b/awx_collection/plugins/modules/tower_settings.py @@ -0,0 +1,104 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2018, Nikhil Jain +# 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)" +version_added: "2.7" +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 + required: True + value: + description: + - Value to be modified for given setting. + required: True +extends_documentation_fragment: awx.awx.auth +''' + +RETURN = ''' # ''' + +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 +''' + +from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode + +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), + value=dict(required=True), + ) + + module = TowerModule( + argument_spec=argument_spec, + supports_check_mode=False + ) + + json_output = {} + + name = module.params.get('name') + value = module.params.get('value') + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + tower_check_mode(module) + try: + setting = tower_cli.get_resource('setting') + result = setting.modify(setting=name, value=value) + + json_output['id'] = result['id'] + json_output['value'] = result['value'] + + except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo: + module.fail_json(msg='Failed to modify the setting: {0}'.format(excinfo), changed=False) + + json_output['changed'] = result['changed'] + module.exit_json(**json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_team.py b/awx_collection/plugins/modules/tower_team.py new file mode 100644 index 0000000000..165f801b20 --- /dev/null +++ b/awx_collection/plugins/modules/tower_team.py @@ -0,0 +1,107 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III +# 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)" +version_added: "2.3" +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 + organization: + description: + - Organization the team should be made a member of. + required: True + state: + description: + - Desired state of the resource. + choices: ["present", "absent"] + default: "present" +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.ansible_tower import TowerModule, tower_auth_config, tower_check_mode + +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(), + organization=dict(required=True), + state=dict(choices=['present', 'absent'], default='present'), + ) + + module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) + + name = module.params.get('name') + description = module.params.get('description') + organization = module.params.get('organization') + state = module.params.get('state') + + json_output = {'team': name, 'state': state} + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + tower_check_mode(module) + team = tower_cli.get_resource('team') + + try: + org_res = tower_cli.get_resource('organization') + org = org_res.get(name=organization) + + if state == 'present': + result = team.modify(name=name, organization=org['id'], + description=description, create_on_missing=True) + json_output['id'] = result['id'] + elif state == 'absent': + result = team.delete(name=name, organization=org['id']) + except (exc.NotFound) as excinfo: + module.fail_json(msg='Failed to update team, organization not found: {0}'.format(excinfo), changed=False) + except (exc.ConnectionError, exc.BadRequest, exc.NotFound, exc.AuthError) as excinfo: + module.fail_json(msg='Failed to update team: {0}'.format(excinfo), changed=False) + + json_output['changed'] = result['changed'] + module.exit_json(**json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_user.py b/awx_collection/plugins/modules/tower_user.py new file mode 100644 index 0000000000..a5fea6d861 --- /dev/null +++ b/awx_collection/plugins/modules/tower_user.py @@ -0,0 +1,159 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2017, Wayne Witzel III +# 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: "Wayne Witzel III (@wwitzel3)" +version_added: "2.3" +short_description: create, update, or destroy Ansible Tower user. +description: + - Create, update, or destroy Ansible Tower users. See + U(https://www.ansible.com/tower) for an overview. +options: + username: + description: + - The username of the user. + required: True + first_name: + description: + - First name of the user. + last_name: + description: + - Last name of the user. + email: + description: + - Email address of the user. + required: True + password: + description: + - Password of the user. + superuser: + description: + - User is a system wide administrator. + type: bool + default: 'no' + auditor: + description: + - User is a system wide auditor. + type: bool + default: 'no' + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] + +requirements: + - ansible-tower-cli >= 3.2.0 + +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.ansible_tower import TowerModule, tower_auth_config, tower_check_mode + +try: + import tower_cli + import tower_cli.exceptions as exc + + from tower_cli.conf import settings +except ImportError: + pass + + +def main(): + argument_spec = dict( + username=dict(required=True), + first_name=dict(), + last_name=dict(), + password=dict(no_log=True), + email=dict(required=True), + superuser=dict(type='bool', default=False), + auditor=dict(type='bool', default=False), + state=dict(choices=['present', 'absent'], default='present'), + ) + + module = TowerModule(argument_spec=argument_spec, supports_check_mode=True) + + username = module.params.get('username') + first_name = module.params.get('first_name') + last_name = module.params.get('last_name') + password = module.params.get('password') + email = module.params.get('email') + superuser = module.params.get('superuser') + auditor = module.params.get('auditor') + state = module.params.get('state') + + json_output = {'username': username, 'state': state} + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + tower_check_mode(module) + user = tower_cli.get_resource('user') + try: + if state == 'present': + result = user.modify(username=username, first_name=first_name, last_name=last_name, + email=email, password=password, is_superuser=superuser, + is_system_auditor=auditor, create_on_missing=True) + json_output['id'] = result['id'] + elif state == 'absent': + result = user.delete(username=username) + except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo: + module.fail_json(msg='Failed to update the user: {0}'.format(excinfo), changed=False) + + json_output['changed'] = result['changed'] + module.exit_json(**json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_workflow_launch.py b/awx_collection/plugins/modules/tower_workflow_launch.py new file mode 100644 index 0000000000..8e0c06d33c --- /dev/null +++ b/awx_collection/plugins/modules/tower_workflow_launch.py @@ -0,0 +1,164 @@ +#!/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)" +version_added: "2.8" +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: + workflow_template: + description: + - The name of the workflow template to run. + required: True + extra_vars: + description: + - Any extra vars required to launch the job. + required: False + wait: + description: + - Wait for the workflow to complete. + required: False + default: True + type: bool + timeout: + description: + - If waiting for the workflow to complete this will abort after this + amount of seconds + +requirements: + - "python >= 2.6" +extends_documentation_fragment: awx.awx.auth +''' + +RETURN = ''' +tower_version: + description: The version of Tower we connected to + returned: If connection to Tower works + type: str + sample: '3.4.0' +job_info: + description: dictionary containing information about the workflow executed + returned: If workflow launched + type: dict +''' + + +EXAMPLES = ''' +- name: Launch a workflow + tower_workflow_launch: + name: "Test Workflow" + delegate_to: localhost + run_once: true + register: workflow_results + +- name: Launch a Workflow with parameters without waiting + tower_workflow_launch: + workflow_template: "Test workflow" + extra_vars: "---\nmy: var" + wait: False + delegate_to: localhost + run_once: true + register: workflow_task_info +''' + +from ..module_utils.ansible_tower import TowerModule, tower_auth_config + +try: + import tower_cli + from tower_cli.api import client + from tower_cli.conf import settings + from tower_cli.exceptions import ServerError, ConnectionError, BadRequest, TowerCLIError +except ImportError: + pass + + +def main(): + argument_spec = dict( + workflow_template=dict(required=True), + extra_vars=dict(required=False), + wait=dict(required=False, default=True, type='bool'), + timeout=dict(required=False, default=None, type='int'), + ) + + module = TowerModule( + argument_spec=argument_spec, + supports_check_mode=True + ) + + workflow_template = module.params.get('workflow_template') + extra_vars = module.params.get('extra_vars') + wait = module.params.get('wait') + timeout = module.params.get('timeout') + + # If we are going to use this result to return we can consider ourselfs changed + result = dict( + changed=False, + msg='initial message' + ) + + tower_auth = tower_auth_config(module) + with settings.runtime_values(**tower_auth): + # First we will test the connection. This will be a test for both check and run mode + # Note, we are not using the tower_check_mode method here because we want to do more than just a ping test + # If we are in check mode we also want to validate that we can find the workflow + try: + ping_result = client.get('/ping').json() + # Stuff the version into the results as an FYI + result['tower_version'] = ping_result['version'] + except(ServerError, ConnectionError, BadRequest) as excinfo: + result['msg'] = "Failed to reach Tower: {0}".format(excinfo) + module.fail_json(**result) + + # Now that we know we can connect, lets verify that we can resolve the workflow_template + try: + workflow = tower_cli.get_resource("workflow").get(**{'name': workflow_template}) + except TowerCLIError as e: + result['msg'] = "Failed to find workflow: {0}".format(e) + module.fail_json(**result) + + # Since we were able to find the workflow, if we are in check mode we can return now + if module.check_mode: + result['msg'] = "Check mode passed" + module.exit_json(**result) + + # We are no ready to run the workflow + try: + result['job_info'] = tower_cli.get_resource('workflow_job').launch( + workflow_job_template=workflow['id'], + monitor=False, + wait=wait, + timeout=timeout, + extra_vars=extra_vars + ) + if wait: + # If we were waiting for a result we will fail if the workflow failed + if result['job_info']['failed']: + result['msg'] = "Workflow execution failed" + module.fail_json(**result) + else: + module.exit_json(**result) + + # We were not waiting and there should be no way we can make it here without the workflow fired off so we can return a success + module.exit_json(**result) + + except TowerCLIError as e: + result['msg'] = "Failed to execute workflow: {0}".format(e) + module.fail_json(**result) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/plugins/modules/tower_workflow_template.py b/awx_collection/plugins/modules/tower_workflow_template.py new file mode 100644 index 0000000000..3805079d01 --- /dev/null +++ b/awx_collection/plugins/modules/tower_workflow_template.py @@ -0,0 +1,202 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright: (c) 2018, Adrien Fleury +# 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_workflow_template +author: "Adrien Fleury (@fleu42)" +version_added: "2.7" +short_description: create, update, or destroy Ansible Tower workflow template. +description: + - Create, update, or destroy Ansible Tower workflows. See + U(https://www.ansible.com/tower) for an overview. +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 + version_added: "2.9" + ask_inventory: + description: + - Prompt user for inventory on launch. + type: bool + version_added: "2.9" + description: + description: + - The description to use for the workflow. + extra_vars: + description: + - Extra variables used by Ansible in YAML or key=value format. + inventory: + description: + - Name of the inventory to use for the job template. + version_added: "2.9" + name: + description: + - The name to use for the workflow. + required: True + organization: + description: + - The organization the workflow is linked to. + 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. + 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. + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] +extends_documentation_fragment: awx.awx.auth +''' + + +EXAMPLES = ''' +- tower_workflow_template: + name: Workflow Template + description: My very first Workflow Template + organization: My optional Organization + schema: "{{ lookup('file', 'my_workflow.json') }}" + +- tower_worflow_template: + name: Workflow Template + state: absent +''' + + +RETURN = ''' # ''' + + +from ..module_utils.ansible_tower import ( + TowerModule, + tower_auth_config, + tower_check_mode +) + +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(required=False), + extra_vars=dict(required=False), + organization=dict(required=False), + allow_simultaneous=dict(type='bool', required=False), + schema=dict(required=False), + survey=dict(required=False), + survey_enabled=dict(type='bool', required=False), + inventory=dict(required=False), + ask_inventory=dict(type='bool', required=False), + ask_extra_vars=dict(type='bool', required=False), + state=dict(choices=['present', 'absent'], default='present'), + ) + + module = TowerModule( + argument_spec=argument_spec, + supports_check_mode=False + ) + + 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', 'extra_vars', 'inventory', + 'survey_enabled', 'description'): + if module.params.get(key): + params[key] = module.params.get(key) + + 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'], 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/awx_collection/setup.cfg b/awx_collection/setup.cfg new file mode 100644 index 0000000000..fdfea44c7e --- /dev/null +++ b/awx_collection/setup.cfg @@ -0,0 +1,3 @@ +[flake8] +max-line-length=160 +ignore=E402 \ No newline at end of file diff --git a/awx_collection/template_galaxy.yml b/awx_collection/template_galaxy.yml new file mode 100644 index 0000000000..bfbea58bf7 --- /dev/null +++ b/awx_collection/template_galaxy.yml @@ -0,0 +1,28 @@ +- 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 + + tasks: + - name: Do file content replacements for non-default namespace or package name + block: + - name: Find all module files + find: + paths: "{{ playbook_dir }}/plugins/modules" + patterns: "*.py" + register: module_files + + - name: Change files to support desired namespace and package names + replace: + path: "{{ item.path }}" + regexp: '^extends_documentation_fragment: awx.awx.auth$' + replace: 'extends_documentation_fragment: {{ collection_namespace }}.{{ collection_package }}.auth' + with_items: "{{ module_files.files }}" + when: + - (collection_package != 'awx') or (collection_namespace != 'awx') + + - name: Template the galaxy.yml file + template: src={{ playbook_dir }}/galaxy.yml.j2 dest={{ playbook_dir }}/galaxy.yml diff --git a/awx_collection/test/awx/conftest.py b/awx_collection/test/awx/conftest.py new file mode 100644 index 0000000000..bdaa0db3bf --- /dev/null +++ b/awx_collection/test/awx/conftest.py @@ -0,0 +1,121 @@ +import io +import json +import datetime +import importlib +from contextlib import redirect_stdout +from unittest import mock + +from requests.models import Response + +import pytest + +from awx.main.tests.functional.conftest import _request +from awx.main.models import Organization, Project, Inventory, Credential, CredentialType + + +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 +def run_module(): + def rf(module_name, module_params, request_user): + + def new_request(self, method, url, **kwargs): + kwargs_copy = kwargs.copy() + if 'data' in kwargs: + kwargs_copy['data'] = json.loads(kwargs['data']) + + # 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 + return resp + + 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 = importlib.import_module('plugins.modules.{}'.format(module_name)) + + # 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 + + with mock.patch.object(resource_module.TowerModule, '_load_params', new=mock_load_params): + # Call the test utility (like a mock server) instead of issuing HTTP requests + with mock.patch('tower_cli.api.Session.request', new=new_request): + # Ansible modules return data to the mothership over stdout + with redirect_stdout(stdout_buffer): + try: + resource_module.main() + except SystemExit: + pass # A system exit indicates successful execution + + module_stdout = stdout_buffer.getvalue().strip() + result = json.loads(module_stdout) + return result + + return rf + + +@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 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'} + ) diff --git a/awx_collection/test/awx/test_job_template.py b/awx_collection/test/awx/test_job_template.py new file mode 100644 index 0000000000..e5ecfeb793 --- /dev/null +++ b/awx_collection/test/awx/test_job_template.py @@ -0,0 +1,59 @@ +import pytest + +from awx.main.models import JobTemplate + + +@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, + 'job_type': 'run', + 'state': 'present' + } + + result = run_module('tower_job_template', module_args, admin_user) + + jt = JobTemplate.objects.get(name='foo') + + assert result == { + "job_template": "foo", + "state": "present", + "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 +@pytest.mark.xfail(reason='Known limitation and needs to be fixed.') +def test_create_job_template_with_old_machine_cred(run_module, admin_user, project, inventory, machine_credential): + + module_args = { + 'name': 'foo', 'playbook': 'helloworld.yml', + 'project': project.name, 'inventory': inventory.name, 'credential': machine_credential.name, + 'job_type': 'run', + 'state': 'present' + } + + result = run_module('tower_job_template', module_args, admin_user) + + jt = JobTemplate.objects.get(name='foo') + + assert result == { + "job_template": "foo", + "state": "present", + "id": jt.id, + "changed": True, + "invocation": { + "module_args": module_args + } + } + + assert machine_credential.id in [cred.pk for cred in jt.credentials.all()] diff --git a/awx_collection/test/awx/test_organization.py b/awx_collection/test/awx/test_organization.py new file mode 100644 index 0000000000..ea76940fd4 --- /dev/null +++ b/awx_collection/test/awx/test_organization.py @@ -0,0 +1,25 @@ +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'} + + result = run_module('tower_organization', module_args, admin_user) + + org = Organization.objects.get(name='foo') + + assert result == { + "organization": "foo", + "state": "present", + "id": org.id, + "changed": True, + "invocation": { + "module_args": module_args + } + } + + assert org.description == 'barfoo' diff --git a/setup.cfg b/setup.cfg index e99856b7b1..beb4521b99 100755 --- a/setup.cfg +++ b/setup.cfg @@ -18,4 +18,4 @@ exclude=.tox,venv,awx/lib/site-packages,awx/plugins/inventory/ec2.py,awx/plugins [flake8] max-line-length=160 ignore=E201,E203,E221,E225,E231,E241,E251,E261,E265,E303,W291,W391,W293,E731,W504 -exclude=.tox,venv,awx/lib/site-packages,awx/plugins/inventory,awx/ui,awx/api/urls.py,awx/main/migrations,awx/main/tests/data,node_modules/,awx/projects/,tools/docker,awx/settings/local_*.py,installer/openshift/settings.py,build/,installer/,awxkit/test +exclude=.tox,venv,awx/lib/site-packages,awx/plugins/inventory,awx/ui,awx/api/urls.py,awx/main/migrations,awx/main/tests/data,node_modules/,awx/projects/,tools/docker,awx/settings/local_*.py,installer/openshift/settings.py,build/,installer/,awxkit/test,awx_collection/