From 8cb997ec381282b8888e66c53a8504627fea72e2 Mon Sep 17 00:00:00 2001 From: Packit Date: Sep 03 2020 10:11:48 +0000 Subject: ansible-freeipa-0.1.8 base --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6df2a02 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyc +*.retry diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/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/README-group.md b/README-group.md new file mode 100644 index 0000000..8bba323 --- /dev/null +++ b/README-group.md @@ -0,0 +1,153 @@ +Group module +============ + +Description +----------- + +The group module allows to ensure presence and absence of groups and members of groups. + +The group module is as compatible as possible to the Ansible upstream `ipa_group` module, but additionally offers to add users to a group and also to remove users from a group. + + +Features +-------- +* Group management + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.4.0 and up are supported by the ipagroup module. + + +Requirements +------------ + +**Controller** +* Ansible version: 2.8+ + +**Node** +* Supported FreeIPA version (see above) + + +Usage +===== + +Example inventory file + +```ini +[ipaserver] +ipaserver.test.local +``` + + +Example playbook to add groups: + +```yaml +--- +- name: Playbook to handle groups + hosts: ipaserver + become: true + + tasks: + # Create group ops with gid 1234 + - ipagroup: + ipaadmin_password: MyPassword123 + name: ops + gidnumber: 1234 + + # Create group sysops + - ipagroup: + ipaadmin_password: MyPassword123 + name: sysops + user: + - pinky + + # Create group appops + - ipagroup: + ipaadmin_password: MyPassword123 + name: appops +``` + +Example playbook to add users to a group: + +```yaml +--- +- name: Playbook to handle groups + hosts: ipaserver + become: true + + tasks: + # Add user member brain to group sysops + - ipagroup: + ipaadmin_password: MyPassword123 + name: sysops + action: member + user: + - brain +``` +`action` controls if a the group or member will be handled. To add or remove members, set `action` to `member`. + + +Example playbook to add group members to a group: + +```yaml +--- +- name: Playbook to handle groups + hosts: ipaserver + become: true + + tasks: + # Add group members sysops and appops to group sysops + - ipagroup: + ipaadmin_password: MyPassword123 + name: ops + group: + - sysops + - appops +``` + +Example playbook to remove groups: + +```yaml +--- +- name: Playbook to handle groups + hosts: ipaserver + become: true + + tasks: + # Remove goups sysops, appops and ops + - ipagroup: + ipaadmin_password: MyPassword123 + name: sysops,appops,ops + state: absent +``` + + +Variables +========= + +ipagroup +------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no +`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no +`name` \| `cn` | The list of group name strings. | no +`description` | The group description string. | no +`gid` \| `gidnumber` | The GID integer. | no +`nonposix` | Create as a non-POSIX group. (bool) | no +`external` | Allow adding external non-IPA members from trusted domains. (flag) | no +`nomembers` | Suppress processing of membership attributes. (bool) | no +`user` | List of user name strings assigned to this group. | no +`group` | List of group name strings assigned to this group. | no +`service` | List of service name strings assigned to this group. Only usable with IPA versions 4.7 and up. | no +`action` | Work on group or member level. It can be on of `member` or `group` and defaults to `group`. | no +`state` | The state to ensure. It can be one of `present` or `absent`, default: `present`. | yes + + +Authors +======= + +Thomas Woerner diff --git a/README-hbacrule.md b/README-hbacrule.md new file mode 100644 index 0000000..39a242c --- /dev/null +++ b/README-hbacrule.md @@ -0,0 +1,158 @@ +HBACrule module +=============== + +Description +----------- + +The hbacrule (HBAC Rule) module allows to ensure presence and absence of HBAC Rules and host, hostgroups, HBAC Services, HBAC Service Groups, users, and user groups as members of HBAC Rule. + + +Features +-------- +* HBAC Rule management + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.4.0 and up are supported by the ipahbacrule module. + + +Requirements +------------ + +**Controller** +* Ansible version: 2.8+ + +**Node** +* Supported FreeIPA version (see above) + + +Usage +===== + +Example inventory file + +```ini +[ipaserver] +ipaserver.test.local +``` + + +Example playbook to make sure HBAC Rule login exists: + +```yaml +--- +- name: Playbook to handle hbacrules + hbacsvcs: ipaserver + become: true + + tasks: + # Ensure HBAC Rule login is present + - ipahbacrule: + ipaadmin_password: MyPassword123 + name: login +``` + + +Example playbook to make sure HBAC Rule login exists with the only HBAC Service sshd: + +```yaml +--- +- name: Playbook to handle hbacrules + hbacsvcs: ipaserver + become: true + + tasks: + # Ensure HBAC Rule login is present with the only HBAC Service sshd + - ipahbacrule: + ipaadmin_password: MyPassword123 + name: login + hbacsvc: + - sshd +``` + +Example playbook to make sure HBAC Service sshd is present in HBAC Rule login: + +```yaml +--- +- name: Playbook to handle hbacrules + hbacsvcs: ipaserver + become: true + + tasks: + # Ensure HBAC Service sshd is present in HBAC Rule login + - ipahbacrule: + ipaadmin_password: MyPassword123 + name: login + hbacsvc: + - sshd + action: member +``` + +Example playbook to make sure HBAC Service sshd is absent in HBAC Rule login: + +```yaml +--- +- name: Playbook to handle hbacrules + hbacsvcs: ipaserver + become: true + + tasks: + # Ensure HBAC Service sshd is present in HBAC Rule login + - ipahbacrule: + ipaadmin_password: MyPassword123 + name: login + hbacsvc: + - sshd + action: member + state: absent +``` + +Example playbook to make sure HBAC Rule login is absent: + +```yaml +--- +- name: Playbook to handle hbacrules + hbacsvcs: ipaserver + become: true + + tasks: + # Ensure HBAC Rule login is present + - ipahbacrule: + ipaadmin_password: MyPassword123 + name: login + state: absent +``` + + +Variables +========= + +ipahbacrule +--------------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no +`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no +`name` \| `cn` | The list of hbacrule name strings. | yes +`description` | The hbacrule description string. | no +`usercategory` \| `usercat` | User category the rule applies to. Choices: ["all"] | no +`hostcategory` \| `hostcat` | Host category the rule applies to. Choices: ["all"] | no +`servicecategory` \| `servicecat` | HBAC service category the rule applies to. Choices: ["all"] | no +`nomembers` | Suppress processing of membership attributes. (bool) | no +`host` | List of host name strings assigned to this hbacrule. | no +`hostgroup` | List of host group name strings assigned to this hbacrule. | no +`hbacsvc` | List of HBAC Service name strings assigned to this hbacrule. | no +`hbacsvcgroup` | List of HBAC Service Group name strings assigned to this hbacrule. | no +`user` | List of user name strings assigned to this hbacrule. | no +`group` | List of user group name strings assigned to this hbacrule. | no +`action` | Work on hbacrule or member level. It can be on of `member` or `hbacrule` and defaults to `hbacrule`. | no +`state` | The state to ensure. It can be one of `present`, `absent`, `enabled` or `disabled`, default: `present`. | no + + +Authors +======= + +Thomas Woerner diff --git a/README-hbacsvc.md b/README-hbacsvc.md new file mode 100644 index 0000000..861ed6f --- /dev/null +++ b/README-hbacsvc.md @@ -0,0 +1,109 @@ +HBACsvc module +============== + +Description +----------- + +The hbacsvc (HBAC Service) module allows to ensure presence and absence of HBAC Services. + + +Features +-------- +* HBACsvc management + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.4.0 and up are supported by the ipahbacsvc module. + + +Requirements +------------ + +**Controller** +* Ansible version: 2.8+ + +**Node** +* Supported FreeIPA version (see above) + + +Usage +===== + +Example inventory file + +```ini +[ipaserver] +ipaserver.test.local +``` + + +Example playbook to make sure HBAC Service for http is present + +```yaml +--- +- name: Playbook to handle HBAC Services + hosts: ipaserver + become: true + + tasks: + # Ensure HBAC Service for http is present + - ipahbacsvc: + ipaadmin_password: MyPassword123 + name: http + description: Web service +``` + +Example playbook to make sure HBAC Service for tftp is present + +```yaml +--- +- name: Playbook to handle HBAC Services + hosts: ipaserver + become: true + + tasks: + # Ensure HBAC Service for tftp is present + - ipahbacsvc: + ipaadmin_password: MyPassword123 + name: tftp + description: TFTPWeb service +``` + +Example playbook to make sure HBAC Services for http and tftp are absent + +```yaml +--- +- name: Playbook to handle HBAC Services + hosts: ipaserver + become: true + + tasks: + # Ensure HBAC Service for http and tftp are absent + - ipahbacsvc: + ipaadmin_password: MyPassword123 + name: http,tftp + state: absent +``` + + +Variables +========= + +ipahbacsvc +---------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no +`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no +`name` \| `cn` \| `service` | The list of hbacsvc name strings. | no +`description` | The hbacsvc description string. | no +`state` | The state to ensure. It can be one of `present` or `absent`, default: `present`. | no + + +Authors +======= + +Thomas Woerner diff --git a/README-hbacsvcgroup.md b/README-hbacsvcgroup.md new file mode 100644 index 0000000..e2a8260 --- /dev/null +++ b/README-hbacsvcgroup.md @@ -0,0 +1,150 @@ +HBACsvcgroup module +=================== + +Description +----------- + +The hbacsvcgroup (HBAC Service Group) module allows to ensure presence and absence of HBAP Service Groups and members of the groups. + + +Features +-------- +* HBAC Service Group management + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.4.0 and up are supported by the ipahbacsvcgroup module. + + +Requirements +------------ + +**Controller** +* Ansible version: 2.8+ + +**Node** +* Supported FreeIPA version (see above) + + +Usage +===== + +Example inventory file + +```ini +[ipaserver] +ipaserver.test.local +``` + + +Example playbook to make sure HBAC Service Group login exists: + +```yaml +--- +- name: Playbook to handle hbacsvcgroups + hbacsvcs: ipaserver + become: true + + tasks: + # Ensure HBAC Service Group login is present + - ipahbacsvcgroup: + ipaadmin_password: MyPassword123 + name: login +``` + + +Example playbook to make sure HBAC Service Group login exists with the only HBAC Service sshd: + +```yaml +--- +- name: Playbook to handle hbacsvcgroups + hbacsvcs: ipaserver + become: true + + tasks: + # Ensure HBAC Service Group login is present with the only HBAC Service sshd + - ipahbacsvcgroup: + ipaadmin_password: MyPassword123 + name: login + hbacsvc: + - sshd +``` + +Example playbook to make sure HBAC Service sshd is present in HBAC Service Group login: + +```yaml +--- +- name: Playbook to handle hbacsvcgroups + hbacsvcs: ipaserver + become: true + + tasks: + # Ensure HBAC Service sshd is present in HBAC Service Group login + - ipahbacsvcgroup: + ipaadmin_password: MyPassword123 + name: login + hbacsvc: + - sshd + action: member +``` + +Example playbook to make sure HBAC Service sshd is absent in HBAC Service Group login: + +```yaml +--- +- name: Playbook to handle hbacsvcgroups + hbacsvcs: ipaserver + become: true + + tasks: + # Ensure HBAC Service sshd is present in HBAC Service Group login + - ipahbacsvcgroup: + ipaadmin_password: MyPassword123 + name: login + hbacsvc: + - sshd + action: member + state: absent +``` + +Example playbook to make sure HBAC Service Group login is absent: + +```yaml +--- +- name: Playbook to handle hbacsvcgroups + hbacsvcs: ipaserver + become: true + + tasks: + # Ensure HBAC Service Group login is present + - ipahbacsvcgroup: + ipaadmin_password: MyPassword123 + name: login + state: absent +``` + + +Variables +========= + +ipahbacsvcgroup +--------------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no +`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no +`name` \| `cn` | The list of hbacsvcgroup name strings. | no +`description` | The hbacsvcgroup description string. | no +`nomembers` | Suppress processing of membership attributes. (bool) | no +`hbacsvc` | List of hbacsvc name strings assigned to this hbacsvcgroup. | no +`action` | Work on hbacsvcgroup or member level. It can be on of `member` or `hbacsvcgroup` and defaults to `hbacsvcgroup`. | no +`state` | The state to ensure. It can be one of `present` or `absent`, default: `present`. | no + + +Authors +======= + +Thomas Woerner diff --git a/README-host.md b/README-host.md new file mode 100644 index 0000000..edec8d9 --- /dev/null +++ b/README-host.md @@ -0,0 +1,311 @@ +Host module +=========== + +Description +----------- + +The host module allows to ensure presence, absence and disablement of hosts. + +The host module is as compatible as possible to the Ansible upstream `ipa_host` module, but additionally offers to disable hosts. + + +Features +-------- +* Host management + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.4.0 and up are supported by the ipahost module. + + +Requirements +------------ + +**Controller** +* Ansible version: 2.8+ + +**Node** +* Supported FreeIPA version (see above) + + +Usage +===== + +Example inventory file + +```ini +[ipaserver] +ipaserver.test.local +``` + + +Example playbook to ensure host presence: + +```yaml +--- +- name: Playbook to handle hosts + hosts: ipaserver + become: true + + tasks: + # Ensure host is present + - ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + description: Example host + ip_address: 192.168.0.123 + locality: Lab + ns_host_location: Lab + ns_os_version: CentOS 7 + ns_hardware_platform: Lenovo T61 + mac_address: + - "08:00:27:E3:B1:2D" + - "52:54:00:BD:97:1E" + state: present +``` + + +Example playbook to ensure host presence without DNS: + +```yaml +--- +- name: Playbook to handle hosts + hosts: ipaserver + become: true + + tasks: + # Ensure host is present without DNS + - ipahost: + ipaadmin_password: MyPassword123 + name: host02.example.com + description: Example host + force: yes +``` + + +Example playbook to ensure host presence with a random password: + +```yaml +--- +- name: Ensure host with random password + hosts: ipaserver + become: true + + tasks: + - name: Host host01.example.com present with random password + ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + random: yes + force: yes + register: ipahost + + - name: Print generated random password + debug: + var: ipahost.host.randompassword +``` +Please remember that the `force` tag will also force the generation of a new random password even if the host already exists and if `update_password` is limited to `on_create`. + + +Example playbook to ensure presence of several hosts with a random password: + +```yaml +--- +- name: Ensure hosts with random password + hosts: ipaserver + become: true + + tasks: + - name: Hosts host01.example.com and host01.example.com present with random passwords + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: host01.example.com + random: yes + force: yes + - name: host02.example.com + random: yes + force: yes + register: ipahost + + - name: Print generated random password for host01.example.com + debug: + var: ipahost.host["host01.example.com"].randompassword + + - name: Print generated random password for host02.example.com + debug: + var: ipahost.host["host02.example.com"].randompassword +``` +Please remember that the `force` tag will also force the generation of a new random password even if the host alreay exists and if `update_password` is limited to `on_create`. + + +Example playbook to ensure presence of host member principal: + +```yaml +--- +- name: Host present with principal + hosts: ipaserver + become: true + + tasks: + - name: Host host01.example.com present with principals host/testhost01.example.com and host/myhost01.example.com + ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + principal: + - host/testhost01.example.com + - host/myhost01.example.com + action: member +``` + + +Example playbook to ensure presence of host member certificate: + +```yaml +- name: Host present with certificate + hosts: ipaserver + become: true + + tasks: + - name: Host host01.example.com present with certificate + ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + certificate: + - MIIC/zCCAeegAwIBAg... + action: member +``` + + +Example playbook to ensure presence of member managedby_host for serveral hosts: + +```yaml +--- +- name: Host present with managedby_host + hosts: ipaserver + become: true + + tasks: + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: host01.exmaple.com + managedby_host: server.exmaple.com + - name: host02.exmaple.com + managedby_host: server.exmaple.com + action: member +``` + + +Example playbook to disable a host: + +```yaml +--- +- name: Playbook to handle hosts + hosts: ipaserver + become: true + + tasks: + # Ensure host is disabled + - ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + update_dns: yes + state: disabled +``` +`update_dns` controls if the DNS entries will be updated. + + +Example playbook to ensure a host is absent: + +```yaml +--- +- name: Playbook to handle hosts + hosts: ipaserver + become: true + + tasks: + # Ensure host is absent + - ipahost: + ipaadmin_password: password1 + name: host01.example.com + state: absent +``` + + +Variables +========= + +ipahost +------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no +`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no +`name` \| `fqdn` | The list of host name strings. `name` with *host variables* or `hosts` containing *host variables* need to be used. | no +**Host variables** | Only used with `name` variable in the first level. | no +`hosts` | The list of host dicts. Each `hosts` dict entry can contain **host variables**.
There is one required option in the `hosts` dict:| no +  | `name` \| `fqdn` - The user name string of the entry. | yes +  | **Host variables** | no +`update_password` | Set password for a host in present state only on creation or always. It can be one of `always` or `on_create` and defaults to `always`. | no +`action` | Work on host or member level. It can be on of `member` or `host` and defaults to `host`. | no +`state` | The state to ensure. It can be one of `present`, `absent` or `disabled`, default: `present`. | yes + + +**Host Variables:** + +Variable | Description | Required +-------- | ----------- | -------- +`description` | The host description. | no +`locality` | Host locality (e.g. "Baltimore, MD"). | no +`location` \| `ns_host_location` | Host location (e.g. "Lab 2"). | no +`platform` \| `ns_hardware_platform` | Host hardware platform (e.g. "Lenovo T61"). | no +`os` \| `ns_os_version` | Host operating system and version (e.g. "Fedora 9"). | no +`password` \| `user_password` \| `userpassword` | Password used in bulk enrollment. | no +`random` \| `random_password` | Initiate the generation of a random password to be used in bulk enrollment. | no +`certificate` \| `usercertificate` | List of base-64 encoded host certificates | no +`managedby` \| `principalname` \| `krbprincipalname` | List of hosts that can manage this host | no +`principal` \| `principalname` \| `krbprincipalname` | List of principal aliases for this host | no +`allow_create_keytab_user` \| `ipaallowedtoperform_write_keys_user` | Users allowed to create a keytab of this host. | no +`allow_create_keytab_group` \| `ipaallowedtoperform_write_keys_group` | Groups allowed to create a keytab of this host. | no +`allow_create_keytab_host` \| `ipaallowedtoperform_write_keys_host` | Hosts allowed to create a keytab of this host. | no +`allow_create_keytab_hostgroup` \| `ipaallowedtoperform_write_keys_hostgroup` | Host groups allowed to create a keytab of this host. | no +`allow_retrieve_keytab_user` \| `ipaallowedtoperform_read_keys_user` | Users allowed to retieve a keytab of this host. | no +`allow_retrieve_keytab_group` \| `ipaallowedtoperform_read_keys_group` | Groups allowed to retieve a keytab of this host. | no +`allow_retrieve_keytab_host` \| `ipaallowedtoperform_read_keys_host` | Hosts allowed to retieve a keytab of this host. | no +`allow_retrieve_keytab_hostgroup` \| `ipaallowedtoperform_read_keys_hostgroup` | Host groups allowed to retieve a keytab of this host. | no +`mac_address` \| `macaddress` | List of hardware MAC addresses. | no +`sshpubkey` \| `ipasshpubkey` | List of SSH public keys | no +`userclass` \| `class` | Host category (semantics placed on this attribute are for local interpretation) | no +`auth_ind` \| `krbprincipalauthind` | Defines a whitelist for Authentication Indicators. Use 'otp' to allow OTP-based 2FA authentications. Use 'radius' to allow RADIUS-based 2FA authentications. Other values may be used for custom configurations. choices: ["radius", "otp", "pkinit", "hardened"] | no +`requires_pre_auth` \| `ipakrbrequirespreauth` | Pre-authentication is required for the service (bool) | no +`ok_as_delegate` \| `ipakrbokasdelegate` | Client credentials may be delegated to the service (bool) | no +`ok_to_auth_as_delegate` \| `ipakrboktoauthasdelegate` | The service is allowed to authenticate on behalf of a client (bool) | no +`force` | Force host name even if not in DNS. | no +`reverse` | Reverse DNS detection. | no +`ip_address` \| `ipaddress` | The host IP address. | no +`update_dns` | Update DNS entries. | no + + +Return Values +============= + +ipahost +------- + +There are only return values if one or more random passwords have been generated. + +Variable | Description | Returned When +-------- | ----------- | ------------- +`host` | Host dict with random password. (dict)
Options: | If random is yes and host did not exist or update_password is yes +  | `randompassword` - The generated random password | If only one host is handled by the module +  | `name` - The host name of the host that got a new random password. (dict)
Options:
  `randompassword` - The generated random password | If several hosts are handled by the module + + +Authors +======= + +Thomas Woerner diff --git a/README-hostgroup.md b/README-hostgroup.md new file mode 100644 index 0000000..39ea77f --- /dev/null +++ b/README-hostgroup.md @@ -0,0 +1,147 @@ +Hostgroup module +================ + +Description +----------- + +The hostgroup module allows to ensure presence and absence of hostgroups and members of hostgroups. + +The hostgroup module is as compatible as possible to the Ansible upstream `ipa_hostgroup` module, but additionally offers to make sure that hosts are present or absent in a hostgroup. + + +Features +-------- +* Hostgroup management + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.4.0 and up are supported by the ipahostgroup module. + + +Requirements +------------ + +**Controller** +* Ansible version: 2.8+ + +**Node** +* Supported FreeIPA version (see above) + + +Usage +===== + +Example inventory file + +```ini +[ipaserver] +ipaserver.test.local +``` + + +Example playbook to make sure hostgroup databases exists: + +```yaml +--- +- name: Playbook to handle hostgroups + hosts: ipaserver + become: true + + tasks: + # Ensure host-group databases is present + - ipahostgroup: + ipaadmin_password: MyPassword123 + name: databases + host: + - db.example.com + hostgroup: + - mysql-server + - oracle-server +``` + +Example playbook to make sure that hosts and hostgroups are present in existing databases hostgroup: + +```yaml +--- +- name: Playbook to handle hostgroups + hosts: ipaserver + become: true + + tasks: + # Ensure hosts and hostgroups are present in existing databases hostgroup + - ipahostgroup: + ipaadmin_password: MyPassword123 + name: databases + host: + - db.example.com + hostgroup: + - mysql-server + - oracle-server + action: member +``` +`action` controls if a the hostgroup or member will be handled. To add or remove members, set `action` to `member`. + +Example playbook to make sure hosts and hostgroups are absent in databases hostgroup: + +```yaml +--- +- name: Playbook to handle hostgroups + hosts: ipaserver + become: true + + tasks: + # Ensure hosts and hostgroups are absent in databases hostgroup + - ipahostgroup: + ipaadmin_password: MyPassword123 + name: databases + host: + - db.example.com + hostgroup: + - mysql-server + - oracle-server + action: member + state: absent +``` + +Example playbook to make sure host-group databases is absent: + +```yaml +--- +- name: Playbook to handle hostgroups + hosts: ipaserver + become: true + + tasks: + # Ensure host-group databases is absent + - ipahostgroup: + ipaadmin_password: MyPassword123 + name: databases + state: absent +``` + + +Variables +========= + +ipahostgroup +------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no +`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no +`name` \| `cn` | The list of hostgroup name strings. | no +`description` | The hostgroup description string. | no +`nomembers` | Suppress processing of membership attributes. (bool) | no +`host` | List of host name strings assigned to this hostgroup. | no +`hostgroup` | List of hostgroup name strings assigned to this hostgroup. | no +`action` | Work on hostgroup or member level. It can be on of `member` or `hostgroup` and defaults to `hostgroup`. | no +`state` | The state to ensure. It can be one of `present` or `absent`, default: `present`. | no + + +Authors +======= + +Thomas Woerner diff --git a/README-pwpolicy.md b/README-pwpolicy.md new file mode 100644 index 0000000..16306b7 --- /dev/null +++ b/README-pwpolicy.md @@ -0,0 +1,102 @@ +Pwpolicy module +=============== + +Description +----------- + +The pwpolicy module allows to ensure presence and absence of pwpolicies. + + +Features +-------- +* Pwpolicy management + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.4.0 and up are supported by the ipapwpolicy module. + + +Requirements +------------ + +**Controller** +* Ansible version: 2.8+ + +**Node** +* Supported FreeIPA version (see above) + + +Usage +===== + +Example inventory file + +```ini +[ipaserver] +ipaserver.test.local +``` + + +Example playbook to ensure presence of pwpolicies for exisiting group ops: + +```yaml + tasks: + - name: Ensure presence of pwpolicies for group ops + ipapwpolicy: + ipaadmin_password: MyPassword123 + name: ops + minlife: 7 + maxlife: 49 + history: 5 + priority: 1 + lockouttime: 300 + minlength: 8 + maxfail: 3 +``` + +Example playbook to ensure absence of pwpolicies for group ops + +```yaml +--- +- name: Playbook to handle pwpolicies + hosts: ipaserver + become: true + + tasks: + # Ensure absence of pwpolicies for group ops + - ipapwpolicy: + ipaadmin_password: MyPassword123 + name: ops + state: absent +``` + + +Variables +========= + +ipapwpolicy +------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no +`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no +`name` \| `cn` | The list of pwpolicy name strings. | no +`maxlife` \| `krbmaxpwdlife` | Maximum password lifetime in days. (int) | no +`minlife` \| `krbminpwdlife` | Minimum password lifetime in hours. (int) | no +`history` \| `krbpwdhistorylength` | Password history size. (int) | no +`minclasses` \| `krbpwdmindiffchars` | Minimum number of character classes. (int) | no +`minlength` \| `krbpwdminlength` | Minimum length of password. (int) | no +`priority` \| `cospriority` | Priority of the policy, higher number means lower priority. (int) | no +`maxfail` \| `krbpwdmaxfailure` | Consecutive failures before lockout. (int) | no +`failinterval` \| `krbpwdfailurecountinterval` | Period after which failure count will be reset in seconds. (int) | no +`lockouttime` \| `krbpwdlockoutduration` | Period for which lockout is enforced in seconds. (int) | no +`state` | The state to ensure. It can be one of `present` or `absent`, default: `present`. | yes + + +Authors +======= + +Thomas Woerner diff --git a/README-sudocmd.md b/README-sudocmd.md new file mode 100644 index 0000000..2842a2f --- /dev/null +++ b/README-sudocmd.md @@ -0,0 +1,95 @@ +Sudocmd module +================ + +Description +----------- + +The sudocmd module allows to ensure presence and absence of sudo command. + +The sudocmd module is as compatible as possible to the Ansible upstream `ipa_sudocmd` module. + + +Features +-------- +* Sudo command management + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.4.0 and up are supported by the ipa_sudocmd module. + + +Requirements +------------ + +**Controller** +* Ansible version: 2.8+ + +**Node** +* Supported FreeIPA version (see above) + + +Usage +===== + +Example inventory file + +```ini +[ipaserver] +ipaserver.test.local +``` + + +Example playbook to make sure sudocmd exists: + +```yaml +--- +- name: Playbook to handle sudocmd + hosts: ipaserver + become: true + + tasks: + # Ensure sudocmd is present + - ipasudocmd: + ipaadmin_password: MyPassword123 + name: /usr/bin/su + state: present +``` + +Example playbook to make sure sudocmd is absent: + +```yaml +--- +- name: Playbook to handle sudocmd + hosts: ipaserver + become: true + + tasks: + # Ensure sudocmd are absent + - ipahostgroup: + ipaadmin_password: MyPassword123 + name: /usr/bin/su + state: absent +``` + +Variables +========= + +ipasudocmd +------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no +`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no +`name` \| `sudocmd` | The sudo command strings. | yes +`description` | The command description string. | no +`nomembers` | Suppress processing of membership attributes. (bool) | no +`state` | The state to ensure. It can be one of `present` or `absent`, default: `present`. | no + + +Authors +======= + +Rafael Guterres Jeffman diff --git a/README-sudocmdgroup.md b/README-sudocmdgroup.md new file mode 100644 index 0000000..f24b2cc --- /dev/null +++ b/README-sudocmdgroup.md @@ -0,0 +1,137 @@ +Sudocmdgroup module +=================== + +Description +----------- + +The sudocmdgroup module allows to ensure presence and absence of sudocmdgroups and members of sudocmdgroups. + +The sudocmdgroup module is as compatible as possible to the Ansible upstream `ipa_sudocmdgroup` module, but additionally offers to make sure that sudocmds are present or absent in a sudocmdgroup. + + +Features +-------- +* Sudocmdgroup management + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.4.0 and up are supported by the ipasudocmdgroup module. + + +Requirements +------------ + +**Controller** +* Ansible version: 2.8+ + +**Node** +* Supported FreeIPA version (see above) + + +Usage +===== + +Example inventory file + +```ini +[ipaserver] +ipaserver.test.local +``` + + +Example playbook to make sure sudocmdgroup is present: + +```yaml +--- +- name: Playbook to handle sudocmdgroups + hosts: ipaserver + become: true + + tasks: + # Ensure sudocmdgroup is present + - ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: group01 + description: Group of important commands +``` + +Example playbook to make sure that a sudo command and sudocmdgroups are present in existing sudocmdgroup: + +```yaml +--- +- name: Playbook to handle sudocmdgroups + hosts: ipaserver + become: true + + tasks: + # Ensure sudo commands are present in existing sudocmdgroup + - ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: group01 + sudocmd: + - /usr/bin/su + - /usr/bin/less + action: member +``` +`action` controls if the sudocmdgroup or member will be handled. To add or remove members, set `action` to `member`. + +Example playbook to make sure that a sudo command and sudocmdgroups are absent in sudocmdgroup: + +```yaml +--- +- name: Playbook to handle sudocmdgroups + hosts: ipaserver + become: true + + tasks: + # Ensure sudocmds are absent in existing sudocmdgroup + - ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: group01 + sudocmd: + - /usr/bin/su + - /usr/bin/less + action: member + state: absent +``` + +Example playbook to make sure sudocmdgroup is absent: + +```yaml +--- +- name: Playbook to handle sudocmdgroups + hosts: ipaserver + become: true + + tasks: + # Ensure sudocmdgroup is absent + - ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: group01 + state: absent +``` + +Variables +========= + +ipasudocmdgroup +------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no +`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no +`name` \| `cn` | The list of sudocmdgroup name strings. | no +`description` | The sudocmdgroup description string. | no +`nomembers` | Suppress processing of membership attributes. (bool) | no +`sudocmd` | List of sudocmdgroup name strings assigned to this sudocmdgroup. | no +`action` | Work on sudocmdgroup or member level. It can be on of `member` or `sudocmdgroup` and defaults to `sudocmdgroup`. | no +`state` | The state to ensure. It can be one of `present` or `absent`, default: `present`. | no + + +Authors +======= + +Rafael Guterres Jeffman diff --git a/README-sudorule.md b/README-sudorule.md new file mode 100644 index 0000000..aca1b2d --- /dev/null +++ b/README-sudorule.md @@ -0,0 +1,141 @@ +Sudorule module +=============== + +Description +----------- + +The sudorule (Sudo Rule) module allows to ensure presence and absence of Sudo Rules and host, hostgroups, users, and user groups as members of Sudo Rule. + + +Features +-------- +* Sudo Rule management + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.4.0 and up are supported by the ipasudorule module. + + +Requirements +------------ + +**Controller** +* Ansible version: 2.8+ + +**Node** +* Supported FreeIPA version (see above) + + +Usage +===== + +Example inventory file + +```ini +[ipaserver] +ipaserver.test.local +``` + + +Example playbook to make sure Sudo Rule is present: + +```yaml +--- +- name: Playbook to handle sudorules + hosts: ipaserver + become: true + + tasks: + # Ensure Sudo Rule is present + - ipasudorule: + ipaadmin_password: MyPassword123 + name: testrule1 +``` + + +Example playbook to make sure sudocmds are present in Sudo Rule: + +```yaml +--- +- name: Playbook to handle sudorules + hosts: ipaserver + become: true + + tasks: + # Ensure Sudo Rule is present + - ipasudorule: + ipaadmin_password: MyPassword123 + name: testrule1 + cmd: + - /sbin/ifconfig + action: member +``` + + +Example playbook to make sure sudocmds are not present in Sudo Rule: + +```yaml +--- +- name: Playbook to handle sudorules + hosts: ipaserver + become: true + + tasks: + # Ensure Sudo Rule is present + - ipasudorule: + ipaadmin_password: MyPassword123 + name: testrule1 + cmd: + - /sbin/ifconfig + action: member + state: absent +``` + +Example playbook to make sure Sudo Rule is absent: + +```yaml +--- +- name: Playbook to handle sudorules + hosts: ipaserver + become: true + + tasks: + # Ensure Sudo Rule is present + - ipasudorule: + ipaadmin_password: MyPassword123 + name: testrule1 +``` + + +Variables +========= + +ipasudorule +--------------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no +`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no +`name` \| `cn` | The list of sudorule name strings. | yes +`description` | The sudorule description string. | no +`usercategory` | User category the rule applies to. Choices: ["all"] | no +`hostcategory` | Host category the rule applies to. Choices: ["all"] | no +`cmdcategory` | Command category the rule applies to. Choices: ["all"] | no +`nomembers` | Suppress processing of membership attributes. (bool) | no +`host` | List of host name strings assigned to this sudorule. | no +`hostgroup` | List of host group name strings assigned to this sudorule. | no +`user` | List of user name strings assigned to this sudorule. | no +`group` | List of user group name strings assigned to this sudorule. | no +`cmd` | List of sudocmd name strings assigned to this sudorule. | no +`cmdgroup` | List of sudocmd group name strings assigned wto this sudorule. | no +`action` | Work on sudorule or member level. It can be on of `member` or `sudorule` and defaults to `sudorule`. | no +`state` | The state to ensure. It can be one of `present`, `absent`, `enabled` or `disabled`, default: `present`. | no + + +Authors +======= + +Rafael Jeffman diff --git a/README-topology.md b/README-topology.md new file mode 100644 index 0000000..dc595eb --- /dev/null +++ b/README-topology.md @@ -0,0 +1,186 @@ +Topology modules +================ + +Description +----------- + +These modules allow to manage the topology. That means that it can made sure that topology segments are present, absent or reinitialized. Also it is possible to verify topology suffixes. + + +Features +-------- +* Topology management + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.4.0 and up are supported by the ipatopologysegment and ipatopologysuffix modules. + + +Requirements +------------ + +**Controller** +* Ansible version: 2.8+ + +**Node** +* Supported FreeIPA version (see above) + + +Usage +===== + +Example inventory file + +```ini +[ipaserver] +ipaserver.test.local +``` + + +Example playbook to add a topology segment with default name (cn): + +```yaml +--- +- name: Playbook to handle topologysegment + hosts: ipaserver + become: true + + tasks: + - name: Add topology segment + ipatopologysegment: + ipaadmin_password: MyPassword123 + suffix: domain + left: ipareplica1.test.local + right: ipareplica2.test.local + state: present +``` +The name (cn) can also be set if it should not be the default `{left}-to-{right}`. + + +Example playbook to delete a topology segment: + +```yaml +--- +- name: Playbook to handle topologysegment + hosts: ipaserver + become: true + + tasks: + - name: Delete topology segment + ipatopologysegment: + ipaadmin_password: MyPassword123 + suffix: domain + left: ipareplica1.test.local + right: ipareplica2.test.local + state: absent +``` +It is possible to either use the name (cn) or left and right nodes. If left and right nodes are used, then the name will be searched and used internally. + + +Example playbook to reinitialize a topology segment: + +```yaml +--- +- name: Playbook to handle topologysegment + hosts: ipaserver + become: true + + tasks: + - name: Reinitialize topology segment + ipatopologysegment: + ipaadmin_password: MyPassword123 + suffix: domain + left: ipareplica1.test.local + right: ipareplica2.test.local + direction: left-to-right + state: reinitialized +``` +It is possible to either use the name (cn) or left and right nodes. If left and right nodes are used, then the name will be searched and used internally. + + +Example playbook to verify a topology suffix: + +```yaml +--- +- name: Playbook to handle topologysuffix + hosts: ipaserver + become: true + + tasks: + - name: Verify topology suffix + ipatopologysuffix: + ipaadmin_password: MyPassword123 + suffix: domain + state: verified +``` + +Example playbook to add or remove or check or reinitialize a list of topology segments: + +```yaml +--- +- name: Add topology segments + hosts: ipaserver + become: true + gather_facts: false + + vars: + ipaadmin_password: password1 + ipatopology_segments: + - {suffix: domain, left: replica1.test.local, right: replica2.test.local} + - {suffix: domain, left: replica2.test.local, right: replica3.test.local} + - {suffix: domain, left: replica3.test.local, right: replica4.test.local} + - {suffix: domain+ca, left: replica4.test.local, right: replica1.test.local} + + tasks: + - name: Add topology segment + ipatopologysegment: + ipaadmin_password: "{{ ipaadmin_password }}" + suffix: "{{ item.suffix }}" + name: "{{ item.name | default(omit) }}" + left: "{{ item.left }}" + right: "{{ item.right }}" + state: present + #state: absent + #state: checked + #state: reinitialized + loop: "{{ ipatopology_segments | default([]) }}" +``` + + +Variables +========= + +ipatopologysegment +------------------ + +Variable | Description | Required +-------- | ----------- | -------- +`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no +`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no +`suffix` | The topology suffix to be used, this can either be `domain`, `ca` or `domain+ca` | yes +`name` \| `cn` | The topology segment name (cn) is the unique identifier for a segment. | no +`left` \| `leftnode` | The left replication node string - an IPA server | no +`right` \| `rightnode` | The right replication node string - an IPA server | no +`direction` | The direction a segment will be reinitialized. It can either be `left-to-right` or `right-to-left` and only used with `state: reinitialized` | +`state` | The state to ensure. It can be one of `present`, `absent`, `enabled`, `disabled`, `checked` or `reinitialized` | yes + + +ipatopologysuffix +----------------- + +Verify FreeIPA topology suffix + +Variable | Description | Required +-------- | ----------- | -------- +`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no +`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no +`suffix` | The topology suffix to be used, this can either be `domain` or `ca` | yes +`state` | The state to ensure. It can only be `verified` | yes + + +Authors +======= + +Thomas Woerner diff --git a/README-user.md b/README-user.md new file mode 100644 index 0000000..56772a7 --- /dev/null +++ b/README-user.md @@ -0,0 +1,447 @@ +User module +=========== + +Description +----------- + +The user module allows to ensure presence, absence, disablement, unlocking and undeletion of users. + +The user module is as compatible as possible to the Ansible upstream `ipa_user` module, but additionally offers to preserve delete, enable, disable, unlock and undelete users. + + +Features +-------- +* User management + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.4.0 and up are supported by the ipauser module. + + +Requirements +------------ + +**Controller** +* Ansible version: 2.8+ + +**Node** +* Supported FreeIPA version (see above) + + +Usage +===== + +Example inventory file + +```ini +[ipaserver] +ipaserver.test.local +``` + + +Example playbook to ensure a user is present: + +```yaml +--- +- name: Playbook to handle users + hosts: ipaserver + become: true + + tasks: + # Ensure user pinky is present + - ipauser: + ipaadmin_password: MyPassword123 + name: pinky + first: pinky + last: Acme + uid: 10001 + gid: 100 + phone: "+555123457" + email: pinky@acme.com + passwordexpiration: "2023-01-19 23:59:59" + password: "no-brain" + update_password: on_create + + # Ensure user brain is present + - ipauser: + ipaadmin_password: MyPassword123 + name: brain + first: brain + last: Acme +``` +`update_password` controls if a password for a user will be set in present state only on creation or every time (always). + + +These two `ipauser` module calls can be combined into one with the `users` variable: + +```yaml +--- +- name: Playbook to handle users + hosts: ipaserver + become: true + + tasks: + # Ensure users pinky and brain are present + - ipauser: + ipaadmin_password: MyPassword123 + users: + - name: pinky + first: pinky + last: Acme + uid: 10001 + gid: 100 + phone: "+555123457" + email: pinky@acme.com + passwordexpiration: "2023-01-19 23:59:59" + password: "no-brain" + - name: brain + first: brain + last: Acme + update_password: on_create +``` + +You can also alternatively use a json file containing the users, here `users_present.json`: + +```json +{ + "users": [ + { + "name": "user1", + "first": "First 1", + "last": "Last 1" + }, + { + "name": "user2", + "first": "First 2", + "last": "Last 2" + }, + ... + ] +} +``` + +And ensure the presence of the users with this example playbook: + +```yaml +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Include users_present.json + include_vars: + file: users_present.json + + - name: Users present + ipauser: + ipaadmin_password: SomeADMINpassword + users: "{{ users }}" +``` + +Ensure user pinky is present with a generated random password and print the random password: + +```yaml +--- +- name: Playbook to handle users + hosts: ipaserver + become: true + + tasks: + # Ensure user pinky is present with a random password + - ipauser: + ipaadmin_password: MyPassword123 + name: brain + first: brain + last: Acme + random: yes + register: ipauser + + - name: Print generated random password + debug: + var: ipauser.user.randompassword +``` + +Ensure users pinky and brain are present with a generated random password and print the random passwords: + +```yaml +--- +- name: Playbook to handle users + hosts: ipaserver + become: true + + tasks: + # Ensure users pinky and brain are present with random password + - ipauser: + ipaadmin_password: MyPassword123 + users: + - name: pinky + first: pinky + last: Acme + uid: 10001 + gid: 100 + phone: "+555123457" + email: pinky@acme.com + passwordexpiration: "2023-01-19 23:59:59" + password: "no-brain" + - name: brain + first: brain + last: Acme + register: ipauser + + - name: Print generated random password of pinky + debug: + var: ipauser.user.pinky.randompassword + + - name: Print generated random password of brain + debug: + var: ipauser.user.brain.randompassword +``` + +Example playbook to delete a user, but preserve it: + +```yaml +--- +- name: Playbook to handle users + hosts: ipaserver + become: true + + tasks: + # Remove but preserve user pinky + - ipauser: + ipaadmin_password: MyPassword123 + name: pinky + preserve: yes + state: absent +``` + +This can also be done with the `users` variable containing only names, this can be combined into one module call: + +Example playbook to delete a user, but preserve it using the `users` variable: + +```yaml +--- +- name: Playbook to handle users + hosts: ipaserver + become: true + + tasks: + # Remove but preserve user pinky + - ipauser: + ipaadmin_password: MyPassword123 + users: + - name: pinky + preserve: yes + state: absent +``` + +This can also be done as an alternative with the `users` variable containing only names. + + +Example playbook to undelete a preserved user. + +```yaml +--- +- name: Playbook to handle users + hosts: ipaserver + become: true + + tasks: + # Undelete preserved user pinky + - ipauser: + ipaadmin_password: MyPassword123 + name: pinky + state: undeleted +``` + +This can also be done as an alternative with the `users` variable containing only names. + + +Example playbook to disable a user: + +```yaml +--- +- name: Playbook to handle users + hosts: ipaserver + become: true + + tasks: + # Disable user pinky + - ipauser: + ipaadmin_password: MyPassword123 + name: pinky + state: disabled +``` + +This can also be done as an alternative with the `users` variable containing only names. + + +Example playbook to enable users: + +```yaml +--- +- name: Playbook to handle users + hosts: ipaserver + become: true + + tasks: + # Enable user pinky and brain + - ipauser: + ipaadmin_password: MyPassword123 + name: pinky,brain + state: enabled +``` + +This can also be done as an alternative with the `users` variable containing only names. + + +Example playbook to unlock users: + +```yaml +--- +- name: Playbook to handle users + hosts: ipaserver + become: true + + tasks: + # Unlock user pinky and brain + - ipauser: + ipaadmin_password: MyPassword123 + name: pinky,brain + state: unlocked +``` + + +Example playbook to ensure users are absent: + +```yaml +--- +- name: Playbook to handle users + hosts: ipaserver + become: true + + tasks: + # Ensure users pinky and brain are absent + - ipauser: + ipaadmin_password: MyPassword123 + name: pinky,brain + state: absent +``` + +This can also be done as an alternative with the `users` variable containing only names. + + +Example playbook to ensure users are absent: + +```yaml +--- +- name: Playbook to handle users + hosts: ipaserver + become: true + + tasks: + # Ensure users pinky and brain are absent + - ipauser: + ipaadmin_password: MyPassword123 + users: + - name: pinky + - name: brain + state: absent +``` + + +Variables +========= + +ipauser +------- + +**General Variables:** + +Variable | Description | Required +-------- | ----------- | -------- +`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no +`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no +`name` | The list of user name strings. `name` with *user variables* or `users` containing *user variables* need to be used. | no +**User variables** | Only used with `name` variable in the first level. | no +`users` | The list of user dicts. Each `users` dict entry can contain **user variables**.
There is one required option in the `users` dict:| no +  | `name` - The user name string of the entry. | yes +  | **User variables** | no +`preserve` | Delete a user, keeping the entry available for future use. (bool) | no +`update_password` | Set password for a user in present state only on creation or always. It can be one of `always` or `on_create` and defaults to `always`. | no +`preserve` | Delete a user, keeping the entry available for future use. (bool) | no +`action` | Work on user or member level. It can be on of `member` or `user` and defaults to `user`. | no +`state` | The state to ensure. It can be one of `present`, `absent`, `enabled`, `disabled`, `unlocked` or `undeleted`, default: `present`. Only `names` or `users` with only `name` set are allowed if state is not `present`. | yes + + + +**User Variables:** + +Variable | Description | Required +-------- | ----------- | -------- +`first` \| `givenname` | The first name string. | no +`last` \| `sn` | The last name string. | no +`fullname` \| `cn` | The full name string. | no +`displayname` | The display name string. | no +`homedir` | The home directory string. | no +`shell` \| `loginshell` | The login shell string. | no +`email` | List of email address strings. | no +`principal` \| `principalnam` \| `krbprincipalname` | The kerberos principal sptring. | no +`principalexpiration` \| `krbprincipalexpiration` | The kerberos principal expiration date. Possible formats: `YYYYMMddHHmmssZ`, `YYYY-MM-ddTHH:mm:ssZ`, `YYYY-MM-ddTHH:mmZ`, `YYYY-MM-ddZ`, `YYYY-MM-dd HH:mm:ssZ` or `YYYY-MM-dd HH:mmZ`. The trailing 'Z' can be skipped. | no +`passwordexpiration` \| `krbpasswordexpiration` | The kerberos password expiration date. Possible formats: `YYYYMMddHHmmssZ`, `YYYY-MM-ddTHH:mm:ssZ`, `YYYY-MM-ddTHH:mmZ`, `YYYY-MM-ddZ`, `YYYY-MM-dd HH:mm:ssZ` or `YYYY-MM-dd HH:mmZ`. The trailing 'Z' can be skipped. Only usable with IPA versions 4.7 and up. | no +`password` | The user password string. | no +`random` | Generate a random user password | no +`uid` \| `uidnumber` | The UID integer. | no +`gid` \| `gidnumber` | The GID integer. | no +`city` | City | no +`userstate` \| `st` | State/Province | no +`postalcode` \| `zip` | Postalcode/ZIP | no +`phone` \| `telephonenumber` | List of telephone number strings, | no +`mobile` | List of mobile telephone number strings. | no +`pager` | List of pager number strings. | no +`fax` \| `facsimiletelephonenumber` | List of fax number strings. | no +`orgunit` | The Organisation unit. | no +`title` | The job title string. | no +`manager` | List of manager user names. | no +`carlicense` | List of car licenses. | no +`sshpubkey` \| `ipasshpubkey` | List of SSH public keys. | no +`userauthtype` | List of supported user authentication types. Choices: `password`, `radius` and `otp` | no +`userclass` | User category. (semantics placed on this attribute are for local interpretation). | no +`radius` | RADIUS proxy configuration | no +`radiususer` | RADIUS proxy username | no +`departmentnumber` | Department Number | no +`employeenumber` | Employee Number | no +`employeetype` | Employee Type | no +`preferredlanguage` | Preferred Language | no +`certificate` | List of base-64 encoded user certificates. | no +`certmapdata` | List of certificate mappings. Either `certificate` or `issuer` together with `subject` need to be specified.
Options: | no +  | `certificate` - Base-64 encoded user certificate | no +  | `issuer` - Issuer of the certificate | no +  | `subject` - Subject of the certificate | no +`noprivate` | Do not create user private group. (bool) | no +`nomembers` | Suppress processing of membership attributes. (bool) | no + + + +Return Values +============= + +ipauser +------- + +There are only return values if one or more random passwords have been generated. + +Variable | Description | Returned When +-------- | ----------- | ------------- +`host` | Host dict with random password. (dict)
Options: | If random is yes and user did not exist or update_password is yes +  | `randompassword` - The generated random password | If only one user is handled by the module +  | `name` - The user name of the user that got a new random password. (dict)
Options:
  `randompassword` - The generated random password | If several users are handled by the module + + +Authors +======= + +Thomas Woerner diff --git a/README.md b/README.md new file mode 100644 index 0000000..4f90ac2 --- /dev/null +++ b/README.md @@ -0,0 +1,420 @@ +FreeIPA Ansible collection +========================== + +This repository contains [Ansible](https://www.ansible.com/) roles and playbooks to install and uninstall [FreeIPA](https://www.freeipa.org/) `servers`, `replicas` and `clients`. Also modules for group, host, topology and user management. + +**Note**: The ansible playbooks and roles require a configured ansible environment where the ansible nodes are reachable and are properly set up to have an IP address and a working package manager. + +Features +-------- +* Server, replica and client deployment +* Cluster deployments: Server, replicas and clients in one playbook +* One-time-password (OTP) support for client installation +* Repair mode for clients +* Modules for group management +* Modules for hbacrule management +* Modules for hbacsvc management +* Modules for hbacsvcgroup management +* Modules for host management +* Modules for hostgroup management +* Modules for pwpolicy management +* Modules for sudocmd management +* Modules for sudocmdgroup management +* Modules for sudorule management +* Modules for topology management +* Modules for user management + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.6 and up are supported by all roles. + +The client role supports versions 4.4 and up, the server role is working with versions 4.5 and up, the replica role is currently only working with versions 4.6 and up. + +Supported Distributions +----------------------- + +* RHEL/CentOS 7.4+ +* Fedora 26+ +* Ubuntu +* Debian 10+ (ipaclient only, no server or replica!) + +Requirements +------------ + +**Controller** +* Ansible version: 2.8+ (ansible-freeipa is an Ansible Collection) +* /usr/bin/kinit is required on the controller if a one time password (OTP) is used +* python3-gssapi is required on the controller if a one time password (OTP) is used with keytab to install the client. + +**Node** +* Supported FreeIPA version (see above) +* Supported distribution (needed for package installation only, see above) + +Limitations +----------- + +**External signed CA** + +External signed CA is now supported. But the currently needed two step process is an issue for the processing in a simple playbook. + +Work is planned to have a new method to handle CSR for external signed CAs in a separate step before starting the server installation. + + +Usage +===== + +How to use ansible-freeipa +-------------------------- + +**GIT repo** + +The simplest method for now is to clone this repository on the controller from github directly and to start the deployment from the ansible-freeipa directory: + +```bash +git clone https://github.com/freeipa/ansible-freeipa.git +cd ansible-freeipa +``` +You can use the roles directly within the top directory of the git repo, but to be able to use the management modules in the plugins subdirectory, you have to either adapt `ansible.cfg` or create links for the roles, modules or directories. + +You can either adapt ansible.cfg: + +``` +roles_path = /my/dir/ansible-freeipa/roles +library = /my/dir/ansible-freeipa/plugins/modules +module_utils = /my/dir/ansible-freeipa/plugins/module_utils +``` + +Or you can link the directories: + +``` +ansible-freeipa/roles to ~/.ansible/ +ansible-freeipa/plugins/modules to ~/.ansible/plugins/ +ansible-freeipa/plugins/module_utils to ~/.ansible/plugins/ +``` + +**RPM package** + +There are RPM packages available for Fedora 29+. These are installing the roles and modules into the global Ansible directories for `roles`, `plugins/modules` and `plugins/module_utils` in the `/usr/share/ansible` directory. Therefore is it possible to use the roles and modules without adapting the names like it is done in the example playbooks. + +**Ansible galaxy** + +This command will get the whole collection from galaxy: + +```bash +ansible-galaxy collection install freeipa.ansible_freeipa +``` + +Installing collections using the ansible-galaxy command is only supported with ansible 2.9+. + +The mazer tool can be used for to install the collection for ansible 2.8: + +```bash +mazer install freeipa.ansible_freeipa +``` + +Ansible galaxy does not support the use of dash ('-') in a name and is automatically replacing this with an underscore ('\_'). Therefore the name is `ansible_freeipa`. The ansible_freeipa collection will be placed in the directory `~/.ansible/collections/ansible_collections/freeipa/ansible_freeipa` where it will be automatically be found for this user. + +The needed adaptions of collection prefixes for `modules` and `module_utils` will be done with ansible-freeipa release `0.1.6` for galaxy. + + +Ansible inventory file +---------------------- + +The most important parts of the inventory file is the definition of the nodes, settings and the management modules. Please remember to use [Ansible vault](https://docs.ansible.com/ansible/latest/user_guide/vault.html) for passwords. The examples here are not using vault for better readability. + +**Master server** + +The master server is defined within the ```[ipaserver]``` group: +```yaml +[ipaserver] +ipaserver.test.local +``` +There are variables that need to be set like ```domain```, ```realm```, ```admin password``` and ```dm password```. These can be set in the ```[ipaserver:vars]``` section: +```yaml +[ipaserver:vars] +ipaadmin_password=ADMPassword1 +ipadm_password=DMPassword1 +ipaserver_domain=test.local +ipaserver_realm=TEST.LOCAL +``` + +The admin principle is ```admin``` by default. Please set ```ipaadmin_principal``` if you need to change it. + +You can also add more setting here, like for example to enable the DNS server or to set auto-forwarders: +```yaml +[ipaserver:vars] +ipaserver_setup_dns=yes +ipaserver_auto_forwarders=yes +``` + +But also to skip package installation or firewalld configuration: +```yaml +[ipaserver:vars] +ipaserver_install_packages=no +ipaserver_setup_firewalld=no +``` +The installation of packages and also the configuration of the firewall are by default enabled. +Note that it is not enough to mask systemd firewalld service to skip the firewalld configuration. You need to set the variable to `no`. + +For more server settings, please have a look at the [server role documentation](roles/ipaserver/README.md). + +**Replica** + +The replicas are defined within the ```[ipareplicas]``` group: +```yaml +[ipareplicas] +ipareplica1.test.local +ipareplica2.test.local +``` + +If the master server is already deployed and there are DNS txt records to be able to auto-detect the server, then it is not needed to set ```domain``` or ```realm``` for the replica deployment. But it might be needed to set the master server of a replica because of the topology. If this is needed, it can be set either in the ```[ipareplicas:vars]``` section if it will apply to all the replicas in the ```[ipareplicas]``` group or it is possible to set this also per replica in the ```[ipareplicas]``` group: +```yaml +[ipareplicas] +ipareplica1.test.local +ipareplica2.test.local ipareplica_servers=ipareplica1.test.local +``` +This will create a chain from ```ipaserver.test.local <- ipareplica1.test.local <- ipareplica2.test.local```. + +If you need to set more than one server for a replica (for fallbacks etc.), simply use a comma separated list for ```ipareplica_servers```: +```yaml +[ipareplicas_tier1] +ipareplica1.test.local + +[ipareplicas_tier2] +ipareplica2.test.local ipareplica_servers=ipareplica1.test.local,ipaserver.test.local +``` +The first entry in ```ipareplica_servers``` will be used as the master. + +In this case you need to have separate tasks in the playbook to first deploy replicas from tier1 and then replicas from tier2: +```yaml +--- +- name: Playbook to configure IPA replicas (tier1) + hosts: ipareplicas_tier1 + become: true + + roles: + - role: ipareplica + state: present + +- name: Playbook to configure IPA replicas (tier2) + hosts: ipareplicas_tier2 + become: true + + roles: + - role: ipareplica + state: present +``` + +You can add settings for replica deployment: +```yaml +[ipareplicas:vars] +ipaadmin_password=ADMPassword1 +ipadm_password=DMPassword1 +ipaserver_domain=test.local +ipaserver_realm=TEST.LOCAL +``` + +You can also add more setting here, like for example to setup DNS or to enable auto-forwarders: +```yaml +[ipareplica:vars] +ipaserver_setup_dns=yes +ipaserver_auto_forwarders=yes +``` + +If you need to skip package installation or firewalld configuration: + +```yaml +[ipareplicas:vars] +ipareplica_install_packages=no +ipareplica_setup_firewalld=no +``` + +The installation of packages and also the configuration of the firewall are by default enabled. +Note that it is not enough to mask systemd firewalld service to skip the firewalld configuration. You need to set the variable to `no`. + +For more replica settings, please have a look at the [replica role documentation](roles/ipareplica/README.md). + + +**Client** + +Clients are defined within the [ipaclients] group: +```yaml +[ipaclients] +ipaclient1.test.local +ipaclient2.test.local +ipaclient3.test.local +ipaclient4.test.local +``` + +For simple setups or in defined client environments it might not be needed to set domain or realm for the replica deployment. But it might be needed to set the master server of a client because of the topology. If this is needed, it can be set either in the [ipaclients:vars} section if it will apply to all the clients in the [ipaclients] group or it is possible to set this also per client in the [ipaclients] group: +```yaml +[ipaclients] +ipaclient1.test.local ipaclient_servers=ipareplica1.test.local +ipaclient2.test.local ipaclient_servers=ipareplica1.test.local +ipaclient3.test.local ipaclient_servers=ipareplica2.test.local +ipaclient4.test.local ipaclient_servers=ipareplica2.test.local +``` +If you need to set more than one server for a client (for fallbacks etc.), simply use a comma separated list for ```ipaclient_servers```. + +You can add settings for client deployment: +```yaml +[ipaclients:vars] +ipaadmin_password=ADMPassword1 +ipaserver_domain=test.local +ipaserver_realm=TEST.LOCAL +``` + +For enhanced security it is possible to use a auto-generated one-time-password (OTP). This will be generated on the controller using the (first) server. It is needed to have the Python gssapi bindings installed on the controller for this. +To enable the generation of the one-time-password: +```yaml +[ipaclients:vars] +ipaclient_use_otp=yes +``` + +For more client settings, please have a look at the [client role documentation](roles/ipaclient/README.md). + +**Cluster** + +If you want to deploy more than a master server at once, then it will be good to define a new group like ```[ipacluster]``` that contains all the other groups ```[ipaserver]```, ```[ipareplicas]``` and ```[ipaclients]```. This way it is not needed to set ```domain```, ```realm```, ```admin password``` or ```dm password``` for the single groups: +```yaml +[ipacluster:children] +ipaserver +ipareplicas +ipaclients + +[ipacluster:vars] +ipaadmin_password=ADMPassword1 +ipadm_password=DMPassword1 +ipaserver_domain=test.local +ipaserver_realm=TEST.LOCAL +``` +All these settings will be available in the ```[ipaserver]```, ```[ipareplicas]``` and ```[ipaclient]``` groups. + +**Topology** + +With this playbook it is possible to add a list of topology segments using the `ipatopologysegment` module. + +```yaml +--- +- name: Add topology segments + hosts: ipaserver + become: true + gather_facts: false + + vars: + ipaadmin_password: password1 + ipatopology_segments: + - {suffix: domain, left: replica1.test.local, right: replica2.test.local} + - {suffix: domain, left: replica2.test.local, right: replica3.test.local} + - {suffix: domain, left: replica3.test.local, right: replica4.test.local} + - {suffix: domain+ca, left: replica4.test.local, right: replica1.test.local} + + tasks: + - name: Add topology segment + ipatopologysegment: + password: "{{ ipaadmin_password }}" + suffix: "{{ item.suffix }}" + name: "{{ item.name | default(omit) }}" + left: "{{ item.left }}" + right: "{{ item.right }}" + #state: present + #state: absent + #state: checked + state: reinitialized + loop: "{{ ipatopology_segments | default([]) }}" +``` + + + +Playbooks +========= + +The playbooks needed to deploy or undeploy server, replicas and clients are part of the repository and placed in the playbooks folder. There are also playbooks to deploy and undeploy clusters. With them it is only needed to add an inventory file: +``` +playbooks\ + install-client.yml + install-cluster.yml + install-replica.yml + install-server.yml + uninstall-client.yml + uninstall-cluster.yml + uninstall-replica.yml + uninstall-server.yml +``` + +How to deploy a master server +----------------------------- + +```bash +ansible-playbook -v -i inventory/hosts install-server.yml +``` +This will deploy the master server defined in the inventory file. + +If Ansible vault is used for passwords, then it is needed to adapt the playbooks in this way: +```yaml +--- +- name: Playbook to configure IPA servers + hosts: ipaserver + become: true + vars_files: + - playbook_sensitive_data.yml + + roles: + - role: ipaserver + state: present +``` + +It is also needed to provide the vault password file on the ansible-playbook command line: +```bash +ansible-playbook -v -i inventory/hosts --vault-password-file .vaul_pass.txt install-server.yml +``` + +How to deploy a replica +----------------------- + +```bash +ansible-playbook -v -i inventory/hosts install-replica.yml +``` +This will deploy the replicas defined in the inventory file. + +How to setup a client +--------------------- + +```bash +ansible-playbook -v -i inventory/hosts install-client.yml +``` +This will deploy the clients defined in the inventory file. + +How to deploy a cluster +----------------------- + +```bash +ansible-playbook -v -i inventory/hosts install-cluster.yml +``` +This will deploy the server, replicas and clients defined in the inventory file. + + +Roles +===== + +* [Server](roles/ipaserver/README.md) +* [Replica](roles/ipareplica/README.md) +* [Client](roles/ipaclient/README.md) + +Modules in plugin/modules +========================= + +* [ipagroup](README-group.md) +* [ipahbacrule](README-hbacrule.md) +* [ipahbacsvc](README-hbacsvc.md) +* [ipahbacsvcgroup](README-hbacsvc.md) +* [ipahost](README-host.md) +* [ipahostgroup](README-hostgroup.md) +* [ipapwpolicy](README-pwpolicy.md) +* [ipasudocmd](README-sudocmd.md) +* [ipasudocmdgroup](README-sudocmdgroup.md) +* [ipasudorule](README-sudorule.md) +* [ipatopologysegment](README-topology.md) +* [ipatopologysuffix](README-topology.md) +* [ipauser](README-user.md) diff --git a/galaxy.yml b/galaxy.yml new file mode 100644 index 0000000..9740ba3 --- /dev/null +++ b/galaxy.yml @@ -0,0 +1,25 @@ +namespace: "freeipa" +name: "ansible_freeipa" +version: "A.B.C" +description: "Ansible roles and modules for FreeIPA" + +authors: + - "Thomas Woerner " + +repository: "https://github.com/freeipa/ansible-freeipa" +documentation: "https://github.com/freeipa/ansible-freeipa/blob/master/README.md" +homepage: "https://github.com/freeipa/ansible-freeipa" +issues: "https://github.com/freeipa/ansible-freeipa/issues" + +readme: "README.md" +license: "GPL-3.0-or-later" +license_file: "COPYING" + +dependencies: + +tags: + - "identity" + - "ipa" + - "freeipa" + - "cluster" + - "collection" diff --git a/inventory/hosts b/inventory/hosts new file mode 100644 index 0000000..ceba9cd --- /dev/null +++ b/inventory/hosts @@ -0,0 +1,29 @@ +[ipaclients] +ipaclient1.mine.dom +#ipaclient2.mine.dom +#ipaclient3.mine.dom +#ipaclient4.mine.dom +#ipaclient5.mine.dom +ipaclient6.mine.dom +ipaclient7.mine.dom + +#[ipaservers] +#ipa.mine.dom + +[ipaclients:vars] +#ipaclient_keytab=/tmp/krb5.keytab +#ipaclient_domain=mine.dom +#ipaclient_realm=MINE.DOM +#ipaadmin_principal=admin +#ipaadmin_password=password1 +#ipaclient_use_otp=yes +#ipaclient_force_join=yes +#ipaclient_kinit_attempts=3 +#ipaclient_mkhomedir=yes + +#ipaadmin_principal=admin +#ipaclient_use_otp=yes +#ipaclient_force_join=yes + +#ipaclient_use_otp=yes +ipaclient_allow_repair=yes diff --git a/inventory/hosts.cluster b/inventory/hosts.cluster new file mode 100644 index 0000000..b1d0ea4 --- /dev/null +++ b/inventory/hosts.cluster @@ -0,0 +1,35 @@ +[ipaserver] +ipaserver.test.local + +[ipaserver:vars] +#ipaserver_setup_dns=yes +#ipaserver_auto_forwarders=yes +#ipaserver_no_firewalld=no + + +[ipareplicas] +ipareplica1.test.local + +[ipareplicas:vars] +ipaclient_force_join=yes + + +[ipaclients] +ipaclient1.test.local +ipaclient2.test.local + +[ipaclients:vars] +#ipaclient_use_otp=yes +ipaclient_allow_repair=yes + + +[ipa:children] +ipaserver +ipareplicas +ipaclients + +[ipa:vars] +ipaadmin_password=password1 +ipadm_password=password1 +ipaserver_domain=test.local +ipaserver_realm=TEST.LOCAL diff --git a/inventory/hosts.replica b/inventory/hosts.replica new file mode 100644 index 0000000..653f927 --- /dev/null +++ b/inventory/hosts.replica @@ -0,0 +1,10 @@ +[ipaservers] +ipaserver.test.local + +[ipareplicas] +ipareplica1.test.local + +[ipareplicas:vars] +ipaadmin_password=password1 +ipadm_password=password1 +ipaclient_force_join=yes diff --git a/playbooks/hbacrule/ensure-hbarule-allhosts-absent.yml b/playbooks/hbacrule/ensure-hbarule-allhosts-absent.yml new file mode 100644 index 0000000..73dfd2a --- /dev/null +++ b/playbooks/hbacrule/ensure-hbarule-allhosts-absent.yml @@ -0,0 +1,12 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure HBAC Rule allhosts is absent + ipahbacrule: + ipaadmin_password: MyPassword123 + name: allhosts + state: absent diff --git a/playbooks/hbacrule/ensure-hbarule-allhosts-disabled.yml b/playbooks/hbacrule/ensure-hbarule-allhosts-disabled.yml new file mode 100644 index 0000000..33196df --- /dev/null +++ b/playbooks/hbacrule/ensure-hbarule-allhosts-disabled.yml @@ -0,0 +1,12 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure HBAC Rule allhosts is disabled + ipahbacrule: + ipaadmin_password: MyPassword123 + name: allhosts + state: disabled diff --git a/playbooks/hbacrule/ensure-hbarule-allhosts-enabled.yml b/playbooks/hbacrule/ensure-hbarule-allhosts-enabled.yml new file mode 100644 index 0000000..ad97b97 --- /dev/null +++ b/playbooks/hbacrule/ensure-hbarule-allhosts-enabled.yml @@ -0,0 +1,12 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure HBAC Rule allhosts is enabled + ipahbacrule: + ipaadmin_password: MyPassword123 + name: allhosts + state: enabled diff --git a/playbooks/hbacrule/ensure-hbarule-allhosts-present.yml b/playbooks/hbacrule/ensure-hbarule-allhosts-present.yml new file mode 100644 index 0000000..203f914 --- /dev/null +++ b/playbooks/hbacrule/ensure-hbarule-allhosts-present.yml @@ -0,0 +1,12 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure HBAC Rule allhosts is present + ipahbacrule: + ipaadmin_password: MyPassword123 + name: allhosts + usercategory: all diff --git a/playbooks/hbacrule/ensure-hbarule-allhosts-server-member-absent.yml b/playbooks/hbacrule/ensure-hbarule-allhosts-server-member-absent.yml new file mode 100644 index 0000000..d3a7739 --- /dev/null +++ b/playbooks/hbacrule/ensure-hbarule-allhosts-server-member-absent.yml @@ -0,0 +1,14 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure host server is absent in HBAC Rule allhosts + ipahbacrule: + ipaadmin_password: MyPassword123 + name: allhosts + host: server + action: member + state: absent diff --git a/playbooks/hbacrule/ensure-hbarule-allhosts-server-member-present.yml b/playbooks/hbacrule/ensure-hbarule-allhosts-server-member-present.yml new file mode 100644 index 0000000..286402f --- /dev/null +++ b/playbooks/hbacrule/ensure-hbarule-allhosts-server-member-present.yml @@ -0,0 +1,13 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure host server is present in HBAC Rule allhosts + ipahbacrule: + ipaadmin_password: MyPassword123 + name: allhosts + host: server + action: member diff --git a/playbooks/hbacsvc/ensure-hbacsvc-absent.yml b/playbooks/hbacsvc/ensure-hbacsvc-absent.yml new file mode 100644 index 0000000..cb766e7 --- /dev/null +++ b/playbooks/hbacsvc/ensure-hbacsvc-absent.yml @@ -0,0 +1,12 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure HBAC Services for http and tftp are absent + ipahbacsvc: + ipaadmin_password: MyPassword123 + name: http,tftp + state: absent diff --git a/playbooks/hbacsvc/ensure-hbacsvc-present.yml b/playbooks/hbacsvc/ensure-hbacsvc-present.yml new file mode 100644 index 0000000..1ed1d7c --- /dev/null +++ b/playbooks/hbacsvc/ensure-hbacsvc-present.yml @@ -0,0 +1,18 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure HBAC Service for http is present + ipahbacsvc: + ipaadmin_password: MyPassword123 + name: http + description: Web service + + - name: Ensure HBAC Service for tftp is present + ipahbacsvc: + ipaadmin_password: MyPassword123 + name: tftp + description: TFTP service diff --git a/playbooks/hbacsvcgroup/ensure-hbacsvcgroup-absent.yml b/playbooks/hbacsvcgroup/ensure-hbacsvcgroup-absent.yml new file mode 100644 index 0000000..c4e24d1 --- /dev/null +++ b/playbooks/hbacsvcgroup/ensure-hbacsvcgroup-absent.yml @@ -0,0 +1,14 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure HBAC Service Group login is absent + ipahbacsvcgroup: + ipaadmin_password: MyPassword123 + name: login + hbacsvc: + - sshd + state: absent diff --git a/playbooks/hbacsvcgroup/ensure-hbacsvcgroup-member-absent.yml b/playbooks/hbacsvcgroup/ensure-hbacsvcgroup-member-absent.yml new file mode 100644 index 0000000..b72703c --- /dev/null +++ b/playbooks/hbacsvcgroup/ensure-hbacsvcgroup-member-absent.yml @@ -0,0 +1,15 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure HBAC Services sshd is absent in HBAC Service Group login + ipahbacsvcgroup: + ipaadmin_password: MyPassword123 + name: login + hbacsvc: + - sshd + action: member + state: absent diff --git a/playbooks/hbacsvcgroup/ensure-hbacsvcgroup-member-present.yml b/playbooks/hbacsvcgroup/ensure-hbacsvcgroup-member-present.yml new file mode 100644 index 0000000..9b213c7 --- /dev/null +++ b/playbooks/hbacsvcgroup/ensure-hbacsvcgroup-member-present.yml @@ -0,0 +1,14 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure HBAC Service sshd is present in HBAC Service Group login + ipahbacsvcgroup: + ipaadmin_password: MyPassword123 + name: login + hbacsvc: + - sshd + action: member diff --git a/playbooks/hbacsvcgroup/ensure-hbacsvcgroup-present.yml b/playbooks/hbacsvcgroup/ensure-hbacsvcgroup-present.yml new file mode 100644 index 0000000..9b213c7 --- /dev/null +++ b/playbooks/hbacsvcgroup/ensure-hbacsvcgroup-present.yml @@ -0,0 +1,14 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure HBAC Service sshd is present in HBAC Service Group login + ipahbacsvcgroup: + ipaadmin_password: MyPassword123 + name: login + hbacsvc: + - sshd + action: member diff --git a/playbooks/host/add-host.yml b/playbooks/host/add-host.yml new file mode 120000 index 0000000..6d5bc9a --- /dev/null +++ b/playbooks/host/add-host.yml @@ -0,0 +1 @@ +host-present.yml \ No newline at end of file diff --git a/playbooks/host/delete-host.yml b/playbooks/host/delete-host.yml new file mode 100644 index 0000000..30eaf3e --- /dev/null +++ b/playbooks/host/delete-host.yml @@ -0,0 +1,11 @@ +--- +- name: Playbook to handle hosts + hosts: ipaserver + become: true + + tasks: + - name: Ensure host host01.example.com is absent + ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + state: absent diff --git a/playbooks/host/disable-host.yml b/playbooks/host/disable-host.yml new file mode 100644 index 0000000..3e265fe --- /dev/null +++ b/playbooks/host/disable-host.yml @@ -0,0 +1,11 @@ +--- +- name: Playbook to handle hosts + hosts: ipaserver + become: true + + tasks: + - name: Disable host host01.example.com + ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + state: disabled diff --git a/playbooks/host/ensure_host_with_randompassword.yml b/playbooks/host/ensure_host_with_randompassword.yml new file mode 100644 index 0000000..cd1a133 --- /dev/null +++ b/playbooks/host/ensure_host_with_randompassword.yml @@ -0,0 +1,18 @@ +--- +- name: Ensure host with random password + hosts: ipaserver + become: true + + tasks: + - name: Host "{{ 'host1.' + ipaserver_domain }}" present with random password + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ 'host1.' + ipaserver_domain }}" + random: yes + force: yes + update_password: on_create + register: ipahost + + - name: Print generated random password + debug: + var: ipahost.host.randompassword diff --git a/playbooks/host/host-member-allow_create_keytab-absent.yml b/playbooks/host/host-member-allow_create_keytab-absent.yml new file mode 100644 index 0000000..55e3110 --- /dev/null +++ b/playbooks/host/host-member-allow_create_keytab-absent.yml @@ -0,0 +1,24 @@ +--- +- name: Host member allow_create_keytab absent + hosts: ipaserver + become: true + + tasks: + - name: Host host1.example.com members allow_create_keytab absent for users, groups, hosts and hostgroups + ipahost: + ipaadmin_password: MyPassword123 + name: host01.exmaple.com + allow_create_keytab_user: + - user01 + - user02 + allow_create_keytab_group: + - group01 + - group02 + allow_create_keytab_host: + - host02.exmaple.com + - host03.exmaple.com + allow_create_keytab_hostgroup: + - hostgroup01 + - hostgroup02 + action: member + state: absent diff --git a/playbooks/host/host-member-allow_create_keytab-present.yml b/playbooks/host/host-member-allow_create_keytab-present.yml new file mode 100644 index 0000000..f586549 --- /dev/null +++ b/playbooks/host/host-member-allow_create_keytab-present.yml @@ -0,0 +1,23 @@ +--- +- name: Host member allow_create_keytab present + hosts: ipaserver + become: true + + tasks: + - name: Host host1.example.com members allow_create_keytab present for users, groups, hosts and hostgroups + ipahost: + ipaadmin_password: MyPassword123 + name: host01.exmaple.com + allow_create_keytab_user: + - user01 + - user02 + allow_create_keytab_group: + - group01 + - group02 + allow_create_keytab_host: + - host02.exmaple.com + - host03.exmaple.com + allow_create_keytab_hostgroup: + - hostgroup01 + - hostgroup02 + action: member diff --git a/playbooks/host/host-member-allow_retrieve_keytab-absent.yml b/playbooks/host/host-member-allow_retrieve_keytab-absent.yml new file mode 100644 index 0000000..b8830f6 --- /dev/null +++ b/playbooks/host/host-member-allow_retrieve_keytab-absent.yml @@ -0,0 +1,24 @@ +--- +- name: Host member allow_retrieve_keytab absent + hosts: ipaserver + become: true + + tasks: + - name: Host host1.example.com members allow_retrieve_keytab absent for users, groups, hosts and hostgroups + ipahost: + ipaadmin_password: MyPassword123 + name: host01.exmaple.com + allow_retrieve_keytab_user: + - user01 + - user02 + allow_retrieve_keytab_group: + - group01 + - group02 + allow_retrieve_keytab_host: + - host02.exmaple.com + - host03.exmaple.com + allow_retrieve_keytab_hostgroup: + - hostgroup01 + - hostgroup02 + action: member + state: absent diff --git a/playbooks/host/host-member-allow_retrieve_keytab-present.yml b/playbooks/host/host-member-allow_retrieve_keytab-present.yml new file mode 100644 index 0000000..fde116c --- /dev/null +++ b/playbooks/host/host-member-allow_retrieve_keytab-present.yml @@ -0,0 +1,23 @@ +--- +- name: Host member allow_retrieve_keytab present + hosts: ipaserver + become: true + + tasks: + - name: Host host1.example.com members allow_retrieve_keytab present for users, groups, hosts and hostgroups + ipahost: + ipaadmin_password: MyPassword123 + name: host01.exmaple.com + allow_retrieve_keytab_user: + - user01 + - user02 + allow_retrieve_keytab_group: + - group01 + - group02 + allow_retrieve_keytab_host: + - host02.exmaple.com + - host03.exmaple.com + allow_retrieve_keytab_hostgroup: + - hostgroup01 + - hostgroup02 + action: member diff --git a/playbooks/host/host-member-certificate-absent.yml b/playbooks/host/host-member-certificate-absent.yml new file mode 100644 index 0000000..918951f --- /dev/null +++ b/playbooks/host/host-member-certificate-absent.yml @@ -0,0 +1,13 @@ +- name: Host member certificate absent + hosts: ipaserver + become: true + + tasks: + - name: Host host01.example.com member certificate absent + ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + certificate: + - MIIC/zCCAeegAwIBAgIUZGHLaSYg1myp6EI4VGWSC27vOrswDQYJKoZIhvcNAQELBQAwDzENMAsGA1UEAwwEdGVzdDAeFw0xOTEwMTQxNjI4MzVaFw0yMDEwMTMxNjI4MzVaMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDER/lB8wUAmPTSwSc/NOXNlzdpPOQDSwrhKH6XsqZF4KpQoSY/nmCjAhJmOVpOUo4K2fGRZ0yAH9fkGv6yJP6c7IAFjLeec7GPHVwN4bZrP1DXfTAmfmXhcRQbCYkV+wmq8Puzw/+xA9EJrrodnJPPsE6E8HnSVLF6Ys9+cJMJ7HuwOI+wYt3gkmspsir1tccmf4x1PP+yHJWdcXyetlFRcmZ8gspjqOR2jb89xSQsh8gcyDW6rPNlSTzYZ2FmNtjES6ZhCsYL31fQbF2QglidlLGpAlvHUUS+xCigW73cvhFPMWXcfO51Mr15RcgYTckY+7QZ2nYqplRBoDlQl6DnAgMBAAGjUzBRMB0GA1UdDgQWBBTPG99XVRdxpOXMZo3Nhy+ldnf13TAfBgNVHSMEGDAWgBTPG99XVRdxpOXMZo3Nhy+ldnf13TAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAjWTcnIl2mpNbfHAN8DB4Kk+RNRmhsH0y+r/47MXVTMMMToCfofeNY3Jeohu+2lIXMPQfTvXUbDTkNAGsGLv6LtQEUfSREqgk1eY7bT9BFfpH1uV2ZFhCO9jBA+E4bf55Kx7bgUNG31ykBshOsOblOJM1lS/0q4TWHAxrsU2PNwPi8X0ten+eGeB8aRshxS17Ij2cH0fdAMmSA+jMAvTIZl853Bxe0HuozauKwOFWL4qHm61c4O/j1mQCLqJKYfJ9mBDWFQLszd/tF+ePKiNhZCQly60F8Lumn2CDZj5UIkl8wk9Wls5n1BIQs+M8AN65NAdv7+js8jKUKCuyji8r3 + action: member + state: absent diff --git a/playbooks/host/host-member-certificate-present.yml b/playbooks/host/host-member-certificate-present.yml new file mode 100644 index 0000000..066ae0a --- /dev/null +++ b/playbooks/host/host-member-certificate-present.yml @@ -0,0 +1,12 @@ +- name: Host member certificate present + hosts: ipaserver + become: true + + tasks: + - name: Host host01.example.com member certificate present + ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + certificate: + - MIIC/zCCAeegAwIBAgIUZGHLaSYg1myp6EI4VGWSC27vOrswDQYJKoZIhvcNAQELBQAwDzENMAsGA1UEAwwEdGVzdDAeFw0xOTEwMTQxNjI4MzVaFw0yMDEwMTMxNjI4MzVaMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDER/lB8wUAmPTSwSc/NOXNlzdpPOQDSwrhKH6XsqZF4KpQoSY/nmCjAhJmOVpOUo4K2fGRZ0yAH9fkGv6yJP6c7IAFjLeec7GPHVwN4bZrP1DXfTAmfmXhcRQbCYkV+wmq8Puzw/+xA9EJrrodnJPPsE6E8HnSVLF6Ys9+cJMJ7HuwOI+wYt3gkmspsir1tccmf4x1PP+yHJWdcXyetlFRcmZ8gspjqOR2jb89xSQsh8gcyDW6rPNlSTzYZ2FmNtjES6ZhCsYL31fQbF2QglidlLGpAlvHUUS+xCigW73cvhFPMWXcfO51Mr15RcgYTckY+7QZ2nYqplRBoDlQl6DnAgMBAAGjUzBRMB0GA1UdDgQWBBTPG99XVRdxpOXMZo3Nhy+ldnf13TAfBgNVHSMEGDAWgBTPG99XVRdxpOXMZo3Nhy+ldnf13TAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAjWTcnIl2mpNbfHAN8DB4Kk+RNRmhsH0y+r/47MXVTMMMToCfofeNY3Jeohu+2lIXMPQfTvXUbDTkNAGsGLv6LtQEUfSREqgk1eY7bT9BFfpH1uV2ZFhCO9jBA+E4bf55Kx7bgUNG31ykBshOsOblOJM1lS/0q4TWHAxrsU2PNwPi8X0ten+eGeB8aRshxS17Ij2cH0fdAMmSA+jMAvTIZl853Bxe0HuozauKwOFWL4qHm61c4O/j1mQCLqJKYfJ9mBDWFQLszd/tF+ePKiNhZCQly60F8Lumn2CDZj5UIkl8wk9Wls5n1BIQs+M8AN65NAdv7+js8jKUKCuyji8r3 + action: member diff --git a/playbooks/host/host-member-managedby_host-absent.yml b/playbooks/host/host-member-managedby_host-absent.yml new file mode 100644 index 0000000..f899a52 --- /dev/null +++ b/playbooks/host/host-member-managedby_host-absent.yml @@ -0,0 +1,12 @@ +--- +- name: Host member managedby_host absent + hosts: ipaserver + become: true + + tasks: + ipahost: + ipaadmin_password: MyPassword123 + name: host01.exmaple.com + managedby_host: server.exmaple.com + action: member + state: absent diff --git a/playbooks/host/host-member-managedby_host-present.yml b/playbooks/host/host-member-managedby_host-present.yml new file mode 100644 index 0000000..073d81a --- /dev/null +++ b/playbooks/host/host-member-managedby_host-present.yml @@ -0,0 +1,11 @@ +--- +- name: Host member managedby_host present + hosts: ipaserver + become: true + + tasks: + ipahost: + ipaadmin_password: MyPassword123 + name: host01.exmaple.com + managedby_host: server.exmaple.com + action: member diff --git a/playbooks/host/host-member-principal-absent.yml b/playbooks/host/host-member-principal-absent.yml new file mode 100644 index 0000000..b2c3a8d --- /dev/null +++ b/playbooks/host/host-member-principal-absent.yml @@ -0,0 +1,15 @@ +--- +- name: Host member principal absent + hosts: ipaserver + become: true + + tasks: + - name: Host host01.example.com principals host/testhost01.example.com and host/myhost01.example.com absent + ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + principal: + - host/testhost01.example.com + - host/myhost01.example.com + action: member + state: absent diff --git a/playbooks/host/host-member-principal-present.yml b/playbooks/host/host-member-principal-present.yml new file mode 100644 index 0000000..b56fd59 --- /dev/null +++ b/playbooks/host/host-member-principal-present.yml @@ -0,0 +1,14 @@ +--- +- name: Host member principal present + hosts: ipaserver + become: true + + tasks: + - name: Host host01.example.com principals host/testhost01.example.com and host/myhost01.example.com present + ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + principal: + - host/testhost01.example.com + - host/myhost01.example.com + action: member diff --git a/playbooks/host/host-present-with-allow_create_keytab.yml b/playbooks/host/host-present-with-allow_create_keytab.yml new file mode 100644 index 0000000..f7b9c6f --- /dev/null +++ b/playbooks/host/host-present-with-allow_create_keytab.yml @@ -0,0 +1,23 @@ +--- +- name: Host present with allow_create_keytab + hosts: ipaserver + become: true + + tasks: + - name: Host host1.example.com present with allow_create_keytab for users, groups, hosts and hostgroups + ipahost: + ipaadmin_password: MyPassword123 + name: host01.exmaple.com + allow_create_keytab_user: + - user01 + - user02 + allow_create_keytab_group: + - group01 + - group02 + allow_create_keytab_host: + - host02.exmaple.com + - host03.exmaple.com + allow_create_keytab_hostgroup: + - hostgroup01 + - hostgroup02 + ip_address: 192.168.0.123 diff --git a/playbooks/host/host-present-with-allow_retrieve_keytab.yml b/playbooks/host/host-present-with-allow_retrieve_keytab.yml new file mode 100644 index 0000000..5a9f3af --- /dev/null +++ b/playbooks/host/host-present-with-allow_retrieve_keytab.yml @@ -0,0 +1,23 @@ +--- +- name: Host present with allow_retrieve_keytab + hosts: ipaserver + become: true + + tasks: + - name: Host host1.example.com present with allow_retrieve_keytab for users, groups, hosts and hostgroups + ipahost: + ipaadmin_password: MyPassword123 + name: host01.exmaple.com + allow_retrieve_keytab_user: + - user01 + - user02 + allow_retrieve_keytab_group: + - group01 + - group02 + allow_retrieve_keytab_host: + - host02.exmaple.com + - host03.exmaple.com + allow_retrieve_keytab_hostgroup: + - hostgroup01 + - hostgroup02 + ip_address: 192.168.0.123 diff --git a/playbooks/host/host-present-with-certificate.yml b/playbooks/host/host-present-with-certificate.yml new file mode 100644 index 0000000..f5da46d --- /dev/null +++ b/playbooks/host/host-present-with-certificate.yml @@ -0,0 +1,12 @@ +- name: Host present with certificate + hosts: ipaserver + become: true + + tasks: + - name: Host host01.example.com present with certificate + ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + certificate: + - MIIC/zCCAeegAwIBAgIUZGHLaSYg1myp6EI4VGWSC27vOrswDQYJKoZIhvcNAQELBQAwDzENMAsGA1UEAwwEdGVzdDAeFw0xOTEwMTQxNjI4MzVaFw0yMDEwMTMxNjI4MzVaMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDER/lB8wUAmPTSwSc/NOXNlzdpPOQDSwrhKH6XsqZF4KpQoSY/nmCjAhJmOVpOUo4K2fGRZ0yAH9fkGv6yJP6c7IAFjLeec7GPHVwN4bZrP1DXfTAmfmXhcRQbCYkV+wmq8Puzw/+xA9EJrrodnJPPsE6E8HnSVLF6Ys9+cJMJ7HuwOI+wYt3gkmspsir1tccmf4x1PP+yHJWdcXyetlFRcmZ8gspjqOR2jb89xSQsh8gcyDW6rPNlSTzYZ2FmNtjES6ZhCsYL31fQbF2QglidlLGpAlvHUUS+xCigW73cvhFPMWXcfO51Mr15RcgYTckY+7QZ2nYqplRBoDlQl6DnAgMBAAGjUzBRMB0GA1UdDgQWBBTPG99XVRdxpOXMZo3Nhy+ldnf13TAfBgNVHSMEGDAWgBTPG99XVRdxpOXMZo3Nhy+ldnf13TAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAjWTcnIl2mpNbfHAN8DB4Kk+RNRmhsH0y+r/47MXVTMMMToCfofeNY3Jeohu+2lIXMPQfTvXUbDTkNAGsGLv6LtQEUfSREqgk1eY7bT9BFfpH1uV2ZFhCO9jBA+E4bf55Kx7bgUNG31ykBshOsOblOJM1lS/0q4TWHAxrsU2PNwPi8X0ten+eGeB8aRshxS17Ij2cH0fdAMmSA+jMAvTIZl853Bxe0HuozauKwOFWL4qHm61c4O/j1mQCLqJKYfJ9mBDWFQLszd/tF+ePKiNhZCQly60F8Lumn2CDZj5UIkl8wk9Wls5n1BIQs+M8AN65NAdv7+js8jKUKCuyji8r3 + force: yes diff --git a/playbooks/host/host-present-with-managedby_host.yml b/playbooks/host/host-present-with-managedby_host.yml new file mode 100644 index 0000000..b85f5e6 --- /dev/null +++ b/playbooks/host/host-present-with-managedby_host.yml @@ -0,0 +1,11 @@ +--- +- name: Host present with managedby_host + hosts: ipaserver + become: true + + tasks: + ipahost: + ipaadmin_password: MyPassword123 + name: host01.exmaple.com + managedby_host: server.exmaple.com + force: yes diff --git a/playbooks/host/host-present-with-principal.yml b/playbooks/host/host-present-with-principal.yml new file mode 100644 index 0000000..5b8ad4e --- /dev/null +++ b/playbooks/host/host-present-with-principal.yml @@ -0,0 +1,14 @@ +--- +- name: Host present with principal + hosts: ipaserver + become: true + + tasks: + - name: Host host01.example.com present with principals host/testhost01.example.com and host/myhost01.example.com + ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + principal: + - host/testhost01.example.com + - host/myhost01.example.com + force: yes diff --git a/playbooks/host/host-present-with-randompassword.yml b/playbooks/host/host-present-with-randompassword.yml new file mode 100644 index 0000000..9063c48 --- /dev/null +++ b/playbooks/host/host-present-with-randompassword.yml @@ -0,0 +1,17 @@ +--- +- name: Host present with random password + hosts: ipaserver + become: true + + tasks: + - name: Host host01.example.com present with random password + ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + random: yes + force: yes + register: ipahost + + - name: Print generated random password + debug: + var: ipahost.host.randompassword diff --git a/playbooks/host/host-present.yml b/playbooks/host/host-present.yml new file mode 100644 index 0000000..d40c1ec --- /dev/null +++ b/playbooks/host/host-present.yml @@ -0,0 +1,20 @@ +--- +- name: Host present + hosts: ipaserver + become: true + + tasks: + - name: Ensure host is present + ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + description: Example host + ip_address: 192.168.0.123 + locality: Lab + ns_host_location: Lab + ns_os_version: CentOS 7 + ns_hardware_platform: Lenovo T61 + mac_address: + - "08:00:27:E3:B1:2D" + - "52:54:00:BD:97:1E" + state: present diff --git a/playbooks/host/hosts-member-certificate-absent.yml b/playbooks/host/hosts-member-certificate-absent.yml new file mode 100644 index 0000000..bb2d5b0 --- /dev/null +++ b/playbooks/host/hosts-member-certificate-absent.yml @@ -0,0 +1,18 @@ +--- +- name: Hosts member certificate absent + hosts: ipaserver + become: true + + tasks: + - name: Hosts host01.example.com and host01.exmaple.com member certificate absent + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: host01.example.com + certificate: + - MIIC/zCCAeegAwIBAgIUZGHLaSYg1myp6EI4VGWSC27vOrswDQYJKoZIhvcNAQELBQAwDzENMAsGA1UEAwwEdGVzdDAeFw0xOTEwMTQxNjI4MzVaFw0yMDEwMTMxNjI4MzVaMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDER/lB8wUAmPTSwSc/NOXNlzdpPOQDSwrhKH6XsqZF4KpQoSY/nmCjAhJmOVpOUo4K2fGRZ0yAH9fkGv6yJP6c7IAFjLeec7GPHVwN4bZrP1DXfTAmfmXhcRQbCYkV+wmq8Puzw/+xA9EJrrodnJPPsE6E8HnSVLF6Ys9+cJMJ7HuwOI+wYt3gkmspsir1tccmf4x1PP+yHJWdcXyetlFRcmZ8gspjqOR2jb89xSQsh8gcyDW6rPNlSTzYZ2FmNtjES6ZhCsYL31fQbF2QglidlLGpAlvHUUS+xCigW73cvhFPMWXcfO51Mr15RcgYTckY+7QZ2nYqplRBoDlQl6DnAgMBAAGjUzBRMB0GA1UdDgQWBBTPG99XVRdxpOXMZo3Nhy+ldnf13TAfBgNVHSMEGDAWgBTPG99XVRdxpOXMZo3Nhy+ldnf13TAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAjWTcnIl2mpNbfHAN8DB4Kk+RNRmhsH0y+r/47MXVTMMMToCfofeNY3Jeohu+2lIXMPQfTvXUbDTkNAGsGLv6LtQEUfSREqgk1eY7bT9BFfpH1uV2ZFhCO9jBA+E4bf55Kx7bgUNG31ykBshOsOblOJM1lS/0q4TWHAxrsU2PNwPi8X0ten+eGeB8aRshxS17Ij2cH0fdAMmSA+jMAvTIZl853Bxe0HuozauKwOFWL4qHm61c4O/j1mQCLqJKYfJ9mBDWFQLszd/tF+ePKiNhZCQly60F8Lumn2CDZj5UIkl8wk9Wls5n1BIQs+M8AN65NAdv7+js8jKUKCuyji8r3 + - name: host02.example.com + certificate: + - MIIC/zCCAeegAwIBAgIUAWE1vaA+mZd3nwZqwWH64EbHvR0wDQYJKoZIhvcNAQELBQAwDzENMAsGA1UEAwwEdGVzdDAeFw0xOTEwMTQxNjI4NDVaFw0yMDEwMTMxNjI4NDVaMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCWzJibKtN8Zf7LgandINhFonx99AKi44iaZkrlMKEObE6Faf8NTUbUgK3VfJNYmCbA1baLVJ0YZJijJ7S/4o7h7eeqcJVXJkEhWNTimWXNW/YCzTHe3SSapnSYOKmdHHRClplysL8OyyEG7pbX/aB9iAfFb/+vUFCX5sMwFFrYxOimKJ9Pc/NRFtdv1wNw1rqWKF1ZzagWRlG4QgzRGwQ4quc7yO98TKikj2OPiIt7Zd46hbqQxmgGBtCkVOZIhxu77OmNrFsXmM4rZZpmqh0UdqcpwkRojVnGXmNqeMCd6dNTnLhr9wukUYw0KgE57zCDVr9Ix+p/dA5R1mG4RJ2XAgMBAAGjUzBRMB0GA1UdDgQWBBSbuiH2lNVrID3yt1SsFwtOFKOnpTAfBgNVHSMEGDAWgBSbuiH2lNVrID3yt1SsFwtOFKOnpTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBCVWd293wWyohFqMFMHRBBg97T2Uc1yeT0dMH4BpuOaCqQp4q5ep+uLcXEI6+3mEwm8pa/ULQCD8yLLdotIWlG3+h/4boFpdiPFcBDgT8kGe+0KOzB8Nt7E13QYOu12MNi10qwGrjKhdhu1xBe4fpY5VCetVU1OLyuTsUyucQsFrtZI0SR83h+blbyoMZ7IhMngCfGUe1bnYeWnLbpFbigKfPuVDWsMH2kgj05EAd5EgHkWbX8QA8hmcmDKfNT3YZM8kiGQwmFrnQdq8bN0uHR8Nz+24cbmdbHcD65wlDW6GmYxi8mW+V6bAqn9pir/J14r4YFnqMGgjmdt81tscJV + action: member + state: absent diff --git a/playbooks/host/hosts-member-certificate-present.yml b/playbooks/host/hosts-member-certificate-present.yml new file mode 100644 index 0000000..c402ef4 --- /dev/null +++ b/playbooks/host/hosts-member-certificate-present.yml @@ -0,0 +1,17 @@ +--- +- name: Hosts member certificate present + hosts: ipaserver + become: true + + tasks: + - name: Hosts host01.example.com and host01.exmaple.com member certificate present + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: host01.example.com + certificate: + - MIIC/zCCAeegAwIBAgIUZGHLaSYg1myp6EI4VGWSC27vOrswDQYJKoZIhvcNAQELBQAwDzENMAsGA1UEAwwEdGVzdDAeFw0xOTEwMTQxNjI4MzVaFw0yMDEwMTMxNjI4MzVaMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDER/lB8wUAmPTSwSc/NOXNlzdpPOQDSwrhKH6XsqZF4KpQoSY/nmCjAhJmOVpOUo4K2fGRZ0yAH9fkGv6yJP6c7IAFjLeec7GPHVwN4bZrP1DXfTAmfmXhcRQbCYkV+wmq8Puzw/+xA9EJrrodnJPPsE6E8HnSVLF6Ys9+cJMJ7HuwOI+wYt3gkmspsir1tccmf4x1PP+yHJWdcXyetlFRcmZ8gspjqOR2jb89xSQsh8gcyDW6rPNlSTzYZ2FmNtjES6ZhCsYL31fQbF2QglidlLGpAlvHUUS+xCigW73cvhFPMWXcfO51Mr15RcgYTckY+7QZ2nYqplRBoDlQl6DnAgMBAAGjUzBRMB0GA1UdDgQWBBTPG99XVRdxpOXMZo3Nhy+ldnf13TAfBgNVHSMEGDAWgBTPG99XVRdxpOXMZo3Nhy+ldnf13TAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAjWTcnIl2mpNbfHAN8DB4Kk+RNRmhsH0y+r/47MXVTMMMToCfofeNY3Jeohu+2lIXMPQfTvXUbDTkNAGsGLv6LtQEUfSREqgk1eY7bT9BFfpH1uV2ZFhCO9jBA+E4bf55Kx7bgUNG31ykBshOsOblOJM1lS/0q4TWHAxrsU2PNwPi8X0ten+eGeB8aRshxS17Ij2cH0fdAMmSA+jMAvTIZl853Bxe0HuozauKwOFWL4qHm61c4O/j1mQCLqJKYfJ9mBDWFQLszd/tF+ePKiNhZCQly60F8Lumn2CDZj5UIkl8wk9Wls5n1BIQs+M8AN65NAdv7+js8jKUKCuyji8r3 + - name: host02.example.com + certificate: + - MIIC/zCCAeegAwIBAgIUAWE1vaA+mZd3nwZqwWH64EbHvR0wDQYJKoZIhvcNAQELBQAwDzENMAsGA1UEAwwEdGVzdDAeFw0xOTEwMTQxNjI4NDVaFw0yMDEwMTMxNjI4NDVaMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCWzJibKtN8Zf7LgandINhFonx99AKi44iaZkrlMKEObE6Faf8NTUbUgK3VfJNYmCbA1baLVJ0YZJijJ7S/4o7h7eeqcJVXJkEhWNTimWXNW/YCzTHe3SSapnSYOKmdHHRClplysL8OyyEG7pbX/aB9iAfFb/+vUFCX5sMwFFrYxOimKJ9Pc/NRFtdv1wNw1rqWKF1ZzagWRlG4QgzRGwQ4quc7yO98TKikj2OPiIt7Zd46hbqQxmgGBtCkVOZIhxu77OmNrFsXmM4rZZpmqh0UdqcpwkRojVnGXmNqeMCd6dNTnLhr9wukUYw0KgE57zCDVr9Ix+p/dA5R1mG4RJ2XAgMBAAGjUzBRMB0GA1UdDgQWBBSbuiH2lNVrID3yt1SsFwtOFKOnpTAfBgNVHSMEGDAWgBSbuiH2lNVrID3yt1SsFwtOFKOnpTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBCVWd293wWyohFqMFMHRBBg97T2Uc1yeT0dMH4BpuOaCqQp4q5ep+uLcXEI6+3mEwm8pa/ULQCD8yLLdotIWlG3+h/4boFpdiPFcBDgT8kGe+0KOzB8Nt7E13QYOu12MNi10qwGrjKhdhu1xBe4fpY5VCetVU1OLyuTsUyucQsFrtZI0SR83h+blbyoMZ7IhMngCfGUe1bnYeWnLbpFbigKfPuVDWsMH2kgj05EAd5EgHkWbX8QA8hmcmDKfNT3YZM8kiGQwmFrnQdq8bN0uHR8Nz+24cbmdbHcD65wlDW6GmYxi8mW+V6bAqn9pir/J14r4YFnqMGgjmdt81tscJV + action: member diff --git a/playbooks/host/hosts-member-managedby_host-absent.yml b/playbooks/host/hosts-member-managedby_host-absent.yml new file mode 100644 index 0000000..9b584e5 --- /dev/null +++ b/playbooks/host/hosts-member-managedby_host-absent.yml @@ -0,0 +1,15 @@ +--- +- name: Hosts member managedby_host absent + hosts: ipaserver + become: true + + tasks: + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: host01.exmaple.com + managedby_host: server.exmaple.com + - name: host02.exmaple.com + managedby_host: server.exmaple.com + action: member + state: absent diff --git a/playbooks/host/hosts-member-managedby_host-present.yml b/playbooks/host/hosts-member-managedby_host-present.yml new file mode 100644 index 0000000..f8d4e2b --- /dev/null +++ b/playbooks/host/hosts-member-managedby_host-present.yml @@ -0,0 +1,14 @@ +--- +- name: Hosts member managedby_host present + hosts: ipaserver + become: true + + tasks: + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: host01.exmaple.com + managedby_host: server.exmaple.com + - name: host02.exmaple.com + managedby_host: server.exmaple.com + action: member diff --git a/playbooks/host/hosts-member-principal-absent.yml b/playbooks/host/hosts-member-principal-absent.yml new file mode 100644 index 0000000..edd3352 --- /dev/null +++ b/playbooks/host/hosts-member-principal-absent.yml @@ -0,0 +1,18 @@ +--- +- name: Host member principal absent + hosts: ipaserver + become: true + + tasks: + - name: Hosts host01.exmaple.com and host02.exmaple.com member principals host/testhost0X.exmaple.com absent + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: host01.exmaple.com + principal: + - host/testhost01.exmaple.com + - name: host02.exmaple.com + principal: + - host/testhost02.exmaple.com + action: member + state: absent diff --git a/playbooks/host/hosts-member-principal-present.yml b/playbooks/host/hosts-member-principal-present.yml new file mode 100644 index 0000000..54c9a8f --- /dev/null +++ b/playbooks/host/hosts-member-principal-present.yml @@ -0,0 +1,17 @@ +--- +- name: Hosts member principal present + hosts: ipaserver + become: true + + tasks: + - name: Hosts host01.exmaple.com and host02.exmaple.com member principals host/testhost0X.exmaple.com present + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: host01.exmaple.com + principal: + - host/testhost01.exmaple.com + - name: host02.exmaple.com + principal: + - host/testhost02.exmaple.com + action: member diff --git a/playbooks/host/hosts-present-with-certificate.yml b/playbooks/host/hosts-present-with-certificate.yml new file mode 100644 index 0000000..34e402f --- /dev/null +++ b/playbooks/host/hosts-present-with-certificate.yml @@ -0,0 +1,17 @@ +--- +- name: Hosts present with certificate + hosts: ipaserver + become: true + + tasks: + - name: Hosts host01.example.com and host01.exmaple.com present with certificate + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: host01.example.com + certificate: + - MIIC/zCCAeegAwIBAgIUZGHLaSYg1myp6EI4VGWSC27vOrswDQYJKoZIhvcNAQELBQAwDzENMAsGA1UEAwwEdGVzdDAeFw0xOTEwMTQxNjI4MzVaFw0yMDEwMTMxNjI4MzVaMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDER/lB8wUAmPTSwSc/NOXNlzdpPOQDSwrhKH6XsqZF4KpQoSY/nmCjAhJmOVpOUo4K2fGRZ0yAH9fkGv6yJP6c7IAFjLeec7GPHVwN4bZrP1DXfTAmfmXhcRQbCYkV+wmq8Puzw/+xA9EJrrodnJPPsE6E8HnSVLF6Ys9+cJMJ7HuwOI+wYt3gkmspsir1tccmf4x1PP+yHJWdcXyetlFRcmZ8gspjqOR2jb89xSQsh8gcyDW6rPNlSTzYZ2FmNtjES6ZhCsYL31fQbF2QglidlLGpAlvHUUS+xCigW73cvhFPMWXcfO51Mr15RcgYTckY+7QZ2nYqplRBoDlQl6DnAgMBAAGjUzBRMB0GA1UdDgQWBBTPG99XVRdxpOXMZo3Nhy+ldnf13TAfBgNVHSMEGDAWgBTPG99XVRdxpOXMZo3Nhy+ldnf13TAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAjWTcnIl2mpNbfHAN8DB4Kk+RNRmhsH0y+r/47MXVTMMMToCfofeNY3Jeohu+2lIXMPQfTvXUbDTkNAGsGLv6LtQEUfSREqgk1eY7bT9BFfpH1uV2ZFhCO9jBA+E4bf55Kx7bgUNG31ykBshOsOblOJM1lS/0q4TWHAxrsU2PNwPi8X0ten+eGeB8aRshxS17Ij2cH0fdAMmSA+jMAvTIZl853Bxe0HuozauKwOFWL4qHm61c4O/j1mQCLqJKYfJ9mBDWFQLszd/tF+ePKiNhZCQly60F8Lumn2CDZj5UIkl8wk9Wls5n1BIQs+M8AN65NAdv7+js8jKUKCuyji8r3 + - name: host02.example.com + certificate: + - MIIC/zCCAeegAwIBAgIUAWE1vaA+mZd3nwZqwWH64EbHvR0wDQYJKoZIhvcNAQELBQAwDzENMAsGA1UEAwwEdGVzdDAeFw0xOTEwMTQxNjI4NDVaFw0yMDEwMTMxNjI4NDVaMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCWzJibKtN8Zf7LgandINhFonx99AKi44iaZkrlMKEObE6Faf8NTUbUgK3VfJNYmCbA1baLVJ0YZJijJ7S/4o7h7eeqcJVXJkEhWNTimWXNW/YCzTHe3SSapnSYOKmdHHRClplysL8OyyEG7pbX/aB9iAfFb/+vUFCX5sMwFFrYxOimKJ9Pc/NRFtdv1wNw1rqWKF1ZzagWRlG4QgzRGwQ4quc7yO98TKikj2OPiIt7Zd46hbqQxmgGBtCkVOZIhxu77OmNrFsXmM4rZZpmqh0UdqcpwkRojVnGXmNqeMCd6dNTnLhr9wukUYw0KgE57zCDVr9Ix+p/dA5R1mG4RJ2XAgMBAAGjUzBRMB0GA1UdDgQWBBSbuiH2lNVrID3yt1SsFwtOFKOnpTAfBgNVHSMEGDAWgBSbuiH2lNVrID3yt1SsFwtOFKOnpTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBCVWd293wWyohFqMFMHRBBg97T2Uc1yeT0dMH4BpuOaCqQp4q5ep+uLcXEI6+3mEwm8pa/ULQCD8yLLdotIWlG3+h/4boFpdiPFcBDgT8kGe+0KOzB8Nt7E13QYOu12MNi10qwGrjKhdhu1xBe4fpY5VCetVU1OLyuTsUyucQsFrtZI0SR83h+blbyoMZ7IhMngCfGUe1bnYeWnLbpFbigKfPuVDWsMH2kgj05EAd5EgHkWbX8QA8hmcmDKfNT3YZM8kiGQwmFrnQdq8bN0uHR8Nz+24cbmdbHcD65wlDW6GmYxi8mW+V6bAqn9pir/J14r4YFnqMGgjmdt81tscJV + force: yes diff --git a/playbooks/host/hosts-present-with-managedby_host.yml b/playbooks/host/hosts-present-with-managedby_host.yml new file mode 100644 index 0000000..5f3546b --- /dev/null +++ b/playbooks/host/hosts-present-with-managedby_host.yml @@ -0,0 +1,15 @@ +--- +- name: Host present with managedby_host + hosts: ipaserver + become: true + + tasks: + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: host01.exmaple.com + managedby_host: server.exmaple.com + force: yes + - name: host02.exmaple.com + managedby_host: server.exmaple.com + force: yes diff --git a/playbooks/host/hosts-present-with-randompasswords.yml b/playbooks/host/hosts-present-with-randompasswords.yml new file mode 100644 index 0000000..f747ca3 --- /dev/null +++ b/playbooks/host/hosts-present-with-randompasswords.yml @@ -0,0 +1,26 @@ +--- +- name: Hosts present with random passwords + hosts: ipaserver + become: true + + tasks: + - name: Hosts host01.example.com and host01.example.com present with random passwords + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: host01.example.com + random: yes + force: yes + - name: host02.example.com + random: yes + force: yes + register: ipahost + + - name: Print generated random password for host01.example.com + debug: + var: ipahost.host["host01.example.com"].randompassword + + - name: Print generated random password for host02.example.com + debug: + var: ipahost.host["host02.example.com"].randompassword + diff --git a/playbooks/hostgroup/ensure-hostgroup-is-absent.yml b/playbooks/hostgroup/ensure-hostgroup-is-absent.yml new file mode 100644 index 0000000..86508fc --- /dev/null +++ b/playbooks/hostgroup/ensure-hostgroup-is-absent.yml @@ -0,0 +1,11 @@ +--- +- name: Playbook to handle hostgroups + hosts: ipaserver + become: true + + tasks: + # Ensure host-group databases is present + - ipahostgroup: + ipaadmin_password: MyPassword123 + name: databases + state: absent diff --git a/playbooks/hostgroup/ensure-hostgroup-is-present.yml b/playbooks/hostgroup/ensure-hostgroup-is-present.yml new file mode 100644 index 0000000..a2c7ae9 --- /dev/null +++ b/playbooks/hostgroup/ensure-hostgroup-is-present.yml @@ -0,0 +1,15 @@ +--- +- name: Playbook to handle hostgroups + hosts: ipaserver + become: true + + tasks: + # Ensure host-group databases is present + - ipahostgroup: + ipaadmin_password: MyPassword123 + name: databases + host: + - db.example.com + hostgroup: + - mysql-server + - oracle-server diff --git a/playbooks/hostgroup/ensure-hosts-and-hostgroups-are-absent-in-hostgroup.yml b/playbooks/hostgroup/ensure-hosts-and-hostgroups-are-absent-in-hostgroup.yml new file mode 100644 index 0000000..149dfcf --- /dev/null +++ b/playbooks/hostgroup/ensure-hosts-and-hostgroups-are-absent-in-hostgroup.yml @@ -0,0 +1,17 @@ +--- +- name: Playbook to handle hostgroups + hosts: ipaserver + become: true + + tasks: + # Ensure hosts and hostgroups are present in existing databases hostgroup + - ipahostgroup: + ipaadmin_password: MyPassword123 + name: databases + host: + - db.example.com + hostgroup: + - mysql-server + - oracle-server + action: member + state: absent diff --git a/playbooks/hostgroup/ensure-hosts-and-hostgroups-are-present-in-hostgroup.yml b/playbooks/hostgroup/ensure-hosts-and-hostgroups-are-present-in-hostgroup.yml new file mode 100644 index 0000000..4b22fd6 --- /dev/null +++ b/playbooks/hostgroup/ensure-hosts-and-hostgroups-are-present-in-hostgroup.yml @@ -0,0 +1,16 @@ +--- +- name: Playbook to handle hostgroups + hosts: ipaserver + become: true + + tasks: + # Ensure hosts and hostgroups are present in existing databases hostgroup + - ipahostgroup: + ipaadmin_password: MyPassword123 + name: databases + host: + - db.example.com + hostgroup: + - mysql-server + - oracle-server + action: member diff --git a/playbooks/install-client.yml b/playbooks/install-client.yml new file mode 100644 index 0000000..62e9744 --- /dev/null +++ b/playbooks/install-client.yml @@ -0,0 +1,8 @@ +--- +- name: Playbook to configure IPA clients with username/password + hosts: ipaclients + become: true + + roles: + - role: ipaclient + state: present diff --git a/playbooks/install-cluster.yml b/playbooks/install-cluster.yml new file mode 100644 index 0000000..5a0781d --- /dev/null +++ b/playbooks/install-cluster.yml @@ -0,0 +1,24 @@ +--- +- name: Install IPA servers + hosts: ipaserver + become: true + + roles: + - role: ipaserver + state: present + +- name: Install IPA replicas + hosts: ipareplicas + become: true + + roles: + - role: ipareplica + state: present + +- name: Install IPA clients + hosts: ipaclients + become: true + + roles: + - role: ipaclient + state: present diff --git a/playbooks/install-replica.yml b/playbooks/install-replica.yml new file mode 100644 index 0000000..fef9654 --- /dev/null +++ b/playbooks/install-replica.yml @@ -0,0 +1,8 @@ +--- +- name: Playbook to configure IPA replicas + hosts: ipareplicas + become: true + + roles: + - role: ipareplica + state: present diff --git a/playbooks/install-server.yml b/playbooks/install-server.yml new file mode 100644 index 0000000..711d696 --- /dev/null +++ b/playbooks/install-server.yml @@ -0,0 +1,8 @@ +--- +- name: Playbook to configure IPA servers + hosts: ipaserver + become: true + + roles: + - role: ipaserver + state: present diff --git a/playbooks/pwpolicy/pwpolicy_absent.yml b/playbooks/pwpolicy/pwpolicy_absent.yml new file mode 100644 index 0000000..4c61e53 --- /dev/null +++ b/playbooks/pwpolicy/pwpolicy_absent.yml @@ -0,0 +1,12 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure absence of pwpolicies for group ops + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + name: ops + state: absent diff --git a/playbooks/pwpolicy/pwpolicy_present.yml b/playbooks/pwpolicy/pwpolicy_present.yml new file mode 100644 index 0000000..fab29c4 --- /dev/null +++ b/playbooks/pwpolicy/pwpolicy_present.yml @@ -0,0 +1,20 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure presence of pwpolicies for group ops + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + name: ops + minlife: 7 + maxlife: 49 + history: 5 + priority: 1 + lockouttime: 300 + minlength: 8 + minclasses: 5 + maxfail: 3 + failinterval: 5 diff --git a/playbooks/sudocmd/ensure-sudocmd-is-absent.yml b/playbooks/sudocmd/ensure-sudocmd-is-absent.yml new file mode 100644 index 0000000..1b3bbf4 --- /dev/null +++ b/playbooks/sudocmd/ensure-sudocmd-is-absent.yml @@ -0,0 +1,11 @@ +--- +- name: Playbook to manage sudo command + hosts: ipaserver + become: true + + tasks: + # Ensure sudo command is absent + - ipasudocmd: + ipaadmin_password: MyPassword123 + name: /usr/bin/su + state: absent diff --git a/playbooks/sudocmd/ensure-sudocmd-is-present.yml b/playbooks/sudocmd/ensure-sudocmd-is-present.yml new file mode 100644 index 0000000..3aa0f47 --- /dev/null +++ b/playbooks/sudocmd/ensure-sudocmd-is-present.yml @@ -0,0 +1,11 @@ +--- +- name: Playbook to manage sudo command + hosts: ipaserver + become: true + + tasks: + # Ensure sudo command is present + - ipasudocmd: + ipaadmin_password: MyPassword123 + name: /usr/bin/su + state: present diff --git a/playbooks/sudocmdgroup/ensure-sudocmd-are-absent-in-sudocmdgroup.yml b/playbooks/sudocmdgroup/ensure-sudocmd-are-absent-in-sudocmdgroup.yml new file mode 100644 index 0000000..bde823e --- /dev/null +++ b/playbooks/sudocmdgroup/ensure-sudocmd-are-absent-in-sudocmdgroup.yml @@ -0,0 +1,15 @@ +--- +- name: Playbook to handle sudocmdgroups + hosts: ipaserver + become: true + + tasks: + # Ensure sudocmds are absent in sudocmdgroup + - ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + action: member + state: absent diff --git a/playbooks/sudocmdgroup/ensure-sudocmd-are-present-in-sudocmdgroup.yml b/playbooks/sudocmdgroup/ensure-sudocmd-are-present-in-sudocmdgroup.yml new file mode 100644 index 0000000..c415695 --- /dev/null +++ b/playbooks/sudocmdgroup/ensure-sudocmd-are-present-in-sudocmdgroup.yml @@ -0,0 +1,22 @@ +--- +- name: Playbook to handle sudocmdgroups + hosts: ipaserver + become: true + + tasks: + # Ensure sudo commands are present + - ipasudocmd: + ipaadmin_password: MyPassword123 + name: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + state: present + + # Ensure sudo commands are present in existing sudocmdgroup + - ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + action: member diff --git a/playbooks/sudocmdgroup/ensure-sudocmdgroup-is-absent.yml b/playbooks/sudocmdgroup/ensure-sudocmdgroup-is-absent.yml new file mode 100644 index 0000000..7674e5d --- /dev/null +++ b/playbooks/sudocmdgroup/ensure-sudocmdgroup-is-absent.yml @@ -0,0 +1,12 @@ +--- +- name: Playbook to handle sudocmdgroups + hosts: ipaserver + become: true + + tasks: + # Ensure sudocmdgroup is absent + - ipasudocmdgroup: + ipaadmin_password: pass1234 + name: network + state: absent + action: sudocmdgroup diff --git a/playbooks/sudocmdgroup/ensure-sudocmdgroup-is-present.yml b/playbooks/sudocmdgroup/ensure-sudocmdgroup-is-present.yml new file mode 100644 index 0000000..6809080 --- /dev/null +++ b/playbooks/sudocmdgroup/ensure-sudocmdgroup-is-present.yml @@ -0,0 +1,15 @@ +--- +- name: Playbook to handle sudocmdgroups + hosts: ipaserver + become: true + + tasks: + # Ensure sudocmdgroup sudocmds are present + - ipasudocmdgroup: + ipaadmin_password: pass1234 + name: network + description: Group of important commands. + sudocmd: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + state: present diff --git a/playbooks/sudorule/ensure-sudorule-host-member-is-absent.yml b/playbooks/sudorule/ensure-sudorule-host-member-is-absent.yml new file mode 100644 index 0000000..f74765c --- /dev/null +++ b/playbooks/sudorule/ensure-sudorule-host-member-is-absent.yml @@ -0,0 +1,14 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + # Ensure host server is absent in Sudo Rule + - ipasudorule: + ipaadmin_password: MyPassword123 + name: testrule1 + host: server + action: member + state: absent diff --git a/playbooks/sudorule/ensure-sudorule-host-member-is-present.yml b/playbooks/sudorule/ensure-sudorule-host-member-is-present.yml new file mode 100644 index 0000000..4ecf3f3 --- /dev/null +++ b/playbooks/sudorule/ensure-sudorule-host-member-is-present.yml @@ -0,0 +1,13 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + # Ensure host server is present in Sudo Rule + - ipasudorule: + ipaadmin_password: MyPassword123 + name: testrule1 + host: server + action: member diff --git a/playbooks/sudorule/ensure-sudorule-hostgroup-member-is-absent.yml b/playbooks/sudorule/ensure-sudorule-hostgroup-member-is-absent.yml new file mode 100644 index 0000000..301030f --- /dev/null +++ b/playbooks/sudorule/ensure-sudorule-hostgroup-member-is-absent.yml @@ -0,0 +1,14 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + # Ensure hostgroup cluster is absent in Sudo Rule + - ipasudorule: + ipaadmin_password: MyPassword123 + name: testrule1 + hostgroup: cluster + action: member + state: absent diff --git a/playbooks/sudorule/ensure-sudorule-hostgroup-member-is-present.yml b/playbooks/sudorule/ensure-sudorule-hostgroup-member-is-present.yml new file mode 100644 index 0000000..b4473b2 --- /dev/null +++ b/playbooks/sudorule/ensure-sudorule-hostgroup-member-is-present.yml @@ -0,0 +1,13 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + # Ensure hostgrep cluster is present in Sudo Rule + - ipasudorule: + ipaadmin_password: MyPassword123 + name: testrule1 + hostgroup: cluster + action: member diff --git a/playbooks/sudorule/ensure-sudorule-is-absent.yml b/playbooks/sudorule/ensure-sudorule-is-absent.yml new file mode 100644 index 0000000..4b87902 --- /dev/null +++ b/playbooks/sudorule/ensure-sudorule-is-absent.yml @@ -0,0 +1,11 @@ +--- +- name: Tests + hosts: ipaserver + become: true + + tasks: + # Ensure sudorule command is absent + - ipasudorule: + ipaadmin_password: pass1234 + name: testrule1 + state: absent diff --git a/playbooks/sudorule/ensure-sudorule-is-disabled.yml b/playbooks/sudorule/ensure-sudorule-is-disabled.yml new file mode 100644 index 0000000..90afbd2 --- /dev/null +++ b/playbooks/sudorule/ensure-sudorule-is-disabled.yml @@ -0,0 +1,11 @@ +--- +- name: Tests + hosts: ipaserver + become: true + + tasks: + # Ensure sudorule command is disabled + - ipasudorule: + ipaadmin_password: MyPassword123 + name: testrule1 + state: disabled diff --git a/playbooks/sudorule/ensure-sudorule-is-enabled.yml b/playbooks/sudorule/ensure-sudorule-is-enabled.yml new file mode 100644 index 0000000..6618344 --- /dev/null +++ b/playbooks/sudorule/ensure-sudorule-is-enabled.yml @@ -0,0 +1,11 @@ +--- +- name: Tests + hosts: ipaserver + become: true + + tasks: + # Ensure sudorule command is enabled + - ipasudorule: + ipaadmin_password: MyPassword123 + name: testrule1 + state: enabled diff --git a/playbooks/sudorule/ensure-sudorule-is-present.yml b/playbooks/sudorule/ensure-sudorule-is-present.yml new file mode 100644 index 0000000..5b8f32b --- /dev/null +++ b/playbooks/sudorule/ensure-sudorule-is-present.yml @@ -0,0 +1,12 @@ +--- +- name: Tests + hosts: ipaserver + become: true + + tasks: + # Ensure sudorule command is present + - ipasudorule: + ipaadmin_password: MyPassword123 + name: testrule1 + description: A test sudo rule. + state: present diff --git a/playbooks/sudorule/ensure-sudorule-sudocmd-is-absent.yml b/playbooks/sudorule/ensure-sudorule-sudocmd-is-absent.yml new file mode 100644 index 0000000..942d0b5 --- /dev/null +++ b/playbooks/sudorule/ensure-sudorule-sudocmd-is-absent.yml @@ -0,0 +1,15 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - ipasudorule: + ipaadmin_password: MyPassword123 + name: testrule1 + cmd: + - /sbin/ifconfig + - /usr/bin/vim + action: member + state: absent diff --git a/playbooks/sudorule/ensure-sudorule-sudocmd-is-present.yml b/playbooks/sudorule/ensure-sudorule-sudocmd-is-present.yml new file mode 100644 index 0000000..61fcbb0 --- /dev/null +++ b/playbooks/sudorule/ensure-sudorule-sudocmd-is-present.yml @@ -0,0 +1,14 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - ipasudorule: + ipaadmin_password: MyPassword123 + name: testrule1 + cmd: + - /sbin/ifconfig + - /usr/bin/vim + action: member diff --git a/playbooks/topology/add-topologysegment.yml b/playbooks/topology/add-topologysegment.yml new file mode 100644 index 0000000..ebf5544 --- /dev/null +++ b/playbooks/topology/add-topologysegment.yml @@ -0,0 +1,13 @@ +--- +- name: Playbook to handle topologysegment + hosts: ipaserver + become: true + + tasks: + - name: Add topology segment + ipatopologysegment: + ipaadmin_password: MyPassword123 + suffix: domain + left: ipareplica1.test.local + right: ipareplica2.test.local + state: present diff --git a/playbooks/topology/add-topologysegments.yml b/playbooks/topology/add-topologysegments.yml new file mode 100644 index 0000000..c285a9f --- /dev/null +++ b/playbooks/topology/add-topologysegments.yml @@ -0,0 +1,23 @@ +--- +- name: Add topology segments + hosts: ipaserver + become: true + gather_facts: false + + vars: + ipatopology_segments: + - {suffix: domain, left: replica1.test.local, right: replica2.test.local} + - {suffix: domain, left: replica2.test.local, right: replica3.test.local} + - {suffix: domain, left: replica3.test.local, right: replica4.test.local} + - {suffix: domain+ca, left: replica4.test.local, right: replica1.test.local} + + tasks: + - name: Add topology segment + ipatopologysegment: + ipaadmin_password: "{{ ipaadmin_password }}" + suffix: "{{ item.suffix }}" + name: "{{ item.name | default(omit) }}" + left: "{{ item.left }}" + right: "{{ item.right }}" + state: present + loop: "{{ ipatopology_segments | default([]) }}" diff --git a/playbooks/topology/check-topologysegments.yml b/playbooks/topology/check-topologysegments.yml new file mode 100644 index 0000000..5770c17 --- /dev/null +++ b/playbooks/topology/check-topologysegments.yml @@ -0,0 +1,23 @@ +--- +- name: Add topology segments + hosts: ipaserver + become: true + gather_facts: false + + vars: + ipatopology_segments: + - {suffix: domain, left: replica1.test.local, right: replica2.test.local} + - {suffix: domain, left: replica2.test.local, right: replica3.test.local} + - {suffix: domain, left: replica3.test.local, right: replica4.test.local} + - {suffix: domain+ca, left: replica4.test.local, right: replica1.test.local} + + tasks: + - name: Add topology segment + ipatopologysegment: + ipaadmin_password: "{{ ipaadmin_password }}" + suffix: "{{ item.suffix }}" + name: "{{ item.name | default(omit) }}" + left: "{{ item.left }}" + right: "{{ item.right }}" + state: checked + loop: "{{ ipatopology_segments | default([]) }}" diff --git a/playbooks/topology/delete-topologysegment.yml b/playbooks/topology/delete-topologysegment.yml new file mode 100644 index 0000000..a3f2c68 --- /dev/null +++ b/playbooks/topology/delete-topologysegment.yml @@ -0,0 +1,13 @@ +--- +- name: Playbook to handle topologysegment + hosts: ipaserver + become: true + + tasks: + - name: Delete topology segment + ipatopologysegment: + ipaadmin_password: MyPassword123 + suffix: domain + left: ipareplica1.test.local + right: ipareplica2.test.local + state: absent diff --git a/playbooks/topology/delete-topologysegments.yml b/playbooks/topology/delete-topologysegments.yml new file mode 100644 index 0000000..68b95b8 --- /dev/null +++ b/playbooks/topology/delete-topologysegments.yml @@ -0,0 +1,23 @@ +--- +- name: Add topology segments + hosts: ipaserver + become: true + gather_facts: false + + vars: + ipatopology_segments: + - {suffix: domain, left: replica1.test.local, right: replica2.test.local} + - {suffix: domain, left: replica2.test.local, right: replica3.test.local} + - {suffix: domain, left: replica3.test.local, right: replica4.test.local} + - {suffix: domain+ca, left: replica4.test.local, right: replica1.test.local} + + tasks: + - name: Add topology segment + ipatopologysegment: + ipaadmin_password: "{{ ipaadmin_password }}" + suffix: "{{ item.suffix }}" + name: "{{ item.name | default(omit) }}" + left: "{{ item.left }}" + right: "{{ item.right }}" + state: absent + loop: "{{ ipatopology_segments | default([]) }}" diff --git a/playbooks/topology/reinitialize-topologysegment.yml b/playbooks/topology/reinitialize-topologysegment.yml new file mode 100644 index 0000000..1f21cd2 --- /dev/null +++ b/playbooks/topology/reinitialize-topologysegment.yml @@ -0,0 +1,14 @@ +--- +- name: Playbook to handle topologysegment + hosts: ipaserver + become: true + + tasks: + - name: Reinitialize topology segment + ipatopologysegment: + ipaadmin_password: MyPassword123 + suffix: domain + left: ipareplica1.test.local + right: ipareplica2.test.local + direction: left-to-right + state: reinitialized diff --git a/playbooks/topology/verify-topologysuffix.yml b/playbooks/topology/verify-topologysuffix.yml new file mode 100644 index 0000000..b27ca3d --- /dev/null +++ b/playbooks/topology/verify-topologysuffix.yml @@ -0,0 +1,11 @@ +--- +- name: Playbook to handle topologysuffix + hosts: ipaserver + become: true + + tasks: + - name: Verify topology suffix + ipatopologysuffix: + ipaadmin_password: MyPassword123 + suffix: domain + state: verified diff --git a/playbooks/uninstall-client.yml b/playbooks/uninstall-client.yml new file mode 100644 index 0000000..8031375 --- /dev/null +++ b/playbooks/uninstall-client.yml @@ -0,0 +1,8 @@ +--- +- name: Playbook to unconfigure IPA clients + hosts: ipaclients + become: true + + roles: + - role: ipaclient + state: absent diff --git a/playbooks/uninstall-cluster.yml b/playbooks/uninstall-cluster.yml new file mode 100644 index 0000000..b8cec9b --- /dev/null +++ b/playbooks/uninstall-cluster.yml @@ -0,0 +1,24 @@ +--- +- name: Uninstall IPA clients + hosts: ipaclients + become: true + + roles: + - role: ipaclient + state: absent + +- name: Uninstall IPA replicas + hosts: ipareplicas + become: true + + roles: + - role: ipareplica + state: absent + +- name: Uninstall IPA servers + hosts: ipaserver + become: true + + roles: + - role: ipaserver + state: absent diff --git a/playbooks/uninstall-replica.yml b/playbooks/uninstall-replica.yml new file mode 100644 index 0000000..53f0654 --- /dev/null +++ b/playbooks/uninstall-replica.yml @@ -0,0 +1,8 @@ +--- +- name: Playbook to unconfigure IPA replicas + hosts: ipareplicas + become: true + + roles: + - role: ipareplica + state: absent diff --git a/playbooks/uninstall-server.yml b/playbooks/uninstall-server.yml new file mode 100644 index 0000000..1104285 --- /dev/null +++ b/playbooks/uninstall-server.yml @@ -0,0 +1,8 @@ +--- +- name: Playbook to unconfigure IPA servers + hosts: ipaserver + become: true + + roles: + - role: ipaserver + state: absent diff --git a/playbooks/user/add-group.yml b/playbooks/user/add-group.yml new file mode 100644 index 0000000..a36ca14 --- /dev/null +++ b/playbooks/user/add-group.yml @@ -0,0 +1,24 @@ +--- +- name: Playbook to handle groups + hosts: ipaserver + become: true + + tasks: + - name: Create group ops with gid 1234 + ipagroup: + ipaadmin_password: MyPassword123 + name: ops + gidnumber: 1234 + + - name: Create group sysops + ipagroup: + ipaadmin_password: MyPassword123 + name: sysops + user: + - pinky + + - name: Create group appops + ipagroup: + ipaadmin_password: MyPassword123 + name: appops + diff --git a/playbooks/user/add-groups-to-group.yml b/playbooks/user/add-groups-to-group.yml new file mode 100644 index 0000000..ecc9b45 --- /dev/null +++ b/playbooks/user/add-groups-to-group.yml @@ -0,0 +1,13 @@ +--- +- name: Playbook to handle groups + hosts: ipaserver + become: true + + tasks: + - name: Add group members sysops and appops to group sysops + ipagroup: + ipaadmin_password: MyPassword123 + name: ops + group: + - sysops + - appops diff --git a/playbooks/user/add-user-to-group.yml b/playbooks/user/add-user-to-group.yml new file mode 100644 index 0000000..9ed5eb1 --- /dev/null +++ b/playbooks/user/add-user-to-group.yml @@ -0,0 +1,13 @@ +--- +- name: Playbook to handle groups + hosts: ipaserver + become: true + + tasks: + - name: Add user member brain to group sysops + ipagroup: + ipaadmin_password: MyPassword123 + name: sysops + action: member + user: + - brain diff --git a/playbooks/user/add-user.yml b/playbooks/user/add-user.yml new file mode 100644 index 0000000..345a028 --- /dev/null +++ b/playbooks/user/add-user.yml @@ -0,0 +1,20 @@ +--- +- name: Playbook to handle users + hosts: ipaserver + become: true + + tasks: + - name: Create user pinky + ipauser: + ipaadmin_password: MyPassword123 + name: pinky + first: pinky + last: Acme + uid: 10001 + gid: 100 + phone: "+555123457" + email: pinky@acme.com + passwordexpiration: "2023-01-19 23:59:59" + password: "no-brain" + update_password: on_create + diff --git a/playbooks/user/delete-group.yml b/playbooks/user/delete-group.yml new file mode 100644 index 0000000..2d863c3 --- /dev/null +++ b/playbooks/user/delete-group.yml @@ -0,0 +1,11 @@ +--- +- name: Playbook to handle groups + hosts: ipaserver + become: true + + tasks: + - name: Remove goups sysops, appops and ops + ipagroup: + ipaadmin_password: MyPassword123 + name: sysops,appops,ops + state: absent diff --git a/playbooks/user/delete-preserve-user.yml b/playbooks/user/delete-preserve-user.yml new file mode 100644 index 0000000..c202b15 --- /dev/null +++ b/playbooks/user/delete-preserve-user.yml @@ -0,0 +1,12 @@ +--- +- name: Playbook to handle users + hosts: ipaserver + become: true + + tasks: + - name: Delete and preserve user pinky + ipauser: + ipaadmin_password: MyPassword123 + name: pinky + preserve: yes + state: absent diff --git a/playbooks/user/delete-user.yml b/playbooks/user/delete-user.yml new file mode 100644 index 0000000..101a2b4 --- /dev/null +++ b/playbooks/user/delete-user.yml @@ -0,0 +1,11 @@ +--- +- name: Playbook to handle users + hosts: ipaserver + become: true + + tasks: + - name: Remove user pinky and brain + ipauser: + ipaadmin_password: MyPassword123 + name: pinky + state: absent diff --git a/playbooks/user/disable-user.yml b/playbooks/user/disable-user.yml new file mode 100644 index 0000000..192fd53 --- /dev/null +++ b/playbooks/user/disable-user.yml @@ -0,0 +1,11 @@ +--- +- name: Playbook to handle users + hosts: ipaserver + become: true + + tasks: + - name: Disable user pinky + ipauser: + ipaadmin_password: MyPassword123 + name: pinky + state: disabled diff --git a/playbooks/user/enable-user.yml b/playbooks/user/enable-user.yml new file mode 100644 index 0000000..dfbba49 --- /dev/null +++ b/playbooks/user/enable-user.yml @@ -0,0 +1,11 @@ +--- +- name: Playbook to handle users + hosts: ipaserver + become: true + + tasks: + - name: Enable user pinky + ipauser: + ipaadmin_password: MyPassword123 + name: pinky + state: enabled diff --git a/playbooks/user/ensure_user_with_randompassword.yml b/playbooks/user/ensure_user_with_randompassword.yml new file mode 100644 index 0000000..4ca9f21 --- /dev/null +++ b/playbooks/user/ensure_user_with_randompassword.yml @@ -0,0 +1,19 @@ +--- +- name: Ensure user with random password + hosts: ipaserver + become: true + + tasks: + - name: User user1 present with random password + ipauser: + ipaadmin_password: MyPassword123 + name: user1 + first: first1 + last: last1 + random: yes + update_password: on_create + register: ipauser + + - name: Print generated random password + debug: + var: ipauser.user.randompassword diff --git a/playbooks/user/ensure_users_with_randompasswords.yml b/playbooks/user/ensure_users_with_randompasswords.yml new file mode 100644 index 0000000..06f50c7 --- /dev/null +++ b/playbooks/user/ensure_users_with_randompasswords.yml @@ -0,0 +1,28 @@ +--- +- name: Tests + hosts: ipaserver + become: true + + tasks: + - name: Users user1 and user1 present with random password + ipauser: + ipaadmin_password: MyPassword123 + users: + - name: user1 + first: first1 + last: last1 + random: yes + - name: user2 + first: first2 + last: last2 + random: yes + update_password: on_create + register: ipauser + + - name: Print generated random password for user1 + debug: + var: ipauser.user.user1.randompassword + + - name: Print generated random password for user2 + debug: + var: ipauser.user.user2.randompassword diff --git a/playbooks/user/undelete-user.yml b/playbooks/user/undelete-user.yml new file mode 100644 index 0000000..79feda0 --- /dev/null +++ b/playbooks/user/undelete-user.yml @@ -0,0 +1,11 @@ +--- +- name: Playbook to handle users + hosts: ipaserver + become: true + + tasks: + - name: Undelete preserved user pinky + ipauser: + ipaadmin_password: MyPassword123 + name: pinky + state: undeleted diff --git a/playbooks/user/unlock-users.yml b/playbooks/user/unlock-users.yml new file mode 100644 index 0000000..337e97f --- /dev/null +++ b/playbooks/user/unlock-users.yml @@ -0,0 +1,11 @@ +--- +- name: Playbook to handle users + hosts: ipaserver + become: true + + tasks: + - name: Unlock users pinky and brain + ipauser: + ipaadmin_password: MyPassword123 + name: pinky,brain + state: unlocked diff --git a/playbooks/user/user_certificate_absent.yml b/playbooks/user/user_certificate_absent.yml new file mode 100644 index 0000000..d50b40f --- /dev/null +++ b/playbooks/user/user_certificate_absent.yml @@ -0,0 +1,16 @@ +--- +- name: Test user certificates + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: User test cert absent + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certificate: + - 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 + state: absent diff --git a/playbooks/user/user_certificate_present.yml b/playbooks/user/user_certificate_present.yml new file mode 100644 index 0000000..b322084 --- /dev/null +++ b/playbooks/user/user_certificate_present.yml @@ -0,0 +1,15 @@ +--- +- name: Test user certificates + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: User test cert present + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certificate: + - 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 diff --git a/playbooks/user/user_present.yml b/playbooks/user/user_present.yml new file mode 100644 index 0000000..9abf26a --- /dev/null +++ b/playbooks/user/user_present.yml @@ -0,0 +1,40 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: User pinky present + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + uid: 10001 + gid: 100 + phone: "+555123457" + email: pinky@acme.com + principalexpiration: "20220119235959" + passwordexpiration: "2022-01-19 23:59:59" + first: pinky + last: Acme + initials: pa + principal: pa + random: yes + city: PinkyCity + userstate: PinkyState + postalcode: 321 + mobile: "+555123458,+555123459" + pager: "+555123450,+555123451" + fax: "+555123452,+555123453" + orgunit: PinkyOrgUnit + manager: manager1,manager2 + update_password: on_create + carlicense: PinkyCarLicense1,PinkyCarLicense2 + userauthtype: password,radius,otp + userclass: PinkyUserClass + departmentnumber: "1234" + employeenumber: "0815" + employeetype: "PinkyExmployeeType" + preferredlanguage: "en" + noprivate: yes + nomembers: false diff --git a/playbooks/user/users_absent.yml b/playbooks/user/users_absent.yml new file mode 100644 index 0000000..a592a06 --- /dev/null +++ b/playbooks/user/users_absent.yml @@ -0,0 +1,42 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Users user1..10 absent + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: user1 + givenname: user1 + last: Last + - name: user2 + first: user2 + last: Last + - name: user3 + first: user3 + last: Last + - name: user4 + first: user4 + last: Last + - name: user5 + first: user5 + last: Last + - name: user6 + first: user6 + last: Last + - name: user7 + first: user7 + last: Last + - name: user8 + first: user8 + last: Last + - name: user9 + first: user9 + last: Last + - name: user10 + first: user10 + last: Last + state: absent diff --git a/playbooks/user/users_certificate_absent.yml b/playbooks/user/users_certificate_absent.yml new file mode 100644 index 0000000..0963e4f --- /dev/null +++ b/playbooks/user/users_certificate_absent.yml @@ -0,0 +1,17 @@ +--- +- name: Test user certificates + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: User test cert absent + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + certificate: + - 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 + state: absent diff --git a/playbooks/user/users_certificate_present.yml b/playbooks/user/users_certificate_present.yml new file mode 100644 index 0000000..8d82a87 --- /dev/null +++ b/playbooks/user/users_certificate_present.yml @@ -0,0 +1,16 @@ +--- +- name: Test user certificates + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: User test cert present + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + certificate: + - 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 diff --git a/playbooks/user/users_present.yml b/playbooks/user/users_present.yml new file mode 100644 index 0000000..3b9303b --- /dev/null +++ b/playbooks/user/users_present.yml @@ -0,0 +1,41 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Users user1..10 present + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: user1 + first: user1 + last: Last + - name: user2 + first: user2 + last: Last + - name: user3 + first: user3 + last: Last + - name: user4 + first: user4 + last: Last + - name: user5 + first: user5 + last: Last + - name: user6 + first: user6 + last: Last + - name: user7 + first: user7 + last: Last + - name: user8 + first: user8 + last: Last + - name: user9 + first: user9 + last: Last + - name: user10 + first: user10 + last: Last diff --git a/plugins/module_utils/ansible_freeipa_module.py b/plugins/module_utils/ansible_freeipa_module.py new file mode 100644 index 0000000..8154a12 --- /dev/null +++ b/plugins/module_utils/ansible_freeipa_module.py @@ -0,0 +1,277 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Copyright (C) 2019 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + + +import os +import uuid +import tempfile +import shutil +import gssapi +from datetime import datetime +from ipalib import api +from ipalib.config import Env +from ipalib.constants import DEFAULT_CONFIG, LDAP_GENERALIZED_TIME_FORMAT +try: + from ipalib.install.kinit import kinit_password, kinit_keytab +except ImportError: + from ipapython.ipautil import kinit_password, kinit_keytab +from ipapython.ipautil import run +from ipaplatform.paths import paths +from ipalib.krb_utils import get_credentials_if_valid +from ansible.module_utils._text import to_text +try: + from ipalib.x509 import Encoding +except ImportError: + from cryptography.hazmat.primitives.serialization import Encoding +import base64 +import six + + +if six.PY3: + unicode = str + + +def valid_creds(module, principal): + """ + Get valid credintials matching the princial, try GSSAPI first + """ + if "KRB5CCNAME" in os.environ: + ccache = os.environ["KRB5CCNAME"] + module.debug('KRB5CCNAME set to %s' % ccache) + + try: + cred = gssapi.Credentials(usage='initiate', + store={'ccache': ccache}) + except gssapi.raw.misc.GSSError as e: + module.fail_json(msg='Failed to find default ccache: %s' % e) + else: + module.debug("Using principal %s" % str(cred.name)) + return True + + elif "KRB5_CLIENT_KTNAME" in os.environ: + keytab = os.environ.get('KRB5_CLIENT_KTNAME', None) + module.debug('KRB5_CLIENT_KTNAME set to %s' % keytab) + + ccache_name = "MEMORY:%s" % str(uuid.uuid4()) + os.environ["KRB5CCNAME"] = ccache_name + + try: + cred = kinit_keytab(principal, keytab, ccache_name) + except gssapi.raw.misc.GSSError as e: + module.fail_json(msg='Kerberos authentication failed : %s' % e) + else: + module.debug("Using principal %s" % str(cred.name)) + return True + + creds = get_credentials_if_valid() + if creds and \ + creds.lifetime > 0 and \ + "%s@" % principal in creds.name.display_as(creds.name.name_type): + return True + return False + + +def temp_kinit(principal, password): + """ + kinit with password using a temporary ccache + """ + if not password: + raise RuntimeError("The password is not set") + if not principal: + principal = "admin" + + ccache_dir = tempfile.mkdtemp(prefix='krbcc') + ccache_name = os.path.join(ccache_dir, 'ccache') + + try: + kinit_password(principal, password, ccache_name) + except RuntimeError as e: + raise RuntimeError("Kerberos authentication failed: {}".format(e)) + + return ccache_dir, ccache_name + + +def temp_kdestroy(ccache_dir, ccache_name): + """ + Destroy temporary ticket and remove temporary ccache + """ + if ccache_name is not None: + run([paths.KDESTROY, '-c', ccache_name], raiseonerr=False) + if ccache_dir is not None: + shutil.rmtree(ccache_dir, ignore_errors=True) + + +def api_connect(context=None): + """ + Create environment, initialize api and connect to ldap2 + """ + env = Env() + env._bootstrap() + env._finalize_core(**dict(DEFAULT_CONFIG)) + + # available contexts are 'server', 'ansible-freeipa' and 'cli_installer' + if context is None: + context = 'server' + + api.bootstrap(context=context, debug=env.debug, log=None) + api.finalize() + + if api.env.in_server: + backend = api.Backend.ldap2 + else: + backend = api.Backend.rpcclient + + if not backend.isconnected(): + backend.connect() + + +def api_command(module, command, name, args): + """ + Call ipa.Command + """ + return api.Command[command](name, **args) + + +def api_command_no_name(module, command, args): + """ + Call ipa.Command without a name. + """ + return api.Command[command](**args) + + +def api_check_param(command, name): + """ + Return if param exists in command param list + """ + return name in api.Command[command].params + + +def execute_api_command(module, principal, password, command, name, args): + """ + Get KRB ticket if not already there, initialize api, connect, + execute command and destroy ticket again if it has been created also. + """ + ccache_dir = None + ccache_name = None + try: + if not valid_creds(module, principal): + ccache_dir, ccache_name = temp_kinit(principal, password) + api_connect() + + return api_command(module, command, name, args) + except Exception as e: + module.fail_json(msg=str(e)) + + finally: + temp_kdestroy(ccache_dir, ccache_name) + + +def date_format(value): + accepted_date_formats = [ + LDAP_GENERALIZED_TIME_FORMAT, # generalized time + '%Y-%m-%dT%H:%M:%SZ', # ISO 8601, second precision + '%Y-%m-%dT%H:%MZ', # ISO 8601, minute precision + '%Y-%m-%dZ', # ISO 8601, date only + '%Y-%m-%d %H:%M:%SZ', # non-ISO 8601, second precision + '%Y-%m-%d %H:%MZ', # non-ISO 8601, minute precision + ] + + for date_format in accepted_date_formats: + try: + return datetime.strptime(value, date_format) + except ValueError: + pass + raise ValueError("Invalid date '%s'" % value) + + +def compare_args_ipa(module, args, ipa): + for key in args.keys(): + if key not in ipa: + return False + else: + arg = args[key] + ipa_arg = ipa[key] + # If ipa_arg is a list and arg is not, replace arg + # with list containing arg. Most args in a find result + # are lists, but not all. + if isinstance(ipa_arg, tuple): + ipa_arg = list(ipa_arg) + if isinstance(ipa_arg, list): + if not isinstance(arg, list): + arg = [arg] + if isinstance(ipa_arg[0], str) and isinstance(arg[0], int): + arg = [to_text(_arg) for _arg in arg] + if isinstance(ipa_arg[0], unicode) and isinstance(arg[0], int): + arg = [to_text(_arg) for _arg in arg] + # module.warn("%s <=> %s" % (arg, ipa_arg)) + if set(arg) != set(ipa_arg): + # module.warn("DIFFERENT") + return False + + return True + + +def _afm_convert(value): + if value is not None: + if isinstance(value, list): + return [_afm_convert(x) for x in value] + elif isinstance(value, dict): + return {_afm_convert(k): _afm_convert(v) for k, v in value.items()} + elif isinstance(value, str): + return to_text(value) + else: + return value + else: + return value + + +def module_params_get(module, name): + return _afm_convert(module.params.get(name)) + + +def api_get_realm(): + return api.env.realm + + +def gen_add_del_lists(user_list, res_list): + """ + Generate the lists for the addition and removal of members using the + provided user and ipa settings + """ + add_list = list(set(user_list or []) - set(res_list or [])) + del_list = list(set(res_list or []) - set(user_list or [])) + + return add_list, del_list + + +def encode_certificate(cert): + """ + Encode a certificate using base64 with also taking FreeIPA and Python + versions into account + """ + if isinstance(cert, str) or isinstance(cert, unicode): + encoded = base64.b64encode(cert) + else: + encoded = base64.b64encode(cert.public_bytes(Encoding.DER)) + if not six.PY2: + encoded = encoded.decode('ascii') + return encoded diff --git a/plugins/modules/ipagroup.py b/plugins/modules/ipagroup.py new file mode 100644 index 0000000..50838cb --- /dev/null +++ b/plugins/modules/ipagroup.py @@ -0,0 +1,448 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Copyright (C) 2019 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "supported_by": "community", + "status": ["preview"], +} + +DOCUMENTATION = """ +--- +module: ipagroup +short description: Manage FreeIPA groups +description: Manage FreeIPA groups +options: + ipaadmin_principal: + description: The admin principal + default: admin + ipaadmin_password: + description: The admin password + required: false + name: + description: The group name + required: false + aliases: ["cn"] + description: + description: The group description + required: false + gid: + description: The GID + required: false + aliases: ["gidnumber"] + nonposix: + description: Create as a non-POSIX group + required: false + type: bool + external: + description: Allow adding external non-IPA members from trusted domains + required: false + type: bool + nomembers: + description: Suppress processing of membership attributes + required: false + type: bool + user: + description: List of user names assigned to this group. + required: false + type: list + group: + description: List of group names assigned to this group. + required: false + type: list + service: + description: + - List of service names assigned to this group. + - Only usable with IPA versions 4.7 and up. + required: false + type: list + action: + description: Work on group or member level + default: group + choices: ["member", "group"] + state: + description: State to ensure + default: present + choices: ["present", "absent"] +author: + - Thomas Woerner +""" + +EXAMPLES = """ +# Create group ops with gid 1234 +- ipagroup: + ipaadmin_password: MyPassword123 + name: ops + gidnumber: 1234 + +# Create group sysops +- ipagroup: + ipaadmin_password: MyPassword123 + name: sysops + +# Create group appops +- ipagroup: + ipaadmin_password: MyPassword123 + name: appops + +# Add user member pinky to group sysops +- ipagroup: + ipaadmin_password: MyPassword123 + name: sysops + action: member + user: + - pinky + +# Add user member brain to group sysops +- ipagroup: + ipaadmin_password: MyPassword123 + name: sysops + action: member + user: + - brain + +# Add group members sysops and appops to group sysops +- ipagroup: + ipaadmin_password: MyPassword123 + name: ops + group: + - sysops + - appops + +# Remove goups sysops, appops and ops +- ipagroup: + ipaadmin_password: MyPassword123 + name: sysops,appops,ops + state: absent +""" + +RETURN = """ +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_freeipa_module import temp_kinit, \ + temp_kdestroy, valid_creds, api_connect, api_command, compare_args_ipa, \ + api_check_param, module_params_get + + +def find_group(module, name): + _args = { + "all": True, + "cn": name, + } + + _result = api_command(module, "group_find", name, _args) + + if len(_result["result"]) > 1: + module.fail_json( + msg="There is more than one group '%s'" % (name)) + elif len(_result["result"]) == 1: + return _result["result"][0] + else: + return None + + +def gen_args(description, gid, nonposix, external, nomembers): + _args = {} + if description is not None: + _args["description"] = description + if gid is not None: + _args["gidnumber"] = gid + if nonposix is not None: + _args["nonposix"] = nonposix + if external is not None: + _args["external"] = external + if nomembers is not None: + _args["nomembers"] = nomembers + + return _args + + +def gen_member_args(user, group, service): + _args = {} + if user is not None: + _args["member_user"] = user + if group is not None: + _args["member_group"] = group + if service is not None: + _args["member_service"] = service + + return _args + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # general + ipaadmin_principal=dict(type="str", default="admin"), + ipaadmin_password=dict(type="str", required=False, no_log=True), + + name=dict(type="list", aliases=["cn"], default=None, + required=True), + # present + description=dict(type="str", default=None), + gid=dict(type="int", aliases=["gidnumber"], default=None), + nonposix=dict(required=False, type='bool', default=None), + external=dict(required=False, type='bool', default=None), + nomembers=dict(required=False, type='bool', default=None), + user=dict(required=False, type='list', default=None), + group=dict(required=False, type='list', default=None), + service=dict(required=False, type='list', default=None), + action=dict(type="str", default="group", + choices=["member", "group"]), + # state + state=dict(type="str", default="present", + choices=["present", "absent"]), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + + # Get parameters + + # general + ipaadmin_principal = module_params_get(ansible_module, "ipaadmin_principal") + ipaadmin_password = module_params_get(ansible_module, "ipaadmin_password") + names = module_params_get(ansible_module, "name") + + # present + description = module_params_get(ansible_module, "description") + gid = module_params_get(ansible_module, "gid") + nonposix = module_params_get(ansible_module, "nonposix") + external = module_params_get(ansible_module, "external") + nomembers = module_params_get(ansible_module, "nomembers") + user = module_params_get(ansible_module, "user") + group = module_params_get(ansible_module, "group") + service = module_params_get(ansible_module, "service") + action = module_params_get(ansible_module, "action") + # state + state = module_params_get(ansible_module, "state") + + # Check parameters + + if state == "present": + if len(names) != 1: + ansible_module.fail_json( + msg="Only one group can be added at a time.") + if action == "member": + invalid = ["description", "gid", "nonposix", "external", + "nomembers"] + for x in invalid: + if vars()[x] is not None: + ansible_module.fail_json( + msg="Argument '%s' can not be used with action " + "'%s'" % (x, action)) + + if state == "absent": + if len(names) < 1: + ansible_module.fail_json( + msg="No name given.") + invalid = ["description", "gid", "nonposix", "external", "nomembers"] + if action == "group": + invalid.extend(["user", "group", "service"]) + for x in invalid: + if vars()[x] is not None: + ansible_module.fail_json( + msg="Argument '%s' can not be used with state '%s'" % + (x, state)) + + # Init + + changed = False + exit_args = {} + ccache_dir = None + ccache_name = None + try: + if not valid_creds(ansible_module, ipaadmin_principal): + ccache_dir, ccache_name = temp_kinit(ipaadmin_principal, + ipaadmin_password) + api_connect() + + has_add_member_service = api_check_param("group_add_member", "service") + if service is not None and not has_add_member_service: + ansible_module.fail_json( + msg="Managing a service as part of a group is not supported " + "by your IPA version") + + commands = [] + + for name in names: + # Make sure group exists + res_find = find_group(ansible_module, name) + + # Create command + if state == "present": + # Generate args + args = gen_args(description, gid, nonposix, external, + nomembers) + + if action == "group": + # Found the group + if res_find is not None: + # For all settings is args, check if there are + # different settings in the find result. + # If yes: modify + if not compare_args_ipa(ansible_module, args, + res_find): + commands.append([name, "group_mod", args]) + else: + commands.append([name, "group_add", args]) + # Set res_find to empty dict for next step + res_find = {} + + member_args = gen_member_args(user, group, service) + if not compare_args_ipa(ansible_module, member_args, + res_find): + # Generate addition and removal lists + user_add = list( + set(user or []) - + set(res_find.get("member_user", []))) + user_del = list( + set(res_find.get("member_user", [])) - + set(user or [])) + group_add = list( + set(group or []) - + set(res_find.get("member_group", []))) + group_del = list( + set(res_find.get("member_group", [])) - + set(group or [])) + service_add = list( + set(service or []) - + set(res_find.get("member_service", []))) + service_del = list( + set(res_find.get("member_service", [])) - + set(service or [])) + + if has_add_member_service: + # Add members + if len(user_add) > 0 or len(group_add) > 0 or \ + len(service_add) > 0: + commands.append([name, "group_add_member", + { + "user": user_add, + "group": group_add, + "service": service_add, + }]) + # Remove members + if len(user_del) > 0 or len(group_del) > 0 or \ + len(service_del) > 0: + commands.append([name, "group_remove_member", + { + "user": user_del, + "group": group_del, + "service": service_del, + }]) + else: + # Add members + if len(user_add) > 0 or len(group_add) > 0: + commands.append([name, "group_add_member", + { + "user": user_add, + "group": group_add, + }]) + # Remove members + if len(user_del) > 0 or len(group_del) > 0: + commands.append([name, "group_remove_member", + { + "user": user_del, + "group": group_del, + }]) + elif action == "member": + if res_find is None: + ansible_module.fail_json(msg="No group '%s'" % name) + if has_add_member_service: + commands.append([name, "group_add_member", + { + "user": user, + "group": group, + "service": service, + }]) + else: + commands.append([name, "group_add_member", + { + "user": user, + "group": group, + }]) + + elif state == "absent": + if action == "group": + if res_find is not None: + commands.append([name, "group_del", {}]) + + elif action == "member": + if res_find is None: + ansible_module.fail_json(msg="No group '%s'" % name) + + commands.append([name, "group_remove_member", + { + "user": user, + "group": group, + "service": service, + }]) + else: + ansible_module.fail_json(msg="Unkown state '%s'" % state) + + # Execute commands + + for name, command, args in commands: + try: + result = api_command(ansible_module, command, name, + args) + if "completed" in result: + if result["completed"] > 0: + changed = True + else: + changed = True + except Exception as e: + ansible_module.fail_json(msg="%s: %s: %s" % (command, name, + str(e))) + # Get all errors + # All "already a member" and "not a member" failures in the + # result are ignored. All others are reported. + errors = [] + if "failed" in result and len(result["failed"]) > 0: + for item in result["failed"]: + failed_item = result["failed"][item] + for member_type in failed_item: + for member, failure in failed_item[member_type]: + if "already a member" in failure \ + or "not a member" in failure: + continue + errors.append("%s: %s %s: %s" % ( + command, member_type, member, failure)) + if len(errors) > 0: + ansible_module.fail_json(msg=", ".join(errors)) + + except Exception as e: + ansible_module.fail_json(msg=str(e)) + + finally: + temp_kdestroy(ccache_dir, ccache_name) + + # Done + + ansible_module.exit_json(changed=changed, **exit_args) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ipahbacrule.py b/plugins/modules/ipahbacrule.py new file mode 100644 index 0000000..385876b --- /dev/null +++ b/plugins/modules/ipahbacrule.py @@ -0,0 +1,556 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Copyright (C) 2019 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "supported_by": "community", + "status": ["preview"], +} + +DOCUMENTATION = """ +--- +module: ipahbacrule +short description: Manage FreeIPA HBAC rules +description: Manage FreeIPA HBAC rules +options: + ipaadmin_principal: + description: The admin principal + default: admin + ipaadmin_password: + description: The admin password + required: false + name: + description: The hbacrule name + required: true + aliases: ["cn"] + description: + description: The hbacrule description + required: false + usercategory: + description: User category the rule applies to + required: false + aliases: ["usercat"] + choices: ["all"] + hostcategory: + description: Host category the rule applies to + required: false + aliases: ["hostcat"] + choices: ["all"] + servicecategory: + description: Service category the rule applies to + required: false + aliases: ["servicecat"] + choices: ["all"] + nomembers: + description: Suppress processing of membership attributes + required: false + type: bool + host: + description: List of host names assigned to this hbacrule. + required: false + type: list + hostgroup: + description: List of host groups assigned to this hbacrule. + required: false + type: list + hbacsvc: + description: List of HBAC service names assigned to this hbacrule. + required: false + type: list + hbacsvcgroup: + description: List of HBAC service names assigned to this hbacrule. + required: false + type: list + user: + description: List of user names assigned to this hbacrule. + required: false + type: list + group: + description: List of user groups assigned to this hbacrule. + required: false + type: list + action: + description: Work on hbacrule or member level + default: hbacrule + choices: ["member", "hbacrule"] + state: + description: State to ensure + default: present + choices: ["present", "absent", "enabled", "disabled"] +author: + - Thomas Woerner +""" + +EXAMPLES = """ +# Ensure HBAC Rule allhosts is present +- ipahbacrule: + ipaadmin_password: MyPassword123 + name: allhosts + usercategory: all + +# Ensure host server is present in HBAC Rule allhosts +- ipahbacrule: + ipaadmin_password: MyPassword123 + name: allhosts + host: server + action: member + +# Ensure HBAC Rule sshd-pinky is present +- ipahbacrule: + ipaadmin_password: MyPassword123 + name: sshd-pinky + hostcategory: all + +# Ensure user pinky is present in HBAC Rule sshd-pinky +- ipahbacrule: + ipaadmin_password: MyPassword123 + name: sshd-pinky + user: pinky + action: member + +# Ensure HBAC service sshd is present in HBAC Rule sshd-pinky +- ipahbacrule: + ipaadmin_password: MyPassword123 + name: sshd-pinky + hbacsvc: sshd + action: member + +# Ensure HBAC Rule sshd-pinky is disabled +- ipahbacrule: + ipaadmin_password: MyPassword123 + name: sshd-pinky + state: disabled + +# Ensure HBAC Rule sshd-pinky is enabled +- ipahbacrule: + ipaadmin_password: MyPassword123 + name: sshd-pinky + state: enabled + +# Ensure HBAC Rule sshd-pinky is absent +- ipahbacrule: + ipaadmin_password: MyPassword123 + name: sshd-pinky + state: absent +""" + +RETURN = """ +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_freeipa_module import temp_kinit, \ + temp_kdestroy, valid_creds, api_connect, api_command, compare_args_ipa, \ + module_params_get + + +def find_hbacrule(module, name): + _args = { + "all": True, + "cn": name, + } + + _result = api_command(module, "hbacrule_find", name, _args) + + if len(_result["result"]) > 1: + module.fail_json( + msg="There is more than one hbacrule '%s'" % (name)) + elif len(_result["result"]) == 1: + return _result["result"][0] + else: + return None + + +def gen_args(description, usercategory, hostcategory, servicecategory, + nomembers): + _args = {} + if description is not None: + _args["description"] = description + if usercategory is not None: + _args["usercategory"] = usercategory + if hostcategory is not None: + _args["hostcategory"] = hostcategory + if servicecategory is not None: + _args["servicecategory"] = servicecategory + if nomembers is not None: + _args["nomembers"] = nomembers + + return _args + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # general + ipaadmin_principal=dict(type="str", default="admin"), + ipaadmin_password=dict(type="str", required=False, no_log=True), + + name=dict(type="list", aliases=["cn"], default=None, + required=True), + # present + description=dict(type="str", default=None), + usercategory=dict(type="str", default=None, + aliases=["usercat"], choices=["all"]), + hostcategory=dict(type="str", default=None, + aliases=["hostcat"], choices=["all"]), + servicecategory=dict(type="str", default=None, + aliases=["servicecat"], choices=["all"]), + nomembers=dict(required=False, type='bool', default=None), + host=dict(required=False, type='list', default=None), + hostgroup=dict(required=False, type='list', default=None), + hbacsvc=dict(required=False, type='list', default=None), + hbacsvcgroup=dict(required=False, type='list', default=None), + user=dict(required=False, type='list', default=None), + group=dict(required=False, type='list', default=None), + action=dict(type="str", default="hbacrule", + choices=["member", "hbacrule"]), + # state + state=dict(type="str", default="present", + choices=["present", "absent", + "enabled", "disabled"]), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + + # Get parameters + + # general + ipaadmin_principal = module_params_get(ansible_module, + "ipaadmin_principal") + ipaadmin_password = module_params_get(ansible_module, "ipaadmin_password") + names = module_params_get(ansible_module, "name") + + # present + description = module_params_get(ansible_module, "description") + usercategory = module_params_get(ansible_module, "usercategory") + hostcategory = module_params_get(ansible_module, "hostcategory") + servicecategory = module_params_get(ansible_module, "servicecategory") + nomembers = module_params_get(ansible_module, "nomembers") + host = module_params_get(ansible_module, "host") + hostgroup = module_params_get(ansible_module, "hostgroup") + hbacsvc = module_params_get(ansible_module, "hbacsvc") + hbacsvcgroup = module_params_get(ansible_module, "hbacsvcgroup") + user = module_params_get(ansible_module, "user") + group = module_params_get(ansible_module, "group") + action = module_params_get(ansible_module, "action") + # state + state = module_params_get(ansible_module, "state") + + # Check parameters + + if state == "present": + if len(names) != 1: + ansible_module.fail_json( + msg="Only one hbacrule can be added at a time.") + if action == "member": + invalid = ["description", "usercategory", "hostcategory", + "servicecategory", "nomembers"] + for x in invalid: + if vars()[x] is not None: + ansible_module.fail_json( + msg="Argument '%s' can not be used with action " + "'%s'" % (x, action)) + + elif state == "absent": + if len(names) < 1: + ansible_module.fail_json(msg="No name given.") + invalid = ["description", "usercategory", "hostcategory", + "servicecategory", "nomembers"] + if action == "hbacrule": + invalid.extend(["host", "hostgroup", "hbacsvc", "hbacsvcgroup", + "user", "group"]) + for x in invalid: + if vars()[x] is not None: + ansible_module.fail_json( + msg="Argument '%s' can not be used with state '%s'" % + (x, state)) + + elif state in ["enabled", "disabled"]: + if len(names) < 1: + ansible_module.fail_json(msg="No name given.") + if action == "member": + ansible_module.fail_json( + msg="Action member can not be used with states enabled and " + "disabled") + invalid = ["description", "usercategory", "hostcategory", + "servicecategory", "nomembers", "host", "hostgroup", + "hbacsvc", "hbacsvcgroup", "user", "group"] + for x in invalid: + if vars()[x] is not None: + ansible_module.fail_json( + msg="Argument '%s' can not be used with state '%s'" % + (x, state)) + else: + ansible_module.fail_json(msg="Invalid state '%s'" % state) + + # Init + + changed = False + exit_args = {} + ccache_dir = None + ccache_name = None + try: + if not valid_creds(ansible_module, ipaadmin_principal): + ccache_dir, ccache_name = temp_kinit(ipaadmin_principal, + ipaadmin_password) + api_connect() + + commands = [] + + for name in names: + # Make sure hbacrule exists + res_find = find_hbacrule(ansible_module, name) + + # Create command + if state == "present": + # Generate args + args = gen_args(description, usercategory, hostcategory, + servicecategory, nomembers) + + if action == "hbacrule": + # Found the hbacrule + if res_find is not None: + # For all settings is args, check if there are + # different settings in the find result. + # If yes: modify + if not compare_args_ipa(ansible_module, args, + res_find): + commands.append([name, "hbacrule_mod", args]) + else: + commands.append([name, "hbacrule_add", args]) + # Set res_find to empty dict for next step + res_find = {} + + # Generate addition and removal lists + host_add = list( + set(host or []) - + set(res_find.get("member_host", []))) + host_del = list( + set(res_find.get("member_host", [])) - + set(host or [])) + hostgroup_add = list( + set(hostgroup or []) - + set(res_find.get("member_hostgroup", []))) + hostgroup_del = list( + set(res_find.get("member_hostgroup", [])) - + set(hostgroup or [])) + + hbacsvc_add = list( + set(hbacsvc or []) - + set(res_find.get("member_hbacsvc", []))) + hbacsvc_del = list( + set(res_find.get("member_hbacsvc", [])) - + set(hbacsvc or [])) + hbacsvcgroup_add = list( + set(hbacsvcgroup or []) - + set(res_find.get("member_hbacsvcgroup", []))) + hbacsvcgroup_del = list( + set(res_find.get("member_hbacsvcgroup", [])) - + set(hbacsvcgroup or [])) + + user_add = list( + set(user or []) - + set(res_find.get("member_user", []))) + user_del = list( + set(res_find.get("member_user", [])) - + set(user or [])) + group_add = list( + set(group or []) - + set(res_find.get("member_group", []))) + group_del = list( + set(res_find.get("member_group", [])) - + set(group or [])) + + # Add hosts and hostgroups + if len(host_add) > 0 or len(hostgroup_add) > 0: + commands.append([name, "hbacrule_add_host", + { + "host": host_add, + "hostgroup": hostgroup_add, + }]) + # Remove hosts and hostgroups + if len(host_del) > 0 or len(hostgroup_del) > 0: + commands.append([name, "hbacrule_remove_host", + { + "host": host_del, + "hostgroup": hostgroup_del, + }]) + + # Add hbacsvcs and hbacsvcgroups + if len(hbacsvc_add) > 0 or len(hbacsvcgroup_add) > 0: + commands.append([name, "hbacrule_add_service", + { + "hbacsvc": hbacsvc_add, + "hbacsvcgroup": hbacsvcgroup_add, + }]) + # Remove hbacsvcs and hbacsvcgroups + if len(hbacsvc_del) > 0 or len(hbacsvcgroup_del) > 0: + commands.append([name, "hbacrule_remove_service", + { + "hbacsvc": hbacsvc_del, + "hbacsvcgroup": hbacsvcgroup_del, + }]) + + # Add users and groups + if len(user_add) > 0 or len(group_add) > 0: + commands.append([name, "hbacrule_add_user", + { + "user": user_add, + "group": group_add, + }]) + # Remove users and groups + if len(user_del) > 0 or len(group_del) > 0: + commands.append([name, "hbacrule_remove_user", + { + "user": user_del, + "group": group_del, + }]) + + elif action == "member": + if res_find is None: + ansible_module.fail_json(msg="No hbacrule '%s'" % name) + + # Add hosts and hostgroups + if host is not None or hostgroup is not None: + commands.append([name, "hbacrule_add_host", + { + "host": host, + "hostgroup": hostgroup, + }]) + + # Add hbacsvcs and hbacsvcgroups + if hbacsvc is not None or hbacsvcgroup is not None: + commands.append([name, "hbacrule_add_service", + { + "hbacsvc": hbacsvc, + "hbacsvcgroup": hbacsvcgroup, + }]) + + # Add users and groups + if user is not None or group is not None: + commands.append([name, "hbacrule_add_user", + { + "user": user, + "group": group, + }]) + + elif state == "absent": + if action == "hbacrule": + if res_find is not None: + commands.append([name, "hbacrule_del", {}]) + + elif action == "member": + if res_find is None: + ansible_module.fail_json(msg="No hbacrule '%s'" % name) + + # Remove hosts and hostgroups + if host is not None or hostgroup is not None: + commands.append([name, "hbacrule_remove_host", + { + "host": host, + "hostgroup": hostgroup, + }]) + + # Remove hbacsvcs and hbacsvcgroups + if hbacsvc is not None or hbacsvcgroup is not None: + commands.append([name, "hbacrule_remove_service", + { + "hbacsvc": hbacsvc, + "hbacsvcgroup": hbacsvcgroup, + }]) + + # Remove users and groups + if user is not None or group is not None: + commands.append([name, "hbacrule_remove_user", + { + "user": user, + "group": group, + }]) + + elif state == "enabled": + if res_find is None: + ansible_module.fail_json(msg="No hbacrule '%s'" % name) + # hbacrule_enable is not failing on an enabled hbacrule + # Therefore it is needed to have a look at the ipaenabledflag + # in res_find. + if "ipaenabledflag" not in res_find or \ + res_find["ipaenabledflag"][0] != "TRUE": + commands.append([name, "hbacrule_enable", {}]) + + elif state == "disabled": + if res_find is None: + ansible_module.fail_json(msg="No hbacrule '%s'" % name) + # hbacrule_disable is not failing on an disabled hbacrule + # Therefore it is needed to have a look at the ipaenabledflag + # in res_find. + if "ipaenabledflag" not in res_find or \ + res_find["ipaenabledflag"][0] != "FALSE": + commands.append([name, "hbacrule_disable", {}]) + + else: + ansible_module.fail_json(msg="Unkown state '%s'" % state) + + # Execute commands + + errors = [] + for name, command, args in commands: + try: + result = api_command(ansible_module, command, name, + args) + if "completed" in result: + if result["completed"] > 0: + changed = True + else: + changed = True + except Exception as e: + ansible_module.fail_json(msg="%s: %s: %s" % (command, name, + str(e))) + # Get all errors + # All "already a member" and "not a member" failures in the + # result are ignored. All others are reported. + if "failed" in result and len(result["failed"]) > 0: + for item in result["failed"]: + failed_item = result["failed"][item] + for member_type in failed_item: + for member, failure in failed_item[member_type]: + if "already a member" in failure \ + or "not a member" in failure: + continue + errors.append("%s: %s %s: %s" % ( + command, member_type, member, failure)) + if len(errors) > 0: + ansible_module.fail_json(msg=", ".join(errors)) + + except Exception as e: + ansible_module.fail_json(msg=str(e)) + + finally: + temp_kdestroy(ccache_dir, ccache_name) + + # Done + + ansible_module.exit_json(changed=changed, **exit_args) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ipahbacsvc.py b/plugins/modules/ipahbacsvc.py new file mode 100644 index 0000000..cf9bc60 --- /dev/null +++ b/plugins/modules/ipahbacsvc.py @@ -0,0 +1,220 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Copyright (C) 2019 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "supported_by": "community", + "status": ["preview"], +} + +DOCUMENTATION = """ +--- +module: ipahbacsvc +short description: Manage FreeIPA HBAC Services +description: Manage FreeIPA HBAC Services +options: + ipaadmin_principal: + description: The admin principal + default: admin + ipaadmin_password: + description: The admin password + required: false + name: + description: The group name + required: false + aliases: ["cn", "service"] + description: + description: The HBAC Service description + required: false + state: + description: State to ensure + default: present + choices: ["present", "absent"] +author: + - Thomas Woerner +""" + +EXAMPLES = """ +# Ensure HBAC Service for http is present +- ipahbacsvc: + ipaadmin_password: MyPassword123 + name: http + description: Web service + +# Ensure HBAC Service for tftp is absent +- ipahbacsvc: + ipaadmin_password: MyPassword123 + name: tftp + state: absent +""" + +RETURN = """ +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text +from ansible.module_utils.ansible_freeipa_module import temp_kinit, \ + temp_kdestroy, valid_creds, api_connect, api_command, compare_args_ipa + + +def find_hbacsvc(module, name): + _args = { + "all": True, + "cn": to_text(name), + } + + _result = api_command(module, "hbacsvc_find", to_text(name), _args) + + if len(_result["result"]) > 1: + module.fail_json( + msg="There is more than one hbacsvc '%s'" % (name)) + elif len(_result["result"]) == 1: + return _result["result"][0] + else: + return None + + +def gen_args(description): + _args = {} + if description is not None: + _args["description"] = to_text(description) + + return _args + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # general + ipaadmin_principal=dict(type="str", default="admin"), + ipaadmin_password=dict(type="str", required=False, no_log=True), + + name=dict(type="list", aliases=["cn", "service"], default=None, + required=True), + # present + + description=dict(type="str", default=None), + + # state + state=dict(type="str", default="present", + choices=["present", "absent"]), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + + # Get parameters + + # general + ipaadmin_principal = ansible_module.params.get("ipaadmin_principal") + ipaadmin_password = ansible_module.params.get("ipaadmin_password") + names = ansible_module.params.get("name") + + # present + description = ansible_module.params.get("description") + + # state + state = ansible_module.params.get("state") + + # Check parameters + + if state == "present": + if len(names) != 1: + ansible_module.fail_json( + msg="Only one hbacsvc can be set at a time.") + + if state == "absent": + if len(names) < 1: + ansible_module.fail_json( + msg="No name given.") + invalid = ["description"] + for x in invalid: + if vars()[x] is not None: + ansible_module.fail_json( + msg="Argument '%s' can not be used with state '%s'" % + (x, state)) + + # Init + + changed = False + exit_args = {} + ccache_dir = None + ccache_name = None + try: + if not valid_creds(ansible_module, ipaadmin_principal): + ccache_dir, ccache_name = temp_kinit(ipaadmin_principal, + ipaadmin_password) + api_connect() + + commands = [] + + for name in names: + # Try to find hbacsvc + res_find = find_hbacsvc(ansible_module, name) + + # Create command + if state == "present": + # Generate args + args = gen_args(description) + + # Found the hbacsvc + if res_find is not None: + # For all settings is args, check if there are + # different settings in the find result. + # If yes: modify + if not compare_args_ipa(ansible_module, args, + res_find): + commands.append([name, "hbacsvc_mod", args]) + else: + commands.append([name, "hbacsvc_add", args]) + + elif state == "absent": + if res_find is not None: + commands.append([name, "hbacsvc_del", {}]) + + else: + ansible_module.fail_json(msg="Unkown state '%s'" % state) + + # Execute commands + + for name, command, args in commands: + try: + api_command(ansible_module, command, to_text(name), args) + changed = True + except Exception as e: + ansible_module.fail_json(msg="%s: %s: %s" % (command, name, + str(e))) + + except Exception as e: + ansible_module.fail_json(msg=str(e)) + + finally: + temp_kdestroy(ccache_dir, ccache_name) + + # Done + + ansible_module.exit_json(changed=changed, **exit_args) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ipahbacsvcgroup.py b/plugins/modules/ipahbacsvcgroup.py new file mode 100644 index 0000000..643f280 --- /dev/null +++ b/plugins/modules/ipahbacsvcgroup.py @@ -0,0 +1,346 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Copyright (C) 2019 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "supported_by": "community", + "status": ["preview"], +} + + +DOCUMENTATION = """ +--- +module: ipahbacsvcgroup +short description: Manage FreeIPA hbacsvcgroups +description: Manage FreeIPA hbacsvcgroups +options: + ipaadmin_principal: + description: The admin principal + default: admin + ipaadmin_password: + description: The admin password + required: false + name: + description: The hbacsvcgroup name + required: false + aliases: ["cn"] + description: + description: The hbacsvcgroup description + required: false + hbacsvc: + description: List of hbacsvc names assigned to this hbacsvcgroup. + required: false + type: list + nomembers: + description: Suppress processing of membership attributes + required: false + type: bool + action: + description: Work on hbacsvcgroup or member level + default: hbacsvcgroup + choices: ["member", "hbacsvcgroup"] + state: + description: State to ensure + default: present + choices: ["present", "absent"] +author: + - Thomas Woerner +""" + +EXAMPLES = """ +# Ensure hbacsvcgroup login is present +- ipahbacsvcgroup: + ipaadmin_password: MyPassword123 + name: login + hbacsvc: + - sshd + +# Ensure hbacsvc sshd is present in existing login hbacsvcgroup +- ipahbacsvcgroup: + ipaadmin_password: MyPassword123 + name: databases + hbacsvc: + - sshd + action: member + +# Ensure hbacsvc sshd is abdsent in existing login hbacsvcgroup +- ipahbacsvcgroup: + ipaadmin_password: MyPassword123 + name: databases + hbacsvc: + - sshd + action: member + state: absent + +# Ensure hbacsvcgroup login is absent +- ipahbacsvcgroup: + ipaadmin_password: MyPassword123 + name: login + state: absent +""" + +RETURN = """ +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text +from ansible.module_utils.ansible_freeipa_module import temp_kinit, \ + temp_kdestroy, valid_creds, api_connect, api_command, compare_args_ipa + + +def find_hbacsvcgroup(module, name): + _args = { + "all": True, + "cn": to_text(name), + } + + _result = api_command(module, "hbacsvcgroup_find", to_text(name), _args) + + if len(_result["result"]) > 1: + module.fail_json( + msg="There is more than one hbacsvcgroup '%s'" % (name)) + elif len(_result["result"]) == 1: + return _result["result"][0] + else: + return None + + +def gen_args(description, nomembers): + _args = {} + if description is not None: + _args["description"] = to_text(description) + if nomembers is not None: + _args["nomembers"] = nomembers + + return _args + + +def gen_member_args(hbacsvc): + _args = {} + if hbacsvc is not None: + _args["member_hbacsvc"] = [to_text(svc) for svc in hbacsvc] + + return _args + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # general + ipaadmin_principal=dict(type="str", default="admin"), + ipaadmin_password=dict(type="str", required=False, no_log=True), + + name=dict(type="list", aliases=["cn"], default=None, + required=True), + # present + description=dict(type="str", default=None), + nomembers=dict(required=False, type='bool', default=None), + hbacsvc=dict(required=False, type='list', default=None), + action=dict(type="str", default="hbacsvcgroup", + choices=["member", "hbacsvcgroup"]), + # state + state=dict(type="str", default="present", + choices=["present", "absent"]), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + + # Get parameters + + # general + ipaadmin_principal = ansible_module.params.get("ipaadmin_principal") + ipaadmin_password = ansible_module.params.get("ipaadmin_password") + names = ansible_module.params.get("name") + + # present + description = ansible_module.params.get("description") + nomembers = ansible_module.params.get("nomembers") + hbacsvc = ansible_module.params.get("hbacsvc") + action = ansible_module.params.get("action") + # state + state = ansible_module.params.get("state") + + # Check parameters + + if state == "present": + if len(names) != 1: + ansible_module.fail_json( + msg="Only one hbacsvcgroup can be added at a time.") + if action == "member": + invalid = ["description", "nomembers"] + for x in invalid: + if vars()[x] is not None: + ansible_module.fail_json( + msg="Argument '%s' can not be used with action " + "'%s'" % (x, action)) + + if state == "absent": + if len(names) < 1: + ansible_module.fail_json( + msg="No name given.") + invalid = ["description", "nomembers"] + if action == "hbacsvcgroup": + invalid.extend(["hbacsvc"]) + for x in invalid: + if vars()[x] is not None: + ansible_module.fail_json( + msg="Argument '%s' can not be used with state '%s'" % + (x, state)) + + # Init + + changed = False + exit_args = {} + ccache_dir = None + ccache_name = None + try: + if not valid_creds(ansible_module, ipaadmin_principal): + ccache_dir, ccache_name = temp_kinit(ipaadmin_principal, + ipaadmin_password) + api_connect() + + commands = [] + + for name in names: + # Make sure hbacsvcgroup exists + res_find = find_hbacsvcgroup(ansible_module, name) + + # Create command + if state == "present": + # Generate args + args = gen_args(description, nomembers) + + if action == "hbacsvcgroup": + # Found the hbacsvcgroup + if res_find is not None: + # For all settings is args, check if there are + # different settings in the find result. + # If yes: modify + if not compare_args_ipa(ansible_module, args, + res_find): + commands.append([name, "hbacsvcgroup_mod", args]) + else: + commands.append([name, "hbacsvcgroup_add", args]) + # Set res_find to empty dict for next step + res_find = {} + + member_args = gen_member_args(hbacsvc) + if not compare_args_ipa(ansible_module, member_args, + res_find): + # Generate addition and removal lists + hbacsvc_add = list( + set(hbacsvc or []) - + set(res_find.get("member_hbacsvc", []))) + hbacsvc_del = list( + set(res_find.get("member_hbacsvc", [])) - + set(hbacsvc or [])) + + # Add members + if len(hbacsvc_add) > 0: + commands.append([name, "hbacsvcgroup_add_member", + { + "hbacsvc": + [to_text(svc) + for svc in hbacsvc_add], + }]) + # Remove members + if len(hbacsvc_del) > 0: + commands.append([name, + "hbacsvcgroup_remove_member", + { + "hbacsvc": + [to_text(svc) + for svc in hbacsvc_del], + }]) + elif action == "member": + if res_find is None: + ansible_module.fail_json( + msg="No hbacsvcgroup '%s'" % name) + + # Ensure members are present + commands.append([name, "hbacsvcgroup_add_member", + { + "hbacsvc": [to_text(svc) + for svc in hbacsvc], + }]) + elif state == "absent": + if action == "hbacsvcgroup": + if res_find is not None: + commands.append([name, "hbacsvcgroup_del", {}]) + + elif action == "member": + if res_find is None: + ansible_module.fail_json( + msg="No hbacsvcgroup '%s'" % name) + + # Ensure members are absent + commands.append([name, "hbacsvcgroup_remove_member", + { + "hbacsvc": [to_text(svc) + for svc in hbacsvc], + }]) + else: + ansible_module.fail_json(msg="Unkown state '%s'" % state) + + # Execute commands + errors = [] + for name, command, args in commands: + try: + result = api_command(ansible_module, command, to_text(name), + args) + if "completed" in result: + if result["completed"] > 0: + changed = True + else: + changed = True + except Exception as e: + ansible_module.fail_json(msg="%s: %s: %s" % (command, name, + str(e))) + # Get all errors + # All "already a member" and "not a member" failures in the + # result are ignored. All others are reported. + if "failed" in result and "member" in result["failed"]: + failed = result["failed"]["member"] + for member_type in failed: + for member, failure in failed[member_type]: + if "already a member" not in failure \ + and "not a member" not in failure: + errors.append("%s: %s %s: %s" % ( + command, member_type, member, failure)) + if len(errors) > 0: + ansible_module.fail_json(msg=", ".join(errors)) + + except Exception as e: + ansible_module.fail_json(msg=str(e)) + + finally: + temp_kdestroy(ccache_dir, ccache_name) + + # Done + + ansible_module.exit_json(changed=changed, **exit_args) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ipahost.py b/plugins/modules/ipahost.py new file mode 100644 index 0000000..ec5e196 --- /dev/null +++ b/plugins/modules/ipahost.py @@ -0,0 +1,1279 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Copyright (C) 2019 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "supported_by": "community", + "status": ["preview"], +} + +DOCUMENTATION = """ +--- +module: ipahost +short description: Manage FreeIPA hosts +description: Manage FreeIPA hosts +options: + ipaadmin_principal: + description: The admin principal + default: admin + ipaadmin_password: + description: The admin password + required: false + name: + description: The full qualified domain name. + aliases: ["fqdn"] + required: true + + hosts: + description: The list of user host dicts + required: false + options: + name: + description: The host (internally uid). + aliases: ["fqdn"] + required: true + description: + description: The host description + required: false + locality: + description: Host locality (e.g. "Baltimore, MD") + required: false + location: + description: Host location (e.g. "Lab 2") + aliases: ["ns_host_location"] + required: false + platform: + description: Host hardware platform (e.g. "Lenovo T61") + aliases: ["ns_hardware_platform"] + required: false + os: + description: Host operating system and version (e.g. "Fedora 9") + aliases: ["ns_os_version"] + required: false + password: + description: Password used in bulk enrollment + aliases: ["user_password", "userpassword"] + required: false + random: + description: + Initiate the generation of a random password to be used in bulk + enrollment + aliases: ["random_password"] + required: false + certificate: + description: List of base-64 encoded host certificates + type: list + aliases: ["usercertificate"] + required: false + managedby_host: + description: List of hosts that can manage this host + type: list + aliases: ["principalname", "krbprincipalname"] + required: false + principal: + description: List of principal aliases for this host + type: list + aliases: ["principalname", "krbprincipalname"] + required: false + allow_create_keytab_user: + description: Users allowed to create a keytab of this host + aliases: ["ipaallowedtoperform_write_keys_user"] + required: false + allow_create_keytab_group: + description: Groups allowed to create a keytab of this host + aliases: ["ipaallowedtoperform_write_keys_group"] + required: false + allow_create_keytab_host: + description: Hosts allowed to create a keytab of this host + aliases: ["ipaallowedtoperform_write_keys_host"] + required: false + allow_create_keytab_hostgroup: + description: Hostgroups allowed to create a keytab of this host + aliases: ["ipaallowedtoperform_write_keys_hostgroup"] + required: false + allow_retrieve_keytab_user: + description: Users allowed to retrieve a keytab of this host + aliases: ["ipaallowedtoperform_read_keys_user"] + required: false + allow_retrieve_keytab_group: + description: Groups allowed to retrieve a keytab of this host + aliases: ["ipaallowedtoperform_read_keys_group"] + required: false + allow_retrieve_keytab_host: + description: Hosts allowed to retrieve a keytab of this host + aliases: ["ipaallowedtoperform_read_keys_host"] + required: false + allow_retrieve_keytab_hostgroup: + description: Hostgroups allowed to retrieve a keytab of this host + aliases: ["ipaallowedtoperform_read_keys_hostgroup"] + required: false + mac_address: + description: List of hardware MAC addresses. + type: list + aliases: ["macaddress"] + required: false + sshpubkey: + description: List of SSH public keys + type: list + aliases: ["ipasshpubkey"] + required: false + userclass: + description: + Host category (semantics placed on this attribute are for local + interpretation) + aliases: ["class"] + required: false + auth_ind: + description: + Defines a whitelist for Authentication Indicators. Use 'otp' to allow + OTP-based 2FA authentications. Use 'radius' to allow RADIUS-based 2FA + authentications. Other values may be used for custom configurations. + type: list + aliases: ["krbprincipalauthind"] + choices: ["radius", "otp", "pkinit", "hardened"] + required: false + requires_pre_auth: + description: Pre-authentication is required for the service + type: bool + aliases: ["ipakrbrequirespreauth"] + required: false + ok_as_delegate: + description: Client credentials may be delegated to the service + type: bool + aliases: ["ipakrbokasdelegate"] + required: false + ok_to_auth_as_delegate: + description: + The service is allowed to authenticate on behalf of a client + type: bool + aliases: ["ipakrboktoauthasdelegate"] + required: false + force: + description: Force host name even if not in DNS + required: false + reverse: + description: Reverse DNS detection + default: true + required: false + ip_address: + description: The host IP address + aliases: ["ipaddress"] + required: false + update_dns: + description: Update DNS entries + required: false + description: + description: The host description + required: false + locality: + description: Host locality (e.g. "Baltimore, MD") + required: false + location: + description: Host location (e.g. "Lab 2") + aliases: ["ns_host_location"] + required: false + platform: + description: Host hardware platform (e.g. "Lenovo T61") + aliases: ["ns_hardware_platform"] + required: false + os: + description: Host operating system and version (e.g. "Fedora 9") + aliases: ["ns_os_version"] + required: false + password: + description: Password used in bulk enrollment + aliases: ["user_password", "userpassword"] + required: false + random: + description: + Initiate the generation of a random password to be used in bulk + enrollment + aliases: ["random_password"] + required: false + certificate: + description: List of base-64 encoded host certificates + type: list + aliases: ["usercertificate"] + required: false + managedby_host: + description: List of hosts that can manage this host + type: list + aliases: ["principalname", "krbprincipalname"] + required: false + principal: + description: List of principal aliases for this host + type: list + aliases: ["principalname", "krbprincipalname"] + required: false + allow_create_keytab_user: + description: Users allowed to create a keytab of this host + aliases: ["ipaallowedtoperform_write_keys_user"] + required: false + allow_create_keytab_group: + description: Groups allowed to create a keytab of this host + aliases: ["ipaallowedtoperform_write_keys_group"] + required: false + allow_create_keytab_host: + description: Hosts allowed to create a keytab of this host + aliases: ["ipaallowedtoperform_write_keys_host"] + required: false + allow_create_keytab_hostgroup: + description: Hostgroups allowed to create a keytab of this host + aliases: ["ipaallowedtoperform_write_keys_hostgroup"] + required: false + allow_retrieve_keytab_user: + description: Users allowed to retrieve a keytab of this host + aliases: ["ipaallowedtoperform_read_keys_user"] + required: false + allow_retrieve_keytab_group: + description: Groups allowed to retrieve a keytab of this host + aliases: ["ipaallowedtoperform_read_keys_group"] + required: false + allow_retrieve_keytab_host: + description: Hosts allowed to retrieve a keytab of this host + aliases: ["ipaallowedtoperform_read_keys_host"] + required: false + allow_retrieve_keytab_hostgroup: + description: Hostgroups allowed to retrieve a keytab of this host + aliases: ["ipaallowedtoperform_read_keys_hostgroup"] + required: false + mac_address: + description: List of hardware MAC addresses. + type: list + aliases: ["macaddress"] + required: false + sshpubkey: + description: List of SSH public keys + type: list + aliases: ["ipasshpubkey"] + required: false + userclass: + description: + Host category (semantics placed on this attribute are for local + interpretation) + aliases: ["class"] + required: false + auth_ind: + description: + Defines a whitelist for Authentication Indicators. Use 'otp' to allow + OTP-based 2FA authentications. Use 'radius' to allow RADIUS-based 2FA + authentications. Other values may be used for custom configurations. + type: list + aliases: ["krbprincipalauthind"] + choices: ["radius", "otp", "pkinit", "hardened"] + required: false + requires_pre_auth: + description: Pre-authentication is required for the service + type: bool + aliases: ["ipakrbrequirespreauth"] + required: false + ok_as_delegate: + description: Client credentials may be delegated to the service + type: bool + aliases: ["ipakrbokasdelegate"] + required: false + ok_to_auth_as_delegate: + description: The service is allowed to authenticate on behalf of a client + type: bool + aliases: ["ipakrboktoauthasdelegate"] + required: false + force: + description: Force host name even if not in DNS + required: false + reverse: + description: Reverse DNS detection + default: true + required: false + ip_address: + description: The host IP address + aliases: ["ipaddress"] + required: false + update_dns: + description: Update DNS entries + required: false + update_password: + description: + Set password for a host in present state only on creation or always + default: 'always' + choices: ["always", "on_create"] + action: + description: Work on host or member level + default: "host" + choices: ["member", "host"] + state: + description: State to ensure + default: present + choices: ["present", "absent", + "disabled"] +author: + - Thomas Woerner +""" + +EXAMPLES = """ +# Ensure host is present +- ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + description: Example host + ip_address: 192.168.0.123 + locality: Lab + ns_host_location: Lab + ns_os_version: CentOS 7 + ns_hardware_platform: Lenovo T61 + mac_address: + - "08:00:27:E3:B1:2D" + - "52:54:00:BD:97:1E" + state: present + +# Ensure host is present without DNS +- ipahost: + ipaadmin_password: MyPassword123 + name: host02.example.com + description: Example host + force: yes + +# Initiate generation of a random password for the host +- ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + description: Example host + ip_address: 192.168.0.123 + random: yes + +# Ensure host is disabled +- ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + update_dns: yes + state: disabled + +# Ensure host is absent +- ipahost: + ipaadmin_password: password1 + name: host01.example.com + state: absent +""" + +RETURN = """ +host: + description: Host dict with random password + returned: If random is yes and user did not exist or update_password is yes + type: dict + options: + randompassword: + description: The generated random password + returned: If only one user is handled by the module + name: + description: The user name of the user that got a new random password + returned: If several users are handled by the module + type: dict + options: + randompassword: + description: The generated random password + returned: always +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text +from ansible.module_utils.ansible_freeipa_module import temp_kinit, \ + temp_kdestroy, valid_creds, api_connect, api_command, compare_args_ipa, \ + module_params_get, gen_add_del_lists, encode_certificate, api_get_realm +import six + + +if six.PY3: + unicode = str + + +def find_host(module, name): + _args = { + "all": True, + "fqdn": to_text(name), + } + + _result = api_command(module, "host_find", to_text(name), _args) + + if len(_result["result"]) > 1: + module.fail_json( + msg="There is more than one host '%s'" % (name)) + elif len(_result["result"]) == 1: + _res = _result["result"][0] + certs = _res.get("usercertificate") + if certs is not None: + _res["usercertificate"] = [encode_certificate(cert) for + cert in certs] + return _res + else: + return None + + +def show_host(module, name): + _result = api_command(module, "host_show", to_text(name), {}) + return _result["result"] + + +def gen_args(description, locality, location, platform, os, password, random, + mac_address, sshpubkey, userclass, auth_ind, requires_pre_auth, + ok_as_delegate, ok_to_auth_as_delegate, force, reverse, + ip_address, update_dns): + # certificate, managedby_host, principal, create_keytab_* and + # allow_retrieve_keytab_* are not handled here + _args = {} + if description is not None: + _args["description"] = description + if locality is not None: + _args["l"] = locality + if location is not None: + _args["nshostlocation"] = location + if platform is not None: + _args["nshardwareplatform"] = platform + if os is not None: + _args["nsosversion"] = os + if password is not None: + _args["userpassword"] = password + if random is not None: + _args["random"] = random + if mac_address is not None: + _args["macaddress"] = mac_address + if sshpubkey is not None: + _args["ipasshpubkey"] = sshpubkey + if userclass is not None: + _args["userclass"] = userclass + if auth_ind is not None: + _args["krbprincipalauthind"] = auth_ind + if requires_pre_auth is not None: + _args["ipakrbrequirespreauth"] = requires_pre_auth + if ok_as_delegate is not None: + _args["ipakrbokasdelegate"] = ok_as_delegate + if ok_to_auth_as_delegate is not None: + _args["ipakrboktoauthasdelegate"] = ok_to_auth_as_delegate + if force is not None: + _args["force"] = force + if reverse is not None: + _args["no_reverse"] = not reverse + if ip_address is not None: + _args["ip_address"] = ip_address + if update_dns is not None: + _args["updatedns"] = update_dns + + return _args + + +def check_parameters( + module, state, action, + description, locality, location, platform, os, password, random, + certificate, managedby_host, principal, allow_create_keytab_user, + allow_create_keytab_group, allow_create_keytab_host, + allow_create_keytab_hostgroup, allow_retrieve_keytab_user, + allow_retrieve_keytab_group, allow_retrieve_keytab_host, + allow_retrieve_keytab_hostgroup, mac_address, sshpubkey, + userclass, auth_ind, requires_pre_auth, ok_as_delegate, + ok_to_auth_as_delegate, force, reverse, ip_address, update_dns, + update_password): + if state == "present": + if action == "member": + # certificate, managedby_host, principal, + # allow_create_keytab_*, allow_retrieve_keytab_*, + invalid = ["description", "locality", "location", "platform", + "os", "password", "random", "mac_address", "sshpubkey", + "userclass", "auth_ind", "requires_pre_auth", + "ok_as_delegate", "ok_to_auth_as_delegate", "force", + "reverse", "ip_address", "update_dns", + "update_password"] + for x in invalid: + if vars()[x] is not None: + module.fail_json( + msg="Argument '%s' can not be used with action " + "'%s'" % (x, action)) + + if state == "absent": + invalid = ["description", "locality", "location", "platform", "os", + "password", "random", "mac_address", "sshpubkey", + "userclass", "auth_ind", "requires_pre_auth", + "ok_as_delegate", "ok_to_auth_as_delegate", "force", + "reverse", "ip_address", "update_password"] + if action == "host": + invalid.extend([ + "certificate", "managedby_host", "principal", + "allow_create_keytab_user", "allow_create_keytab_group", + "allow_create_keytab_host", "allow_create_keytab_hostgroup", + "allow_retrieve_keytab_user", "allow_retrieve_keytab_group", + "allow_retrieve_keytab_host", + "allow_retrieve_keytab_hostgroup"]) + for x in invalid: + if vars()[x] is not None: + module.fail_json( + msg="Argument '%s' can not be used with state '%s'" % + (x, state)) + + +def main(): + host_spec = dict( + # present + description=dict(type="str", default=None), + locality=dict(type="str", default=None), + location=dict(type="str", aliases=["ns_host_location"], + default=None), + platform=dict(type="str", aliases=["ns_hardware_platform"], + default=None), + os=dict(type="str", aliases=["ns_os_version"], default=None), + password=dict(type="str", + aliases=["user_password", "userpassword"], + default=None, no_log=True), + random=dict(type="bool", aliases=["random_password"], + default=None), + + + + certificate=dict(type="list", aliases=["usercertificate"], + default=None), + managedby_host=dict(type="list", + default=None), + principal=dict(type="list", aliases=["krbprincipalname"], + default=None), + allow_create_keytab_user=dict( + type="list", + aliases=["ipaallowedtoperform_write_keys_user"], + default=None), + allow_create_keytab_group=dict( + type="list", + aliases=["ipaallowedtoperform_write_keys_group"], + default=None), + allow_create_keytab_host=dict( + type="list", + aliases=["ipaallowedtoperform_write_keys_host"], + default=None), + allow_create_keytab_hostgroup=dict( + type="list", + aliases=["ipaallowedtoperform_write_keys_hostgroup"], + default=None), + allow_retrieve_keytab_user=dict( + type="list", + aliases=["ipaallowedtoperform_write_keys_user"], + default=None), + allow_retrieve_keytab_group=dict( + type="list", + aliases=["ipaallowedtoperform_write_keys_group"], + default=None), + allow_retrieve_keytab_host=dict( + type="list", + aliases=["ipaallowedtoperform_write_keys_host"], + default=None), + allow_retrieve_keytab_hostgroup=dict( + type="list", + aliases=["ipaallowedtoperform_write_keys_hostgroup"], + default=None), + mac_address=dict(type="list", aliases=["macaddress"], + default=None), + sshpubkey=dict(type="str", aliases=["ipasshpubkey"], + default=None), + userclass=dict(type="list", aliases=["class"], + default=None), + auth_ind=dict(type='list', aliases=["krbprincipalauthind"], + default=None, + choices=['password', 'radius', 'otp']), + requires_pre_auth=dict(type="bool", aliases=["ipakrbrequirespreauth"], + default=None), + ok_as_delegate=dict(type="bool", aliases=["ipakrbokasdelegate"], + default=None), + ok_to_auth_as_delegate=dict(type="bool", + aliases=["ipakrboktoauthasdelegate"], + default=None), + force=dict(type='bool', default=None), + reverse=dict(type='bool', default=None), + ip_address=dict(type="str", aliases=["ipaddress"], + default=None), + update_dns=dict(type="bool", aliases=["updatedns"], + default=None), + # no_members + + # for update: + # krbprincipalname + ) + + ansible_module = AnsibleModule( + argument_spec=dict( + # general + ipaadmin_principal=dict(type="str", default="admin"), + ipaadmin_password=dict(type="str", no_log=True), + + name=dict(type="list", aliases=["fqdn"], default=None, + required=False), + + hosts=dict(type="list", default=None, + options=dict( + # Here name is a simple string + name=dict(type="str", aliases=["fqdn"], + required=True), + # Add host specific parameters + **host_spec + ), + elements='dict', required=False), + + # mod + update_password=dict(type='str', default=None, + choices=['always', 'on_create']), + + # general + action=dict(type="str", default="host", + choices=["member", "host"]), + state=dict(type="str", default="present", + choices=["present", "absent", "disabled"]), + + # Add host specific parameters for simple use case + **host_spec + ), + mutually_exclusive=[["name", "hosts"]], + required_one_of=[["name", "hosts"]], + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + + # Get parameters + + # general + ipaadmin_principal = module_params_get(ansible_module, + "ipaadmin_principal") + ipaadmin_password = module_params_get(ansible_module, + "ipaadmin_password") + names = module_params_get(ansible_module, "name") + hosts = module_params_get(ansible_module, "hosts") + + # present + description = module_params_get(ansible_module, "description") + locality = module_params_get(ansible_module, "locality") + location = module_params_get(ansible_module, "location") + platform = module_params_get(ansible_module, "platform") + os = module_params_get(ansible_module, "os") + password = module_params_get(ansible_module, "password") + random = module_params_get(ansible_module, "random") + certificate = module_params_get(ansible_module, "certificate") + managedby_host = module_params_get(ansible_module, "managedby_host") + principal = module_params_get(ansible_module, "principal") + allow_create_keytab_user = module_params_get( + ansible_module, "allow_create_keytab_user") + allow_create_keytab_group = module_params_get( + ansible_module, "allow_create_keytab_group") + allow_create_keytab_host = module_params_get( + ansible_module, "allow_create_keytab_host") + allow_create_keytab_hostgroup = module_params_get( + ansible_module, "allow_create_keytab_hostgroup") + allow_retrieve_keytab_user = module_params_get( + ansible_module, "allow_retrieve_keytab_user") + allow_retrieve_keytab_group = module_params_get( + ansible_module, "allow_retrieve_keytab_group") + allow_retrieve_keytab_host = module_params_get( + ansible_module, "allow_retrieve_keytab_host") + allow_retrieve_keytab_hostgroup = module_params_get( + ansible_module, "allow_retrieve_keytab_hostgroup") + mac_address = module_params_get(ansible_module, "mac_address") + sshpubkey = module_params_get(ansible_module, "sshpubkey") + userclass = module_params_get(ansible_module, "userclass") + auth_ind = module_params_get(ansible_module, "auth_ind") + requires_pre_auth = module_params_get(ansible_module, "requires_pre_auth") + ok_as_delegate = module_params_get(ansible_module, "ok_as_delegate") + ok_to_auth_as_delegate = module_params_get(ansible_module, + "ok_to_auth_as_delegate") + force = module_params_get(ansible_module, "force") + reverse = module_params_get(ansible_module, "reverse") + ip_address = module_params_get(ansible_module, "ip_address") + update_dns = module_params_get(ansible_module, "update_dns") + update_password = module_params_get(ansible_module, "update_password") + # general + action = module_params_get(ansible_module, "action") + state = module_params_get(ansible_module, "state") + + # Check parameters + + if (names is None or len(names) < 1) and \ + (hosts is None or len(hosts) < 1): + ansible_module.fail_json(msg="One of name and hosts is required") + + if state == "present": + if names is not None and len(names) != 1: + ansible_module.fail_json( + msg="Only one host can be added at a time.") + + check_parameters( + ansible_module, state, action, + description, locality, location, platform, os, password, random, + certificate, managedby_host, principal, allow_create_keytab_user, + allow_create_keytab_group, allow_create_keytab_host, + allow_create_keytab_hostgroup, allow_retrieve_keytab_user, + allow_retrieve_keytab_group, allow_retrieve_keytab_host, + allow_retrieve_keytab_hostgroup, mac_address, sshpubkey, userclass, + auth_ind, requires_pre_auth, ok_as_delegate, ok_to_auth_as_delegate, + force, reverse, ip_address, update_dns, update_password) + + # Use hosts if names is None + if hosts is not None: + names = hosts + + # Init + + changed = False + exit_args = {} + ccache_dir = None + ccache_name = None + try: + if not valid_creds(ansible_module, ipaadmin_principal): + ccache_dir, ccache_name = temp_kinit(ipaadmin_principal, + ipaadmin_password) + api_connect() + + # Check version specific settings + + server_realm = api_get_realm() + + commands = [] + + for host in names: + if isinstance(host, dict): + name = host.get("name") + description = host.get("description") + locality = host.get("locality") + location = host.get("location") + platform = host.get("platform") + os = host.get("os") + password = host.get("password") + random = host.get("random") + certificate = host.get("certificate") + managedby_host = host.get("managedby_host") + principal = host.get("principal") + allow_create_keytab_user = host.get( + "allow_create_keytab_user") + allow_create_keytab_group = host.get( + "allow_create_keytab_group") + allow_create_keytab_host = host.get( + "allow_create_keytab_host") + allow_create_keytab_hostgroup = host.get( + "allow_create_keytab_hostgroup") + allow_retrieve_keytab_user = host.get( + "allow_retrieve_keytab_user") + allow_retrieve_keytab_group = host.get( + "allow_retrieve_keytab_group") + allow_retrieve_keytab_host = host.get( + "allow_retrieve_keytab_host") + allow_retrieve_keytab_hostgroup = host.get( + "allow_retrieve_keytab_hostgroup") + mac_address = host.get("mac_address") + sshpubkey = host.get("sshpubkey") + userclass = host.get("userclass") + auth_ind = host.get("auth_ind") + requires_pre_auth = host.get("requires_pre_auth") + ok_as_delegate = host.get("ok_as_delegate") + ok_to_auth_as_delegate = host.get("ok_to_auth_as_delegate") + force = host.get("force") + reverse = host.get("reverse") + ip_address = host.get("ip_address") + update_dns = host.get("update_dns") + # update_password is not part of hosts structure + # action is not part of hosts structure + # state is not part of hosts structure + + check_parameters( + ansible_module, state, action, + description, locality, location, platform, os, password, + random, certificate, managedby_host, principal, + allow_create_keytab_user, allow_create_keytab_group, + allow_create_keytab_host, allow_create_keytab_hostgroup, + allow_retrieve_keytab_user, allow_retrieve_keytab_group, + allow_retrieve_keytab_host, + allow_retrieve_keytab_hostgroup, mac_address, sshpubkey, + userclass, auth_ind, requires_pre_auth, ok_as_delegate, + ok_to_auth_as_delegate, force, reverse, ip_address, + update_dns, update_password) + + elif isinstance(host, str) or isinstance(host, unicode): + name = host + else: + ansible_module.fail_json(msg="Host '%s' is not valid" % + repr(host)) + + # Make sure host exists + res_find = find_host(ansible_module, name) + + # Create command + if state == "present": + # Generate args + args = gen_args( + description, locality, location, platform, os, password, + random, mac_address, sshpubkey, userclass, auth_ind, + requires_pre_auth, ok_as_delegate, ok_to_auth_as_delegate, + force, reverse, ip_address, update_dns) + + if action == "host": + # Found the host + if res_find is not None: + # Ignore password with update_password == on_create + if update_password == "on_create" and \ + "userpassword" in args: + del args["userpassword"] + + # Ignore force, ip_address and no_reverse for mod + for x in ["force", "ip_address", "no_reverse"]: + if x in args: + del args[x] + + # For all settings is args, check if there are + # different settings in the find result. + # If yes: modify + if not compare_args_ipa(ansible_module, args, + res_find): + commands.append([name, "host_mod", args]) + elif random and "userpassword" in res_find: + # Host exists and random is set, return + # userpassword + if len(names) == 1: + exit_args["userpassword"] = \ + res_find["userpassword"] + else: + exit_args.setdefault("hosts", {})[name] = { + "userpassword": res_find["userpassword"] + } + + else: + # Remove update_dns as it is not supported by host_add + if "updatedns" in args: + del args["updatedns"] + commands.append([name, "host_add", args]) + + # Handle members: certificate, managedby_host, principal, + # allow_create_keytab and allow_retrieve_keytab + if res_find is not None: + certificate_add, certificate_del = gen_add_del_lists( + certificate, res_find.get("usercertificate")) + managedby_host_add, managedby_host_del = \ + gen_add_del_lists(managedby_host, + res_find.get("managedby_host")) + principal_add, principal_del = gen_add_del_lists( + principal, res_find.get("principal")) + # Principals are not returned as utf8 for IPA using + # python2 using host_find, therefore we need to + # convert the principals that we should remove. + principal_del = [to_text(x) for x in principal_del] + + (allow_create_keytab_user_add, + allow_create_keytab_user_del) = \ + gen_add_del_lists( + allow_create_keytab_user, + res_find.get( + "ipaallowedtoperform_write_keys_user")) + (allow_create_keytab_group_add, + allow_create_keytab_group_del) = \ + gen_add_del_lists( + allow_create_keytab_group, + res_find.get( + "ipaallowedtoperform_write_keys_group")) + (allow_create_keytab_host_add, + allow_create_keytab_host_del) = \ + gen_add_del_lists( + allow_create_keytab_host, + res_find.get( + "ipaallowedtoperform_write_keys_host")) + (allow_create_keytab_hostgroup_add, + allow_create_keytab_hostgroup_del) = \ + gen_add_del_lists( + allow_create_keytab_hostgroup, + res_find.get( + "ipaallowedtoperform_write_keys_" + "hostgroup")) + (allow_retrieve_keytab_user_add, + allow_retrieve_keytab_user_del) = \ + gen_add_del_lists( + allow_retrieve_keytab_user, + res_find.get( + "ipaallowedtoperform_read_keys_user")) + (allow_retrieve_keytab_group_add, + allow_retrieve_keytab_group_del) = \ + gen_add_del_lists( + allow_retrieve_keytab_group, + res_find.get( + "ipaallowedtoperform_read_keys_group")) + (allow_retrieve_keytab_host_add, + allow_retrieve_keytab_host_del) = \ + gen_add_del_lists( + allow_retrieve_keytab_host, + res_find.get( + "ipaallowedtoperform_read_keys_host")) + (allow_retrieve_keytab_hostgroup_add, + allow_retrieve_keytab_hostgroup_del) = \ + gen_add_del_lists( + allow_retrieve_keytab_hostgroup, + res_find.get( + "ipaallowedtoperform_read_keys_hostgroup")) + + else: + certificate_add = certificate or [] + certificate_del = [] + managedby_host_add = managedby_host or [] + managedby_host_del = [] + principal_add = principal or [] + principal_del = [] + allow_create_keytab_user_add = \ + allow_create_keytab_user or [] + allow_create_keytab_user_del = [] + allow_create_keytab_group_add = \ + allow_create_keytab_group or [] + allow_create_keytab_group_del = [] + allow_create_keytab_host_add = \ + allow_create_keytab_host or [] + allow_create_keytab_host_del = [] + allow_create_keytab_hostgroup_add = \ + allow_create_keytab_hostgroup or [] + allow_create_keytab_hostgroup_del = [] + allow_retrieve_keytab_user_add = \ + allow_retrieve_keytab_user or [] + allow_retrieve_keytab_user_del = [] + allow_retrieve_keytab_group_add = \ + allow_retrieve_keytab_group or [] + allow_retrieve_keytab_group_del = [] + allow_retrieve_keytab_host_add = \ + allow_retrieve_keytab_host or [] + allow_retrieve_keytab_host_del = [] + allow_retrieve_keytab_hostgroup_add = \ + allow_retrieve_keytab_hostgroup or [] + allow_retrieve_keytab_hostgroup_del = [] + + else: + certificate_add = certificate or [] + certificate_del = [] + managedby_host_add = managedby_host or [] + managedby_host_del = [] + principal_add = principal or [] + principal_del = [] + allow_create_keytab_user_add = \ + allow_create_keytab_user or [] + allow_create_keytab_user_del = [] + allow_create_keytab_group_add = \ + allow_create_keytab_group or [] + allow_create_keytab_group_del = [] + allow_create_keytab_host_add = \ + allow_create_keytab_host or [] + allow_create_keytab_host_del = [] + allow_create_keytab_hostgroup_add = \ + allow_create_keytab_hostgroup or [] + allow_create_keytab_hostgroup_del = [] + allow_retrieve_keytab_user_add = \ + allow_retrieve_keytab_user or [] + allow_retrieve_keytab_user_del = [] + allow_retrieve_keytab_group_add = \ + allow_retrieve_keytab_group or [] + allow_retrieve_keytab_group_del = [] + allow_retrieve_keytab_host_add = \ + allow_retrieve_keytab_host or [] + allow_retrieve_keytab_host_del = [] + allow_retrieve_keytab_hostgroup_add = \ + allow_retrieve_keytab_hostgroup or [] + allow_retrieve_keytab_hostgroup_del = [] + + # Remove canonical principal from principal_del + canonical_principal = "host/" + name + "@" + server_realm + if canonical_principal in principal_del and \ + action == "host" and (principal is not None or + canonical_principal not in principal): + principal_del.remove(canonical_principal) + + # Remove canonical managedby managedby_host_del for + # action host if managedby_host is set and the canonical + # managedby host is not in the managedby_host list. + canonical_managedby_host = name + if canonical_managedby_host in managedby_host_del and \ + action == "host" and (managedby_host is None or + canonical_managedby_host not in + managedby_host): + managedby_host_del.remove(canonical_managedby_host) + + # Certificates need to be added and removed one by one, + # because if entry already exists, the processing of + # the remaining enries is stopped. The same applies to + # the removal of non-existing entries. + + # Add certificates + for _certificate in certificate_add: + commands.append([name, "host_add_cert", + { + "usercertificate": + _certificate, + }]) + # Remove certificates + for _certificate in certificate_del: + commands.append([name, "host_remove_cert", + { + "usercertificate": + _certificate, + }]) + + # Managedby_Hosts need to be added and removed one by one, + # because if entry already exists, the processing of + # the remaining enries is stopped. The same applies to + # the removal of non-existing entries. + + # Add managedby_hosts + for _managedby_host in managedby_host_add: + commands.append([name, "host_add_managedby", + { + "host": + _managedby_host, + }]) + # Remove managedby_hosts + for _managedby_host in managedby_host_del: + commands.append([name, "host_remove_managedby", + { + "host": + _managedby_host, + }]) + + # Principals need to be added and removed one by one, + # because if entry already exists, the processing of + # the remaining enries is stopped. The same applies to + # the removal of non-existing entries. + + # Add principals + for _principal in principal_add: + commands.append([name, "host_add_principal", + { + "krbprincipalname": + _principal, + }]) + # Remove principals + for _principal in principal_del: + commands.append([name, "host_remove_principal", + { + "krbprincipalname": + _principal, + }]) + + # Allow create keytab + if len(allow_create_keytab_user_add) > 0 or \ + len(allow_create_keytab_group_add) > 0 or \ + len(allow_create_keytab_host_add) > 0 or \ + len(allow_create_keytab_hostgroup_add) > 0: + commands.append( + [name, "host_allow_create_keytab", + { + "user": allow_create_keytab_user_add, + "group": allow_create_keytab_group_add, + "host": allow_create_keytab_host_add, + "hostgroup": allow_create_keytab_hostgroup_add, + }]) + + # Disallow create keytab + if len(allow_create_keytab_user_del) > 0 or \ + len(allow_create_keytab_group_del) > 0 or \ + len(allow_create_keytab_host_del) > 0 or \ + len(allow_create_keytab_hostgroup_del) > 0: + commands.append( + [name, "host_disallow_create_keytab", + { + "user": allow_create_keytab_user_del, + "group": allow_create_keytab_group_del, + "host": allow_create_keytab_host_del, + "hostgroup": allow_create_keytab_hostgroup_del, + }]) + + # Allow retrieve keytab + if len(allow_retrieve_keytab_user_add) > 0 or \ + len(allow_retrieve_keytab_group_add) > 0 or \ + len(allow_retrieve_keytab_host_add) > 0 or \ + len(allow_retrieve_keytab_hostgroup_add) > 0: + commands.append( + [name, "host_allow_retrieve_keytab", + { + "user": allow_retrieve_keytab_user_add, + "group": allow_retrieve_keytab_group_add, + "host": allow_retrieve_keytab_host_add, + "hostgroup": allow_retrieve_keytab_hostgroup_add, + }]) + + # Disallow retrieve keytab + if len(allow_retrieve_keytab_user_del) > 0 or \ + len(allow_retrieve_keytab_group_del) > 0 or \ + len(allow_retrieve_keytab_host_del) > 0 or \ + len(allow_retrieve_keytab_hostgroup_del) > 0: + commands.append( + [name, "host_disallow_retrieve_keytab", + { + "user": allow_retrieve_keytab_user_del, + "group": allow_retrieve_keytab_group_del, + "host": allow_retrieve_keytab_host_del, + "hostgroup": allow_retrieve_keytab_hostgroup_del, + }]) + + elif state == "absent": + if action == "host": + + if res_find is not None: + args = {} + if update_dns is not None: + args["updatedns"] = update_dns + commands.append([name, "host_del", args]) + else: + + # Certificates need to be added and removed one by one, + # because if entry already exists, the processing of + # the remaining enries is stopped. The same applies to + # the removal of non-existing entries. + + # Remove certificates + if certificate is not None: + for _certificate in certificate: + commands.append([name, "host_remove_cert", + { + "usercertificate": + _certificate, + }]) + + # Managedby_Hosts need to be added and removed one by one, + # because if entry already exists, the processing of + # the remaining enries is stopped. The same applies to + # the removal of non-existing entries. + + # Remove managedby_hosts + if managedby_host is not None: + for _managedby_host in managedby_host: + commands.append([name, "host_remove_managedby", + { + "host": + _managedby_host, + }]) + + # Principals need to be added and removed one by one, + # because if entry already exists, the processing of + # the remaining enries is stopped. The same applies to + # the removal of non-existing entries. + + # Remove principals + if principal is not None: + for _principal in principal: + commands.append([name, "host_remove_principal", + { + "krbprincipalname": + _principal, + }]) + + # Disallow create keytab + if allow_create_keytab_user is not None or \ + allow_create_keytab_group is not None or \ + allow_create_keytab_host is not None or \ + allow_create_keytab_hostgroup is not None: + commands.append( + [name, "host_disallow_create_keytab", + { + "user": allow_create_keytab_user, + "group": allow_create_keytab_group, + "host": allow_create_keytab_host, + "hostgroup": allow_create_keytab_hostgroup, + }]) + + # Disallow retrieve keytab + if allow_retrieve_keytab_user is not None or \ + allow_retrieve_keytab_group is not None or \ + allow_retrieve_keytab_host is not None or \ + allow_retrieve_keytab_hostgroup is not None: + commands.append( + [name, "host_disallow_retrieve_keytab", + { + "user": allow_retrieve_keytab_user, + "group": allow_retrieve_keytab_group, + "host": allow_retrieve_keytab_host, + "hostgroup": allow_retrieve_keytab_hostgroup, + }]) + + elif state == "disabled": + if res_find is not None: + commands.append([name, "host_disable", {}]) + else: + raise ValueError("No host '%s'" % name) + + else: + ansible_module.fail_json(msg="Unkown state '%s'" % state) + + # Execute commands + + errors = [] + for name, command, args in commands: + try: + result = api_command(ansible_module, command, to_text(name), + args) + if "completed" in result: + if result["completed"] > 0: + changed = True + else: + changed = True + + if "random" in args and command in ["host_add", "host_mod"] \ + and "randompassword" in result["result"]: + if len(names) == 1: + exit_args["randompassword"] = \ + result["result"]["randompassword"] + else: + exit_args.setdefault(name, {})["randompassword"] = \ + result["result"]["randompassword"] + + except Exception as e: + msg = str(e) + if "already contains" in msg \ + or "does not contain" in msg: + continue + + # The canonical principal name may not be removed + if "equal to the canonical principal name must" in msg: + continue + + # Host is already disabled, ignore error + if "This entry is already disabled" in msg: + continue + ansible_module.fail_json(msg="%s: %s: %s" % (command, name, + msg)) + + # Get all errors + # All "already a member" and "not a member" failures in the + # result are ignored. All others are reported. + if "failed" in result and len(result["failed"]) > 0: + for item in result["failed"]: + failed_item = result["failed"][item] + for member_type in failed_item: + for member, failure in failed_item[member_type]: + if "already a member" in failure \ + or "not a member" in failure: + continue + errors.append("%s: %s %s: %s" % ( + command, member_type, member, failure)) + + if len(errors) > 0: + ansible_module.fail_json(msg=", ".join(errors)) + + except Exception as e: + ansible_module.fail_json(msg=str(e)) + + finally: + temp_kdestroy(ccache_dir, ccache_name) + + # Done + + ansible_module.exit_json(changed=changed, host=exit_args) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ipahostgroup.py b/plugins/modules/ipahostgroup.py new file mode 100644 index 0000000..9dbfa62 --- /dev/null +++ b/plugins/modules/ipahostgroup.py @@ -0,0 +1,367 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Copyright (C) 2019 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "supported_by": "community", + "status": ["preview"], +} + + +DOCUMENTATION = """ +--- +module: ipahostgroup +short description: Manage FreeIPA hostgroups +description: Manage FreeIPA hostgroups +options: + ipaadmin_principal: + description: The admin principal + default: admin + ipaadmin_password: + description: The admin password + required: false + name: + description: The hostgroup name + required: false + aliases: ["cn"] + description: + description: The hostgroup description + required: false + nomembers: + description: Suppress processing of membership attributes + required: false + type: bool + host: + description: List of host names assigned to this hostgroup. + required: false + type: list + hostgroup: + description: List of hostgroup names assigned to this hostgroup. + required: false + type: list + action: + description: Work on hostgroup or member level + default: hostgroup + choices: ["member", "hostgroup"] + state: + description: State to ensure + default: present + choices: ["present", "absent"] +author: + - Thomas Woerner +""" + +EXAMPLES = """ +# Ensure host-group databases is present +- ipahostgroup: + ipaadmin_password: MyPassword123 + name: databases + host: + - db.example.com + hostgroup: + - mysql-server + - oracle-server + +# Ensure hosts and hostgroups are present in existing databases hostgroup +- ipahostgroup: + ipaadmin_password: MyPassword123 + name: databases + host: + - db.example.com + hostgroup: + - mysql-server + - oracle-server + action: member + +# Ensure hosts and hostgroups are absent in databases hostgroup +- ipahostgroup: + ipaadmin_password: MyPassword123 + name: databases + host: + - db.example.com + hostgroup: + - mysql-server + - oracle-server + action: member + state: absent + +# Ensure host-group databases is absent +- ipahostgroup: + ipaadmin_password: MyPassword123 + name: databases + state: absent +""" + +RETURN = """ +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_freeipa_module import temp_kinit, \ + temp_kdestroy, valid_creds, api_connect, api_command, compare_args_ipa, \ + module_params_get + + +def find_hostgroup(module, name): + _args = { + "all": True, + "cn": name, + } + + _result = api_command(module, "hostgroup_find", name, _args) + + if len(_result["result"]) > 1: + module.fail_json( + msg="There is more than one hostgroup '%s'" % (name)) + elif len(_result["result"]) == 1: + return _result["result"][0] + else: + return None + + +def gen_args(description, nomembers): + _args = {} + if description is not None: + _args["description"] = description + if nomembers is not None: + _args["nomembers"] = nomembers + + return _args + + +def gen_member_args(host, hostgroup): + _args = {} + if host is not None: + _args["member_host"] = host + if hostgroup is not None: + _args["member_hostgroup"] = hostgroup + + return _args + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # general + ipaadmin_principal=dict(type="str", default="admin"), + ipaadmin_password=dict(type="str", required=False, no_log=True), + + name=dict(type="list", aliases=["cn"], default=None, + required=True), + # present + description=dict(type="str", default=None), + nomembers=dict(required=False, type='bool', default=None), + host=dict(required=False, type='list', default=None), + hostgroup=dict(required=False, type='list', default=None), + action=dict(type="str", default="hostgroup", + choices=["member", "hostgroup"]), + # state + state=dict(type="str", default="present", + choices=["present", "absent"]), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + + # Get parameters + + # general + ipaadmin_principal = module_params_get(ansible_module, + "ipaadmin_principal") + ipaadmin_password = module_params_get(ansible_module, + "ipaadmin_password") + names = module_params_get(ansible_module, "name") + + # present + description = module_params_get(ansible_module, "description") + nomembers = module_params_get(ansible_module, "nomembers") + host = module_params_get(ansible_module, "host") + hostgroup = module_params_get(ansible_module, "hostgroup") + action = module_params_get(ansible_module, "action") + # state + state = module_params_get(ansible_module, "state") + + # Check parameters + + if state == "present": + if len(names) != 1: + ansible_module.fail_json( + msg="Only one hostgroup can be added at a time.") + if action == "member": + invalid = ["description", "nomembers"] + for x in invalid: + if vars()[x] is not None: + ansible_module.fail_json( + msg="Argument '%s' can not be used with action " + "'%s'" % (x, action)) + + if state == "absent": + if len(names) < 1: + ansible_module.fail_json( + msg="No name given.") + invalid = ["description", "nomembers"] + if action == "hostgroup": + invalid.extend(["host", "hostgroup"]) + for x in invalid: + if vars()[x] is not None: + ansible_module.fail_json( + msg="Argument '%s' can not be used with state '%s'" % + (x, state)) + + # Init + + changed = False + exit_args = {} + ccache_dir = None + ccache_name = None + try: + if not valid_creds(ansible_module, ipaadmin_principal): + ccache_dir, ccache_name = temp_kinit(ipaadmin_principal, + ipaadmin_password) + api_connect() + + commands = [] + + for name in names: + # Make sure hostgroup exists + res_find = find_hostgroup(ansible_module, name) + + # Create command + if state == "present": + # Generate args + args = gen_args(description, nomembers) + + if action == "hostgroup": + # Found the hostgroup + if res_find is not None: + # For all settings is args, check if there are + # different settings in the find result. + # If yes: modify + if not compare_args_ipa(ansible_module, args, + res_find): + commands.append([name, "hostgroup_mod", args]) + else: + commands.append([name, "hostgroup_add", args]) + # Set res_find to empty dict for next step + res_find = {} + + member_args = gen_member_args(host, hostgroup) + if not compare_args_ipa(ansible_module, member_args, + res_find): + # Generate addition and removal lists + host_add = list( + set(host or []) - + set(res_find.get("member_host", []))) + host_del = list( + set(res_find.get("member_host", [])) - + set(host or [])) + hostgroup_add = list( + set(hostgroup or []) - + set(res_find.get("member_hostgroup", []))) + hostgroup_del = list( + set(res_find.get("member_hostgroup", [])) - + set(hostgroup or [])) + + # Add members + if len(host_add) > 0 or len(hostgroup_add) > 0: + commands.append([name, "hostgroup_add_member", + { + "host": host_add, + "hostgroup": hostgroup_add, + }]) + # Remove members + if len(host_del) > 0 or len(hostgroup_del) > 0: + commands.append([name, "hostgroup_remove_member", + { + "host": host_del, + "hostgroup": hostgroup_del, + }]) + elif action == "member": + if res_find is None: + ansible_module.fail_json( + msg="No hostgroup '%s'" % name) + + # Ensure members are present + commands.append([name, "hostgroup_add_member", + { + "host": host, + "hostgroup": hostgroup, + }]) + elif state == "absent": + if action == "hostgroup": + if res_find is not None: + commands.append([name, "hostgroup_del", {}]) + + elif action == "member": + if res_find is None: + ansible_module.fail_json( + msg="No hostgroup '%s'" % name) + + # Ensure members are absent + commands.append([name, "hostgroup_remove_member", + { + "host": host, + "hostgroup": hostgroup, + }]) + else: + ansible_module.fail_json(msg="Unkown state '%s'" % state) + + # Execute commands + for name, command, args in commands: + try: + result = api_command(ansible_module, command, name, args) + if "completed" in result: + if result["completed"] > 0: + changed = True + else: + changed = True + except Exception as e: + ansible_module.fail_json(msg="%s: %s: %s" % (command, name, + str(e))) + # Get all errors + # All "already a member" and "not a member" failures in the + # result are ignored. All others are reported. + errors = [] + if "failed" in result and "member" in result["failed"]: + failed = result["failed"]["member"] + for member_type in failed: + for member, failure in failed[member_type]: + if "already a member" not in failure \ + and "not a member" not in failure: + errors.append("%s: %s %s: %s" % ( + command, member_type, member, failure)) + if len(errors) > 0: + ansible_module.fail_json(msg=", ".join(errors)) + + except Exception as e: + ansible_module.fail_json(msg=str(e)) + + finally: + temp_kdestroy(ccache_dir, ccache_name) + + # Done + + ansible_module.exit_json(changed=changed, **exit_args) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ipapwpolicy.py b/plugins/modules/ipapwpolicy.py new file mode 100644 index 0000000..9437b59 --- /dev/null +++ b/plugins/modules/ipapwpolicy.py @@ -0,0 +1,304 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Copyright (C) 2019 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "supported_by": "community", + "status": ["preview"], +} + +DOCUMENTATION = """ +--- +module: ipapwpolicy +short description: Manage FreeIPA pwpolicies +description: Manage FreeIPA pwpolicies +options: + ipaadmin_principal: + description: The admin principal + default: admin + ipaadmin_password: + description: The admin password + required: false + name: + description: The group name + required: false + aliases: ["cn"] + maxlife: + description: Maximum password lifetime (in days) + type: int + required: false + aliases: ["krbmaxpwdlife"] + minlife: + description: Minimum password lifetime (in hours) + type: int + required: false + aliases: ["krbminpwdlife"] + history: + description: Password history size + type: int + required: false + aliases: ["krbpwdhistorylength"] + minclasses: + description: Minimum number of character classes + type: int + required: false + aliases: ["krbpwdmindiffchars"] + minlength: + description: Minimum length of password + type: int + required: false + aliases: ["krbpwdminlength"] + priority: + description: Priority of the policy (higher number means lower priority) + type: int + required: false + aliases: ["cospriority"] + maxfail: + description: Consecutive failures before lockout + type: int + required: false + aliases: ["krbpwdmaxfailure"] + failinterval: + description: Period after which failure count will be reset (seconds) + type: int + required: false + aliases: ["krbpwdfailurecountinterval"] + lockouttime: + description: Period for which lockout is enforced (seconds) + type: int + required: false + aliases: ["krbpwdlockoutduration"] + state: + description: State to ensure + default: present + choices: ["present", "absent"] +author: + - Thomas Woerner +""" + +EXAMPLES = """ +# Ensure pwpolicy is set for ops +- ipapwpolicy: + ipaadmin_password: MyPassword123 + name: ops + minlife: 7 + maxlife: 49 + history: 5 + priority: 1 + lockouttime: 300 + minlength: 8 +""" + +RETURN = """ +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text +from ansible.module_utils.ansible_freeipa_module import temp_kinit, \ + temp_kdestroy, valid_creds, api_connect, api_command, compare_args_ipa + + +def find_pwpolicy(module, name): + _args = { + "all": True, + "cn": to_text(name), + } + + _result = api_command(module, "pwpolicy_find", to_text(name), _args) + + if len(_result["result"]) > 1: + module.fail_json( + msg="There is more than one pwpolicy '%s'" % (name)) + elif len(_result["result"]) == 1: + return _result["result"][0] + else: + return None + + +def gen_args(maxlife, minlife, history, minclasses, minlength, priority, + maxfail, failinterval, lockouttime): + _args = {} + if maxlife is not None: + _args["krbmaxpwdlife"] = maxlife + if minlife is not None: + _args["krbminpwdlife"] = minlife + if history is not None: + _args["krbpwdhistorylength"] = history + if minclasses is not None: + _args["krbpwdmindiffchars"] = minclasses + if minlength is not None: + _args["krbpwdminlength"] = minlength + if priority is not None: + _args["cospriority"] = priority + if maxfail is not None: + _args["krbpwdmaxfailure"] = maxfail + if failinterval is not None: + _args["krbpwdfailurecountinterval"] = failinterval + if lockouttime is not None: + _args["krbpwdlockoutduration"] = lockouttime + + return _args + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # general + ipaadmin_principal=dict(type="str", default="admin"), + ipaadmin_password=dict(type="str", required=False, no_log=True), + + name=dict(type="list", aliases=["cn"], default=None, + required=True), + # present + + maxlife=dict(type="int", aliases=["krbmaxpwdlife"], default=None), + minlife=dict(type="int", aliases=["krbminpwdlife"], default=None), + history=dict(type="int", aliases=["krbpwdhistorylength"], + default=None), + minclasses=dict(type="int", aliases=["krbpwdmindiffchars"], + default=None), + minlength=dict(type="int", aliases=["krbpwdminlength"], + default=None), + priority=dict(type="int", aliases=["cospriority"], default=None), + maxfail=dict(type="int", aliases=["krbpwdmaxfailure"], + default=None), + failinterval=dict(type="int", + aliases=["krbpwdfailurecountinterval"], + default=None), + lockouttime=dict(type="int", aliases=["krbpwdlockoutduration"], + default=None), + # state + state=dict(type="str", default="present", + choices=["present", "absent"]), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + + # Get parameters + + # general + ipaadmin_principal = ansible_module.params.get("ipaadmin_principal") + ipaadmin_password = ansible_module.params.get("ipaadmin_password") + names = ansible_module.params.get("name") + + # present + maxlife = ansible_module.params.get("maxlife") + minlife = ansible_module.params.get("minlife") + history = ansible_module.params.get("history") + minclasses = ansible_module.params.get("minclasses") + minlength = ansible_module.params.get("minlength") + priority = ansible_module.params.get("priority") + maxfail = ansible_module.params.get("maxfail") + failinterval = ansible_module.params.get("failinterval") + lockouttime = ansible_module.params.get("lockouttime") + + # state + state = ansible_module.params.get("state") + + # Check parameters + + if state == "present": + if len(names) != 1: + ansible_module.fail_json( + msg="Only one pwpolicy can be set at a time.") + + if state == "absent": + if len(names) < 1: + ansible_module.fail_json( + msg="No name given.") + invalid = ["maxlife", "minlife", "history", "minclasses", + "minlength", "priority", "maxfail", "failinterval", + "lockouttime"] + for x in invalid: + if vars()[x] is not None: + ansible_module.fail_json( + msg="Argument '%s' can not be used with state '%s'" % + (x, state)) + + # Init + + changed = False + exit_args = {} + ccache_dir = None + ccache_name = None + try: + if not valid_creds(ansible_module, ipaadmin_principal): + ccache_dir, ccache_name = temp_kinit(ipaadmin_principal, + ipaadmin_password) + api_connect() + + commands = [] + + for name in names: + # Try to find pwpolicy + res_find = find_pwpolicy(ansible_module, name) + + # Create command + if state == "present": + # Generate args + args = gen_args(maxlife, minlife, history, minclasses, + minlength, priority, maxfail, failinterval, + lockouttime) + + # Found the pwpolicy + if res_find is not None: + # For all settings is args, check if there are + # different settings in the find result. + # If yes: modify + if not compare_args_ipa(ansible_module, args, + res_find): + commands.append([name, "pwpolicy_mod", args]) + else: + commands.append([name, "pwpolicy_add", args]) + + elif state == "absent": + if res_find is not None: + commands.append([name, "pwpolicy_del", {}]) + + else: + ansible_module.fail_json(msg="Unkown state '%s'" % state) + + # Execute commands + + for name, command, args in commands: + try: + api_command(ansible_module, command, to_text(name), args) + changed = True + except Exception as e: + ansible_module.fail_json(msg="%s: %s: %s" % (command, name, + str(e))) + + except Exception as e: + ansible_module.fail_json(msg=str(e)) + + finally: + temp_kdestroy(ccache_dir, ccache_name) + + # Done + + ansible_module.exit_json(changed=changed, **exit_args) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ipasudocmd.py b/plugins/modules/ipasudocmd.py new file mode 100644 index 0000000..8e25561 --- /dev/null +++ b/plugins/modules/ipasudocmd.py @@ -0,0 +1,211 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Rafael Guterres Jeffman +# +# Copyright (C) 2019 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "supported_by": "community", + "status": ["preview"], +} + + +DOCUMENTATION = """ +--- +module: ipasudocmd +short description: Manage FreeIPA sudo command +description: Manage FreeIPA sudo command +options: + ipaadmin_principal: + description: The admin principal + default: admin + ipaadmin_password: + description: The admin password + required: false + name: + description: The sudo command + required: true + aliases: ["sudocmd"] + description: + description: The command description + required: false + state: + description: State to ensure + default: present + choices: ["present", "absent"] +author: + - Rafael Jeffman +""" + +EXAMPLES = """ +# Ensure sudocmd is present +- ipacommand: + ipaadmin_password: MyPassword123 + name: su + state: present + +# Ensure sudocmd is absent +- ipacommand: + ipaadmin_password: MyPassword123 + name: su + state: absent +""" + +RETURN = """ +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text +from ansible.module_utils.ansible_freeipa_module import temp_kinit, \ + temp_kdestroy, valid_creds, api_connect, api_command, compare_args_ipa + + +def find_sudocmd(module, name): + _args = { + "all": True, + "sudocmd": to_text(name), + } + + _result = api_command(module, "sudocmd_find", to_text(name), _args) + + if len(_result["result"]) > 1: + module.fail_json( + msg="There is more than one sudocmd '%s'" % (name)) + elif len(_result["result"]) == 1: + return _result["result"][0] + else: + return None + + +def gen_args(description): + _args = {} + if description is not None: + _args["description"] = description + + return _args + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # general + ipaadmin_principal=dict(type="str", default="admin"), + ipaadmin_password=dict(type="str", required=False, no_log=True), + + name=dict(type="list", aliases=["sudocmd"], default=None, + required=True), + # present + description=dict(type="str", default=None), + # state + state=dict(type="str", default="present", + choices=["present", "absent"]), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + + # Get parameters + + # general + ipaadmin_principal = ansible_module.params.get("ipaadmin_principal") + ipaadmin_password = ansible_module.params.get("ipaadmin_password") + names = ansible_module.params.get("name") + + # present + description = ansible_module.params.get("description") + # state + state = ansible_module.params.get("state") + + # Check parameters + if state == "absent": + invalid = ["description"] + for x in invalid: + if vars()[x] is not None: + ansible_module.fail_json( + msg="Argument '%s' can not be used with state '%s'" % + (x, state)) + + # Init + + changed = False + exit_args = {} + ccache_dir = None + ccache_name = None + try: + if not valid_creds(ansible_module, ipaadmin_principal): + ccache_dir, ccache_name = temp_kinit(ipaadmin_principal, + ipaadmin_password) + api_connect() + + commands = [] + + for name in names: + # Make sure hostgroup exists + res_find = find_sudocmd(ansible_module, name) + + # Create command + if state == "present": + # Generate args + args = gen_args(description) + if res_find is not None: + # For all settings in args, check if there are + # different settings in the find result. + # If yes: modify + if not compare_args_ipa(ansible_module, args, + res_find): + commands.append([name, "sudocmd_mod", args]) + else: + commands.append([name, "sudocmd_add", args]) + # Set res_find to empty dict for next step + res_find = {} + elif state == "absent": + if res_find is not None: + commands.append([name, "sudocmd_del", {}]) + else: + ansible_module.fail_json(msg="Unkown state '%s'" % state) + + # Execute commands + for name, command, args in commands: + try: + result = api_command(ansible_module, command, to_text(name), + args) + # Check if any changes were made by any command + if command == 'sudocmd_del': + changed |= "Deleted" in result['summary'] + elif command == 'sudocmd_add': + changed |= "Added" in result['summary'] + except Exception as e: + ansible_module.fail_json(msg="%s: %s: %s" % (command, name, + str(e))) + + except Exception as e: + ansible_module.fail_json(msg=str(e)) + + finally: + temp_kdestroy(ccache_dir, ccache_name) + + # Done + + ansible_module.exit_json(changed=changed, **exit_args) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ipasudocmdgroup.py b/plugins/modules/ipasudocmdgroup.py new file mode 100644 index 0000000..bfa0130 --- /dev/null +++ b/plugins/modules/ipasudocmdgroup.py @@ -0,0 +1,355 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Rafael Guterres Jeffman +# +# Copyright (C) 2019 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "supported_by": "community", + "status": ["preview"], +} + + +DOCUMENTATION = """ +--- +module: ipasudocmdgroup +short description: Manage FreeIPA sudocmd groups +description: Manage FreeIPA sudocmd groups +options: + ipaadmin_principal: + description: The admin principal + default: admin + ipaadmin_password: + description: The admin password + required: false + name: + description: The sudocmodgroup name + required: false + aliases: ["cn"] + description: + description: The sudocmdgroup description + required: false + nomembers: + description: Suppress processing of membership attributes + required: false + type: bool + sudocmdgroup: + description: List of sudocmdgroup names assigned to this sudocmdgroup. + required: false + type: list + sudocmd: + description: List of sudocmds assigned to this sudocmdgroup. + required: false + type: list + action: + description: Work on sudocmdgroup or member level + default: hostgroup + choices: ["member", "sudocmdgroup"] + state: + description: State to ensure + default: present + choices: ["present", "absent"] +author: + - Rafael Guterres Jeffman +""" + +EXAMPLES = """ +# Ensure sudocmd-group 'network' is present +- ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + state: present + +# Ensure sudocmdgroup and sudocmd are present in 'network' sudocmdgroup +- ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + action: member + +# Ensure sudocmdgroup and sudocmd are absent in 'network' sudocmdgroup +- ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + action: member + state: absent + +# Ensure sudocmd-group 'network' is absent +- ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + action: member + state: absent +""" + +RETURN = """ +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text +from ansible.module_utils.ansible_freeipa_module import temp_kinit, \ + temp_kdestroy, valid_creds, api_connect, api_command, compare_args_ipa + + +def find_sudocmdgroup(module, name): + _args = { + "all": True, + "cn": to_text(name), + } + + _result = api_command(module, "sudocmdgroup_find", to_text(name), _args) + + if len(_result["result"]) > 1: + module.fail_json( + msg="There is more than one sudocmdgroup '%s'" % (name)) + elif len(_result["result"]) == 1: + return _result["result"][0] + else: + return None + + +def gen_args(description, nomembers): + _args = {} + if description is not None: + _args["description"] = to_text(description) + if nomembers is not None: + _args["nomembers"] = nomembers + + return _args + + +def gen_member_args(sudocmdgroup): + _args = {} + if sudocmdgroup is not None: + _args["member_sudocmdgroup"] = sudocmdgroup + + return _args + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # general + ipaadmin_principal=dict(type="str", default="admin"), + ipaadmin_password=dict(type="str", required=False, no_log=True), + + name=dict(type="list", aliases=["cn"], default=None, + required=True), + # present + description=dict(type="str", default=None), + nomembers=dict(required=False, type='bool', default=None), + sudocmdgroup=dict(required=False, type='list', default=None), + sudocmd=dict(required=False, type='list', default=None), + action=dict(type="str", default="sudocmdgroup", + choices=["member", "sudocmdgroup"]), + # state + state=dict(type="str", default="present", + choices=["present", "absent"]), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + + # Get parameters + + # general + ipaadmin_principal = ansible_module.params.get("ipaadmin_principal") + ipaadmin_password = ansible_module.params.get("ipaadmin_password") + names = ansible_module.params.get("name") + + # present + description = ansible_module.params.get("description") + nomembers = ansible_module.params.get("nomembers") + sudocmdgroup = ansible_module.params.get("sudocmdgroup") + sudocmd = ansible_module.params.get("sudocmd") + action = ansible_module.params.get("action") + # state + state = ansible_module.params.get("state") + + # Check parameters + + if state == "present": + if len(names) != 1: + ansible_module.fail_json( + msg="Only one sudocmdgroup can be added at a time.") + if action == "member": + invalid = ["description", "nomembers"] + for x in invalid: + if vars()[x] is not None: + ansible_module.fail_json( + msg="Argument '%s' can not be used with action " + "'%s'" % (x, action)) + + if state == "absent": + if len(names) < 1: + ansible_module.fail_json( + msg="No name given.") + invalid = ["description", "nomembers"] + if action == "sudocmdgroup": + invalid.extend(["sudocmd"]) + for x in invalid: + if vars()[x] is not None: + ansible_module.fail_json( + msg="Argument '%s' can not be used with state '%s'" % + (x, state)) + + # Init + + changed = False + exit_args = {} + ccache_dir = None + ccache_name = None + try: + if not valid_creds(ansible_module, ipaadmin_principal): + ccache_dir, ccache_name = temp_kinit(ipaadmin_principal, + ipaadmin_password) + api_connect() + + commands = [] + + for name in names: + # Make sure hostgroup exists + res_find = find_sudocmdgroup(ansible_module, name) + + # Create command + if state == "present": + # Generate args + args = gen_args(description, nomembers) + + if action == "sudocmdgroup": + # Found the hostgroup + if res_find is not None: + # For all settings is args, check if there are + # different settings in the find result. + # If yes: modify + if not compare_args_ipa(ansible_module, args, + res_find): + commands.append([name, "sudocmdgroup_mod", args]) + else: + commands.append([name, "sudocmdgroup_add", args]) + # Set res_find to empty dict for next step + res_find = {} + + member_args = gen_member_args(sudocmd) + if not compare_args_ipa(ansible_module, member_args, + res_find): + # Generate addition and removal lists + sudocmdgroup_add = list( + set(sudocmdgroup or []) - + set(res_find.get("member_sudocmdgroup", []))) + sudocmdgroup_del = list( + set(res_find.get("member_sudocmdgroup", [])) - + set(sudocmdgroup or [])) + + # Add members + if len(sudocmdgroup_add) > 0: + commands.append([name, "sudocmdgroup_add_member", + { + "sudocmd": [to_text(c) + for c in + sudocmdgroup_add] + } + ]) + # Remove members + if len(sudocmdgroup_del) > 0: + commands.append([name, + "sudocmdgroup_remove_member", + { + "sudocmd": [to_text(c) + for c in + sudocmdgroup_del] + } + ]) + elif action == "member": + if res_find is None: + ansible_module.fail_json( + msg="No sudocmdgroup '%s'" % name) + + # Ensure members are present + commands.append([name, "sudocmdgroup_add_member", + {"sudocmd": [to_text(c) for c in sudocmd]} + ]) + elif state == "absent": + if action == "sudocmdgroup": + if res_find is not None: + commands.append([name, "sudocmdgroup_del", {}]) + + elif action == "member": + if res_find is None: + ansible_module.fail_json( + msg="No sudocmdgroup '%s'" % name) + + # Ensure members are absent + commands.append([name, "sudocmdgroup_remove_member", + {"sudocmd": [to_text(c) for c in sudocmd]} + ]) + else: + ansible_module.fail_json(msg="Unkown state '%s'" % state) + + # Execute commands + for name, command, args in commands: + try: + result = api_command(ansible_module, command, to_text(name), + args) + if action == "member": + if "completed" in result and result["completed"] > 0: + changed = True + else: + if command == "sudocmdgroup_del": + changed |= "Deleted" in result['summary'] + elif command == "sudocmdgroup_add": + changed |= "Added" in result['summary'] + except Exception as e: + ansible_module.fail_json(msg="%s: %s: %s" % (command, name, + str(e))) + # Get all errors + # All "already a member" and "not a member" failures in the + # result are ignored. All others are reported. + errors = [] + if "failed" in result and "member" in result["failed"]: + failed = result["failed"]["member"] + for member_type in failed: + for member, failure in failed[member_type]: + if "already a member" not in failure \ + and "not a member" not in failure: + errors.append("%s: %s %s: %s" % ( + command, member_type, member, failure)) + if len(errors) > 0: + ansible_module.fail_json(msg=", ".join(errors)) + + except Exception as e: + ansible_module.fail_json(msg=str(e)) + + finally: + temp_kdestroy(ccache_dir, ccache_name) + + # Done + + ansible_module.exit_json(changed=changed, **exit_args) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ipasudorule.py b/plugins/modules/ipasudorule.py new file mode 100644 index 0000000..c21f247 --- /dev/null +++ b/plugins/modules/ipasudorule.py @@ -0,0 +1,564 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Rafael Guterres Jeffman +# +# Copyright (C) 2019 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "supported_by": "community", + "status": ["preview"], +} + +DOCUMENTATION = """ +--- +module: ipasudorule +short description: Manage FreeIPA sudo rules +description: Manage FreeIPA sudo rules +options: + ipaadmin_principal: + description: The admin principal + default: admin + ipaadmin_password: + description: The admin password + required: false + name: + description: The sudorule name + required: true + aliases: ["cn"] + description: + description: The sudorule description + required: false + user: + description: List of users assigned to the sudo rule. + required: false + usercategory: + description: User category the sudo rule applies to + required: false + choices: ["all"] + usergroup: + description: List of user groups assigned to the sudo rule. + required: false + runasgroupcategory: + description: RunAs Group category applied to the sudo rule. + required: false + choices: ["all"] + runasusercategory: + description: RunAs User category applied to the sudorule. + required: false + choices: ["all"] + nomembers: + description: Suppress processing of membership attributes + required: false + type: bool + host: + description: List of host names assigned to this sudorule. + required: false + type: list + hostgroup: + description: List of host groups assigned to this sudorule. + required: false + type: list + hostcategory: + description: Host category the sudo rule applies to. + required: false + choices: ["all"] + cmd: + description: List of sudocmds assigned to this sudorule. + required: false + type: list + cmdgroup: + description: List of sudocmd groups assigned to this sudorule. + required: false + type: list + cmdcategory: + description: Cammand category the sudo rule applies to + required: false + choices: ["all"] + action: + description: Work on sudorule or member level + default: sudorule + choices: ["member", "sudorule"] + state: + description: State to ensure + default: present + choices: ["present", "absent", "enabled", "disabled"] +author: + - Rafael Jeffman +""" + +EXAMPLES = """ +# Ensure Sudo Rule tesrule1 is present +- ipasudorule: + ipaadmin_password: MyPassword123 + name: testrule1 + +# Ensure sudocmd is present in Sudo Rule +- ipasudorule: + ipaadmin_password: pass1234 + name: testrule1 + cmd: + - /sbin/ifconfig + - /usr/bin/vim + action: member + state: absent + +# Ensure host server is present in Sudo Rule +- ipasudorule: + ipaadmin_password: MyPassword123 + name: testrule1 + host: server + action: member + +# Ensure hostgroup cluster is present in Sudo Rule +- ipasudorule: + ipaadmin_password: MyPassword123 + name: testrule1 + hostgroup: cluster + action: member + +# Ensure sudo rule for usercategory "all" +- ipasudorule: + ipaadmin_password: MyPassword123 + name: allusers + usercategory: all + action: enabled + +# Ensure sudo rule for hostcategory "all" +- ipasudorule: + ipaadmin_password: MyPassword123 + name: allhosts + hostcategory: all + action: enabled + +# Ensure Sudo Rule tesrule1 is absent +- ipasudorule: + ipaadmin_password: MyPassword123 + name: testrule1 + state: absent +""" + +RETURN = """ +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_freeipa_module import temp_kinit, \ + temp_kdestroy, valid_creds, api_connect, api_command, compare_args_ipa, \ + module_params_get + + +def find_sudorule(module, name): + _args = { + "all": True, + "cn": name, + } + + _result = api_command(module, "sudorule_find", name, _args) + + if len(_result["result"]) > 1: + module.fail_json( + msg="There is more than one sudorule '%s'" % (name)) + elif len(_result["result"]) == 1: + return _result["result"][0] + else: + return None + + +def gen_args(ansible_module): + arglist = ['description', 'usercategory', 'hostcategory', 'cmdcategory', + 'runasusercategory', 'runasgroupcategory', 'nomembers'] + _args = {} + for arg in arglist: + value = module_params_get(ansible_module, arg) + if value is not None: + _args[arg] = value + + return _args + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # general + ipaadmin_principal=dict(type="str", default="admin"), + ipaadmin_password=dict(type="str", required=False, no_log=True), + + name=dict(type="list", aliases=["cn"], default=None, + required=True), + # present + description=dict(required=False, type="str", default=None), + usercategory=dict(required=False, type="str", default=None, + choices=["all"]), + hostcategory=dict(required=False, type="str", default=None, + choices=["all"]), + nomembers=dict(required=False, type='bool', default=None), + host=dict(required=False, type='list', default=None), + hostgroup=dict(required=False, type='list', default=None), + user=dict(required=False, type='list', default=None), + group=dict(required=False, type='list', default=None), + cmd=dict(required=False, type="list", default=None), + cmdcategory=dict(required=False, type="str", default=None, + choices=["all"]), + runasusercategory=dict(required=False, type="str", default=None, + choices=["all"]), + runasgroupcategory=dict(required=False, type="str", default=None, + choices=["all"]), + action=dict(type="str", default="sudorule", + choices=["member", "sudorule"]), + # state + state=dict(type="str", default="present", + choices=["present", "absent", + "enabled", "disabled"]), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + + # Get parameters + + # general + ipaadmin_principal = module_params_get(ansible_module, + "ipaadmin_principal") + ipaadmin_password = module_params_get(ansible_module, "ipaadmin_password") + names = module_params_get(ansible_module, "name") + + # present + # The 'noqa' variables are not used here, but required for vars(). + # The use of 'noqa' ensures flake8 does not complain about them. + description = module_params_get(ansible_module, "description") # noqa + cmdcategory = module_params_get(ansible_module, 'cmdcategory') # noqa + usercategory = module_params_get(ansible_module, "usercategory") # noqa + hostcategory = module_params_get(ansible_module, "hostcategory") # noqa + runasusercategory = module_params_get(ansible_module, # noqa + "runasusercategory") + runasgroupcategory = module_params_get(ansible_module, # noqa + "runasgroupcategory") + hostcategory = module_params_get(ansible_module, "hostcategory") # noqa + nomembers = module_params_get(ansible_module, "nomembers") # noqa + host = module_params_get(ansible_module, "host") + hostgroup = module_params_get(ansible_module, "hostgroup") + user = module_params_get(ansible_module, "user") + group = module_params_get(ansible_module, "group") + cmd = module_params_get(ansible_module, 'cmd') + cmdgroup = module_params_get(ansible_module, 'cmdgroup') + action = module_params_get(ansible_module, "action") + + # state + state = module_params_get(ansible_module, "state") + + # Check parameters + + if state == "present": + if len(names) != 1: + ansible_module.fail_json( + msg="Only one sudorule can be added at a time.") + if action == "member": + invalid = ["description", "usercategory", "hostcategory", + "cmdcategory", "runasusercategory", + "runasgroupcategory", "nomembers"] + + for x in invalid: + if x in vars() and vars()[x] is not None: + ansible_module.fail_json( + msg="Argument '%s' can not be used with action " + "'%s'" % (x, action)) + + elif state == "absent": + if len(names) < 1: + ansible_module.fail_json(msg="No name given.") + invalid = ["description", "usercategory", "hostcategory", + "cmdcategory", "runasusercategory", + "runasgroupcategory", "nomembers"] + if action == "sudorule": + invalid.extend(["host", "hostgroup", "user", "group", + "cmd", "cmdgroup"]) + for x in invalid: + if vars()[x] is not None: + ansible_module.fail_json( + msg="Argument '%s' can not be used with state '%s'" % + (x, state)) + + elif state in ["enabled", "disabled"]: + if len(names) < 1: + ansible_module.fail_json(msg="No name given.") + if action == "member": + ansible_module.fail_json( + msg="Action member can not be used with states enabled and " + "disabled") + invalid = ["description", "usercategory", "hostcategory", + "cmdcategory", "runasusercategory", "runasgroupcategory", + "nomembers", "nomembers", "host", "hostgroup", + "user", "group", "cmd", "cmdgroup"] + for x in invalid: + if vars()[x] is not None: + ansible_module.fail_json( + msg="Argument '%s' can not be used with state '%s'" % + (x, state)) + else: + ansible_module.fail_json(msg="Invalid state '%s'" % state) + + # Init + + changed = False + exit_args = {} + ccache_dir = None + ccache_name = None + try: + if not valid_creds(ansible_module, ipaadmin_principal): + ccache_dir, ccache_name = temp_kinit(ipaadmin_principal, + ipaadmin_password) + api_connect() + + commands = [] + + for name in names: + # Make sure sudorule exists + res_find = find_sudorule(ansible_module, name) + + # Create command + if state == "present": + # Generate args + args = gen_args(ansible_module) + if action == "sudorule": + # Found the sudorule + if res_find is not None: + # For all settings is args, check if there are + # different settings in the find result. + # If yes: modify + if not compare_args_ipa(ansible_module, args, + res_find): + commands.append([name, "sudorule_mod", args]) + else: + commands.append([name, "sudorule_add", args]) + # Set res_find to empty dict for next step + res_find = {} + + # Generate addition and removal lists + host_add = list( + set(host or []) - + set(res_find.get("member_host", []))) + host_del = list( + set(res_find.get("member_host", [])) - + set(host or [])) + hostgroup_add = list( + set(hostgroup or []) - + set(res_find.get("member_hostgroup", []))) + hostgroup_del = list( + set(res_find.get("member_hostgroup", [])) - + set(hostgroup or [])) + + user_add = list( + set(user or []) - + set(res_find.get("member_user", []))) + user_del = list( + set(res_find.get("member_user", [])) - + set(user or [])) + group_add = list( + set(group or []) - + set(res_find.get("member_group", []))) + group_del = list( + set(res_find.get("member_group", [])) - + set(group or [])) + + cmd_add = list( + set(cmd or []) - + set(res_find.get("member_cmd", []))) + cmd_del = list( + set(res_find.get("member_cmd", [])) - + set(cmd or [])) + cmdgroup_add = list( + set(cmdgroup or []) - + set(res_find.get("member_cmdgroup", []))) + cmdgroup_del = list( + set(res_find.get("member_cmdgroup", [])) - + set(cmdgroup or [])) + + # Add hosts and hostgroups + if len(host_add) > 0 or len(hostgroup_add) > 0: + commands.append([name, "sudorule_add_host", + { + "host": host_add, + "hostgroup": hostgroup_add, + }]) + # Remove hosts and hostgroups + if len(host_del) > 0 or len(hostgroup_del) > 0: + commands.append([name, "sudorule_remove_host", + { + "host": host_del, + "hostgroup": hostgroup_del, + }]) + + # Add users and groups + if len(user_add) > 0 or len(group_add) > 0: + commands.append([name, "sudorule_add_user", + { + "user": user_add, + "group": group_add, + }]) + # Remove users and groups + if len(user_del) > 0 or len(group_del) > 0: + commands.append([name, "sudorule_remove_user", + { + "user": user_del, + "group": group_del, + }]) + + # Add commands + if len(cmd_add) > 0 or len(cmdgroup_add) > 0: + commands.append([name, "sudorule_add_allow_command", + { + "sudocmd": cmd_add, + "sudocmdgroup": cmdgroup_add, + }]) + + if len(cmd_del) > 0 or len(cmdgroup_del) > 0: + commands.append([name, "sudorule_add_deny_command", + { + "sudocmd": cmd_del, + "sudocmdgroup": cmdgroup_del + }]) + + elif action == "member": + if res_find is None: + ansible_module.fail_json(msg="No sudorule '%s'" % name) + + # Add hosts and hostgroups + if host is not None or hostgroup is not None: + commands.append([name, "sudorule_add_host", + { + "host": host, + "hostgroup": hostgroup, + }]) + + # Add users and groups + if user is not None or group is not None: + commands.append([name, "sudorule_add_user", + { + "user": user, + "group": group, + }]) + + # Add commands + if cmd is not None: + commands.append([name, "sudorule_add_allow_command", + { + "sudocmd": cmd, + }]) + + elif state == "absent": + if action == "sudorule": + if res_find is not None: + commands.append([name, "sudorule_del", {}]) + + elif action == "member": + if res_find is None: + ansible_module.fail_json(msg="No sudorule '%s'" % name) + + # Remove hosts and hostgroups + if host is not None or hostgroup is not None: + commands.append([name, "sudorule_remove_host", + { + "host": host, + "hostgroup": hostgroup, + }]) + + # Remove users and groups + if user is not None or group is not None: + commands.append([name, "sudorule_remove_user", + { + "user": user, + "group": group, + }]) + + # Remove commands + if cmd is not None: + commands.append([name, "sudorule_add_deny_command", + { + "sudocmd": cmd, + }]) + + elif state == "enabled": + if res_find is None: + ansible_module.fail_json(msg="No sudorule '%s'" % name) + # sudorule_enable is not failing on an enabled sudorule + # Therefore it is needed to have a look at the ipaenabledflag + # in res_find. + if "ipaenabledflag" not in res_find or \ + res_find["ipaenabledflag"][0] != "TRUE": + commands.append([name, "sudorule_enable", {}]) + + elif state == "disabled": + if res_find is None: + ansible_module.fail_json(msg="No sudorule '%s'" % name) + # sudorule_disable is not failing on an disabled sudorule + # Therefore it is needed to have a look at the ipaenabledflag + # in res_find. + if "ipaenabledflag" not in res_find or \ + res_find["ipaenabledflag"][0] != "FALSE": + commands.append([name, "sudorule_disable", {}]) + + else: + ansible_module.fail_json(msg="Unkown state '%s'" % state) + + # Execute commands + + errors = [] + for name, command, args in commands: + try: + result = api_command(ansible_module, command, name, + args) + + if "completed" in result: + if result["completed"] > 0: + changed = True + else: + changed = True + except Exception as e: + ansible_module.fail_json(msg="%s: %s: %s" % (command, name, + str(e))) + # Get all errors + # All "already a member" and "not a member" failures in the + # result are ignored. All others are reported. + if "failed" in result and len(result["failed"]) > 0: + for item in result["failed"]: + failed_item = result["failed"][item] + for member_type in failed_item: + for member, failure in failed_item[member_type]: + if "already a member" in failure \ + or "not a member" in failure: + continue + errors.append("%s: %s %s: %s" % ( + command, member_type, member, failure)) + if len(errors) > 0: + ansible_module.fail_json(msg=", ".join(errors)) + + except Exception as e: + ansible_module.fail_json(msg=str(e)) + + finally: + temp_kdestroy(ccache_dir, ccache_name) + + # Done + + ansible_module.exit_json(changed=changed, **exit_args) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ipatopologysegment.py b/plugins/modules/ipatopologysegment.py new file mode 100644 index 0000000..5740cde --- /dev/null +++ b/plugins/modules/ipatopologysegment.py @@ -0,0 +1,347 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Copyright (C) 2019 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "supported_by": "community", + "status": ["preview"], +} + +DOCUMENTATION = """ +--- +module: ipatopologysegment +short description: Manage FreeIPA topology segments +description: Manage FreeIPA topology segments +options: + ipaadmin_principal: + description: The admin principal + default: admin + ipaadmin_password: + description: The admin password + required: false + suffix: + description: Topology suffix + required: true + choices: ["domain", "ca", "domain+ca"] + name: + description: Topology segment name, unique identifier. + required: false + aliases: ["cn"] + left: + description: Left replication node - an IPA server + aliases: ["leftnode"] + right: + description: Right replication node - an IPA server + aliases: ["rightnode"] + direction: + description: The direction a segment will be reinitialized + required: false + choices: ["left-to-right", "right-to-left"] + state: + description: State to ensure + default: present + choices: ["present", "absent", "enabled", "disabled", "reinitialized" + "checked" ] +author: + - Thomas Woerner +""" + +EXAMPLES = """ +- ipatopologysegment: + suffix: domain + left: ipaserver.test.local + right: ipareplica1.test.local + state: present + +- ipatopologysegment: + suffix: domain + name: ipaserver.test.local-to-replica1.test.local + state: absent + +- ipatopologysegment: + suffix: domain + left: ipaserver.test.local + right: ipareplica1.test.local + state: absent + +- ipatopologysegment: + suffix: ca + name: ipaserver.test.local-to-replica1.test.local + direction: left-to-right + state: reinitialized + +- ipatopologysegment: + suffix: domain+ca + left: ipaserver.test.local + right: ipareplica1.test.local + state: absent + +- ipatopologysegment: + suffix: domain+ca + left: ipaserver.test.local + right: ipareplica1.test.local + state: checked +""" + +RETURN = """ +found: + description: List of found segments + returned: if state is checked + type: list +not-found: + description: List of not found segments + returned: if state is checked + type: list +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text +from ansible.module_utils.ansible_freeipa_module import temp_kinit, \ + temp_kdestroy, valid_creds, api_connect, api_command + + +def find_left_right(module, suffix, left, right): + _args = { + "iparepltoposegmentleftnode": to_text(left), + "iparepltoposegmentrightnode": to_text(right), + } + _result = api_command(module, "topologysegment_find", + to_text(suffix), _args) + if len(_result["result"]) > 1: + module.fail_json( + msg="Combination of left node '%s' and right node '%s' is " + "not unique for suffix '%s'" % (left, right, suffix)) + elif len(_result["result"]) == 1: + return _result["result"][0] + else: + return None + + +def find_cn(module, suffix, name): + _args = { + "cn": to_text(name), + } + _result = api_command(module, "topologysegment_find", + to_text(suffix), _args) + if len(_result["result"]) > 1: + module.fail_json( + msg="CN '%s' is not unique for suffix '%s'" % (name, suffix)) + elif len(_result["result"]) == 1: + return _result["result"][0] + else: + return None + + +def find_left_right_cn(module, suffix, left, right, name): + if left is not None and right is not None: + left_right = find_left_right(module, suffix, left, right) + if left_right is not None: + if name is not None and \ + left_right["cn"][0] != to_text(name): + module.fail_json( + msg="Left and right nodes do not match " + "given name name (cn) '%s'" % name) + return left_right + # else: Nothing to change + elif name is not None: + cn = find_cn(module, suffix, name) + if cn is not None: + return cn + # else: Nothing to change + else: + module.fail_json( + msg="Either left and right or name need to be set.") + return None + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + ipaadmin_principal=dict(type="str", default="admin"), + ipaadmin_password=dict(type="str", required=False, no_log=True), + suffix=dict(choices=["domain", "ca", "domain+ca"], required=True), + name=dict(type="str", aliases=["cn"], default=None), + left=dict(type="str", aliases=["leftnode"], default=None), + right=dict(type="str", aliases=["rightnode"], default=None), + direction=dict(type="str", default=None, + choices=["left-to-right", "right-to-left"]), + state=dict(type="str", default="present", + choices=["present", "absent", "enabled", "disabled", + "reinitialized", "checked"]), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + + # Get parameters + + ipaadmin_principal = ansible_module.params.get("ipaadmin_principal") + ipaadmin_password = ansible_module.params.get("ipaadmin_password") + suffixes = ansible_module.params.get("suffix") + name = ansible_module.params.get("name") + left = ansible_module.params.get("left") + right = ansible_module.params.get("right") + direction = ansible_module.params.get("direction") + state = ansible_module.params.get("state") + + # Check parameters + + if state != "reinitialized" and direction is not None: + ansible_module.fail_json( + msg="Direction is not supported in this mode.") + + # Init + + changed = False + exit_args = {} + ccache_dir = None + ccache_name = None + try: + if not valid_creds(ansible_module, ipaadmin_principal): + ccache_dir, ccache_name = temp_kinit(ipaadmin_principal, + ipaadmin_password) + api_connect() + + commands = [] + + for suffix in suffixes.split("+"): + # Create command + if state in ["present", "enabled"]: + # Make sure topology segment exists + + if left is None or right is None: + ansible_module.fail_json( + msg="Left and right need to be set.") + args = { + "iparepltoposegmentleftnode": to_text(left), + "iparepltoposegmentrightnode": to_text(right), + } + if name is not None: + args["cn"] = to_text(name) + + res_left_right = find_left_right(ansible_module, suffix, + left, right) + if res_left_right is not None: + if name is not None and \ + res_left_right["cn"][0] != to_text(name): + ansible_module.fail_json( + msg="Left and right nodes already used with " + "different name (cn) '%s'" % res_left_right["cn"]) + + # Left and right nodes and also the name can not be + # changed + for key in ["iparepltoposegmentleftnode", + "iparepltoposegmentrightnode"]: + if key in args: + del args[key] + if len(args) > 1: + # cn needs to be in args always + commands.append(["topologysegment_mod", args, suffix]) + # else: Nothing to change + else: + if name is None: + args["cn"] = to_text("%s-to-%s" % (left, right)) + commands.append(["topologysegment_add", args, suffix]) + + elif state in ["absent", "disabled"]: + # Make sure topology segment does not exist + + res_find = find_left_right_cn(ansible_module, suffix, + left, right, name) + if res_find is not None: + # Found either given name or found name from left and right + # node + args = { + "cn": res_find["cn"][0] + } + commands.append(["topologysegment_del", args, suffix]) + + elif state == "checked": + # Check if topology segment does exists + + res_find = find_left_right_cn(ansible_module, suffix, + left, right, name) + if res_find is not None: + # Found either given name or found name from left and right + # node + exit_args.setdefault("found", []).append(suffix) + else: + # Not found + exit_args.setdefault("not-found", []).append(suffix) + + elif state == "reinitialized": + # Reinitialize segment + + if direction not in ["left-to-right", "right-to-left"]: + ansible_module.fail_json(msg="Unknown direction '%s'" % + direction) + + res_find = find_left_right_cn(ansible_module, suffix, + left, right, name) + if res_find is not None: + # Found either given name or found name from left and right + # node + args = { + "cn": res_find["cn"][0] + } + if direction == "left-to-right": + args["left"] = True + elif direction == "right-to-left": + args["right"] = True + + commands.append(["topologysegment_reinitialize", args, + suffix]) + else: + params = [] + if name is not None: + params.append("name=%s" % name) + if left is not None: + params.append("left=%s" % left) + if right is not None: + params.append("right=%s" % right) + ansible_module.fail_json( + msg="No entry '%s' for suffix '%s'" % + (",".join(params), suffix)) + + else: + ansible_module.fail_json(msg="Unkown state '%s'" % state) + + # Execute command + + for command, args, _suffix in commands: + api_command(ansible_module, command, to_text(_suffix), args) + changed = True + + except Exception as e: + ansible_module.fail_json(msg=str(e)) + + finally: + temp_kdestroy(ccache_dir, ccache_name) + + # Done + + ansible_module.exit_json(changed=changed, **exit_args) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ipatopologysuffix.py b/plugins/modules/ipatopologysuffix.py new file mode 100644 index 0000000..ab1e413 --- /dev/null +++ b/plugins/modules/ipatopologysuffix.py @@ -0,0 +1,111 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Copyright (C) 2019 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "supported_by": "community", + "status": ["preview"], +} + +DOCUMENTATION = """ +--- +module: ipatopologysuffix +short description: Verify FreeIPA topology suffix +description: Verify FreeIPA topology suffix +options: + ipaadmin_principal: + description: The admin principal + default: admin + ipaadmin_password: + description: The admin password + required: false + suffix: + description: Topology suffix + required: true + choices: ["domain", "ca"] + state: + description: State to ensure + default: verified + choices: ["verified"] +author: + - Thomas Woerner +""" + +EXAMPLES = """ +- ipatopologysuffix: + suffix: domain + state: verified +""" + +RETURN = """ +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text +from ansible.module_utils.ansible_freeipa_module import execute_api_command + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + ipaadmin_principal=dict(type="str", default="admin"), + ipaadmin_password=dict(type="str", required=False, no_log=True), + suffix=dict(choices=["domain", "ca"], required=True), + state=dict(type="str", default="verified", + choices=["verified"]), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + + # Get parameters + + ipaadmin_principal = ansible_module.params.get("ipaadmin_principal") + ipaadmin_password = ansible_module.params.get("ipaadmin_password") + suffix = ansible_module.params.get("suffix") + state = ansible_module.params.get("state") + + # Check parameters + + # Init + + # Create command + + if state in ["verified"]: + command = "topologysuffix_verify" + args = {} + else: + ansible_module.fail_json(msg="Unkown state '%s'" % state) + + # Execute command + + execute_api_command(ansible_module, ipaadmin_principal, ipaadmin_password, + command, to_text(suffix), args) + + # Done + + ansible_module.exit_json(changed=True) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/ipauser.py b/plugins/modules/ipauser.py new file mode 100644 index 0000000..3722b32 --- /dev/null +++ b/plugins/modules/ipauser.py @@ -0,0 +1,1370 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Copyright (C) 2019 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "supported_by": "community", + "status": ["preview"], +} + +DOCUMENTATION = """ +--- +module: ipauser +short description: Manage FreeIPA users +description: Manage FreeIPA users +options: + ipaadmin_principal: + description: The admin principal + default: admin + ipaadmin_password: + description: The admin password + required: false + name: + description: The list of users (internally uid). + required: false + users: + description: The list of user dicts (internally uid). + options: + name: + description: The user (internally uid). + required: true + first: + description: The first name + required: false + aliases: ["givenname"] + last: + description: The last name + required: false + aliases: ["sn"] + fullname: + description: The full name + required: false + aliases: ["cn"] + displayname: + description: The display name + required: false + initials: + description: Initials + required: false + homedir: + description: The home directory + required: false + shell: + description: The login shell + required: false + aliases: ["loginshell"] + email: + description: List of email addresses + required: false + principal: + description: The kerberos principal + required: false + aliases: ["principalname", "krbprincipalname"] + principalexpiration: + description: + - The kerberos principal expiration date + - (possible formats: YYYYMMddHHmmssZ, YYYY-MM-ddTHH:mm:ssZ, + - YYYY-MM-ddTHH:mmZ, YYYY-MM-ddZ, YYYY-MM-dd HH:mm:ssZ, + - YYYY-MM-dd HH:mmZ) The trailing 'Z' can be skipped. + required: false + aliases: ["krbprincipalexpiration"] + passwordexpiration: + description: + - The kerberos password expiration date (FreeIPA-4.7+) + - (possible formats: YYYYMMddHHmmssZ, YYYY-MM-ddTHH:mm:ssZ, + - YYYY-MM-ddTHH:mmZ, YYYY-MM-ddZ, YYYY-MM-dd HH:mm:ssZ, + - YYYY-MM-dd HH:mmZ) The trailing 'Z' can be skipped. + - Only usable with IPA versions 4.7 and up. + required: false + aliases: ["krbpasswordexpiration"] + password: + description: The user password + required: false + random: + description: Generate a random user password + required: false + type: bool + uid: + description: The UID + required: false + aliases: ["uidnumber"] + gid: + description: The GID + required: false + aliases: ["gidnumber"] + city: + description: City + required: false + userstate: + description: State/Province + required: false + aliases: ["st"] + postalcode: + description: Postalcode/ZIP + required: false + aliases: ["zip"] + phone: + description: List of telephone numbers + required: false + aliases: ["telephonenumber"] + mobile: + description: List of mobile telephone numbers + required: false + pager: + description: List of pager numbers + required: false + fax: + description: List of fax numbers + required: false + aliases: ["facsimiletelephonenumber"] + orgunit: + description: Org. Unit + required: false + title: + description: The job title + required: false + manager: + description: List of managers + required: false + carlicense: + description: List of car licenses + required: false + sshpubkey: + description: List of SSH public keys + required: false + aliases: ["ipasshpubkey"] + userauthtype: + description: List of supported user authentication types + choices=['password', 'radius', 'otp'] + required: false + userclass: + description: + - User category + - (semantics placed on this attribute are for local interpretation) + required: false + radius: + description: RADIUS proxy configuration + required: false + radiususer: + description: RADIUS proxy username + required: false + departmentnumber: + description: Department Number + required: false + employeenumber: + description: Employee Number + required: false + employeetype: + description: Employee Type + required: false + preferredlanguage: + description: Preferred Language + required: false + certificate: + description: List of base-64 encoded user certificates + required: false + certmapdata: + description: List of certificate mappings + options: + certificate: + description: Base-64 encoded user certificate + required: false + issuer: + description: Issuer of the certificate + required: false + subject: + description: Subject of the certificate + required: false + required: false + noprivate: + description: Don't create user private group + required: false + type: bool + nomembers: + description: Suppress processing of membership attributes + required: false + type: bool + required: false + first: + description: The first name + required: false + aliases: ["givenname"] + last: + description: The last name + required: false + aliases: ["sn"] + fullname: + description: The full name + required: false + aliases: ["cn"] + displayname: + description: The display name + required: false + initials: + description: Initials + required: false + homedir: + description: The home directory + required: false + shell: + description: The login shell + required: false + aliases: ["loginshell"] + email: + description: List of email addresses + required: false + principal: + description: The kerberos principal + required: false + aliases: ["principalname", "krbprincipalname"] + principalexpiration: + description: + - The kerberos principal expiration date + - (possible formats: YYYYMMddHHmmssZ, YYYY-MM-ddTHH:mm:ssZ, + - YYYY-MM-ddTHH:mmZ, YYYY-MM-ddZ, YYYY-MM-dd HH:mm:ssZ, + - YYYY-MM-dd HH:mmZ) The trailing 'Z' can be skipped. + required: false + aliases: ["krbprincipalexpiration"] + passwordexpiration: + description: + - The kerberos password expiration date (FreeIPA-4.7+) + - (possible formats: YYYYMMddHHmmssZ, YYYY-MM-ddTHH:mm:ssZ, + - YYYY-MM-ddTHH:mmZ, YYYY-MM-ddZ, YYYY-MM-dd HH:mm:ssZ, + - YYYY-MM-dd HH:mmZ) The trailing 'Z' can be skipped. + - Only usable with IPA versions 4.7 and up. + required: false + aliases: ["krbpasswordexpiration"] + password: + description: The user password + required: false + random: + description: Generate a random user password + required: false + type: bool + uid: + description: The UID + required: false + aliases: ["uidnumber"] + gid: + description: The GID + required: false + aliases: ["gidnumber"] + city: + description: City + required: false + userstate: + description: State/Province + required: false + aliases: ["st"] + postalcode: + description: ZIP + required: false + aliases: ["zip"] + phone: + description: List of telephone numbers + required: false + aliases: ["telephonenumber"] + mobile: + description: List of mobile telephone numbers + required: false + pager: + description: List of pager numbers + required: false + fax: + description: List of fax numbers + required: false + aliases: ["facsimiletelephonenumber"] + orgunit: + description: Org. Unit + required: false + title: + description: The job title + required: false + manager: + description: List of managers + required: false + carlicense: + description: List of car licenses + required: false + sshpubkey: + description: List of SSH public keys + required: false + aliases: ["ipasshpubkey"] + userauthtype: + description: List of supported user authentication types + choices=['password', 'radius', 'otp'] + required: false + userclass: + description: + - User category + - (semantics placed on this attribute are for local interpretation) + required: false + radius: + description: RADIUS proxy configuration + required: false + radiususer: + description: RADIUS proxy username + required: false + departmentnumber: + description: Department Number + required: false + employeenumber: + description: Employee Number + required: false + employeetype: + description: Employee Type + required: false + preferredlanguage: + description: Preferred Language + required: false + certificate: + description: List of base-64 encoded user certificates + required: false + certmapdata: + description: List of certificate mappings + options: + certificate: + description: Base-64 encoded user certificate + required: false + issuer: + description: Issuer of the certificate + required: false + subject: + description: Subject of the certificate + required: false + required: false + noprivate: + description: Don't create user private group + required: false + type: bool + nomembers: + description: Suppress processing of membership attributes + required: false + type: bool + preserve: + description: Delete a user, keeping the entry available for future use + required: false + update_password: + description: + Set password for a user in present state only on creation or always + default: "always" + choices: ["always", "on_create"] + required: false + action: + description: Work on user or member level + default: "user" + choices: ["member", "user"] + state: + description: State to ensure + default: present + choices: ["present", "absent", + "enabled", "disabled", + "unlocked", "undeleted"] +author: + - Thomas Woerner +""" + +EXAMPLES = """ +# Create user pinky +- ipauser: + ipaadmin_password: MyPassword123 + name: pinky + first: pinky + last: Acme + uid: 10001 + gid: 100 + phone: "+555123457" + email: pinky@acme.com + passwordexpiration: "2023-01-19 23:59:59" + password: "no-brain" + update_password: on_create + +# Create user brain +- ipauser: + ipaadmin_password: MyPassword123 + name: brain + first: brain + last: Acme + +# Delete user pinky, but preserved +- ipauser: + ipaadmin_password: MyPassword123 + name: pinky + preserve: yes + state: absent + +# Undelete user pinky +- ipauser: + ipaadmin_password: MyPassword123 + name: pinky + state: undeleted + +# Disable user pinky +- ipauser: + ipaadmin_password: MyPassword123 + name: pinky,brain + state: disabled + +# Enable user pinky and brain +- ipauser: + ipaadmin_password: MyPassword123 + name: pinky,brain + state: enabled + +# Remove user pinky and brain +- ipauser: + ipaadmin_password: MyPassword123 + name: pinky,brain + state: disabled +""" + +RETURN = """ +user: + description: User dict with random password + returned: If random is yes and user did not exist or update_password is yes + type: dict + options: + randompassword: + description: The generated random password + returned: If only one user is handled by the module + name: + description: The user name of the user that got a new random password + returned: If several users are handled by the module + type: dict + options: + randompassword: + description: The generated random password + returned: always +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text +from ansible.module_utils.ansible_freeipa_module import temp_kinit, \ + temp_kdestroy, valid_creds, api_connect, api_command, date_format, \ + compare_args_ipa, module_params_get, api_check_param, api_get_realm, \ + api_command_no_name +import six + + +if six.PY3: + unicode = str + + +def find_user(module, name, preserved=False): + _args = { + "all": True, + "uid": name, + } + if preserved: + _args["preserved"] = preserved + + _result = api_command(module, "user_find", name, _args) + + if len(_result["result"]) > 1: + module.fail_json( + msg="There is more than one user '%s'" % (name)) + elif len(_result["result"]) == 1: + # Transform each principal to a string + _result = _result["result"][0] + if "krbprincipalname" in _result \ + and _result["krbprincipalname"] is not None: + _list = [] + for x in _result["krbprincipalname"]: + _list.append(str(x)) + _result["krbprincipalname"] = _list + return _result + else: + return None + + +def gen_args(first, last, fullname, displayname, initials, homedir, shell, + email, principalexpiration, passwordexpiration, password, + random, uid, gid, city, userstate, postalcode, phone, mobile, + pager, fax, orgunit, title, carlicense, sshpubkey, userauthtype, + userclass, radius, radiususer, departmentnumber, employeenumber, + employeetype, preferredlanguage, noprivate, nomembers): + # principal, manager, certificate and certmapdata are handled not in here + _args = {} + if first is not None: + _args["givenname"] = first + if last is not None: + _args["sn"] = last + if fullname is not None: + _args["cn"] = fullname + if displayname is not None: + _args["displayname"] = displayname + if initials is not None: + _args["initials"] = initials + if homedir is not None: + _args["homedirectory"] = homedir + if shell is not None: + _args["loginshell"] = shell + if email is not None and len(email) > 0: + _args["mail"] = email + if principalexpiration is not None: + _args["krbprincipalexpiration"] = principalexpiration + if passwordexpiration is not None: + _args["krbpasswordexpiration"] = passwordexpiration + if password is not None: + _args["userpassword"] = password + if random is not None: + _args["random"] = random + if uid is not None: + _args["uidnumber"] = to_text(str(uid)) + if gid is not None: + _args["gidnumber"] = to_text(str(gid)) + if city is not None: + _args["l"] = city + if userstate is not None: + _args["st"] = userstate + if postalcode is not None: + _args["postalcode"] = postalcode + if phone is not None and len(phone) > 0: + _args["telephonenumber"] = phone + if mobile is not None and len(mobile) > 0: + _args["mobile"] = mobile + if pager is not None and len(pager) > 0: + _args["pager"] = pager + if fax is not None and len(fax) > 0: + _args["facsimiletelephonenumber"] = fax + if orgunit is not None: + _args["ou"] = orgunit + if title is not None: + _args["title"] = title + if carlicense is not None and len(carlicense) > 0: + _args["carlicense"] = carlicense + if sshpubkey is not None and len(sshpubkey) > 0: + _args["ipasshpubkey"] = sshpubkey + if userauthtype is not None and len(userauthtype) > 0: + _args["ipauserauthtype"] = userauthtype + if userclass is not None: + _args["userclass"] = userclass + if radius is not None: + _args["ipatokenradiusconfiglink"] = radius + if radiususer is not None: + _args["ipatokenradiususername"] = radiususer + if departmentnumber is not None: + _args["departmentnumber"] = departmentnumber + if employeenumber is not None: + _args["employeenumber"] = employeenumber + if employeetype is not None: + _args["employeetype"] = employeetype + if preferredlanguage is not None: + _args["preferredlanguage"] = preferredlanguage + if noprivate is not None: + _args["noprivate"] = noprivate + if nomembers is not None: + _args["no_members"] = nomembers + return _args + + +def check_parameters(module, state, action, + first, last, fullname, displayname, initials, homedir, + shell, email, principal, principalexpiration, + passwordexpiration, password, random, uid, gid, city, + phone, mobile, pager, fax, orgunit, title, manager, + carlicense, sshpubkey, userauthtype, userclass, radius, + radiususer, departmentnumber, employeenumber, + employeetype, preferredlanguage, certificate, + certmapdata, noprivate, nomembers, preserve, + update_password): + + if state == "present": + if action == "member": + invalid = ["first", "last", "fullname", "displayname", "initials", + "homedir", "shell", "email", "principalexpiration", + "passwordexpiration", "password", "random", "uid", + "gid", "city", "phone", "mobile", "pager", "fax", + "orgunit", "title", "carlicense", "sshpubkey", + "userauthtype", "userclass", "radius", "radiususer", + "departmentnumber", "employeenumber", "employeetype", + "preferredlanguage", "noprivate", "nomembers", + "preserve", "update_password"] + for x in invalid: + if vars()[x] is not None: + module.fail_json( + msg="Argument '%s' can not be used with action " + "'%s'" % (x, action)) + + else: + invalid = ["first", "last", "fullname", "displayname", "initials", + "homedir", "shell", "email", "principalexpiration", + "passwordexpiration", "password", "random", "uid", + "gid", "city", "phone", "mobile", "pager", "fax", + "orgunit", "title", "carlicense", "sshpubkey", + "userauthtype", "userclass", "radius", "radiususer", + "departmentnumber", "employeenumber", "employeetype", + "preferredlanguage", "noprivate", "nomembers", + "update_password"] + if action == "user": + invalid.extend(["principal", "manager", + "certificate", "certmapdata", + ]) + for x in invalid: + if vars()[x] is not None: + module.fail_json( + msg="Argument '%s' can not be used with state '%s'" % + (x, state)) + + if state != "absent" and preserve is not None: + module.fail_json( + msg="Preserve is only possible for state=absent") + + if certmapdata is not None: + for x in certmapdata: + certificate = x.get("certificate") + issuer = x.get("issuer") + subject = x.get("subject") + + if certificate is not None \ + and (issuer is not None or subject is not None): + module.fail_json( + msg="certmapdata: certificate can not be used with " + "issuer or subject") + if certificate is None: + if issuer is None: + module.fail_json(msg="certmapdata: issuer is missing") + if subject is None: + module.fail_json(msg="certmapdata: subject is missing") + + +def extend_emails(email, default_email_domain): + if email is not None: + return [ "%s@%s" % (_email, default_email_domain) + if "@" not in _email else _email + for _email in email] + return email + + +def gen_certmapdata_args(certmapdata): + certificate = certmapdata.get("certificate") + issuer = certmapdata.get("issuer") + subject = certmapdata.get("subject") + + _args = {} + if certificate is not None: + _args["certificate"] = certificate + if issuer is not None: + _args["issuer"] = issuer + if subject is not None: + _args["subject"] = subject + return _args + + +def main(): + user_spec = dict( + # present + first=dict(type="str", aliases=["givenname"], default=None), + last=dict(type="str", aliases=["sn"], default=None), + fullname=dict(type="str", aliases=["cn"], default=None), + displayname=dict(type="str", default=None), + initials=dict(type="str", default=None), + homedir=dict(type="str", default=None), + shell=dict(type="str", aliases=["loginshell"], default=None), + email=dict(type="list", default=None), + principal=dict(type="list", aliases=["principalname", + "krbprincipalname"], + default=None), + principalexpiration=dict(type="str", + aliases=["krbprincipalexpiration"], + default=None), + passwordexpiration=dict(type="str", + aliases=["krbpasswordexpiration"], + default=None), + password=dict(type="str", default=None, no_log=True), + random=dict(type='bool', default=None), + uid=dict(type="int", aliases=["uidnumber"], default=None), + gid=dict(type="int", aliases=["gidnumber"], default=None), + city=dict(type="str", default=None), + userstate=dict(type="str", aliases=["st"], default=None), + postalcode=dict(type="str", aliases=["zip"], default=None), + phone=dict(type="list", aliases=["telephonenumber"], default=None), + mobile=dict(type="list", default=None), + pager=dict(type="list", default=None), + fax=dict(type="list", aliases=["facsimiletelephonenumber"], + default=None), + orgunit=dict(type="str", aliases=["ou"], default=None), + title=dict(type="str", default=None), + manager=dict(type="list", default=None), + carlicense=dict(type="list", default=None), + sshpubkey=dict(type="list", aliases=["ipasshpubkey"], + default=None), + userauthtype=dict(type='list', aliases=["ipauserauthtype"], + default=None, + choices=['password', 'radius', 'otp']), + userclass=dict(type="list", aliases=["class"], + default=None), + radius=dict(type="str", aliases=["ipatokenradiusconfiglink"], + default=None), + radiususer=dict(type="str", aliases=["radiususername", + "ipatokenradiususername"], + default=None), + departmentnumber=dict(type="list", default=None), + employeenumber=dict(type="str", default=None), + employeetype=dict(type="str", default=None), + preferredlanguage=dict(type="str", default=None), + certificate=dict(type="list", aliases=["usercertificate"], + default=None), + certmapdata=dict(type="list", default=None, + options=dict( + # Here certificate is a simple string + certificate=dict(type="str", default=None), + issuer=dict(type="str", default=None), + subject=dict(type="str", default=None) + ), + elements='dict', required=False), + noprivate=dict(type='bool', default=None), + nomembers=dict(type='bool', default=None), + ) + + ansible_module = AnsibleModule( + argument_spec=dict( + # general + ipaadmin_principal=dict(type="str", default="admin"), + ipaadmin_password=dict(type="str", required=False, no_log=True), + + name=dict(type="list", aliases=["login"], default=None, + required=False), + users=dict(type="list", aliases=["login"], default=None, + options=dict( + # Here name is a simple string + name=dict(type="str", required=True), + # Add user specific parameters + **user_spec + ), + elements='dict', required=False), + + # deleted + preserve=dict(required=False, type='bool', default=None), + + # mod + update_password=dict(type='str', default=None, + choices=['always', 'on_create']), + + # general + action=dict(type="str", default="user", + choices=["member", "user"]), + state=dict(type="str", default="present", + choices=["present", "absent", "enabled", "disabled", + "unlocked", "undeleted"]), + + # Add user specific parameters for simple use case + **user_spec + ), + mutually_exclusive=[["name", "users"]], + required_one_of=[["name", "users"]], + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + + # Get parameters + + # general + ipaadmin_principal = module_params_get(ansible_module, + "ipaadmin_principal") + ipaadmin_password = module_params_get(ansible_module, "ipaadmin_password") + names = module_params_get(ansible_module, "name") + users = module_params_get(ansible_module, "users") + + # present + first = module_params_get(ansible_module, "first") + last = module_params_get(ansible_module, "last") + fullname = module_params_get(ansible_module, "fullname") + displayname = module_params_get(ansible_module, "displayname") + initials = module_params_get(ansible_module, "initials") + homedir = module_params_get(ansible_module, "homedir") + shell = module_params_get(ansible_module, "shell") + email = module_params_get(ansible_module, "email") + principal = module_params_get(ansible_module, "principal") + principalexpiration = module_params_get(ansible_module, + "principalexpiration") + if principalexpiration is not None: + if principalexpiration[:-1] != "Z": + principalexpiration = principalexpiration + "Z" + principalexpiration = date_format(principalexpiration) + passwordexpiration = module_params_get(ansible_module, + "passwordexpiration") + if passwordexpiration is not None: + if passwordexpiration[:-1] != "Z": + passwordexpiration = passwordexpiration + "Z" + passwordexpiration = date_format(passwordexpiration) + password = module_params_get(ansible_module, "password") + random = module_params_get(ansible_module, "random") + uid = module_params_get(ansible_module, "uid") + gid = module_params_get(ansible_module, "gid") + city = module_params_get(ansible_module, "city") + userstate = module_params_get(ansible_module, "userstate") + postalcode = module_params_get(ansible_module, "postalcode") + phone = module_params_get(ansible_module, "phone") + mobile = module_params_get(ansible_module, "mobile") + pager = module_params_get(ansible_module, "pager") + fax = module_params_get(ansible_module, "fax") + orgunit = module_params_get(ansible_module, "orgunit") + title = module_params_get(ansible_module, "title") + manager = module_params_get(ansible_module, "manager") + carlicense = module_params_get(ansible_module, "carlicense") + sshpubkey = module_params_get(ansible_module, "sshpubkey") + userauthtype = module_params_get(ansible_module, "userauthtype") + userclass = module_params_get(ansible_module, "userclass") + radius = module_params_get(ansible_module, "radius") + radiususer = module_params_get(ansible_module, "radiususer") + departmentnumber = module_params_get(ansible_module, "departmentnumber") + employeenumber = module_params_get(ansible_module, "employeenumber") + employeetype = module_params_get(ansible_module, "employeetype") + preferredlanguage = module_params_get(ansible_module, "preferredlanguage") + certificate = module_params_get(ansible_module, "certificate") + certmapdata = module_params_get(ansible_module, "certmapdata") + noprivate = module_params_get(ansible_module, "noprivate") + nomembers = module_params_get(ansible_module, "nomembers") + # deleted + preserve = module_params_get(ansible_module, "preserve") + # mod + update_password = module_params_get(ansible_module, "update_password") + # general + action = module_params_get(ansible_module, "action") + state = module_params_get(ansible_module, "state") + + # Check parameters + + if (names is None or len(names) < 1) and \ + (users is None or len(users) < 1): + ansible_module.fail_json(msg="One of name and users is required") + + if state == "present": + if names is not None and len(names) != 1: + ansible_module.fail_json( + msg="Only one user can be added at a time using name.") + if action != "member": + # Only check first and last here if names is set + if names is not None: + if first is None: + ansible_module.fail_json(msg="First name is needed") + if last is None: + ansible_module.fail_json(msg="Last name is needed") + + check_parameters( + ansible_module, state, action, + first, last, fullname, displayname, initials, homedir, shell, email, + principal, principalexpiration, passwordexpiration, password, random, + uid, gid, city, phone, mobile, pager, fax, orgunit, title, manager, + carlicense, sshpubkey, userauthtype, userclass, radius, radiususer, + departmentnumber, employeenumber, employeetype, preferredlanguage, + certificate, certmapdata, noprivate, nomembers, preserve, + update_password) + + # Use users if names is None + if users is not None: + names = users + + # Init + + changed = False + exit_args = {} + ccache_dir = None + ccache_name = None + try: + if not valid_creds(ansible_module, ipaadmin_principal): + ccache_dir, ccache_name = temp_kinit(ipaadmin_principal, + ipaadmin_password) + api_connect() + + # Check version specific settings + + server_realm = api_get_realm() + + # Default email domain + + result = api_command_no_name(ansible_module, "config_show", {}) + default_email_domain = result["result"]["ipadefaultemaildomain"][0] + + # Extend email addresses + + email = extend_emails(email, default_email_domain) + + # commands + + commands = [] + + for user in names: + if isinstance(user, dict): + name = user.get("name") + # present + first = user.get("first") + last = user.get("last") + fullname = user.get("fullname") + displayname = user.get("displayname") + initials = user.get("initials") + homedir = user.get("homedir") + shell = user.get("shell") + email = user.get("email") + principal = user.get("principal") + principalexpiration = user.get("principalexpiration") + if principalexpiration is not None: + if principalexpiration[:-1] != "Z": + principalexpiration = principalexpiration + "Z" + principalexpiration = date_format(principalexpiration) + passwordexpiration = user.get("passwordexpiration") + if passwordexpiration is not None: + if passwordexpiration[:-1] != "Z": + passwordexpiration = passwordexpiration + "Z" + passwordexpiration = date_format(passwordexpiration) + password = user.get("password") + random = user.get("random") + uid = user.get("uid") + gid = user.get("gid") + city = user.get("city") + userstate = user.get("userstate") + postalcode = user.get("postalcode") + phone = user.get("phone") + mobile = user.get("mobile") + pager = user.get("pager") + fax = user.get("fax") + orgunit = user.get("orgunit") + title = user.get("title") + manager = user.get("manager") + carlicense = user.get("carlicense") + sshpubkey = user.get("sshpubkey") + userauthtype = user.get("userauthtype") + userclass = user.get("userclass") + radius = user.get("radius") + radiususer = user.get("radiususer") + departmentnumber = user.get("departmentnumber") + employeenumber = user.get("employeenumber") + employeetype = user.get("employeetype") + preferredlanguage = user.get("preferredlanguage") + certificate = user.get("certificate") + certmapdata = user.get("certmapdata") + noprivate = user.get("noprivate") + nomembers = user.get("nomembers") + + check_parameters( + ansible_module, state, action, + first, last, fullname, displayname, initials, homedir, + shell, email, principal, principalexpiration, + passwordexpiration, password, random, uid, gid, city, + phone, mobile, pager, fax, orgunit, title, manager, + carlicense, sshpubkey, userauthtype, userclass, radius, + radiususer, departmentnumber, employeenumber, + employeetype, preferredlanguage, certificate, + certmapdata, noprivate, nomembers, preserve, + update_password) + + # Extend email addresses + + email = extend_emails(email, default_email_domain) + + elif isinstance(user, str) or isinstance(user, unicode): + name = user + else: + ansible_module.fail_json(msg="User '%s' is not valid" % + repr(user)) + + # Fix principals: add realm if missing + # We need the connected API for the realm, therefore it can not + # be part of check_parameters as this is used also before the + # connection to the API has been established. + if principal is not None: + principal = [x if "@" in x else x + "@" + server_realm + for x in principal] + + # Check passwordexpiration availability. + # We need the connected API for this test, therefore it can not + # be part of check_parameters as this is used also before the + # connection to the API has been established. + if passwordexpiration is not None and \ + not api_check_param("user_add", "krbpasswordexpiration"): + ansible_module.fail_json( + msg="The use of passwordexpiration is not supported by " + "your IPA version") + + # Make sure user exists + res_find = find_user(ansible_module, name) + # Also search for preserved user if the user could not be found + if res_find is None: + res_find_preserved = find_user(ansible_module, name, + preserved=True) + else: + res_find_preserved = None + + # Create command + if state == "present": + # Generate args + args = gen_args( + first, last, fullname, displayname, initials, homedir, + shell, email, principalexpiration, passwordexpiration, + password, random, uid, gid, city, userstate, postalcode, + phone, mobile, pager, fax, orgunit, title, carlicense, + sshpubkey, userauthtype, userclass, radius, radiususer, + departmentnumber, employeenumber, employeetype, + preferredlanguage, noprivate, nomembers) + + # Also check preserved users + if res_find is None and res_find_preserved is not None: + res_find = res_find_preserved + + if action == "user": + # Found the user + if res_find is not None: + # Ignore password and random with + # update_password == on_create + if update_password == "on_create": + if "userpassword" in args: + del args["userpassword"] + if "random" in args: + del args["random"] + if "noprivate" in args: + del args["noprivate"] + + # For all settings is args, check if there are + # different settings in the find result. + # If yes: modify + if not compare_args_ipa(ansible_module, args, + res_find): + commands.append([name, "user_mod", args]) + + else: + commands.append([name, "user_add", args]) + + # Handle members: principal, manager, certificate and + # certmapdata + if res_find is not None: + # Generate addition and removal lists + manager_add = list( + set(manager or []) - + set(res_find.get("manager", []))) + manager_del = list( + set(res_find.get("manager", [])) - + set(manager or [])) + principal_add = list( + set(principal or []) - + set(res_find.get("krbprincipalname", []))) + principal_del = list( + set(res_find.get("krbprincipalname", [])) - + set(principal or [])) + + # Principals are not returned as utf8 for IPA using + # python2 using user_find, therefore we need to + # convert the principals that we should remove. + principal_del = [to_text(x) for x in principal_del] + + certificate_add = list( + set(certificate or []) - + set(res_find.get("certificate", []))) + certificate_del = list( + set(res_find.get("certificate", [])) - + set(certificate or [])) + certmapdata_add = list( + set(certmapdata or []) - + set(res_find.get("ipaCertMapData", []))) + certmapdata_del = list( + set(res_find.get("ipaCertMapData", [])) - + set(certmapdata or [])) + + else: + # Use given managers and principals + manager_add = manager or [] + manager_del = [] + principal_add = principal or [] + principal_del = [] + certificate_add = certificate or [] + certificate_del = [] + certmapdata_add = certmapdata or [] + certmapdata_del = [] + + # Remove canonical principal from principal_del + canonical_principal = name + "@" + server_realm + if canonical_principal in principal_del: + principal_del.remove(canonical_principal) + + # Add managers + if len(manager_add) > 0: + commands.append([name, "user_add_manager", + { + "user": manager_add, + }]) + # Remove managers + if len(manager_del) > 0: + commands.append([name, "user_remove_manager", + { + "user": manager_del, + }]) + + # Principals need to be added and removed one by one, + # because if entry already exists, the processing of + # the remaining enries is stopped. The same applies to + # the removal of non-existing entries. + + # Add principals + if len(principal_add) > 0: + for _principal in principal_add: + commands.append([name, "user_add_principal", + { + "krbprincipalname": + _principal, + }]) + # Remove principals + if len(principal_del) > 0: + for _principal in principal_del: + commands.append([name, "user_remove_principal", + { + "krbprincipalname": + _principal, + }]) + + # Certificates need to be added and removed one by one, + # because if entry already exists, the processing of + # the remaining enries is stopped. The same applies to + # the removal of non-existing entries. + + # Add certificates + if len(certificate_add) > 0: + for _certificate in certificate_add: + commands.append([name, "user_add_cert", + { + "usercertificate": + _certificate, + }]) + # Remove certificates + if len(certificate_del) > 0: + for _certificate in certificate_del: + commands.append([name, "user_remove_cert", + { + "usercertificate": + _certificate, + }]) + + # certmapdata need to be added and removed one by one, + # because issuer and subject can only be done one by + # one reliably (https://pagure.io/freeipa/issue/8097) + + # Add certmapdata + if len(certmapdata_add) > 0: + for _data in certmapdata_add: + commands.append([name, "user_add_certmapdata", + gen_certmapdata_args(_data)]) + # Remove certmapdata + if len(certmapdata_del) > 0: + for _data in certmapdata_del: + commands.append([name, "user_add_certmapdata", + gen_certmapdata_args(_data)]) + + elif action == "member": + if res_find is None: + ansible_module.fail_json( + msg="No user '%s'" % name) + + # Ensure managers are present + if manager is not None and len(manager) > 0: + commands.append([name, "user_add_manager", + { + "user": manager, + }]) + + # Principals need to be added and removed one by one, + # because if entry already exists, the processing of + # the remaining enries is stopped. The same applies to + # the removal of non-existing entries. + + # Ensure principals are present + if principal is not None and len(principal) > 0: + for _principal in principal: + commands.append([name, "user_add_principal", + { + "krbprincipalname": + _principal, + }]) + + # Certificates need to be added and removed one by one, + # because if entry already exists, the processing of + # the remaining enries is stopped. The same applies to + # the removal of non-existing entries. + + # Ensure certificates are present + if certificate is not None and len(certificate) > 0: + for _certificate in certificate: + commands.append([name, "user_add_cert", + { + "usercertificate": + _certificate, + }]) + + # certmapdata need to be added and removed one by one, + # because issuer and subject can only be done one by + # one reliably (https://pagure.io/freeipa/issue/8097) + + # Ensure certmapdata are present + if certmapdata is not None and len(certmapdata) > 0: + for _data in certmapdata: + commands.append([name, "user_add_certmapdata", + gen_certmapdata_args(_data)]) + + elif state == "absent": + # Also check preserved users + if res_find is None and res_find_preserved is not None: + res_find = res_find_preserved + + if action == "user": + if res_find is not None: + args = {} + if preserve is not None: + args["preserve"] = preserve + commands.append([name, "user_del", args]) + elif action == "member": + if res_find is None: + ansible_module.fail_json( + msg="No user '%s'" % name) + + # Ensure managers are absent + if manager is not None and len(manager) > 0: + commands.append([name, "user_remove_manager", + { + "user": manager, + }]) + + # Principals need to be added and removed one by one, + # because if entry already exists, the processing of + # the remaining enries is stopped. The same applies to + # the removal of non-existing entries. + + # Ensure principals are absent + if principal is not None and len(principal) > 0: + commands.append([name, "user_remove_principal", + { + "krbprincipalname": principal, + }]) + + # Certificates need to be added and removed one by one, + # because if entry already exists, the processing of + # the remaining enries is stopped. The same applies to + # the removal of non-existing entries. + + # Ensure certificates are absent + if certificate is not None and len(certificate) > 0: + for _certificate in certificate: + commands.append([name, "user_remove_cert", + { + "usercertificate": + _certificate, + }]) + + # certmapdata need to be added and removed one by one, + # because issuer and subject can only be done one by + # one reliably (https://pagure.io/freeipa/issue/8097) + + # Ensure certmapdata are absent + if certmapdata is not None and len(certmapdata) > 0: + # Using issuer and subject can only be done one by + # one reliably (https://pagure.io/freeipa/issue/8097) + for _data in certmapdata: + commands.append([name, "user_remove_certmapdata", + gen_certmapdata_args(_data)]) + elif state == "undeleted": + if res_find_preserved is not None: + commands.append([name, "user_undel", {}]) + else: + raise ValueError("No preserved user '%s'" % name) + + elif state == "enabled": + if res_find is not None: + if res_find["nsaccountlock"]: + commands.append([name, "user_enable", {}]) + else: + raise ValueError("No disabled user '%s'" % name) + + elif state == "disabled": + if res_find is not None: + if not res_find["nsaccountlock"]: + commands.append([name, "user_disable", {}]) + else: + raise ValueError("No user '%s'" % name) + + elif state == "unlocked": + if res_find is not None: + commands.append([name, "user_unlock", {}]) + + else: + ansible_module.fail_json(msg="Unkown state '%s'" % state) + + # Execute commands + + errors = [] + for name, command, args in commands: + try: + result = api_command(ansible_module, command, name, + args) + if "completed" in result: + if result["completed"] > 0: + changed = True + else: + changed = True + + if "random" in args and command in ["user_add", "user_mod"] \ + and "randompassword" in result["result"]: + if len(names) == 1: + exit_args["randompassword"] = \ + result["result"]["randompassword"] + else: + exit_args.setdefault(name, {})["randompassword"] = \ + result["result"]["randompassword"] + + except Exception as e: + msg = str(e) + if "already contains" in msg \ + or "does not contain" in msg: + continue + # The canonical principal name may not be removed + if "equal to the canonical principal name must" in msg: + continue + ansible_module.fail_json(msg="%s: %s: %s" % (command, name, + msg)) + + # Get all errors + # All "already a member" and "not a member" failures in the + # result are ignored. All others are reported. + if "failed" in result and len(result["failed"]) > 0: + for item in result["failed"]: + failed_item = result["failed"][item] + for member_type in failed_item: + for member, failure in failed_item[member_type]: + if "already a member" in failure \ + or "not a member" in failure: + continue + errors.append("%s: %s %s: %s" % ( + command, member_type, member, failure)) + + if len(errors) > 0: + ansible_module.fail_json(msg=", ".join(errors)) + + except Exception as e: + ansible_module.fail_json(msg=str(e)) + + finally: + temp_kdestroy(ccache_dir, ccache_name) + + # Done + + ansible_module.exit_json(changed=changed, user=exit_args) + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6cfde97 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +ansible>=2.8.0 diff --git a/roles/ipaclient/README.md b/roles/ipaclient/README.md new file mode 100644 index 0000000..acfd9c9 --- /dev/null +++ b/roles/ipaclient/README.md @@ -0,0 +1,209 @@ +ipaclient role +============== + +This [Ansible](https://www.ansible.com/) role allows to join hosts as clients to an IPA domain. This can be done in different ways using auto-discovery of the servers, domain and other settings or by specifying them. + +**Note**: The ansible playbooks and role require a configured ansible environment where the ansible nodes are reachable and are properly set up to have an IP address and a working package manager. + + +Features +-------- +* Client deployment +* One-time-password (OTP) support +* Repair mode + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.5 and up are supported by the client role. There is also limited support for version 4.4. + + +Supported Distributions +----------------------- + +* RHEL/CentOS 7.4+ +* Fedora 26+ +* Ubuntu + + +Requirements +------------ + +**Controller** +* Ansible version: 2.8+ +* /usr/bin/kinit is required on the controller if a one time password (OTP) is used +* python3-gssapi is required on the controller if a one time password (OTP) is used with keytab + +**Node** +* Supported FreeIPA version (see above) +* Supported distribution (needed for package installation only, see above) + + +Usage +===== + +Example inventory file with fixed principal using auto-discovery with DNS records: + +```ini +[ipaclients] +ipaclient1.example.com +ipaclient2.example.com + +[ipaclients:vars] +ipaadmin_principal=admin +``` + +Example playbook to setup the IPA client(s) using principal from inventory file and password from an [Ansible Vault](http://docs.ansible.com/ansible/latest/playbooks_vault.html) file: + +```yaml +- name: Playbook to configure IPA clients with username/password + hosts: ipaclients + become: true + vars_files: + - playbook_sensitive_data.yml + + roles: + - role: ipaclient + state: present +``` + +Example playbook to unconfigure the IPA client(s) using principal and password from inventory file: + +```yaml +- name: Playbook to unconfigure IPA clients + hosts: ipaclients + become: true + + roles: + - role: ipaclient + state: absent +``` + +Example inventory file with fixed servers, principal, password and domain: + +```ini +[ipaclients] +ipaclient1.example.com +ipaclient2.example.com + +[ipaservers] +ipaserver.example.com + +[ipaclients:vars] +ipaclient_domain=example.com +ipaadmin_principal=admin +ipaadmin_password=MySecretPassword123 +``` + +Example playbook to setup the IPA client(s) using principal and password from inventory file: + +```yaml +- name: Playbook to configure IPA clients with username/password + hosts: ipaclients + become: true + + roles: + - role: ipaclient + state: present +``` + + +Playbooks +========= + +The playbooks needed to deploy or undeploy a client are part of the repository in the playbooks folder. There are also playbooks to deploy and undeploy clusters. +``` +install-client.yml +uninstall-client.yml +``` +Please remember to link or copy the playbooks to the base directory of ansible-freeipa if you want to use the roles within the source archive. + + +How to setup a client +--------------------- + +```bash +ansible-playbook -v -i inventory/hosts install-client.yml +``` +This will deploy the clients defined in the inventory file. + + +Variables +========= + +Base Variables +-------------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaclients` | This group is a list of the names of the IPA clients in FQDN form. All these clients will be installed or configured using the playbook. | yes +`ipaclient_domain` | This string value sets the DNS domain that will be used for client installation. Usually the DNS domain is a lower-cased name of the Kerberos realm. If the role is for example used in a cluster inventory and `ipaserver_domain` is set, then it will be used. | no +`ipaclient_realm` | This string value sets the Kerberos realm that will be used for client installation. Usually the Kerberos realm is an upper-cased name of the DNS domain. If the role is for example used in a cluster inventory and `ipaserver_realm` is set, then it will be used. If `ipaclient_realm` is not set, then it will be generated from `ipaclient_domain` if this is set. | no +`ipaclient_mkhomedir` | This bool value defines if PAM will be configured to create a users home directory if it does not exist. `ipaclient_mkhomedir` defaults to `no`. | no + `ipaclient_force_join` | This bool value defines if an already enrolled host can join again. `ipaclient_force_join` defaults to `no`. | no +`ipaclient_kinit_attempts` | The int value defines the number of tries to repeat the request for a failed host Kerberos ticket. `ipaclient_kinit_attempts` defaults to 5.| no +`ipaclient_ntp_servers` | The list defines the NTP servers to be used. | no +`ipaclient_ntp_pool` | The string value defines the ntp server pool to be used. | no +`ipaclient_no_ntp` | The bool value defines if NTP will not be configured and enabled. `ipaclient_no_ntp` defaults to `no`. | no +`ipaclient_ssh_trust_dns` | The bool value defines if OpenSSH client will be configured to trust DNS SSHFP records. `ipaclient_ssh_trust_dns` defaults to `no`. | no +`ipaclient_no_ssh` | The bool value defines if OpenSSH client will be configured. `ipaclient_no_ssh` defaults to `no`. | no +`ipaclient_no_sshd` | The bool value defines if OpenSSH server will be configured. `ipaclient_no_sshd` defaults to `no`. | no +`ipaclient_no_sudo` | The bool value defines if SSSD will be configured as a data source for sudo. `ipaclient_no_sudo` defaults to `no`. | no +`ipaclient_no_dns_sshfp` | The bool value defines if DNS SSHFP records will not be created automatically. `ipaclient_no_dns_sshfp` defaults to `no`. | no +`ipaclient_force` | The bool value defines if settings will be forced even in the error case. `ipaclient_force` defaults to `no`. | no +`ipaclient_force_ntpd` | The bool value defines if ntpd usage will be forced. This is not supported anymore and leads to a warning. `ipaclient_force_ntpd` defaults to `no`. | no +`ipaclient_nisdomain` | This string value defines the NIS domain name. | no +`ipaclient_no_nisdomain` | The bool value defines if the NIS domain name will not be configured. `ipaclient_no_nisdomain` defaults to `no`. | no +`ipaclient_configure_firefox` | The bool value defines if Firefox will be configured to use IPA domain credentials. `ipaclient_configure_firefox` defaults to `no`. | no +`ipaclient_firefox_dir` | The string value defines the Firefox installation directory. For example: '/usr/lib/firefox'. | no +`ipaclient_all_ip_addresses` | The bool value defines if DNS A/AAAA records for each IP address on the client will be created. `ipaclient_all_ip_addresses` defaults to `no`. | no +`ipasssd_fixed_primary` | The bool value defines if SSSD will be configured to use a fixed server as the primary IPA server. `ipasssd_fixed_primary` defaults to `no`. | no +`ipasssd_permit` | The bool value defines if SSSD will be configured to permit all access. Otherwise the machine will be controlled by the Host-based Access Controls (HBAC) on the IPA server. `ipasssd_permit` defaults to `no`. | no +`ipasssd_enable_dns_updates` | The bool value tells SSSD to automatically update DNS with the IP address of this client. `ipasssd_enable_dns_updates` defaults to `no`. | no +`ipasssd_no_krb5_offline_passwords` | The bool value defines if SSSD will be configured not to store user password when the server is offline . `ipasssd_no_krb5_offline_passwords` defaults to `no`. | no +`ipasssd_preserve_sssd` | The bool value defines if the old SSSD configuration will be preserved if it is not possible to merge it with a new one. `ipasssd_preserve_sssd` defaults to `no`. | no +`ipaclient_request_cert` | The bool value defines if the certificate for the machine wil be requested. The certificate will be stored in /etc/ipa/nssdb under the nickname "Local IPA host". . `ipaclient_request_cert` defaults to `no`. The option is deprecated and will be removed in a future release. | no +`ipaclient_keytab` | The string value contains the path on the node of a backup host keytab from a previous enrollment. | no + + +Server Variables +---------------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaservers` | This group is a list of the IPA server full qualified host names. In a topology with a chain of servers and replicas, it is important to use the right server or replica as the server for the client. If there is a need to overwrite the setting for a client in the `ipaclients` group, please use the list `ipaclient_servers` explained below. If no `ipaservers` group is defined than the installation preparation step will try to use DNS autodiscovery to identify the the IPA server using DNS txt records. | mostly +`ipaadmin_keytab` | The string variable enables the use of an admin keytab as an alternative authentication method. The variable needs to contain the local path to the keytab file. If `ipaadmin_keytab` is used, then `ipaadmin_password` does not need to be set. If `ipaadmin_keytab` is used with `ipaclient_use_otp: yes` then the keytab needs to be available on the controller, else on the client node. The use of full path names is recommended. | no +`ipaadmin_principal` | The string variable only needs to be set if the name of the Kerberos admin principal is not "admin". If `ipaadmin_principal` is not set it will be set internally to "admin". | no +`ipaadmin_password` | The string variable contains the Kerberos password of the Kerberos admin principal. If `ipaadmin_keytab` is used, then `ipaadmin_password` does not need to be set. | mostly + + +Topology Variables +------------------ + +These variables can be used to define or change how clients are arranged within a cluster for example. + +Variable | Description | Required +-------- | ----------- | -------- +`ipaclient_no_dns_lookup` | The bool value defines if the `ipaservers` group will be used as servers for the clients automatically. If enabled this deactivates DNS lookup in Kerberos in client installations. `ipaclient_no_dns_lookup` defaults to `no`. | no +`ipaclient_servers` | The optional list can be used to manually override list of servers on a per client basis. The list of servers is normally taken from from `ipaservers` group. | no + + +Special Variables +----------------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaclient_use_otp` | The bool value defines if a one-time password will be generated to join a new or existing host. `ipaclient_use_otp` defaults to `no`. The enforcement on an existing host is not done if there is a working krb5.keytab on the host. If the generation of an otp is enforced for an existing host entry, then the host gets disabled and the containing keytab gets removed. | no +`ipaclient_otp` | The string value sets an already generated one-time password for the host. The role will use it and not try to generate a new one. Do not enable `ipaclient_use_otp` additionally. | no +`ipaclient_allow_repair` | The bool value defines if an already joined or partly set-up client can be repaired. `ipaclient_allow_repair` defaults to `no`. Contrary to `ipaclient_force_join=yes` the host entry will not be changed on the server. | no +`ipaclient_install_packages` | The bool value defines if the needed packages are installed on the node. `ipaclient_install_packages` defaults to `yes`. | no +`ipaclient_on_master` | The bool value is only used in the server and replica installation process to install the client part. It should not be set otherwise. `ipaclient_on_master` defaults to `no`. | no + + +Authors +======= + +Florence Blanc-Renaud + +Thomas Woerner diff --git a/roles/ipaclient/action_plugins/ipaclient_get_otp.py b/roles/ipaclient/action_plugins/ipaclient_get_otp.py new file mode 100644 index 0000000..b7e8056 --- /dev/null +++ b/roles/ipaclient/action_plugins/ipaclient_get_otp.py @@ -0,0 +1,247 @@ +# Authors: +# Florence Blanc-Renaud +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +try: + import gssapi +except ImportError: + gssapi = None +import os +import shutil +import subprocess +import tempfile +from jinja2 import Template + +from ansible.errors import AnsibleError +from ansible.module_utils._text import to_native +from ansible.plugins.action import ActionBase + + +def run_cmd(args, stdin=None): + """ + Execute an external command. + """ + p_in = None + p_out = subprocess.PIPE + p_err = subprocess.PIPE + + if stdin: + p_in = subprocess.PIPE + + p = subprocess.Popen(args, stdin=p_in, stdout=p_out, stderr=p_err, + close_fds=True) + __temp, stderr = p.communicate(stdin) + + if p.returncode != 0: + raise RuntimeError(stderr) + + +def kinit_password(principal, password, ccache_name, config): + """ + Perform kinit using principal/password, with the specified config file + and store the TGT in ccache_name. + """ + args = ["/usr/bin/kinit", principal, '-c', ccache_name] + old_config = os.environ.get('KRB5_CONFIG') + os.environ['KRB5_CONFIG'] = config + + try: + return run_cmd(args, stdin=password.encode()) + finally: + if old_config is not None: + os.environ['KRB5_CONFIG'] = old_config + else: + os.environ.pop('KRB5_CONFIG', None) + + +def kinit_keytab(principal, keytab, ccache_name, config): + """ + Perform kinit using principal/keytab, with the specified config file + and store the TGT in ccache_name. + """ + if gssapi is None: + raise ImportError("gssapi is not available") + + old_config = os.environ.get('KRB5_CONFIG') + os.environ['KRB5_CONFIG'] = config + try: + name = gssapi.Name(principal, gssapi.NameType.kerberos_principal) + store = {'ccache': ccache_name, + 'client_keytab': keytab} + cred = gssapi.Credentials(name=name, store=store, usage='initiate') + return cred + finally: + if old_config is not None: + os.environ['KRB5_CONFIG'] = old_config + else: + os.environ.pop('KRB5_CONFIG', None) + + +KRB5CONF_TEMPLATE = """ +[logging] + default = FILE:/var/log/krb5libs.log + kdc = FILE:/var/log/krb5kdc.log + admin_server = FILE:/var/log/kadmind.log + +[libdefaults] + default_realm = {{ ipa_realm }} + dns_lookup_realm = false + dns_lookup_kdc = true + rdns = false + ticket_lifetime = {{ ipa_lifetime }} + forwardable = true + udp_preference_limit = 0 + default_ccache_name = KEYRING:persistent:%{uid} + +[realms] + {{ ipa_realm }} = { + kdc = {{ ipa_server }}:88 + master_kdc = {{ ipa_server }}:88 + admin_server = {{ ipa_server }}:749 + default_domain = {{ ipa_domain }} +} + +[domain_realm] + .{{ ipa_domain }} = {{ ipa_realm }} + {{ ipa_domain }} = {{ ipa_realm }} +""" + + +class ActionModule(ActionBase): + + def run(self, tmp=None, task_vars=None): + """ + handler for credential cache transfer + + ipa* commands can either provide a password or a keytab file + in order to authenticate on the managed node with Kerberos. + The module is using these credentials to obtain a TGT locally on the + control node: + - need to create a krb5.conf Kerberos client configuration that is + using IPA server + - set the environment variable KRB5_CONFIG to point to this conf file + - set the environment variable KRB5CCNAME to use a specific cache + - perform kinit on the control node + This command creates the credential cache file + - copy the credential cache file on the managed node + + Then the IPA commands can use this credential cache file. + """ + + if task_vars is None: + task_vars = dict() + + result = super(ActionModule, self).run(tmp, task_vars) + principal = self._task.args.get('principal', None) + keytab = self._task.args.get('keytab', None) + password = self._task.args.get('password', None) + lifetime = self._task.args.get('lifetime', '1h') + + if (not keytab and not password): + result['failed'] = True + result['msg'] = "keytab or password is required" + return result + + if not principal: + result['failed'] = True + result['msg'] = "principal is required" + return result + + data = self._execute_module(module_name='ipaclient_get_facts', + module_args=dict(), task_vars=None) + try: + domain = data['ansible_facts']['ipa']['domain'] + realm = data['ansible_facts']['ipa']['realm'] + except KeyError: + result['failed'] = True + result['msg'] = "The host is not an IPA server" + return result + + items = principal.split('@') + if len(items) < 2: + principal = str('%s@%s' % (principal, realm)) + + # Locally create a temp directory to store krb5.conf and ccache + local_temp_dir = tempfile.mkdtemp() + krb5conf_name = os.path.join(local_temp_dir, 'krb5.conf') + ccache_name = os.path.join(local_temp_dir, 'ccache') + + # Create the krb5.conf from the template + template = Template(KRB5CONF_TEMPLATE) + content = template.render(dict( + ipa_server=task_vars['ansible_host'], + ipa_domain=domain, + ipa_realm=realm, + ipa_lifetime=lifetime)) + + with open(krb5conf_name, 'w') as f: + f.write(content) + + if password: + try: + # perform kinit -c ccache_name -l 1h principal + kinit_password(principal, password, ccache_name, + krb5conf_name) + except Exception as e: + result['failed'] = True + result['msg'] = 'kinit %s with password failed: %s' % \ + (principal, to_native(e)) + return result + + else: + # Password not supplied, need to use the keytab file + # Check if the source keytab exists + try: + keytab = self._find_needle('files', keytab) + except AnsibleError as e: + result['failed'] = True + result['msg'] = to_native(e) + return result + # perform kinit -kt keytab + try: + kinit_keytab(principal, keytab, ccache_name, krb5conf_name) + except Exception as e: + result['failed'] = True + result['msg'] = 'kinit %s with keytab %s failed: %s' % \ + (principal, keytab, str(e)) + return result + + try: + # Create the remote tmp dir + tmp = self._make_tmp_path() + tmp_ccache = self._connection._shell.join_path( + tmp, os.path.basename(ccache_name)) + + # Copy the ccache to the remote tmp dir + self._transfer_file(ccache_name, tmp_ccache) + self._fixup_perms2((tmp, tmp_ccache)) + + new_module_args = self._task.args.copy() + new_module_args.pop('password', None) + new_module_args.pop('keytab', None) + new_module_args.pop('lifetime', None) + new_module_args.update(ccache=tmp_ccache) + + # Execute module + result.update(self._execute_module(module_args=new_module_args, + task_vars=task_vars)) + return result + finally: + # delete the local temp directory + shutil.rmtree(local_temp_dir, ignore_errors=True) + run_cmd(['/usr/bin/kdestroy', '-c', tmp_ccache]) diff --git a/roles/ipaclient/defaults/main.yml b/roles/ipaclient/defaults/main.yml new file mode 100644 index 0000000..5776178 --- /dev/null +++ b/roles/ipaclient/defaults/main.yml @@ -0,0 +1,30 @@ +--- +# defaults file for ipaclient + +ipaclient_force_join: no +ipaclient_mkhomedir: no +ipaclient_kinit_attempts: 5 +ipaclient_use_otp: no +ipaclient_allow_repair: no +ipaclient_on_master: no +ipaclient_no_ntp: no +ipaclient_no_dns_lookup: no +ipaclient_ssh_trust_dns: no +ipaclient_no_ssh: no +ipaclient_no_sshd: no +ipaclient_no_sudo: no +ipaclient_no_dns_sshfp: no +ipaclient_force: no +ipaclient_force_ntpd: no +ipaclient_no_nisdomain: no +ipaclient_configure_firefox: no +ipaclient_all_ip_addresses: no +ipasssd_fixed_primary: no +ipasssd_permit: no +ipasssd_enable_dns_updates: no +ipasssd_no_krb5_offline_passwords: no +ipasssd_preserve_sssd: no +ipaclient_request_cert: no + +### packages ### +ipaclient_install_packages: yes diff --git a/roles/ipaclient/files/py3test.py b/roles/ipaclient/files/py3test.py new file mode 100644 index 0000000..d370b68 --- /dev/null +++ b/roles/ipaclient/files/py3test.py @@ -0,0 +1,9 @@ +#!/usr/bin/python3 + +# Test ipaclient python3 binding +from ipaclient.install.client import SECURE_PATH + +# Check ipapython version to be >= 4.6 +from ipapython.version import NUM_VERSION, VERSION +if NUM_VERSION < 40600: + raise Exception("ipa %s not usable with python3" % VERSION) diff --git a/roles/ipaclient/library/ipaclient_api.py b/roles/ipaclient/library/ipaclient_api.py new file mode 100644 index 0000000..910c744 --- /dev/null +++ b/roles/ipaclient/library/ipaclient_api.py @@ -0,0 +1,229 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = {'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ipaclient_api +short description: + Create temporary NSS database, call IPA API for remaining enrollment parts +description: + Create temporary NSS database, call IPA API for remaining enrollment parts +options: + servers: + description: Fully qualified name of IPA servers to enroll to + required: no + realm: + description: Kerberos realm name of the IPA deployment + required: no + hostname: + description: Fully qualified name of this host + required: no + debug: + description: Turn on extra debugging + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +- name: IPA API calls for remaining enrollment parts + ipaclient_api: + servers: ["server1.example.com","server2.example.com"] + domain: example.com + hostname: client1.example.com + register: result_ipaclient_api +''' + +RETURN = ''' +ca_enabled: + description: Wheter the Certificate Authority is enabled or not. + returned: always + type: bool +subject_base: + description: The subject base, needed for certmonger + returned: always + type: string + sample: O=EXAMPLE.COM +''' + +import os +import inspect + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_client import ( + paths, x509, NUM_VERSION, serialization, certdb, api, + delete_persistent_client_session_data, write_tmp_file, + ipa_generate_password, CalledProcessError, errors, disable_ra, DN, + CLIENT_INSTALL_ERROR, logger +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + servers=dict(required=True, type='list'), + realm=dict(required=True), + hostname=dict(required=True), + debug=dict(required=False, type='bool', default="false"), + ), + supports_check_mode=True, + ) + + module._ansible_debug = True + realm = module.params.get('realm') + hostname = module.params.get('hostname') + servers = module.params.get('servers') + debug = module.params.get('debug') + + host_principal = 'host/%s@%s' % (hostname, realm) + os.environ['KRB5CCNAME'] = paths.IPA_DNS_CCACHE + + ca_certs = x509.load_certificate_list_from_file(paths.IPA_CA_CRT) + if 40500 <= NUM_VERSION < 40590: + ca_certs = [cert.public_bytes(serialization.Encoding.DER) + for cert in ca_certs] + elif NUM_VERSION < 40500: + ca_certs = [cert.der_data for cert in ca_certs] + + with certdb.NSSDatabase() as tmp_db: + api.bootstrap(context='cli_installer', + confdir=paths.ETC_IPA, + debug=debug, + delegate=False, + nss_dir=tmp_db.secdir) + + if 'config_loaded' not in api.env: + module.fail_json(msg="Failed to initialize IPA API.") + + # Clear out any current session keyring information + try: + delete_persistent_client_session_data(host_principal) + except ValueError: + pass + + # Add CA certs to a temporary NSS database + try: + argspec = inspect.getargspec(tmp_db.create_db) + if "password_filename" not in argspec.args: + tmp_db.create_db() + else: + pwd_file = write_tmp_file(ipa_generate_password()) + tmp_db.create_db(pwd_file.name) + for i, cert in enumerate(ca_certs): + if hasattr(certdb, "EXTERNAL_CA_TRUST_FLAGS"): + tmp_db.add_cert(cert, + 'CA certificate %d' % (i + 1), + certdb.EXTERNAL_CA_TRUST_FLAGS) + else: + tmp_db.add_cert(cert, 'CA certificate %d' % (i + 1), + 'C,,') + except CalledProcessError: + module.fail_json(msg="Failed to add CA to temporary NSS database.") + + api.finalize() + + # Now, let's try to connect to the server's RPC interface + connected = False + try: + api.Backend.rpcclient.connect() + connected = True + module.debug("Try RPC connection") + api.Backend.rpcclient.forward('ping') + except errors.KerberosError as e: + if connected: + api.Backend.rpcclient.disconnect() + module.log( + "Cannot connect to the server due to Kerberos error: %s. " + "Trying with delegate=True" % e) + try: + api.Backend.rpcclient.connect(delegate=True) + module.debug("Try RPC connection") + api.Backend.rpcclient.forward('ping') + + module.log("Connection with delegate=True successful") + + # The remote server is not capable of Kerberos S4U2Proxy + # delegation. This features is implemented in IPA server + # version 2.2 and higher + module.warn( + "Target IPA server has a lower version than the enrolled " + "client") + module.warn( + "Some capabilities including the ipa command capability " + "may not be available") + except errors.PublicError as e2: + module.fail_json( + msg="Cannot connect to the IPA server RPC interface: " + "%s" % e2) + except errors.PublicError as e: + module.fail_json( + msg="Cannot connect to the server due to generic error: " + "%s" % e) + # Use the RPC directly so older servers are supported + try: + result = api.Backend.rpcclient.forward( + 'ca_is_enabled', + version=u'2.107', + ) + ca_enabled = result['result'] + except (errors.CommandError, errors.NetworkError): + result = api.Backend.rpcclient.forward( + 'env', + server=True, + version=u'2.0', + ) + ca_enabled = result['result']['enable_ra'] + if not ca_enabled: + disable_ra() + + # Get subject base from ipa server + try: + config = api.Command['config_show']()['result'] + subject_base = str(DN(config['ipacertificatesubjectbase'][0])) + except errors.PublicError: + try: + config = api.Backend.rpcclient.forward( + 'config_show', + raw=True, # so that servroles are not queried + version=u'2.0' + )['result'] + except Exception as e: + logger.debug("config_show failed %s", e, exc_info=True) + module.fail_json( + "Failed to retrieve CA certificate subject base: {}".format(e), + rval=CLIENT_INSTALL_ERROR) + else: + subject_base = str(DN(config['ipacertificatesubjectbase'][0])) + + module.exit_json(changed=True, + ca_enabled=ca_enabled, + subject_base=subject_base) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaclient/library/ipaclient_fix_ca.py b/roles/ipaclient/library/ipaclient_fix_ca.py new file mode 100644 index 0000000..2832699 --- /dev/null +++ b/roles/ipaclient/library/ipaclient_fix_ca.py @@ -0,0 +1,126 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = {'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ipaclient_fix_ca +short description: Fix IPA ca certificate +description: +Repair Fix IPA ca certificate +options: + servers: + description: Fully qualified name of IPA servers to enroll to + required: no + realm: + description: Kerberos realm name of the IPA deployment + required: no + basedn: + description: The basedn of the IPA server (of the form dc=example,dc=com) + required: no + allow_repair: + description: + Allow repair of already joined hosts. Contrary to ipaclient_force_join + the host entry will not be changed on the server + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +- name: Fix IPA ca certificate + ipaclient_fix_ca: + servers: ["server1.example.com","server2.example.com"] + realm: EXAMPLE.COM + basedn: dc=example,dc=com + allow_repair: yes +''' + +RETURN = ''' +''' + +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_client import ( + SECURE_PATH, paths, sysrestore, options, NUM_VERSION, get_ca_cert, + get_ca_certs, errors +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + servers=dict(required=True, type='list'), + realm=dict(required=True), + basedn=dict(required=True), + allow_repair=dict(required=True, type='bool'), + ), + ) + + module._ansible_debug = True + servers = module.params.get('servers') + realm = module.params.get('realm') + basedn = module.params.get('basedn') + allow_repair = module.params.get('allow_repair') + + env = {'PATH': SECURE_PATH} + fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) + os.environ['KRB5CCNAME'] = paths.IPA_DNS_CCACHE + + options.ca_cert_file = None + options.principal = None + options.force = False + options.password = None + + changed = False + if not os.path.exists(paths.IPA_CA_CRT): + if not allow_repair: + module.fail_json( + msg="%s missing, enable allow_repair to fix it." % + paths.IPA_CA_CRT) + + # Repair missing ca.crt file + try: + os.environ['KRB5_CONFIG'] = env['KRB5_CONFIG'] = "/etc/krb5.conf" + env['KRB5CCNAME'] = os.environ['KRB5CCNAME'] + if NUM_VERSION < 40100: + get_ca_cert(fstore, options, servers[0], basedn) + else: + get_ca_certs(fstore, options, servers[0], basedn, realm) + changed = True + del os.environ['KRB5_CONFIG'] + except errors.FileError as e: + module.fail_json(msg='%s' % e) + except Exception as e: + module.fail_json(msg="Cannot obtain CA certificate\n%s" % e) + + module.exit_json(changed=changed) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaclient/library/ipaclient_fstore.py b/roles/ipaclient/library/ipaclient_fstore.py new file mode 100644 index 0000000..33cef7b --- /dev/null +++ b/roles/ipaclient/library/ipaclient_fstore.py @@ -0,0 +1,79 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaclient_fstore +short description: Backup files using IPA client sysrestore +description: +Backup files using IPA client sysrestore +options: + backup: + description: File to backup + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +- name: Backup /etc/krb5.conf + ipaclient_fstore: + backup: "/etc/krb5.conf" +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_client import ( + paths, sysrestore +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + backup=dict(required=True), + ), + ) + + module._ansible_debug = True + backup = module.params.get('backup') + + fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) + if not fstore.has_file(backup): + fstore.backup_file(backup) + module.exit_json(changed=True) + + module.exit_json(changed=False) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaclient/library/ipaclient_get_facts.py b/roles/ipaclient/library/ipaclient_get_facts.py new file mode 100644 index 0000000..1492199 --- /dev/null +++ b/roles/ipaclient/library/ipaclient_get_facts.py @@ -0,0 +1,191 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +import os +import re +import six +try: + from six.moves.configparser import RawConfigParser +except ImportError: + from ConfigParser import RawConfigParser + +from ansible.module_utils.basic import AnsibleModule + +# pylint: disable=unused-import +try: + from ipalib import api +except ImportError: + HAS_IPALIB = False +else: + HAS_IPALIB = True + from ipaplatform.paths import paths + try: + # FreeIPA >= 4.5 + from ipalib.install import sysrestore + except ImportError: + # FreeIPA 4.4 and older + from ipapython import sysrestore + +try: + import ipaserver +except ImportError: + HAS_IPASERVER = False +else: + HAS_IPASERVER = True + +SERVER_SYSRESTORE_STATE = "/var/lib/ipa/sysrestore/sysrestore.state" +NAMED_CONF = "/etc/named.conf" +VAR_LIB_PKI_TOMCAT = "/var/lib/pki/pki-tomcat" + + +def is_ntpd_configured(): + # ntpd is configured when sysrestore.state contains the line + # [ntpd] + ntpd_conf_section = re.compile('^\s*\[ntpd\]\s*$') + + try: + with open(SERVER_SYSRESTORE_STATE) as f: + for line in f.readlines(): + if ntpd_conf_section.match(line): + return True + return False + except IOError: + return False + + +def is_dns_configured(): + # dns is configured when /etc/named.conf contains the line + # dyndb "ipa" "/usr/lib64/bind/ldap.so" { + bind_conf_section = re.compile('^\s*dyndb\s+"ipa"\s+"[^"]+"\s+{$') + + try: + with open(NAMED_CONF) as f: + for line in f.readlines(): + if bind_conf_section.match(line): + return True + return False + except IOError: + return False + + +def is_dogtag_configured(subsystem): + # ca / kra is configured when the directory + # /var/lib/pki/pki-tomcat/[ca|kra] # exists + available_subsystems = {'ca', 'kra'} + assert subsystem in available_subsystems + + return os.path.isdir(os.path.join(VAR_LIB_PKI_TOMCAT, subsystem)) + + +def is_ca_configured(): + return is_dogtag_configured('ca') + + +def is_kra_configured(): + return is_dogtag_configured('kra') + + +def is_client_configured(): + # IPA Client is configured when /etc/ipa/default.conf exists + # and /var/lib/ipa-client/sysrestore/sysrestore.state exists + + fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) + return (os.path.isfile(paths.IPA_DEFAULT_CONF) and fstore.has_files()) + + +def is_server_configured(): + # IPA server is configured when /etc/ipa/default.conf exists + # and /var/lib/ipa/sysrestore/sysrestore.state exists + return (os.path.isfile(paths.IPA_DEFAULT_CONF) and + os.path.isfile(SERVER_SYSRESTORE_STATE)) + + +def get_ipa_conf(): + # Extract basedn, realm and domain from /etc/ipa/default.conf + parser = RawConfigParser() + parser.read(paths.IPA_DEFAULT_CONF) + basedn = parser.get('global', 'basedn') + realm = parser.get('global', 'realm') + domain = parser.get('global', 'domain') + return dict( + basedn=basedn, + realm=realm, + domain=domain + ) + + +def get_ipa_version(): + try: + from ipapython import version + except ImportError: + return None + else: + version_info = [] + for part in version.VERSION.split('.'): + # DEV versions look like: + # 4.4.90.201610191151GITd852c00 + # 4.4.90.dev201701071308+git2e43db1 + # 4.6.90.pre2 + if part.startswith('dev') or part.startswith('pre') or \ + 'GIT' in part: + version_info.append(part) + else: + version_info.append(int(part)) + + return dict( + api_version=version.API_VERSION, + num_version=version.NUM_VERSION, + vendor_version=version.VENDOR_VERSION, + version=version.VERSION, + version_info=version_info + ) + + +def main(): + module = AnsibleModule( + argument_spec=dict(), + supports_check_mode=True + ) + + # The module does not change anything, meaning that + # check mode is supported + + facts = dict( + packages=dict( + ipalib=HAS_IPALIB, + ipaserver=HAS_IPASERVER, + ), + configured=dict( + client=False, + server=False, + dns=False, + ca=False, + kra=False, + ntpd=False + ) + ) + + if HAS_IPALIB: + if is_client_configured(): + facts['configured']['client'] = True + + facts['version'] = get_ipa_version() + for key, value in six.iteritems(get_ipa_conf()): + facts[key] = value + + if HAS_IPASERVER: + if is_server_configured(): + facts['configured']['server'] = True + facts['configured']['dns'] = is_dns_configured() + facts['configured']['ca'] = is_ca_configured() + facts['configured']['kra'] = is_kra_configured() + facts['configured']['ntpd'] = is_ntpd_configured() + + module.exit_json( + changed=False, + ansible_facts=dict(ipa=facts) + ) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaclient/library/ipaclient_get_otp.py b/roles/ipaclient/library/ipaclient_get_otp.py new file mode 100644 index 0000000..7ec67dd --- /dev/null +++ b/roles/ipaclient/library/ipaclient_get_otp.py @@ -0,0 +1,330 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Florence Blanc-Renaud +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = {'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: ipaclient_get_otp +short description: Manage IPA hosts +description: + Manage hosts in a IPA domain. + The operation needs to be authenticated with Kerberos either by providing + a password or a keytab corresponding to a principal allowed to perform + host operations. +options: + principal: + description: + User Principal allowed to promote replicas and join IPA realm + required: yes + ccache: + description: The local ccache + required: yes + fqdn: + description: + The fully-qualified hostname of the host to add/modify/remove + required: no + certificates: + description: A list of host certificates + required: yes + sshpubkey: + description: The SSH public key for the host + required: yes + ipaddress: + description: The IP address for the host + required: yes + random: + description: Generate a random password to be used in bulk enrollment + required: yes + state: + description: The desired host state + required: yes +author: + - "Florence Blanc-Renaud" +''' + +EXAMPLES = ''' +# Example from Ansible Playbooks +# Add a new host with a random OTP, authenticate using principal/password +- ipaclient_get_otp: + principal: admin + password: MySecretPassword + fqdn: ipaclient.ipa.domain.com + ipaddress: 192.168.100.23 + random: True + register: result_ipaclient_get_otp +''' + +RETURN = ''' +host: + description: the host structure as returned from IPA API + returned: always + type: complex + contains: + dn: + description: the DN of the host entry + type: string + returned: always + fqdn: + description: the fully qualified host name + type: string + returned: always + has_keytab: + description: whether the host entry contains a keytab + type: bool + returned: always + has_password: + description: whether the host entry contains a password + type: bool + returned: always + managedby_host: + description: the list of hosts managing the host + type: list + returned: always + randompassword: + description: the OneTimePassword generated for this host + type: string + returned: changed + certificates: + description: the list of host certificates + type: list + returned: when present + sshpubkey: + description: the SSH public key for the host + type: string + returned: when present + ipaddress: + description: the IP address for the host + type: string + returned: when present +''' + +import os +import six + +from ansible.module_utils.basic import AnsibleModule + +from ipalib import api, errors +from ipaplatform.paths import paths +from ipapython.ipautil import run + +if six.PY3: + unicode = str + + +def get_host_diff(ipa_host, module_host): + """ + Compares two dictionaries containing host attributes and builds a dict + of differences. + + :param ipa_host: the host structure seen from IPA + :param module_host: the target host structure seen from the module params + + :return: a dict representing the host attributes to apply + """ + non_updateable_keys = ['ip_address'] + data = dict() + for key in non_updateable_keys: + if key in module_host: + del module_host[key] + + for key in module_host.keys(): + ipa_value = ipa_host.get(key, None) + module_value = module_host.get(key, None) + if isinstance(ipa_value, list) and not isinstance(module_value, list): + module_value = [module_value] + if isinstance(ipa_value, list) and isinstance(module_value, list): + ipa_value = sorted(ipa_value) + module_value = sorted(module_value) + if ipa_value != module_value: + data[key] = unicode(module_value) + return data + + +def get_module_host(module): + """ + Creates a structure representing the host information + + Reads the module parameters and builds the host structure as expected from + the module + :param module: the ansible module + :returns: a dict representing the host attributes + """ + data = dict() + certificates = module.params.get('certificates') + if certificates: + data['usercertificate'] = certificates + sshpubkey = module.params.get('sshpubkey') + if sshpubkey: + data['ipasshpubkey'] = unicode(sshpubkey) + ipaddress = module.params.get('ipaddress') + if ipaddress: + data['ip_address'] = unicode(ipaddress) + random = module.params.get('random') + if random: + data['random'] = random + return data + + +def ensure_host_present(module, api, ipahost): + """ + Ensures that the host exists in IPA and has the same attributes. + + :param module: the ansible module + :param api: IPA api handle + :param ipahost: the host information present in IPA, can be none if the + host does not exist + """ + fqdn = unicode(module.params.get('fqdn')) + if ipahost: + # Host already present, need to compare the attributes + module_host = get_module_host(module) + diffs = get_host_diff(ipahost, module_host) + + if not diffs: + # Same attributes, success + module.exit_json(changed=False, host=ipahost) + + # Need to modify the host - only if not in check_mode + if module.check_mode: + module.exit_json(changed=True) + + # If we want to create a random password, and the host + # already has Keytab: true, then we need first to run + # ipa host-disable in order to remove OTP and keytab + if module.params.get('random') and ipahost['has_keytab'] is True: + api.Command.host_disable(fqdn) + + result = api.Command.host_mod(fqdn, **diffs) + # Save random password as it is not displayed by host-show + if module.params.get('random'): + randompassword = result['result']['randompassword'] + result = api.Command.host_show(fqdn) + if module.params.get('random'): + result['result']['randompassword'] = randompassword + module.exit_json(changed=True, host=result['result']) + + if not ipahost: + # Need to add the user, only if not in check_mode + if module.check_mode: + module.exit_json(changed=True) + + # Must add the user + module_host = get_module_host(module) + # force creation of host even if there is no DNS record + module_host["force"] = True + result = api.Command.host_add(fqdn, **module_host) + # Save random password as it is not displayed by host-show + if module.params.get('random'): + randompassword = result['result']['randompassword'] + result = api.Command.host_show(fqdn) + if module.params.get('random'): + result['result']['randompassword'] = randompassword + module.exit_json(changed=True, host=result['result']) + + +def ensure_host_absent(module, api, host): + """ + Ensures that the host does not exist in IPA + + :param module: the ansible module + :param api: the IPA API handle + :param host: the host information present in IPA, can be none if the + host does not exist + """ + if not host: + # Nothing to do, host already removed + module.exit_json(changed=False) + + # Need to remove the host - only if not in check_mode + if module.check_mode: + module.exit_json(changed=True, host=host) + + fqdn = unicode(module.params.get('fqdn')) + try: + api.Command.host_del(fqdn) + except Exception as e: + module.fail_json(msg="Failed to remove host: %s" % e) + + module.exit_json(changed=True) + + +def main(): + """ + Main routine for the ansible module. + """ + module = AnsibleModule( + argument_spec=dict( + principal=dict(default='admin'), + ccache=dict(required=False, type='path'), + fqdn=dict(required=True), + certificates=dict(required=False, type='list'), + sshpubkey=dict(required=False), + ipaddress=dict(required=False), + random=dict(default=False, type='bool'), + state=dict(default='present', choices=['present', 'absent']), + ), + supports_check_mode=True, + ) + + principal = module.params.get('principal', 'admin') + ccache = module.params.get('ccache') + fqdn = unicode(module.params.get('fqdn')) + state = module.params.get('state') + + try: + os.environ['KRB5CCNAME'] = ccache + + cfg = dict( + context='ansible_module', + confdir=paths.ETC_IPA, + in_server=False, + debug=False, + verbose=0, + ) + api.bootstrap(**cfg) + api.finalize() + api.Backend.rpcclient.connect() + + try: + result = api.Command.host_show(fqdn, all=True) + host = result['result'] + except errors.NotFound: + host = None + + if state in ['present', 'disabled']: + ensure_host_present(module, api, host) + elif state == 'absent': + ensure_host_absent(module, api, host) + + except Exception as e: + module.fail_json(msg="ipaclient_get_otp module failed : %s" % str(e)) + finally: + run([paths.KDESTROY], raiseonerr=False, env=os.environ) + + module.exit_json(changed=False, host=host) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaclient/library/ipaclient_ipa_conf.py b/roles/ipaclient/library/ipaclient_ipa_conf.py new file mode 100644 index 0000000..4865772 --- /dev/null +++ b/roles/ipaclient/library/ipaclient_ipa_conf.py @@ -0,0 +1,104 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaclient_ipa_conf +short description: Configure ipa.conf +description: + Configure ipa.conf +options: + domain: + description: Primary DNS domain of the IPA deployment + required: no + servers: + description: Fully qualified name of IPA servers to enroll to + required: no + realm: + description: Kerberos realm name of the IPA deployment + required: no + hostname: + description: Fully qualified name of this host + required: no + basedn: + description: The basedn of the IPA server (of the form dc=example,dc=com) + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +# Backup and set hostname +- name: Backup and set hostname + ipaclient_ipa_conf: + server: server.example.com + domain: example.com + realm: EXAMPLE.COM + hostname: client1.example.com + basedn: dc=example,dc=com +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_client import ( + paths, sysrestore, configure_ipa_conf +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + domain=dict(required=True, default=None), + servers=dict(required=True, type='list', default=None), + realm=dict(required=True, default=None), + hostname=dict(required=True, default=None), + basedn=dict(required=True), + ), + supports_check_mode=True, + ) + + module._ansible_debug = True + servers = module.params.get('servers') + domain = module.params.get('domain') + realm = module.params.get('realm') + hostname = module.params.get('hostname') + basedn = module.params.get('basedn') + + fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) + + configure_ipa_conf(fstore, basedn, realm, domain, servers, hostname) + + module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaclient/library/ipaclient_join.py b/roles/ipaclient/library/ipaclient_join.py new file mode 100644 index 0000000..d0eca2b --- /dev/null +++ b/roles/ipaclient/library/ipaclient_join.py @@ -0,0 +1,343 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaclient_join +short description: + Join a machine to an IPA realm and get a keytab for the host service + principal +description: + Join a machine to an IPA realm and get a keytab for the host service + principal +options: + servers: + description: Fully qualified name of IPA servers to enroll to + required: no + domain: + description: Primary DNS domain of the IPA deployment + required: no + realm: + description: Kerberos realm name of the IPA deployment + required: no + hostname: + description: Fully qualified name of this host + required: no + kdc: + description: The name or address of the host running the KDC + required: no + basedn: + description: The basedn of the IPA server (of the form dc=example,dc=com) + required: no + principal: + description: + User Principal allowed to promote replicas and join IPA realm + required: yes + password: + description: Admin user kerberos password + required: yes + keytab: + description: Path to backed up keytab from previous enrollment + required: yes + admin_keytab: + description: The path to a local admin keytab + required: yes + ca_cert_file: + description: + A CA certificate to use. Do not acquire the IPA CA certificate via + automated means + required: yes + force_join: + description: Force client enrollment even if already enrolled + required: yes + kinit_attempts: + description: Repeat the request for host Kerberos ticket X times + required: yes + debug: + description: Turn on extra debugging + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +# Join IPA to get the keytab +- name: Join IPA in force mode with maximum 5 kinit attempts + ipaclient_join: + servers: ["server1.example.com","server2.example.com"] + domain: example.com + realm: EXAMPLE.COM + kdc: server1.example.com + basedn: dc=example,dc=com + hostname: client1.example.com + principal: admin + password: MySecretPassword + force_join: yes + kinit_attempts: 5 + +# Join IPA to get the keytab using ipadiscovery return values +- name: Join IPA + ipaclient_join: + servers: "{{ ipadiscovery.servers }}" + domain: "{{ ipadiscovery.domain }}" + realm: "{{ ipadiscovery.realm }}" + kdc: "{{ ipadiscovery.kdc }}" + basedn: "{{ ipadiscovery.basedn }}" + hostname: "{{ ipadiscovery.hostname }}" + principal: admin + password: MySecretPassword +''' + +RETURN = ''' +already_joined: + description: The flag describes if the host is arelady joined. + returned: always + type: bool +''' + +import os +import tempfile + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_client import ( + SECURE_PATH, sysrestore, paths, options, configure_krb5_conf, + realm_to_suffix, kinit_keytab, GSSError, kinit_password, NUM_VERSION, + get_ca_cert, get_ca_certs, errors, run +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + servers=dict(required=True, type='list'), + domain=dict(required=True), + realm=dict(required=True), + hostname=dict(required=True), + kdc=dict(required=True), + basedn=dict(required=True), + principal=dict(required=False), + password=dict(required=False, no_log=True), + keytab=dict(required=False), + admin_keytab=dict(required=False), + ca_cert_file=dict(required=False), + force_join=dict(required=False, type='bool'), + kinit_attempts=dict(required=False, type='int', default=5), + debug=dict(required=False, type='bool'), + ), + supports_check_mode=True, + ) + + module._ansible_debug = True + servers = module.params.get('servers') + domain = module.params.get('domain') + realm = module.params.get('realm') + hostname = module.params.get('hostname') + basedn = module.params.get('basedn') + kdc = module.params.get('kdc') + force_join = module.params.get('force_join') + principal = module.params.get('principal') + password = module.params.get('password') + keytab = module.params.get('keytab') + admin_keytab = module.params.get('admin_keytab') + ca_cert_file = module.params.get('ca_cert_file') + kinit_attempts = module.params.get('kinit_attempts') + debug = module.params.get('debug') + + if password is not None and keytab is not None: + module.fail_json(msg="Password and keytab cannot be used together") + + if password is None and admin_keytab is None: + module.fail_json(msg="Password or admin_keytab is needed") + + client_domain = hostname[hostname.find(".")+1:] + nolog = tuple() + env = {'PATH': SECURE_PATH} + fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) + host_principal = 'host/%s@%s' % (hostname, realm) + sssd = True + + options.ca_cert_file = ca_cert_file + options.principal = principal + options.force = False + options.password = password + + ccache_dir = None + changed = False + already_joined = False + try: + (krb_fd, krb_name) = tempfile.mkstemp() + os.close(krb_fd) + configure_krb5_conf( + cli_realm=realm, + cli_domain=domain, + cli_server=servers, + cli_kdc=kdc, + dnsok=False, + filename=krb_name, + client_domain=client_domain, + client_hostname=hostname, + configure_sssd=sssd, + force=False) + env['KRB5_CONFIG'] = krb_name + ccache_dir = tempfile.mkdtemp(prefix='krbcc') + ccache_name = os.path.join(ccache_dir, 'ccache') + join_args = [paths.SBIN_IPA_JOIN, + "-s", servers[0], + "-b", str(realm_to_suffix(realm)), + "-h", hostname] + if debug: + join_args.append("-d") + env['XMLRPC_TRACE_CURL'] = 'yes' + if force_join: + join_args.append("-f") + if principal is not None: + if principal.find('@') == -1: + principal = '%s@%s' % (principal, realm) + if admin_keytab: + join_args.append("-f") + if not os.path.exists(admin_keytab): + module.fail_json( + msg="Keytab file could not be found: %s" % + admin_keytab) + try: + kinit_keytab(principal, + admin_keytab, + ccache_name, + config=krb_name, + attempts=kinit_attempts) + except GSSError as e: + module.fail_json( + msg="Kerberos authentication failed: %s" % str(e)) + else: + try: + kinit_password(principal, password, ccache_name, + config=krb_name) + except RuntimeError as e: + module.fail_json( + msg="Kerberos authentication failed: {}".format(e)) + + elif keytab: + join_args.append("-f") + if os.path.exists(keytab): + try: + kinit_keytab(host_principal, + keytab, + ccache_name, + config=krb_name, + attempts=kinit_attempts) + except GSSError as e: + module.fail_json( + msg="Kerberos authentication failed: {}".format(e)) + else: + module.fail_json( + msg="Keytab file could not be found: {}".format(keytab)) + + elif password: + join_args.append("-w") + join_args.append(password) + nolog = (password,) + + env['KRB5CCNAME'] = os.environ['KRB5CCNAME'] = ccache_name + # Get the CA certificate + try: + os.environ['KRB5_CONFIG'] = env['KRB5_CONFIG'] + if NUM_VERSION < 40100: + get_ca_cert(fstore, options, servers[0], basedn) + else: + get_ca_certs(fstore, options, servers[0], basedn, realm) + del os.environ['KRB5_CONFIG'] + except errors.FileError as e: + module.fail_json(msg='%s' % e) + except Exception as e: + module.fail_json(msg="Cannot obtain CA certificate\n%s" % e) + + # Now join the domain + result = run( + join_args, raiseonerr=False, env=env, nolog=nolog, + capture_error=True) + stderr = result.error_output + + if result.returncode != 0: + if result.returncode == 13: + already_joined = True + module.log("Host is already joined") + else: + if principal: + run([paths.KDESTROY], raiseonerr=False, env=env) + module.fail_json(msg="Joining realm failed: %s" % stderr) + else: + changed = True + module.log("Enrolled in IPA realm %s" % realm) + + # Fail for missing krb5.keytab on already joined host + if already_joined and not os.path.exists(paths.KRB5_KEYTAB): + module.fail_json(msg="krb5.keytab missing! Retry with " + "ipaclient_force_join=yes to generate a new one.") + + if principal: + run([paths.KDESTROY], raiseonerr=False, env=env) + + # Obtain the TGT. We do it with the temporary krb5.conf, sot + # tha only the KDC we're installing under is contacted. + # Other KDCs might not have replicated the principal yet. + # Once we have the TGT, it's usable on any server. + try: + kinit_keytab(host_principal, paths.KRB5_KEYTAB, + paths.IPA_DNS_CCACHE, + config=krb_name, + attempts=kinit_attempts) + env['KRB5CCNAME'] = os.environ['KRB5CCNAME'] = paths.IPA_DNS_CCACHE + except GSSError as e: + # failure to get ticket makes it impossible to login and + # bind from sssd to LDAP, abort installation + module.fail_json(msg="Failed to obtain host TGT: %s" % e) + + finally: + try: + os.remove(krb_name) + except OSError: + module.fail_json(msg="Could not remove %s" % krb_name) + if ccache_dir is not None: + try: + os.rmdir(ccache_dir) + except OSError: + pass + if os.path.exists(krb_name + ".ipabkp"): + try: + os.remove(krb_name + ".ipabkp") + except OSError: + module.fail_json(msg="Could not remove %s.ipabkp" % krb_name) + + module.exit_json(changed=changed, + already_joined=already_joined) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaclient/library/ipaclient_set_hostname.py b/roles/ipaclient/library/ipaclient_set_hostname.py new file mode 100644 index 0000000..be5576e --- /dev/null +++ b/roles/ipaclient/library/ipaclient_set_hostname.py @@ -0,0 +1,82 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaclient_set_hostname +short description: Backup and set hostname +description: + Backup and set hostname +options: + hostname: + description: Fully qualified name of this host + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +# Backup and set hostname +- name: Backup and set hostname + ipaclient_set_hostname: + hostname: client1.example.com +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_client import ( + sysrestore, paths, tasks +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + hostname=dict(required=True), + ), + supports_check_mode=True, + ) + + module._ansible_debug = True + hostname = module.params.get('hostname') + + fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) + statestore = sysrestore.StateFile(paths.IPA_CLIENT_SYSRESTORE) + + tasks.backup_hostname(fstore, statestore) + tasks.set_hostname(hostname) + + module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaclient/library/ipaclient_setup_automount.py b/roles/ipaclient/library/ipaclient_setup_automount.py new file mode 100644 index 0000000..e2bc340 --- /dev/null +++ b/roles/ipaclient/library/ipaclient_setup_automount.py @@ -0,0 +1,93 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaclient_setup_automount +short description: Setup automount for IPA client +description: + Setup automount for IPA client +options: + servers: + description: Fully qualified name of IPA servers to enroll to + required: no + sssd: + description: The installer sssd setting + required: yes + automount_location: + description: The automount location + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +- name: IPA extras configurations + ipaclient_setup_automount: + servers: ["server1.example.com","server2.example.com"] +''' + +RETURN = ''' +''' + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_client import ( + options, configure_automount +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + servers=dict(required=True, type='list'), + sssd=dict(required=False, type='bool', default='yes'), + automount_location=dict(required=False, default=None), + ), + supports_check_mode=True, + ) + + # os.environ['KRB5CCNAME'] = paths.IPA_DNS_CCACHE + + module._ansible_debug = True + options.servers = module.params.get('servers') + options.server = options.servers + options.sssd = module.params.get('sssd') + options.automount_location = module.params.get('automount_location') + options.location = options.automount_location + + if options.automount_location: + configure_automount(options) + + module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaclient/library/ipaclient_setup_firefox.py b/roles/ipaclient/library/ipaclient_setup_firefox.py new file mode 100644 index 0000000..71db33e --- /dev/null +++ b/roles/ipaclient/library/ipaclient_setup_firefox.py @@ -0,0 +1,88 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaclient_setup_firefox +short description: Setup firefox for IPA client +description: + Setup firefox for IPA client +options: + domain: + description: Primary DNS domain of the IPA deployment + required: no + firefox_dir: + description: + Specify directory where Firefox is installed (for example + '/usr/lib/firefox') + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +- name: Setup firefox for IPA client + ipaclient_setup_firefox: + servers: ["server1.example.com","server2.example.com"] + domain: example.com + firefox_dir: /usr/lib/firefox +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_client import ( + sysrestore, paths, options, configure_firefox +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + domain=dict(required=True), + firefox_dir=dict(required=False), + ), + supports_check_mode=True, + ) + + module._ansible_debug = True + domain = module.params.get('domain') + options.firefox_dir = module.params.get('firefox_dir') + + statestore = sysrestore.StateFile(paths.IPA_CLIENT_SYSRESTORE) + + configure_firefox(options, statestore, domain) + + module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaclient/library/ipaclient_setup_krb5.py b/roles/ipaclient/library/ipaclient_setup_krb5.py new file mode 100644 index 0000000..367539b --- /dev/null +++ b/roles/ipaclient/library/ipaclient_setup_krb5.py @@ -0,0 +1,152 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaclient_setup_krb5 +short description: Setup krb5 for IPA client +description: + Setup krb5 for IPA client +options: + domain: + description: Primary DNS domain of the IPA deployment + required: yes + servers: + description: Fully qualified name of IPA servers to enroll to + required: yes + realm: + description: Kerberos realm name of the IPA deployment + required: yes + hostname: + description: Fully qualified name of this host + required: yes + kdc: + description: The name or address of the host running the KDC + required: yes + dnsok: + description: The installer dnsok setting + required: yes + client_domain: + description: Primary DNS domain of the IPA deployment + required: yes + sssd: + description: The installer sssd setting + required: yes + force: + description: Installer force parameter + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +# Backup and set hostname +- name: Backup and set hostname + ipaclient_setup_krb5: + server: + domain: + realm: + hostname: client1.example.com +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_client import ( + sysrestore, paths, configure_krb5_conf, logger +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + domain=dict(required=False, default=None), + servers=dict(required=False, type='list', default=None), + realm=dict(required=False, default=None), + hostname=dict(required=False, default=None), + kdc=dict(required=False, default=None), + dnsok=dict(required=False, type='bool', default=False), + client_domain=dict(required=False, default=None), + sssd=dict(required=False, type='bool', default=False), + force=dict(required=False, type='bool', default=False), + # on_master=dict(required=False, type='bool', default=False), + ), + supports_check_mode=True, + ) + + module._ansible_debug = True + servers = module.params.get('servers') + domain = module.params.get('domain') + realm = module.params.get('realm') + hostname = module.params.get('hostname') + kdc = module.params.get('kdc') + dnsok = module.params.get('dnsok') + client_domain = module.params.get('client_domain') + sssd = module.params.get('sssd') + force = module.params.get('force') + # on_master = module.params.get('on_master') + + fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) + + # if options.on_master: + # # If on master assume kerberos is already configured properly. + # # Get the host TGT. + # try: + # kinit_keytab(host_principal, paths.KRB5_KEYTAB, CCACHE_FILE, + # attempts=options.kinit_attempts) + # os.environ['KRB5CCNAME'] = CCACHE_FILE + # except gssapi.exceptions.GSSError as e: + # logger.error("Failed to obtain host TGT: %s", e) + # raise ScriptError(rval=CLIENT_INSTALL_ERROR) + # else: + + # Configure krb5.conf + fstore.backup_file(paths.KRB5_CONF) + configure_krb5_conf( + cli_realm=realm, + cli_domain=domain, + cli_server=servers, + cli_kdc=kdc, + dnsok=dnsok, + filename=paths.KRB5_CONF, + client_domain=client_domain, + client_hostname=hostname, + configure_sssd=sssd, + force=force) + + logger.info( + "Configured /etc/krb5.conf for IPA realm %s", realm) + + module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaclient/library/ipaclient_setup_nis.py b/roles/ipaclient/library/ipaclient_setup_nis.py new file mode 100644 index 0000000..1f1ba00 --- /dev/null +++ b/roles/ipaclient/library/ipaclient_setup_nis.py @@ -0,0 +1,92 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaclient_setup_nis +short description: Setup NIS for IPA client +description: + Setup NIS for IPA client +options: + domain: + description: Primary DNS domain of the IPA deployment + required: no + nisdomain: + description: The NIS domain name + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +- name: Setup NIS for IPA client + ipaclient_setup_nis: + domain: example.com +''' + +RETURN = ''' +''' + +import inspect + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_client import ( + options, sysrestore, paths, configure_nisdomain +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + domain=dict(required=True), + nisdomain=dict(required=False), + ), + supports_check_mode=True, + ) + + module._ansible_debug = True + domain = module.params.get('domain') + options.nisdomain = module.params.get('nisdomain') + + statestore = sysrestore.StateFile(paths.IPA_CLIENT_SYSRESTORE) + + argspec = inspect.getargspec(configure_nisdomain) + if "statestore" not in argspec.args: + # NUM_VERSION < 40500: + configure_nisdomain(options=options, domain=domain) + else: + configure_nisdomain(options=options, domain=domain, + statestore=statestore) + + module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaclient/library/ipaclient_setup_nss.py b/roles/ipaclient/library/ipaclient_setup_nss.py new file mode 100644 index 0000000..d43eeb9 --- /dev/null +++ b/roles/ipaclient/library/ipaclient_setup_nss.py @@ -0,0 +1,502 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaclient_setup_nss +short description: Create IPA client NSS database +description: +Create IPA NSS database +options: + servers: + description: Fully qualified name of IPA servers to enroll to + required: no + domain: + description: Primary DNS domain of the IPA deployment + required: no + realm: + description: Kerberos realm name of the IPA deployment + required: no + hostname: + description: Fully qualified name of this host + required: no + basedn: + description: The basedn of the IPA server (of the form dc=example,dc=com) + required: no + principal: + description: + User Principal allowed to promote replicas and join IPA realm + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: no + ca_enabled: + description: Whether the Certificate Authority is enabled or not + required: no + mkhomedir: + description: Create home directories for users on their first login + required: yes + on_master: + description: Whether the configuration is done on the master or not + required: yes + dnsok: + description: The installer dnsok setting + required: yes + enable_dns_updates: + description: + Configures the machine to attempt dns updates when the ip address + changes + required: yes + all_ip_addresses: + description: + All routable IP addresses configured on any interface will be added + to DNS + required: yes + ip_addresses: + description: List of Master Server IP Addresses + required: yes + request_cert: + description: Request certificate for the machine + required: yes + preserve_sssd: + description: Preserve old SSSD configuration if possible + required: yes + no_ssh: + description: Do not configure OpenSSH client + required: yes + no_sshd: + description: Do not configure OpenSSH server + required: yes + no_sudo: + description: Do not configure SSSD as data source for sudo + required: yes + fixed_primary: + description: Configure sssd to use fixed server as primary IPA server + required: yes + permit: + description: Disable access rules by default, permit all access + required: yes + no_krb5_offline_passwords: + description: + Configure SSSD not to store user password when the server is offline + required: yes + no_dns_sshfp: + description: Do not automatically create DNS SSHFP records + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +- name: Create IPA client NSS database + ipaclient_setup_nss: + servers: ["server1.example.com","server2.example.com"] + domain: example.com + realm: EXAMPLE.COM + basedn: dc=example,dc=com + hostname: client1.example.com + subject_base: O=EXAMPLE.COM + principal: admin + ca_enabled: yes +''' + +RETURN = ''' +''' + +import os +import time +import inspect + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_client import ( + options, sysrestore, paths, ansible_module_get_parsed_ip_addresses, + api, errors, create_ipa_nssdb, ipautil, ScriptError, CLIENT_INSTALL_ERROR, + get_certs_from_ldap, DN, certstore, x509, logger, certdb, + CalledProcessError, tasks, client_dns, configure_certmonger, services, + update_ssh_keys, save_state, configure_ldap_conf, configure_nslcd_conf, + nosssd_files, configure_openldap_conf, hardcode_ldap_server +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + servers=dict(required=True, type='list'), + domain=dict(required=True), + realm=dict(required=True), + hostname=dict(required=True), + basedn=dict(required=True), + principal=dict(required=False), + subject_base=dict(required=True), + ca_enabled=dict(required=True, type='bool'), + mkhomedir=dict(required=False, type='bool'), + on_master=dict(required=False, type='bool'), + dnsok=dict(required=False, type='bool', default=False), + + enable_dns_updates=dict(required=False, type='bool'), + all_ip_addresses=dict(required=False, type='bool', default=False), + ip_addresses=dict(required=False, type='list', default=None), + request_cert=dict(required=False, type='bool', default=False), + preserve_sssd=dict(required=False, type='bool'), + no_ssh=dict(required=False, type='bool'), + no_sshd=dict(required=False, type='bool'), + no_sudo=dict(required=False, type='bool'), + fixed_primary=dict(required=False, type='bool'), + permit=dict(required=False, type='bool'), + no_krb5_offline_passwords=dict(required=False, type='bool'), + no_dns_sshfp=dict(required=False, type='bool', default=False), + ), + supports_check_mode=True, + ) + + module._ansible_debug = True + cli_server = module.params.get('servers') + cli_realm = module.params.get('realm') + hostname = module.params.get('hostname') + cli_basedn = module.params.get('basedn') + cli_domain = module.params.get('domain') + options.principal = module.params.get('principal') + subject_base = module.params.get('subject_base') + ca_enabled = module.params.get('ca_enabled') + options.mkhomedir = module.params.get('mkhomedir') + options.on_master = module.params.get('on_master') + dnsok = module.params.get('dnsok') + + fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) + statestore = sysrestore.StateFile(paths.IPA_CLIENT_SYSRESTORE) + + os.environ['KRB5CCNAME'] = paths.IPA_DNS_CCACHE + + options.dns_updates = module.params.get('enable_dns_updates') + options.all_ip_addresses = module.params.get('all_ip_addresses') + options.ip_addresses = ansible_module_get_parsed_ip_addresses(module) + options.request_cert = module.params.get('request_cert') + options.hostname = hostname + options.host_name = hostname + options.preserve_sssd = module.params.get('preserve_sssd') + options.no_ssh = module.params.get('no_ssh') + options.conf_ssh = not options.no_ssh + options.no_sshd = module.params.get('no_sshd') + options.conf_sshd = not options.no_sshd + options.no_sudo = module.params.get('no_sudo') + options.conf_sudo = not options.no_sudo + options.primary = module.params.get('fixed_primary') + options.permit = module.params.get('permit') + options.no_krb5_offline_passwords = module.params.get( + 'no_krb5_offline_passwords') + options.krb5_offline_passwords = not options.no_krb5_offline_passwords + options.no_dns_sshfp = module.params.get('no_dns_sshfp') + options.create_sshfp = not options.no_dns_sshfp + options.no_sssd = False + options.sssd = not options.no_sssd + options.no_ac = False + + CCACHE_FILE = paths.IPA_DNS_CCACHE + + api.bootstrap(context='cli_installer', + confdir=paths.ETC_IPA, + debug=False, + delegate=False) + api.finalize() + + api.Backend.rpcclient.connect() + try: + api.Backend.rpcclient.forward('ping') + except errors.KerberosError: + # Cannot connect to the server due to Kerberos error, trying with + # delegate=True + api.Backend.rpcclient.disconnect() + api.Backend.rpcclient.connect(delegate=True) + api.Backend.rpcclient.forward('ping') + + ########################################################################## + + try: + + # Create IPA NSS database + try: + create_ipa_nssdb() + except ipautil.CalledProcessError as e: + raise ScriptError( + "Failed to create IPA NSS database: %s" % e, + rval=CLIENT_INSTALL_ERROR) + + # Get CA certificates from the certificate store + try: + ca_certs = get_certs_from_ldap(cli_server[0], cli_basedn, + cli_realm, ca_enabled) + except errors.NoCertificateError: + if ca_enabled: + ca_subject = DN(('CN', 'Certificate Authority'), subject_base) + else: + ca_subject = None + ca_certs = certstore.make_compat_ca_certs(ca_certs, cli_realm, + ca_subject) + ca_certs_trust = [(c, n, + certstore.key_policy_to_trust_flags(t, True, u)) + for (c, n, t, u) in ca_certs] + + if hasattr(paths, "KDC_CA_BUNDLE_PEM"): + x509.write_certificate_list( + [c for c, n, t, u in ca_certs if t is not False], + paths.KDC_CA_BUNDLE_PEM, + # mode=0o644 + ) + if hasattr(paths, "CA_BUNDLE_PEM"): + x509.write_certificate_list( + [c for c, n, t, u in ca_certs if t is not False], + paths.CA_BUNDLE_PEM, + # mode=0o644 + ) + + # Add the CA certificates to the IPA NSS database + logger.debug("Adding CA certificates to the IPA NSS database.") + ipa_db = certdb.NSSDatabase(paths.IPA_NSSDB_DIR) + for cert, nickname, trust_flags in ca_certs_trust: + try: + ipa_db.add_cert(cert, nickname, trust_flags) + except CalledProcessError: + raise ScriptError( + "Failed to add %s to the IPA NSS database." % nickname, + rval=CLIENT_INSTALL_ERROR) + + # Add the CA certificates to the platform-dependant systemwide CA + # store + tasks.insert_ca_certs_into_systemwide_ca_store(ca_certs) + + if not options.on_master: + client_dns(cli_server[0], hostname, options) + configure_certmonger(fstore, subject_base, cli_realm, hostname, + options, ca_enabled) + + if hasattr(paths, "SSH_CONFIG_DIR"): + ssh_config_dir = paths.SSH_CONFIG_DIR + else: + ssh_config_dir = services.knownservices.sshd.get_config_dir() + update_ssh_keys(hostname, ssh_config_dir, options.create_sshfp) + + try: + os.remove(CCACHE_FILE) + except Exception: + pass + + argspec_save_state = inspect.getargspec(save_state) + + # Name Server Caching Daemon. Disable for SSSD, use otherwise + # (if installed) + nscd = services.knownservices.nscd + if nscd.is_installed(): + if "statestore" in argspec_save_state.args: + save_state(nscd, statestore) + else: + save_state(nscd) + nscd_service_action = None + try: + if options.sssd: + nscd_service_action = 'stop' + nscd.stop() + else: + nscd_service_action = 'restart' + nscd.restart() + except Exception: + logger.warning( + "Failed to %s the %s daemon", + nscd_service_action, nscd.service_name) + if not options.sssd: + logger.warning( + "Caching of users/groups will not be available") + + try: + if options.sssd: + nscd.disable() + else: + nscd.enable() + except Exception: + if not options.sssd: + logger.warning( + "Failed to configure automatic startup of the %s " + "daemon", + nscd.service_name) + logger.info( + "Caching of users/groups will not be " + "available after reboot") + else: + logger.warning( + "Failed to disable %s daemon. Disable it manually.", + nscd.service_name) + + else: + # this is optional service, just log + if not options.sssd: + logger.info( + "%s daemon is not installed, skip configuration", + nscd.service_name) + + nslcd = services.knownservices.nslcd + if nslcd.is_installed(): + if "statestore" in argspec_save_state.args: + save_state(nslcd, statestore) + else: + save_state(nslcd) + + retcode, conf = (0, None) + + if not options.no_ac: + # Modify nsswitch/pam stack + argspec = inspect.getargspec(tasks.modify_nsswitch_pam_stack) + if "sudo" in argspec.args: + tasks.modify_nsswitch_pam_stack( + sssd=options.sssd, + mkhomedir=options.mkhomedir, + statestore=statestore, + sudo=options.conf_sudo + ) + else: + tasks.modify_nsswitch_pam_stack( + sssd=options.sssd, + mkhomedir=options.mkhomedir, + statestore=statestore + ) + + if hasattr(paths, "AUTHSELECT") and paths.AUTHSELECT is not None: + # authselect is used + # if mkhomedir, make sure oddjobd is enabled and started + if options.mkhomedir: + oddjobd = services.service('oddjobd', api) + running = oddjobd.is_running() + enabled = oddjobd.is_enabled() + statestore.backup_state('oddjobd', 'running', running) + statestore.backup_state('oddjobd', 'enabled', enabled) + try: + if not enabled: + oddjobd.enable() + if not running: + oddjobd.start() + except Exception as e: + logger.critical("Unable to start oddjobd: %s", str(e)) + + logger.info("%s enabled", "SSSD" if options.sssd else "LDAP") + + if options.sssd: + sssd = services.service('sssd', api) + try: + sssd.restart() + except CalledProcessError: + logger.warning("SSSD service restart was unsuccessful.") + + try: + sssd.enable() + except CalledProcessError as e: + logger.warning( + "Failed to enable automatic startup of the SSSD " + "daemon: %s", e) + + if not options.sssd: + tasks.modify_pam_to_use_krb5(statestore) + logger.info("Kerberos 5 enabled") + + # Update non-SSSD LDAP configuration after authconfig calls as it + # would change its configuration otherways + if not options.sssd: + for configurer in [configure_ldap_conf, configure_nslcd_conf]: + (retcode, conf, filenames) = configurer( + fstore, cli_basedn, cli_realm, + cli_domain, cli_server, dnsok, + options, nosssd_files[configurer.__name__]) + if retcode: + raise ScriptError(rval=CLIENT_INSTALL_ERROR) + if conf: + logger.info( + "%s configured using configuration file(s) %s", + conf, filenames) + + if configure_openldap_conf(fstore, cli_basedn, cli_server): + logger.info("Configured /etc/openldap/ldap.conf") + else: + logger.info("Failed to configure /etc/openldap/ldap.conf") + + # Check that nss is working properly + if not options.on_master: + user = options.principal + if user is None: + user = "admin@%s" % cli_domain + logger.info("Principal is not set when enrolling with OTP" + "; using principal '%s' for 'getent passwd'", + user) + elif '@' not in user: + user = "%s@%s" % (user, cli_domain) + n = 0 + found = False + # Loop for up to 10 seconds to see if nss is working properly. + # It can sometimes take a few seconds to connect to the remote + # provider. + # Particulary, SSSD might take longer than 6-8 seconds. + if hasattr(paths, "GETENT"): + getent_cmd = paths.GETENT + else: + getent_cmd = '/usr/bin/getent' + while n < 10 and not found: + try: + ipautil.run([getent_cmd, "passwd", user]) + found = True + except Exception: + time.sleep(1) + n = n + 1 + + if not found: + logger.error("Unable to find '%s' user with 'getent " + "passwd %s'!", user.split("@")[0], user) + if conf: + logger.info("Recognized configuration: %s", conf) + else: + logger.error( + "Unable to reliably detect " + "configuration. Check NSS setup manually.") + + try: + hardcode_ldap_server(cli_server) + except Exception as e: + logger.error( + "Adding hardcoded server name to " + "/etc/ldap.conf failed: %s", str(e)) + + except ScriptError as e: + module.fail_json(msg=str(e)) + + ########################################################################## + + module.exit_json(changed=True, + ca_enabled_ra=ca_enabled) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaclient/library/ipaclient_setup_ntp.py b/roles/ipaclient/library/ipaclient_setup_ntp.py new file mode 100644 index 0000000..acc4855 --- /dev/null +++ b/roles/ipaclient/library/ipaclient_setup_ntp.py @@ -0,0 +1,167 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaclient_setup_ntp +short description: Setup NTP for IPA client +description: + Setup NTP for IPA client +options: + ntp_servers: + description: ntp servers to use + required: yes + ntp_pool: + description: ntp server pool to use + required: yes + no_ntp: + description: Do not configure ntp + required: yes + on_master: + description: Whether the configuration is done on the master or not + required: yes + servers: + description: Fully qualified name of IPA servers to enroll to + required: yes + domain: + description: Primary DNS domain of the IPA deployment + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import inspect + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_client import ( + options, sysrestore, paths, sync_time, logger, ipadiscovery, + timeconf +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + # basic + ntp_servers=dict(required=False, type='list', default=None), + ntp_pool=dict(required=False, default=None), + no_ntp=dict(required=False, type='bool', default=False), + # force_ntpd=dict(required=False, type='bool', default=False), + on_master=dict(required=False, type='bool', default=False), + # additional + servers=dict(required=False, type='list', default=None), + domain=dict(required=False, default=None), + ), + supports_check_mode=True, + ) + + # module._ansible_debug = True + options.ntp_servers = module.params.get('ntp_servers') + options.ntp_pool = module.params.get('ntp_pool') + options.no_ntp = module.params.get('no_ntp') + # options.force_ntpd = module.params.get('force_ntpd') + options.on_master = module.params.get('on_master') + cli_server = module.params.get('servers') + cli_domain = module.params.get('domain') + + options.conf_ntp = not options.no_ntp + options.debug = False + + fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) + statestore = sysrestore.StateFile(paths.IPA_CLIENT_SYSRESTORE) + + synced_ntp = False + if sync_time is not None: + if options.conf_ntp: + # Attempt to configure and sync time with NTP server (chrony). + argspec = inspect.getargspec(sync_time) + if "options" not in argspec.args: + synced_ntp = sync_time(options.ntp_servers, options.ntp_pool, + fstore, statestore) + else: + synced_ntp = sync_time(options, fstore, statestore) + elif options.on_master: + # If we're on master skipping the time sync here because it was + # done in ipa-server-install + logger.info( + "Skipping attempt to configure and synchronize time with" + " chrony server as it has been already done on master.") + else: + logger.info("Skipping chrony configuration") + + else: + ntp_srv_servers = [] + if not options.on_master and options.conf_ntp: + # Attempt to sync time with IPA server. + # If we're skipping NTP configuration, we also skip the time sync + # here. + # We assume that NTP servers are discoverable through SRV records + # in the DNS. + # If that fails, we try to sync directly with IPA server, + # assuming it runs NTP + logger.info('Synchronizing time with KDC...') + ds = ipadiscovery.IPADiscovery() + ntp_srv_servers = ds.ipadns_search_srv(cli_domain, '_ntp._udp', + None, break_on_first=False) + synced_ntp = False + ntp_servers = ntp_srv_servers + + # use user specified NTP servers if there are any + if options.ntp_servers: + ntp_servers = options.ntp_servers + + for s in ntp_servers: + synced_ntp = timeconf.synconce_ntp(s, options.debug) + if synced_ntp: + break + + if not synced_ntp and not options.ntp_servers: + synced_ntp = timeconf.synconce_ntp(cli_server[0], + options.debug) + if not synced_ntp: + module.warn( + "Unable to sync time with NTP " + "server, assuming the time is in sync. Please check " + "that 123 UDP port is opened.") + else: + logger.info('Skipping synchronizing time with NTP server.') + + # Done + module.exit_json(changed=synced_ntp) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaclient/library/ipaclient_setup_ssh.py b/roles/ipaclient/library/ipaclient_setup_ssh.py new file mode 100644 index 0000000..2347c85 --- /dev/null +++ b/roles/ipaclient/library/ipaclient_setup_ssh.py @@ -0,0 +1,114 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaclient_setup_ssh +short description: Configure ssh and sshd for IPA client +description: + Configure ssh and sshd for IPA client +options: + servers: + description: Fully qualified name of IPA servers to enroll to + required: no + no_ssh: + description: Do not configure OpenSSH client + required: yes + ssh_trust_dns: + description: Configure OpenSSH client to trust DNS SSHFP records + required: yes + no_sshd: + description: Do not configure OpenSSH server + required: yes + sssd: + description: The installer sssd setting + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +- name: Configure ssh and sshd for IPA client + ipaclient_setup_ssh: + servers: ["server1.example.com","server2.example.com"] + ssh: yes + sshd: yes + sssd: yes +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_client import ( + options, sysrestore, paths, configure_ssh_config, configure_sshd_config +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + servers=dict(required=True, type='list'), + no_ssh=dict(required=False, type='bool', default='no'), + ssh_trust_dns=dict(required=False, type='bool', default='no'), + no_sshd=dict(required=False, type='bool', default='no'), + sssd=dict(required=False, type='bool', default='no'), + ), + supports_check_mode=True, + ) + + module._ansible_debug = True + options.servers = module.params.get('servers') + options.server = options.servers + options.no_ssh = module.params.get('no_ssh') + options.conf_ssh = not options.no_ssh + options.trust_sshfp = module.params.get('ssh_trust_dns') + options.no_sshd = module.params.get('no_sshd') + options.conf_sshd = not options.no_sshd + options.sssd = module.params.get('sssd') + + fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) + + # os.environ['KRB5CCNAME'] = paths.IPA_DNS_CCACHE + + changed = False + if options.conf_ssh: + configure_ssh_config(fstore, options) + changed = True + + if options.conf_sshd: + configure_sshd_config(fstore, options) + changed = True + + module.exit_json(changed=changed) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaclient/library/ipaclient_setup_sssd.py b/roles/ipaclient/library/ipaclient_setup_sssd.py new file mode 100644 index 0000000..2afbdfb --- /dev/null +++ b/roles/ipaclient/library/ipaclient_setup_sssd.py @@ -0,0 +1,168 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaclient_setup_ssd +short description: Setup sssd for IPA client +description: + Setup sssd for IPA client +options: + servers: + description: Fully qualified name of IPA servers to enroll to + required: no + domain: + description: Primary DNS domain of the IPA deployment + required: no + realm: + description: Kerberos realm name of the IPA deployment + required: no + hostname: + description: Fully qualified name of this host + required: no + on_master: + description: Whether the configuration is done on the master or not + required: yes + no_ssh: + description: Do not configure OpenSSH client + required: yes + no_sshd: + description: Do not configure OpenSSH server + required: yes + no_sudo: + description: Do not configure SSSD as data source for sudo + required: yes + all_ip_addresses: + description: + All routable IP addresses configured on any interface will be added + to DNS + required: yes + fixed_primary: + description: Configure sssd to use fixed server as primary IPA server + required: yes + permit: + description: Disable access rules by default, permit all access + required: yes + enable_dns_updates: + description: + Configures the machine to attempt dns updates when the ip address + changes + required: yes + preserve_sssd: + description: Preserve old SSSD configuration if possible + required: yes + no_krb5_offline_passwords: + description: + Configure SSSD not to store user password when the server is offline + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +- name: Configure SSSD + ipaclient_setup_sssd: + servers: ["server1.example.com","server2.example.com"] + domain: example.com + realm: EXAMPLE.COM + hostname: client1.example.com + no_krb5_offline_passwords: yes +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_client import ( + options, sysrestore, paths, configure_sssd_conf, logger +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + servers=dict(required=True, type='list'), + domain=dict(required=True), + realm=dict(required=True), + hostname=dict(required=True), + on_master=dict(required=False, type='bool'), + no_ssh=dict(required=False, type='bool'), + no_sshd=dict(required=False, type='bool'), + no_sudo=dict(required=False, type='bool'), + all_ip_addresses=dict(required=False, type='bool'), + + fixed_primary=dict(required=False, type='bool'), + permit=dict(required=False, type='bool'), + enable_dns_updates=dict(required=False, type='bool'), + preserve_sssd=dict(required=False, type='bool'), + no_krb5_offline_passwords=dict(required=False, type='bool'), + ), + supports_check_mode=True, + ) + # ansible_log = AnsibleModuleLog(module, logger) + # options.set_logger(ansible_log) + + module._ansible_debug = True + cli_server = module.params.get('servers') + cli_domain = module.params.get('domain') + cli_realm = module.params.get('realm') + hostname = module.params.get('hostname') + options.on_master = module.params.get('on_master') + + options.no_ssh = module.params.get('no_ssh') + options.conf_ssh = not options.no_ssh + options.no_sshd = module.params.get('no_sshd') + options.conf_sshd = not options.no_sshd + options.no_sudo = module.params.get('no_sudo') + options.conf_sudo = not options.no_sudo + options.all_ip_addresses = module.params.get('all_ip_addresses') + + options.primary = module.params.get('fixed_primary') + options.permit = module.params.get('permit') + options.dns_updates = module.params.get('enable_dns_updates') + options.preserve_sssd = module.params.get('preserve_sssd') + + options.no_krb5_offline_passwords = module.params.get( + 'no_krb5_offline_passwords') + options.krb5_offline_passwords = not options.no_krb5_offline_passwords + + fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) + client_domain = hostname[hostname.find(".")+1:] + + if configure_sssd_conf(fstore, cli_realm, cli_domain, cli_server, + options, client_domain, hostname): + module.fail_json("configure_sssd_conf failed") + logger.info("Configured /etc/sssd/sssd.conf") + + module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaclient/library/ipaclient_test.py b/roles/ipaclient/library/ipaclient_test.py new file mode 100644 index 0000000..49e517e --- /dev/null +++ b/roles/ipaclient/library/ipaclient_test.py @@ -0,0 +1,932 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaclient_test +short description: Tries to discover IPA server +description: + Tries to discover IPA server using DNS or host name +options: + domain: + description: Primary DNS domain of the IPA deployment + required: yes + servers: + description: Fully qualified name of IPA servers to enroll to + required: yes + realm: + description: Kerberos realm name of the IPA deployment + required: yes + hostname: + description: Fully qualified name of this host + required: yes + ntp_servers: + description: ntp servers to use + required: yes + ntp_pool: + description: ntp server pool to use + required: yes + no_ntp: + description: Do not configure ntp + required: yes + force_ntpd: + description: + Stop and disable any time&date synchronization services besides ntpd + Deprecated since 4.7 + required: yes + nisdomain: + description: The NIS domain name + required: yes + no_nisdomain: + description: Do not configure NIS domain name + required: yes + kinit_attempts: + description: Repeat the request for host Kerberos ticket X times + required: yes + ca_cert_files: + description: + List of files containing CA certificates for the service certificate + files + required: yes + configure_firefox: + description: Configure Firefox to use IPA domain credentials + required: yes + firefox_dir: + description: + Specify directory where Firefox is installed (for example + '/usr/lib/firefox') + required: yes + ip_addresses: + description: List of Master Server IP Addresses + required: yes + all_ip_addresses: + description: + All routable IP addresses configured on any interface will be added + to DNS + required: yes + on_master: + description: Whether the configuration is done on the master or not + required: yes + enable_dns_updates: + description: + Configures the machine to attempt dns updates when the ip address + changes + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +# Complete autodiscovery, register return values as ipaclient_test +- name: IPA discovery + ipaclient_test: + register: register_ipaclient_test + +# Discovery using servers, register return values as ipaclient_test +- name: IPA discovery + ipaclient_test: + servers: server1.domain.com,server2.domain.com + register: register_ipaclient_test + +# Discovery using domain name, register return values as ipaclient_test +- name: IPA discovery + ipaclient_test: + domain: domain.com + register: register_ipaclient_test + +# Discovery using realm, register return values as ipaclient_test +- name: IPA discovery + ipaclient_test: + realm: DOMAIN.COM + register: register_ipaclient_test + +# Discovery using hostname, register return values as ipaclient_test +- name: IPA discovery + ipaclient_test: + hostname: host.domain.com + register: register_ipaclient_test +''' + +RETURN = ''' +servers: + description: The list of detected or passed in IPA servers. + returned: always + type: list + sample: ["server1.example.com","server2.example.com"] +domain: + description: The DNS domain of the detected or passed in IPA deployment. + returned: always + type: string + sample: example.com +realm: + description: The Kerberos realm of the detected or passed in IPA deployment. + returned: always + type: string + sample: EXAMPLE.COM +kdc: + description: The detected KDC server name. + returned: always + type: string + sample: server1.example.com +basedn: + description: The basedn of the detected IPA server. + returned: always + type: string + sample: dc=example,dc=com +hostname: + description: The detected or passed in FQDN hostname of the client. + returned: always + type: string + sample: client1.example.com +client_domain: + description: The domain name of the client. + returned: always + type: string + sample: example.com +dnsok: + description: True if DNS discovery worked and not passed in any servers. + returned: always + type: bool +ntp_servers: + description: The list of detected NTP servers. + returned: always + type: list + sample: ["ntp.example.com"] +ipa_python_version: + description: + - The IPA python version as a number: + - *10000+*100+ + returned: always + type: int + sample: 040400 +''' + +import os +import socket +import inspect + +try: + from six.moves.configparser import RawConfigParser +except ImportError: + from ConfigParser import RawConfigParser + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_client import ( + paths, sysrestore, options, CheckedIPAddress, validate_domain_name, + logger, x509, normalize_hostname, installer, version, ScriptError, + CLIENT_INSTALL_ERROR, tasks, check_ldap_conf, timeconf, constants, + validate_hostname, nssldap_exists, gssapi, remove_file, + check_ip_addresses, ipadiscovery, print_port_conf_info, + IPA_PYTHON_VERSION +) + + +def get_cert_path(cert_path): + """ + If a CA certificate is passed in on the command line, use that. + + Else if a CA file exists in paths.IPA_CA_CRT then use that. + + Otherwise return None. + """ + if cert_path is not None: + return cert_path + + if os.path.exists(paths.IPA_CA_CRT): + return paths.IPA_CA_CRT + + return None + + +def is_client_configured(): + """ + Check if ipa client is configured. + + IPA client is configured when /etc/ipa/default.conf exists and + /var/lib/ipa-client/sysrestore/sysrestore.state exists. + + :returns: boolean + """ + + return (os.path.isfile(paths.IPA_DEFAULT_CONF) and + os.path.isfile(os.path.join(paths.IPA_CLIENT_SYSRESTORE, + sysrestore.SYSRESTORE_STATEFILE))) + + +def get_ipa_conf(): + """ + Return IPA configuration read from /etc/ipa/default.conf + + :returns: dict containing key,value + """ + + parser = RawConfigParser() + parser.read(paths.IPA_DEFAULT_CONF) + result = dict() + for item in ['basedn', 'realm', 'domain', 'server', 'host', 'xmlrpc_uri']: + if parser.has_option('global', item): + value = parser.get('global', item) + else: + value = None + if value: + result[item] = value + + return result + + +def main(): + module = AnsibleModule( + argument_spec=dict( + # basic + domain=dict(required=False, default=None), + servers=dict(required=False, type='list', default=None), + realm=dict(required=False, default=None), + hostname=dict(required=False, default=None), + ntp_servers=dict(required=False, type='list', default=None), + ntp_pool=dict(required=False, default=None), + no_ntp=dict(required=False, type='bool', default=False), + force_ntpd=dict(required=False, type='bool', default=False), + nisdomain=dict(required=False, default=None), + no_nisdomain=dict(required=False, type='bool', default='no'), + kinit_attempts=dict(required=False, type='int'), + ca_cert_files=dict(required=False, type='list', default=None), + configure_firefox=dict(required=False, type='bool', default=False), + firefox_dir=dict(required=False), + ip_addresses=dict(required=False, type='list', default=None), + all_ip_addresses=dict(required=False, type='bool', default=False), + on_master=dict(required=False, type='bool', default=False), + # sssd + enable_dns_updates=dict(required=False, type='bool', + default=False), + ), + supports_check_mode=True, + ) + + # module._ansible_debug = True + options.domain_name = module.params.get('domain') + options.servers = module.params.get('servers') + options.realm_name = module.params.get('realm') + options.host_name = module.params.get('hostname') + options.ntp_servers = module.params.get('ntp_servers') + options.ntp_pool = module.params.get('ntp_pool') + options.no_ntp = module.params.get('no_ntp') + options.force_ntpd = module.params.get('force_ntpd') + options.nisdomain = module.params.get('nisdomain') + options.no_nisdomain = module.params.get('no_nisdomain') + options.kinit_attempts = module.params.get('kinit_attempts') + options.ca_cert_files = module.params.get('ca_cert_files') + options.configure_firefox = module.params.get('configure_firefox') + options.firefox_dir = module.params.get('firefox_dir') + options.ip_addresses = module.params.get('ip_addresses') + options.all_ip_addresses = module.params.get('all_ip_addresses') + options.on_master = module.params.get('on_master') + options.enable_dns_updates = module.params.get('enable_dns_updates') + + # Get domain from first server if domain is not set, but if there are + # servers + if options.domain_name is None and options.servers is not None: + if len(options.servers) > 0: + options.domain_name = options.servers[0][ + options.servers[0].find(".")+1:] + + try: + self = options + + # HostNameInstallInterface + + if options.ip_addresses is not None: + for value in options.ip_addresses: + try: + CheckedIPAddress(value) + except Exception as e: + raise ValueError("invalid IP address {0}: {1}".format( + value, e)) + + # ServiceInstallInterface + + if options.domain_name: + validate_domain_name(options.domain_name) + + if options.realm_name: + argspec = inspect.getargspec(validate_domain_name) + if "entity" in argspec.args: + # NUM_VERSION >= 40690: + validate_domain_name(options.realm_name, entity="realm") + + # ClientInstallInterface + + if options.kinit_attempts < 1: + raise ValueError("expects an integer greater than 0.") + + # ClientInstallInterface.__init__ + + if self.servers and not self.domain_name: + raise RuntimeError( + "--server cannot be used without providing --domain") + + if self.force_ntpd: + logger.warning("Option --force-ntpd has been deprecated") + + if self.ntp_servers and self.no_ntp: + raise RuntimeError( + "--ntp-server cannot be used together with --no-ntp") + + if self.ntp_pool and self.no_ntp: + raise RuntimeError( + "--ntp-pool cannot be used together with --no-ntp") + + if self.no_nisdomain and self.nisdomain: + raise RuntimeError( + "--no-nisdomain cannot be used together with --nisdomain") + + if self.ip_addresses: + if self.enable_dns_updates: + raise RuntimeError( + "--ip-address cannot be used together with" + " --enable-dns-updates") + + if self.all_ip_addresses: + raise RuntimeError( + "--ip-address cannot be used together with" + "--all-ip-addresses") + + # SSSDInstallInterface + + self.no_sssd = False + + # ClientInstall + + if options.ca_cert_files is not None: + for value in options.ca_cert_files: + if not isinstance(value, list): + raise ValueError("Expected list, got {!r}".format(value)) + # this is what init() does + value = value[-1] + if not os.path.exists(value): + raise ValueError("'%s' does not exist" % value) + if not os.path.isfile(value): + raise ValueError("'%s' is not a file" % value) + if not os.path.isabs(value): + raise ValueError("'%s' is not an absolute file path" % + value) + + try: + x509.load_certificate_from_file(value) + except Exception: + raise ValueError("'%s' is not a valid certificate file" % + value) + + # self.prompt_password = self.interactive + + self.no_ac = False + + # ClientInstall.__init__ + + if self.firefox_dir and not self.configure_firefox: + raise RuntimeError( + "--firefox-dir cannot be used without --configure-firefox " + "option") + + except (RuntimeError, ValueError) as e: + module.fail_json(msg=str(e)) + + # ipaclient.install.client.init + + # root_logger + options.debug = False + if options.domain_name: + options.domain = normalize_hostname(installer.domain_name) + else: + options.domain = None + options.server = options.servers + options.realm = options.realm_name + # installer.primary = installer.fixed_primary + # if installer.principal: + # installer.password = installer.admin_password + # else: + # installer.password = installer.host_password + installer.hostname = installer.host_name + options.conf_ntp = not options.no_ntp + # installer.trust_sshfp = installer.ssh_trust_dns + # installer.conf_ssh = not installer.no_ssh + # installer.conf_sshd = not installer.no_sshd + # installer.conf_sudo = not installer.no_sudo + # installer.create_sshfp = not installer.no_dns_sshfp + if installer.ca_cert_files: + installer.ca_cert_file = installer.ca_cert_files[-1] + else: + installer.ca_cert_file = None + # installer.location = installer.automount_location + installer.dns_updates = installer.enable_dns_updates + # installer.krb5_offline_passwords = \ + # not installer.no_krb5_offline_passwords + installer.sssd = not installer.no_sssd + + try: + + # client + + # global variables + hostname = None + hostname_source = None + nosssd_files = None + dnsok = False + cli_domain = None + cli_server = None + # subject_base = None + cli_realm = None + cli_kdc = None + client_domain = None + cli_basedn = None + # end of global variables + + # client.install_check + + logger.info("This program will set up FreeIPA client.") + logger.info("Version %s", version.VERSION) + logger.info("") + + cli_domain_source = 'Unknown source' + cli_server_source = 'Unknown source' + + # fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) + + if not os.getegid() == 0: + raise ScriptError( + "You must be root to run ipa-client-install.", + rval=CLIENT_INSTALL_ERROR) + + tasks.check_selinux_status() + + # if is_ipa_client_installed(fstore, on_master=options.on_master): + # logger.error("IPA client is already configured on this system.") + # logger.info( + # "If you want to reinstall the IPA client, uninstall it first " + # "using 'ipa-client-install --uninstall'.") + # raise ScriptError( + # "IPA client is already configured on this system.", + # rval=CLIENT_ALREADY_CONFIGURED) + + if check_ldap_conf is not None: + check_ldap_conf() + + if options.conf_ntp: + try: + timeconf.check_timedate_services() + except timeconf.NTPConflictingService as e: + logger.info( + "WARNING: conflicting time&date synchronization service " + "'%s' will be disabled in favor of chronyd", + e.conflicting_service) + logger.info("") + except timeconf.NTPConfigurationError: + pass + + # password, principal and keytab are checked in tasks/install.yml + # if options.unattended and ( + # options.password is None and + # options.principal is None and + # options.keytab is None and + # options.prompt_password is False and + # not options.on_master + # ): + # raise ScriptError( + # "One of password / principal / keytab is required.", + # rval=CLIENT_INSTALL_ERROR) + + if options.hostname: + hostname = options.hostname + hostname_source = 'Provided as option' + else: + hostname = socket.getfqdn() + hostname_source = "Machine's FQDN" + if hostname != hostname.lower(): + raise ScriptError( + "Invalid hostname '{}', must be lower-case.".format(hostname), + rval=CLIENT_INSTALL_ERROR + ) + + if hostname in ('localhost', 'localhost.localdomain'): + raise ScriptError( + "Invalid hostname, '{}' must not be used.".format(hostname), + rval=CLIENT_INSTALL_ERROR) + + if hasattr(constants, "MAXHOSTNAMELEN"): + try: + validate_hostname(hostname, maxlen=constants.MAXHOSTNAMELEN) + except ValueError as e: + raise ScriptError( + 'invalid hostname: {}'.format(e), + rval=CLIENT_INSTALL_ERROR) + + if hasattr(tasks, "is_nosssd_supported"): + # --no-sssd is not supported any more for rhel-based distros + if not tasks.is_nosssd_supported() and not options.sssd: + raise ScriptError( + "Option '--no-sssd' is incompatible with the 'authselect' " + "tool provided by this distribution for configuring " + "system authentication resources", + rval=CLIENT_INSTALL_ERROR) + + # --noac is not supported any more for rhel-based distros + if not tasks.is_nosssd_supported() and options.no_ac: + raise ScriptError( + "Option '--noac' is incompatible with the 'authselect' " + "tool provided by this distribution for configuring " + "system authentication resources", + rval=CLIENT_INSTALL_ERROR) + + # when installing with '--no-sssd' option, check whether nss-ldap is + # installed + if not options.sssd: + if not os.path.exists(paths.PAM_KRB5_SO): + raise ScriptError( + "The pam_krb5 package must be installed", + rval=CLIENT_INSTALL_ERROR) + + (nssldap_installed, nosssd_files) = nssldap_exists() + (nssldap_installed, __temp) = nssldap_exists() + if not nssldap_installed: + raise ScriptError( + "One of these packages must be installed: nss_ldap or " + "nss-pam-ldapd", + rval=CLIENT_INSTALL_ERROR) + + # principal and keytab are checked in tasks/install.yml + # if options.keytab and options.principal: + # raise ScriptError( + # "Options 'principal' and 'keytab' cannot be used together.", + # rval=CLIENT_INSTALL_ERROR) + + # keytab and force_join are checked in tasks/install.yml + # if options.keytab and options.force_join: + # logger.warning("Option 'force-join' has no additional effect " + # "when used with together with option 'keytab'.") + + # Added with freeipa-4.7.1 >>> + # Remove invalid keytab file + try: + gssapi.Credentials( + store={'keytab': paths.KRB5_KEYTAB}, + usage='accept', + ) + except gssapi.exceptions.GSSError: + logger.debug("Deleting invalid keytab: '%s'.", paths.KRB5_KEYTAB) + remove_file(paths.KRB5_KEYTAB) + # Added with freeipa-4.7.1 <<< + + # Check if old certificate exist and show warning + if ( + not options.ca_cert_file and + get_cert_path(options.ca_cert_file) == paths.IPA_CA_CRT + ): + logger.warning("Using existing certificate '%s'.", + paths.IPA_CA_CRT) + + if not check_ip_addresses(options): + raise ScriptError( + "Failed to check ip addresses, check installation log", + rval=CLIENT_INSTALL_ERROR) + + # Create the discovery instance + ds = ipadiscovery.IPADiscovery() + + ret = ds.search( + domain=options.domain, + servers=options.server, + realm=options.realm_name, + hostname=hostname, + ca_cert_path=get_cert_path(options.ca_cert_file) + ) + + if options.server and ret != 0: + # There is no point to continue with installation as server list + # was passed as a fixed list of server and thus we cannot discover + # any better result + logger.error( + "Failed to verify that %s is an IPA Server.", + ', '.join(options.server)) + logger.error( + "This may mean that the remote server is not up " + "or is not reachable due to network or firewall settings.") + print_port_conf_info() + raise ScriptError("Failed to verify that %s is an IPA Server." % + ', '.join(options.server), + rval=CLIENT_INSTALL_ERROR) + + if ret == ipadiscovery.BAD_HOST_CONFIG: + logger.error("Can't get the fully qualified name of this host") + logger.info("Check that the client is properly configured") + raise ScriptError( + "Can't get the fully qualified name of this host", + rval=CLIENT_INSTALL_ERROR) + if ret == ipadiscovery.NOT_FQDN: + raise ScriptError( + "{} is not a fully-qualified hostname".format(hostname), + rval=CLIENT_INSTALL_ERROR) + if ret in (ipadiscovery.NO_LDAP_SERVER, ipadiscovery.NOT_IPA_SERVER) \ + or not ds.domain: + if ret == ipadiscovery.NO_LDAP_SERVER: + if ds.server: + logger.debug("%s is not an LDAP server", ds.server) + else: + logger.debug("No LDAP server found") + elif ret == ipadiscovery.NOT_IPA_SERVER: + if ds.server: + logger.debug("%s is not an IPA server", ds.server) + else: + logger.debug("No IPA server found") + else: + logger.debug("Domain not found") + if options.domain: + cli_domain = options.domain + cli_domain_source = 'Provided as option' + elif options.unattended: + raise ScriptError( + "Unable to discover domain, not provided on command line", + rval=CLIENT_INSTALL_ERROR) + else: + raise ScriptError("No interactive installation") + # logger.info( + # "DNS discovery failed to determine your DNS domain") + # cli_domain = user_input( + # "Provide the domain name of your IPA server " + # "(ex: example.com)", + # allow_empty=False) + # cli_domain_source = 'Provided interactively' + # logger.debug( + # "will use interactively provided domain: %s", cli_domain) + ret = ds.search( + domain=cli_domain, + servers=options.server, + hostname=hostname, + ca_cert_path=get_cert_path(options.ca_cert_file)) + + if not cli_domain: + if ds.domain: + cli_domain = ds.domain + cli_domain_source = ds.domain_source + logger.debug("will use discovered domain: %s", cli_domain) + + client_domain = hostname[hostname.find(".")+1:] + + if ret in (ipadiscovery.NO_LDAP_SERVER, ipadiscovery.NOT_IPA_SERVER) \ + or not ds.server: + logger.debug("IPA Server not found") + if options.server: + cli_server = options.server + cli_server_source = 'Provided as option' + elif options.unattended: + raise ScriptError( + "Unable to find IPA Server to join", + rval=CLIENT_INSTALL_ERROR) + else: + raise ScriptError("No interactive installation") + # logger.debug("DNS discovery failed to find the IPA Server") + # cli_server = [ + # user_input( + # "Provide your IPA server name (ex: ipa.example.com)", + # allow_empty=False) + # ] + # cli_server_source = 'Provided interactively' + # logger.debug( + # "will use interactively provided server: %s", cli_server[0]) + ret = ds.search( + domain=cli_domain, + servers=cli_server, + hostname=hostname, + ca_cert_path=get_cert_path(options.ca_cert_file)) + + else: + # Only set dnsok to True if we were not passed in one or more + # servers and if DNS discovery actually worked. + if not options.server: + (server, domain) = ds.check_domain( + ds.domain, set(), "Validating DNS Discovery") + if server and domain: + logger.debug("DNS validated, enabling discovery") + dnsok = True + else: + logger.debug("DNS discovery failed, disabling discovery") + else: + logger.debug( + "Using servers from command line, disabling DNS discovery") + + if not cli_server: + if options.server: + cli_server = ds.servers + cli_server_source = 'Provided as option' + logger.debug( + "will use provided server: %s", ', '.join(options.server)) + elif ds.server: + cli_server = ds.servers + cli_server_source = ds.server_source + logger.debug("will use discovered server: %s", cli_server[0]) + + if ret == ipadiscovery.NOT_IPA_SERVER: + logger.error("%s is not an IPA v2 Server.", cli_server[0]) + print_port_conf_info() + logger.debug("(%s: %s)", cli_server[0], cli_server_source) + raise ScriptError("%s is not an IPA v2 Server." % cli_server[0], + rval=CLIENT_INSTALL_ERROR) + + if ret == ipadiscovery.NO_ACCESS_TO_LDAP: + logger.warning("Anonymous access to the LDAP server is disabled.") + logger.info("Proceeding without strict verification.") + logger.info( + "Note: This is not an error if anonymous access " + "has been explicitly restricted.") + ret = 0 + + if ret == ipadiscovery.NO_TLS_LDAP: + logger.warning( + "The LDAP server requires TLS is but we do not have the CA.") + logger.info("Proceeding without strict verification.") + ret = 0 + + if ret != 0: + logger.error( + "Failed to verify that %s is an IPA Server.", + cli_server[0]) + logger.error( + "This may mean that the remote server is not up " + "or is not reachable due to network or firewall settings.") + print_port_conf_info() + logger.debug("(%s: %s)", cli_server[0], cli_server_source) + raise ScriptError("Failed to verify that %s is an IPA Server." % + cli_server[0], + rval=CLIENT_INSTALL_ERROR) + + cli_kdc = ds.kdc + if dnsok and not cli_kdc: + logger.error( + "DNS domain '%s' is not configured for automatic " + "KDC address lookup.", ds.realm.lower()) + logger.debug("(%s: %s)", ds.realm, ds.realm_source) + logger.error("KDC address will be set to fixed value.") + + if dnsok: + logger.info("Discovery was successful!") + elif not options.unattended: + raise ScriptError("No interactive installation") + # if not options.server: + # logger.warning( + # "The failure to use DNS to find your IPA " + # "server indicates that your resolv.conf file is not properly " + # "configured.") + # logger.info( + # "Autodiscovery of servers for failover cannot work " + # "with this configuration.") + # logger.info( + # "If you proceed with the installation, services " + # "will be configured to always access the discovered server for " + # "all operations and will not fail over to other servers in case " + # "of failure.") + # if not user_input( + # "Proceed with fixed values and no DNS discovery?", False): + # raise ScriptError(rval=CLIENT_INSTALL_ERROR) + + # Do not ask for time source + # if options.conf_ntp: + # if not options.on_master and not options.unattended and not ( + # options.ntp_servers or options.ntp_pool): + # options.ntp_servers, options.ntp_pool = \ + # timeconf.get_time_source() + + cli_realm = ds.realm + cli_realm_source = ds.realm_source + logger.debug("will use discovered realm: %s", cli_realm) + + if options.realm_name and options.realm_name != cli_realm: + logger.error( + "The provided realm name [%s] does not match discovered " + "one [%s]", + options.realm_name, cli_realm) + logger.debug("(%s: %s)", cli_realm, cli_realm_source) + raise ScriptError( + "The provided realm name [%s] does not match discovered " + "one [%s]" % (options.realm_name, cli_realm), + rval=CLIENT_INSTALL_ERROR) + + cli_basedn = ds.basedn + cli_basedn_source = ds.basedn_source + logger.debug("will use discovered basedn: %s", cli_basedn) + # subject_base = DN(('O', cli_realm)) + + logger.info("Client hostname: %s", hostname) + logger.debug("Hostname source: %s", hostname_source) + logger.info("Realm: %s", cli_realm) + logger.debug("Realm source: %s", cli_realm_source) + logger.info("DNS Domain: %s", cli_domain) + logger.debug("DNS Domain source: %s", cli_domain_source) + logger.info("IPA Server: %s", ', '.join(cli_server)) + logger.debug("IPA Server source: %s", cli_server_source) + logger.info("BaseDN: %s", cli_basedn) + logger.debug("BaseDN source: %s", cli_basedn_source) + + if not options.on_master: + if options.ntp_servers: + for server in options.ntp_servers: + logger.info("NTP server: %s", server) + + if options.ntp_pool: + logger.info("NTP pool: %s", options.ntp_pool) + + # ipa-join would fail with IP address instead of a FQDN + for srv in cli_server: + try: + socket.inet_pton(socket.AF_INET, srv) + is_ipaddr = True + except socket.error: + try: + socket.inet_pton(socket.AF_INET6, srv) + is_ipaddr = True + except socket.error: + is_ipaddr = False + + if is_ipaddr: + logger.info() + logger.warning( + "It seems that you are using an IP address " + "instead of FQDN as an argument to --server. The " + "installation may fail.") + break + + # logger.info() + # if not options.unattended and not user_input( + # "Continue to configure the system with these values?", False): + # raise ScriptError(rval=CLIENT_INSTALL_ERROR) + + except ScriptError as e: + module.fail_json(msg=str(e)) + + ######################################################################### + + # client._install + + # May not happen in here at this time + # if not options.on_master: + # # Try removing old principals from the keytab + # purge_host_keytab(cli_realm) + + # Check if ipa client is already configured + if is_client_configured(): + client_already_configured = True + + # Check that realm and domain match + current_config = get_ipa_conf() + if cli_domain != current_config.get('domain'): + module.fail_json(msg="IPA client already installed " + "with a conflicting domain") + if cli_realm != current_config.get('realm'): + module.fail_json(msg="IPA client already installed " + "with a conflicting realm") + else: + client_already_configured = False + + # Done + module.exit_json(changed=False, + servers=cli_server, + domain=cli_domain, + realm=cli_realm, + kdc=cli_kdc, + basedn=str(cli_basedn), + hostname=hostname, + client_domain=client_domain, + dnsok=dnsok, + sssd=options.sssd, + ntp_servers=options.ntp_servers, + ntp_pool=options.ntp_pool, + client_already_configured=client_already_configured, + ipa_python_version=IPA_PYTHON_VERSION) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaclient/library/ipaclient_test_keytab.py b/roles/ipaclient/library/ipaclient_test_keytab.py new file mode 100644 index 0000000..d4a9f9f --- /dev/null +++ b/roles/ipaclient/library/ipaclient_test_keytab.py @@ -0,0 +1,218 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaclient_test_keytab +short description: + Test if the krb5.keytab on the machine is valid and can be used. +description: + Test if the krb5.keytab on the machine is valid and can be used. + A temporary krb5.conf file will be generated to not fail on an invalid one. +options: + servers: + description: Fully qualified name of IPA servers to enroll to + required: no + domain: + description: Primary DNS domain of the IPA deployment + required: no + realm: + description: Kerberos realm name of the IPA deployment + required: no + hostname: + description: Fully qualified name of this host + required: no + kdc: + description: The name or address of the host running the KDC + required: no + kinit_attempts: + description: Repeat the request for host Kerberos ticket X times + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +# Test IPA with local keytab +- name: Test IPA in force mode with maximum 5 kinit attempts + ipaclient_test_keytab: + servers: ["server1.example.com","server2.example.com"] + domain: example.com + realm: EXAMPLE.COM + kdc: server1.example.com + hostname: client1.example.com + kinit_attempts: 5 + +# Test IPA with ipadiscovery return values +- name: Join IPA + ipaclient_test_keytab: + servers: "{{ ipadiscovery.servers }}" + domain: "{{ ipadiscovery.domain }}" + realm: "{{ ipadiscovery.realm }}" + kdc: "{{ ipadiscovery.kdc }}" + hostname: "{{ ipadiscovery.hostname }}" +''' + +RETURN = ''' +krb5_keytab_ok: + description: The flag describes if krb5.keytab on the host is usable. + returned: always + type: bool +ca_crt_exists: + description: The flag describes if ca.crt exists. + returned: always +krb5_conf_ok: + description: The flag describes if krb5.conf on the host is usable. + returned: always + type: bool +ping_test_ok: + description: The flag describes if ipa ping test succeded. + returned: always + type: bool +''' + +import os +import tempfile + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_client import ( + SECURE_PATH, paths, kinit_keytab, run, GSSError, configure_krb5_conf +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + servers=dict(required=True, type='list'), + domain=dict(required=True), + realm=dict(required=True), + hostname=dict(required=True), + kdc=dict(required=True), + kinit_attempts=dict(required=False, type='int', default=5), + ), + supports_check_mode=True, + ) + + module._ansible_debug = True + servers = module.params.get('servers') + domain = module.params.get('domain') + realm = module.params.get('realm') + hostname = module.params.get('hostname') + kdc = module.params.get('kdc') + kinit_attempts = module.params.get('kinit_attempts') + + client_domain = hostname[hostname.find(".")+1:] + host_principal = 'host/%s@%s' % (hostname, realm) + sssd = True + + # Remove IPA_DNS_CCACHE remain if it exists + try: + os.remove(paths.IPA_DNS_CCACHE) + except OSError: + pass + + krb5_keytab_ok = False + krb5_conf_ok = False + ping_test_ok = False + ca_crt_exists = os.path.exists(paths.IPA_CA_CRT) + env = {'PATH': SECURE_PATH, 'KRB5CCNAME': paths.IPA_DNS_CCACHE} + + # First try: Validate krb5 keytab with system krb5 configuraiton + try: + kinit_keytab(host_principal, paths.KRB5_KEYTAB, + paths.IPA_DNS_CCACHE, + config=paths.KRB5_CONF, + attempts=kinit_attempts) + krb5_keytab_ok = True + krb5_conf_ok = True + + # Test IPA + try: + result = run(["/usr/bin/ipa", "ping"], raiseonerr=False, env=env) + if result.returncode == 0: + ping_test_ok = True + except OSError: + pass + except GSSError: + pass + + # Second try: Validate krb5 keytab with temporary krb5 + # configuration + if not krb5_conf_ok: + try: + (krb_fd, krb_name) = tempfile.mkstemp() + os.close(krb_fd) + configure_krb5_conf( + cli_realm=realm, + cli_domain=domain, + cli_server=servers, + cli_kdc=kdc, + dnsok=False, + filename=krb_name, + client_domain=client_domain, + client_hostname=hostname, + configure_sssd=sssd, + force=False) + + try: + kinit_keytab(host_principal, paths.KRB5_KEYTAB, + paths.IPA_DNS_CCACHE, + config=krb_name, + attempts=kinit_attempts) + krb5_keytab_ok = True + + # Test IPA + env['KRB5_CONFIG'] = krb_name + try: + result = run(["/usr/bin/ipa", "ping"], raiseonerr=False, + env=env) + if result.returncode == 0: + ping_test_ok = True + except OSError: + pass + + except GSSError: + pass + + finally: + try: + os.remove(krb_name) + except OSError: + module.fail_json(msg="Could not remove %s" % krb_name) + + module.exit_json(changed=False, + krb5_keytab_ok=krb5_keytab_ok, + krb5_conf_ok=krb5_conf_ok, + ca_crt_exists=ca_crt_exists, + ping_test_ok=ping_test_ok) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaclient/meta/main.yml b/roles/ipaclient/meta/main.yml new file mode 100644 index 0000000..5c0cb49 --- /dev/null +++ b/roles/ipaclient/meta/main.yml @@ -0,0 +1,20 @@ +dependencies: [] + +galaxy_info: + author: Florence Blanc-Renaud, Thomas Woerner + description: A role to join a machine to an IPA domain + company: Red Hat, Inc + license: GPLv3 + min_ansible_version: 2.8 + platforms: + - name: Fedora + versions: + - all + - name: EL + versions: + - 7 + - 8 + galaxy_tags: + - identity + - ipa + - freeipa diff --git a/roles/ipaclient/module_utils/ansible_ipa_client.py b/roles/ipaclient/module_utils/ansible_ipa_client.py new file mode 100644 index 0000000..d45171b --- /dev/null +++ b/roles/ipaclient/module_utils/ansible_ipa_client.py @@ -0,0 +1,286 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +__all__ = ["gssapi", "version", "ipadiscovery", "api", "errors", "x509", + "constants", "sysrestore", "certmonger", "certstore", + "delete_persistent_client_session_data", "ScriptError", + "CheckedIPAddress", "validate_domain_name", "normalize_hostname", + "validate_hostname", "services", "tasks", "CalledProcessError", + "write_tmp_file", "ipa_generate_password", "DN", "kinit_keytab", + "kinit_password", "GSSError", "CLIENT_INSTALL_ERROR", + "is_ipa_client_installed", "CLIENT_ALREADY_CONFIGURED", + "nssldap_exists", "remove_file", "check_ip_addresses", + "print_port_conf_info", "configure_ipa_conf", "purge_host_keytab", + "configure_sssd_conf", "realm_to_suffix", "run", "timeconf", + "serialization", "configure_krb5_conf", "get_ca_certs", + "SECURE_PATH", "get_server_connection_interface", + "disable_ra", "client_dns", + "configure_certmonger", "update_ssh_keys", + "configure_openldap_conf", "hardcode_ldap_server", + "get_certs_from_ldap", "save_state", "create_ipa_nssdb", + "configure_nisdomain", "configure_ldap_conf", + "configure_nslcd_conf", "nosssd_files", "configure_ssh_config", + "configure_sshd_config", "configure_automount", + "configure_firefox", "sync_time", "check_ldap_conf", + "sssd_enable_ifp"] + +from ipapython.version import NUM_VERSION, VERSION + +if NUM_VERSION < 30201: + # See ipapython/version.py + IPA_MAJOR, IPA_MINOR, IPA_RELEASE = [int(x) for x in VERSION.split(".", 2)] + IPA_PYTHON_VERSION = IPA_MAJOR*10000 + IPA_MINOR*100 + IPA_RELEASE +else: + IPA_PYTHON_VERSION = NUM_VERSION + + +class installer_obj(object): + def __init__(self): + pass + + def set_logger(self, logger): + self.logger = logger + + # def __getattribute__(self, attr): + # value = super(installer_obj, self).__getattribute__(attr) + # if not attr.startswith("--") and not attr.endswith("--"): + # logger.debug( + # " <-- Accessing installer.%s (%s)" % (attr, repr(value))) + # return value + + # def __getattr__(self, attr): + # # logger.info(" --> ADDING missing installer.%s" % attr) + # self.logger.warn(" --> ADDING missing installer.%s" % attr) + # setattr(self, attr, None) + # return getattr(self, attr) + + # def __setattr__(self, attr, value): + # logger.debug(" --> Setting installer.%s to %s" % (attr, repr(value))) + # return super(installer_obj, self).__setattr__(attr, value) + + def knobs(self): + for name in self.__dict__: + yield self, name + + +# Initialize installer settings +installer = installer_obj() +# Create options +options = installer +options.interactive = False +options.unattended = not options.interactive + +if NUM_VERSION >= 40400: + # IPA version >= 4.4 + + import sys + import inspect + import gssapi + import logging + + from ipapython import version + try: + from ipaclient.install import ipadiscovery + except ImportError: + from ipaclient import ipadiscovery + from ipalib import api, errors, x509 + from ipalib import constants + try: + from ipalib.install import sysrestore + except ImportError: + from ipapython import sysrestore + try: + from ipalib.install import certmonger + except ImportError: + from ipapython import certmonger + try: + from ipalib.install import certstore + except ImportError: + from ipalib import certstore + from ipalib.rpc import delete_persistent_client_session_data + from ipapython import certdb, ipautil + from ipapython.admintool import ScriptError + from ipapython.ipautil import CheckedIPAddress + from ipalib.util import validate_domain_name, normalize_hostname, \ + validate_hostname + from ipaplatform import services + from ipaplatform.paths import paths + from ipaplatform.tasks import tasks + try: + from cryptography.hazmat.primitives import serialization + except ImportError: + serialization = None + from ipapython.ipautil import CalledProcessError, write_tmp_file, \ + ipa_generate_password + from ipapython.dn import DN + try: + from ipalib.install.kinit import kinit_keytab, kinit_password + except ImportError: + from ipapython.ipautil import kinit_keytab, kinit_password + from ipapython.ipa_log_manager import standard_logging_setup + from gssapi.exceptions import GSSError + try: + from ipaclient.install.client import configure_krb5_conf, \ + get_ca_certs, SECURE_PATH, get_server_connection_interface, \ + disable_ra, client_dns, \ + configure_certmonger, update_ssh_keys, configure_openldap_conf, \ + hardcode_ldap_server, get_certs_from_ldap, save_state, \ + create_ipa_nssdb, configure_ssh_config, configure_sshd_config, \ + configure_automount, configure_firefox, configure_nisdomain, \ + CLIENT_INSTALL_ERROR, is_ipa_client_installed, \ + CLIENT_ALREADY_CONFIGURED, nssldap_exists, remove_file, \ + check_ip_addresses, print_port_conf_info, configure_ipa_conf, \ + purge_host_keytab, configure_sssd_conf, configure_ldap_conf, \ + configure_nslcd_conf, nosssd_files + get_ca_cert = None + except ImportError: + # Create temporary copy of ipa-client-install script (as + # ipa_client_install.py) to be able to import the script easily + # and also to remove the global finally clause in which the + # generated ccache file gets removed. The ccache file will be + # needed in the next step. + # This is done in a temporary directory that gets removed right + # after ipa_client_install has been imported. + import shutil + import tempfile + temp_dir = tempfile.mkdtemp(dir="/tmp") + sys.path.append(temp_dir) + temp_file = "%s/ipa_client_install.py" % temp_dir + + with open("/usr/sbin/ipa-client-install", "r") as f_in: + with open(temp_file, "w") as f_out: + for line in f_in: + if line.startswith("finally:"): + break + f_out.write(line) + import ipa_client_install + + shutil.rmtree(temp_dir, ignore_errors=True) + sys.path.remove(temp_dir) + + argspec = inspect.getargspec(ipa_client_install.configure_krb5_conf) + if argspec.keywords is None: + def configure_krb5_conf( + cli_realm, cli_domain, cli_server, cli_kdc, dnsok, + filename, client_domain, client_hostname, force=False, + configure_sssd=True): + global options + options.force = force + options.sssd = configure_sssd + return ipa_client_install.configure_krb5_conf( + cli_realm, cli_domain, cli_server, cli_kdc, dnsok, options, + filename, client_domain, client_hostname) + else: + configure_krb5_conf = ipa_client_install.configure_krb5_conf + if NUM_VERSION < 40100: + get_ca_cert = ipa_client_install.get_ca_cert + get_ca_certs = None + else: + get_ca_cert = None + get_ca_certs = ipa_client_install.get_ca_certs + SECURE_PATH = ("/bin:/sbin:/usr/kerberos/bin:/usr/kerberos/sbin:" + "/usr/bin:/usr/sbin") + + get_server_connection_interface = \ + ipa_client_install.get_server_connection_interface + disable_ra = ipa_client_install.disable_ra + client_dns = ipa_client_install.client_dns + configure_certmonger = ipa_client_install.configure_certmonger + update_ssh_keys = ipa_client_install.update_ssh_keys + configure_openldap_conf = ipa_client_install.configure_openldap_conf + hardcode_ldap_server = ipa_client_install.hardcode_ldap_server + get_certs_from_ldap = ipa_client_install.get_certs_from_ldap + save_state = ipa_client_install.save_state + + create_ipa_nssdb = certdb.create_ipa_nssdb + + argspec = inspect.getargspec(ipa_client_install.configure_nisdomain) + if len(argspec.args) == 3: + configure_nisdomain = ipa_client_install.configure_nisdomain + else: + def configure_nisdomain(options, domain, statestore=None): + return ipa_client_install.configure_nisdomain(options, domain) + + configure_ldap_conf = ipa_client_install.configure_ldap_conf + configure_nslcd_conf = ipa_client_install.configure_nslcd_conf + nosssd_files = ipa_client_install.nosssd_files + + configure_ssh_config = ipa_client_install.configure_ssh_config + configure_sshd_config = ipa_client_install.configure_sshd_config + configure_automount = ipa_client_install.configure_automount + configure_firefox = ipa_client_install.configure_firefox + + from ipapython.ipautil import realm_to_suffix, run + + try: + from ipaclient.install import timeconf + time_service = "chronyd" + except ImportError: + try: + from ipaclient.install import ntpconf as timeconf + except ImportError: + from ipaclient import ntpconf as timeconf + time_service = "ntpd" + + try: + from ipaclient.install.client import sync_time + except ImportError: + sync_time = None + + try: + from ipaclient.install.client import check_ldap_conf + except ImportError: + check_ldap_conf = None + + try: + from ipaclient.install.client import sssd_enable_ifp + except ImportError: + sssd_enable_ifp = None + + logger = logging.getLogger("ipa-client-install") + standard_logging_setup( + paths.IPACLIENT_INSTALL_LOG, verbose=False, debug=False, + filemode='a', console_format='%(message)s') + root_logger = logger + +else: + # IPA version < 4.4 + + raise Exception("freeipa version '%s' is too old" % VERSION) + + +def ansible_module_get_parsed_ip_addresses(ansible_module, + param='ip_addresses'): + ip_addresses = ansible_module.params.get(param) + if ip_addresses is None: + return None + + ip_addrs = [] + for ip in ip_addresses: + try: + ip_parsed = ipautil.CheckedIPAddress(ip) + except Exception as e: + ansible_module.fail_json(msg="Invalid IP Address %s: %s" % (ip, e)) + ip_addrs.append(ip_parsed) + return ip_addrs diff --git a/roles/ipaclient/tasks/install.yml b/roles/ipaclient/tasks/install.yml new file mode 100644 index 0000000..acb96e0 --- /dev/null +++ b/roles/ipaclient/tasks/install.yml @@ -0,0 +1,383 @@ +--- +# tasks file for ipaclient + +- name: Install - Ensure that IPA client packages are installed + package: + name: "{{ ipaclient_packages }}" + state: present + when: ipaclient_install_packages | bool + +#- name: Install - Include Python2/3 import test +# import_tasks: "{{ role_path }}/tasks/python_2_3_test.yml" + +- name: Install - Set ipaclient_servers + set_fact: + ipaclient_servers: "{{ groups['ipaservers'] | list }}" + when: groups.ipaservers is defined and ipaclient_servers is not defined + +- name: Install - Set ipaclient_servers from cluster inventory + set_fact: + ipaclient_servers: "{{ groups['ipaserver'] | list }}" + when: ipaclient_no_dns_lookup | bool and groups.ipaserver is defined and + ipaclient_servers is not defined + +- name: Install - Check that either principal or keytab is set + fail: msg="ipaadmin_principal and ipaadmin_keytab cannot be used together" + when: ipaadmin_keytab is defined and ipaadmin_principal is defined + +- name: Install - Set default principal if no keytab is given + set_fact: + ipaadmin_principal: admin + when: ipaadmin_principal is undefined and ipaclient_keytab is undefined + +- name: Install - IPA client test + ipaclient_test: + ### basic ### + domain: "{{ ipaserver_domain | default(ipaclient_domain) | default(omit) }}" + servers: "{{ ipaclient_servers | default(omit) }}" + realm: "{{ ipaserver_realm | default(ipaclient_realm) | default(omit) }}" + hostname: "{{ ipaclient_hostname | default(ansible_fqdn) }}" + ntp_servers: "{{ ipaclient_ntp_servers | default(omit) }}" + ntp_pool: "{{ ipaclient_ntp_pool | default(omit) }}" + no_ntp: "{{ ipaclient_no_ntp }}" + force_ntpd: "{{ ipaclient_force_ntpd }}" + nisdomain: "{{ ipaclient_nisdomain | default(omit) }}" + no_nisdomain: "{{ ipaclient_no_nisdomain }}" + kinit_attempts: "{{ ipaclient_kinit_attempts }}" + ca_cert_files: "{{ ipaclient_ca_cert_file | default(omit) }}" + configure_firefox: "{{ ipaclient_configure_firefox }}" + firefox_dir: "{{ ipaclient_firefox_dir | default(omit) }}" + ip_addresses: "{{ ipaclient_ip_addresses | default(omit) }}" + all_ip_addresses: "{{ ipaclient_all_ip_addresses }}" + on_master: "{{ ipaclient_on_master }}" + ### sssd ### + enable_dns_updates: "{{ ipassd_enable_dns_updates + | default(ipasssd_enable_dns_updates) }}" + register: result_ipaclient_test + +- block: + - name: Install - Cleanup leftover ccache + file: + path: "/etc/ipa/.dns_ccache" + state: absent + + - name: Install - Configure NTP + ipaclient_setup_ntp: + ### basic ### + ntp_servers: "{{ result_ipaclient_test.ntp_servers | default(omit) }}" + ntp_pool: "{{ result_ipaclient_test.ntp_pool | default(omit) }}" + no_ntp: "{{ ipaclient_no_ntp }}" + # force_ntpd: "{{ ipaclient_force_ntpd }}" + on_master: "{{ ipaclient_on_master }}" + ### additional ### + servers: "{{ result_ipaclient_test.servers }}" + domain: "{{ result_ipaclient_test.domain }}" + + - name: Install - Make sure One-Time Password is enabled if it's already defined + set_fact: + ipaclient_use_otp: "yes" + when: ipaclient_otp is defined + + - name: Install - Disable One-Time Password for on_master + set_fact: + ipaclient_use_otp: "no" + when: ipaclient_use_otp | bool and ipaclient_on_master | bool + + - name: Install - Test if IPA client has working krb5.keytab + ipaclient_test_keytab: + servers: "{{ result_ipaclient_test.servers }}" + domain: "{{ result_ipaclient_test.domain }}" + realm: "{{ result_ipaclient_test.realm }}" + hostname: "{{ result_ipaclient_test.hostname }}" + kdc: "{{ result_ipaclient_test.kdc }}" + kinit_attempts: "{{ ipaclient_kinit_attempts | default(omit) }}" + register: result_ipaclient_test_keytab + + - name: Install - Disable One-Time Password for client with working + krb5.keytab + set_fact: + ipaclient_use_otp: "no" + when: ipaclient_use_otp | bool and + result_ipaclient_test_keytab.krb5_keytab_ok and + not ipaclient_force_join | bool + + # The following block is executed when using OTP to enroll IPA client and + # the OTP isn't predefined, ie when ipaclient_use_otp is set and ipaclient_otp + # is not set. + # It connects to ipaserver and add the host with --random option in order + # to create a OneTime Password + # If a keytab is specified in the hostent, then the hostent will be disabled + # if ipaclient_use_otp is set. + - block: + - name: Install - Keytab or password is required for getting otp + fail: msg="Keytab or password is required for getting otp" + when: ipaadmin_keytab is undefined and ipaadmin_password is undefined + + #- name: Install - Include Python2/3 import test + # import_tasks: "{{ role_path }}/tasks/python_2_3_test.yml" + # delegate_to: "{{ result_ipaclient_test.servers[0] }}" + + - name: Install - Get One-Time Password for client enrollment + no_log: yes + ipaclient_get_otp: + state: present + principal: "{{ ipaadmin_principal | default(omit) }}" + password: "{{ ipaadmin_password | default(omit) }}" + keytab: "{{ ipaadmin_keytab | default(omit) }}" + fqdn: "{{ result_ipaclient_test.hostname }}" + lifetime: "{{ ipaclient_lifetime | default(omit) }}" + random: True + register: result_ipaclient_get_otp + # If the host is already enrolled, this command will exit on error + # The error can be ignored + failed_when: result_ipaclient_get_otp is failed and + "Password cannot be set on enrolled host" not + in result_ipaclient_get_otp.msg + delegate_to: "{{ result_ipaclient_test.servers[0] }}" + delegate_facts: yes + ignore_errors: yes + + - name: Install - Report error for OTP generation + debug: + msg: "{{ result_ipaclient_get_otp.msg }}" + when: result_ipaclient_get_otp is failed + failed_when: yes + + - name: Install - Store the previously obtained OTP + no_log: yes + set_fact: + ipaadmin_orig_password: "{{ ipaadmin_password | default(omit) }}" + ipaadmin_password: "{{ result_ipaclient_get_otp.host.randompassword + if result_ipaclient_get_otp.host is defined }}" + + when: ipaclient_use_otp | bool and ipaclient_otp is not defined + + - name: Store predefined OTP in admin_password + no_log: yes + set_fact: + ipaadmin_orig_password: "{{ ipaadmin_password | default(omit) }}" + ipaadmin_password: "{{ ipaclient_otp }}" + when: ipaclient_otp is defined + + - block: + # This block is executed only when + # not (not ipaclient_on_master | bool and + # not result_ipaclient_join.changed and + # not ipaclient_allow_repair | bool and + # (result_ipaclient_test_keytab.krb5_keytab_ok or + # (result_ipaclient_join.already_joined is defined and + # result_ipaclient_join.already_joined))) + + - name: Install - Check if principal and keytab are set + fail: msg="Principal and keytab cannot be used together" + when: ipaadmin_principal is defined and ipaclient_keytab is defined + + - name: Install - Check if one of password or keytabs are set + fail: msg="At least one of password or keytabs must be specified" + when: not result_ipaclient_test_keytab.krb5_keytab_ok + and ipaadmin_password is undefined + and ipaadmin_keytab is undefined + and ipaclient_keytab is undefined + when: not ipaclient_on_master | bool + + - name: Install - Purge {{ result_ipaclient_test.realm }} from host keytab + command: > + /usr/sbin/ipa-rmkeytab + -k /etc/krb5.keytab + -r "{{ result_ipaclient_test.realm }}" + register: result_ipa_rmkeytab + # Do not fail on error codes 3 and 5: + # 3 - Unable to open keytab + # 5 - Principal name or realm not found in keytab + failed_when: result_ipa_rmkeytab.rc != 0 and + result_ipa_rmkeytab.rc != 3 and result_ipa_rmkeytab.rc != 5 + when: ipaclient_use_otp | bool or ipaclient_force_join | bool + + - name: Install - Backup and set hostname + ipaclient_set_hostname: + hostname: "{{ result_ipaclient_test.hostname }}" + when: not ipaclient_on_master | bool + + - name: Install - Join IPA + ipaclient_join: + servers: "{{ result_ipaclient_test.servers }}" + domain: "{{ result_ipaclient_test.domain }}" + realm: "{{ result_ipaclient_test.realm }}" + kdc: "{{ result_ipaclient_test.kdc }}" + basedn: "{{ result_ipaclient_test.basedn }}" + hostname: "{{ result_ipaclient_test.hostname }}" + force_join: "{{ ipaclient_force_join | default(omit) }}" + principal: "{{ ipaadmin_principal if not ipaclient_use_otp | bool and + ipaclient_keytab is not defined else omit }}" + password: "{{ ipaadmin_password | default(omit) }}" + keytab: "{{ ipaclient_keytab | default(omit) }}" + admin_keytab: "{{ ipaadmin_keytab if ipaadmin_keytab is defined and not ipaclient_use_otp | bool else omit }}" + # ca_cert_file: "{{ ipaclient_ca_cert_file | default(omit) }}" + kinit_attempts: "{{ ipaclient_kinit_attempts | default(omit) }}" + register: result_ipaclient_join + when: not ipaclient_on_master | bool and + (not result_ipaclient_test_keytab.krb5_keytab_ok or + ipaclient_force_join) + + - block: + - fail: + msg: > + The krb5 configuration is not correct, please enable allow_repair + to fix this. + when: not result_ipaclient_test_keytab.krb5_conf_ok + - fail: + msg: "The IPA test failed, please enable allow_repair to fix this." + when: not result_ipaclient_test_keytab.ping_test_ok + - fail: + msg: > + The ca.crt file is missing, please enable allow_repair to fix this. + when: not result_ipaclient_test_keytab.ca_crt_exists + when: not ipaclient_on_master | bool and + not result_ipaclient_join.changed and + not ipaclient_allow_repair | bool and + (result_ipaclient_test_keytab.krb5_keytab_ok or + (result_ipaclient_join.already_joined is defined and + result_ipaclient_join.already_joined)) + + - block: + - name: Install - Configure IPA default.conf + ipaclient_ipa_conf: + servers: "{{ result_ipaclient_test.servers }}" + domain: "{{ result_ipaclient_test.domain }}" + realm: "{{ result_ipaclient_test.realm }}" + hostname: "{{ result_ipaclient_test.hostname }}" + basedn: "{{ result_ipaclient_test.basedn }}" + when: not ipaclient_on_master | bool + + - name: Install - Configure SSSD + ipaclient_setup_sssd: + servers: "{{ result_ipaclient_test.servers }}" + domain: "{{ result_ipaclient_test.domain }}" + realm: "{{ result_ipaclient_test.realm }}" + hostname: "{{ result_ipaclient_test.hostname }}" + on_master: "{{ ipaclient_on_master }}" + no_ssh: "{{ ipaclient_no_ssh }}" + no_sshd: "{{ ipaclient_no_sshd }}" + no_sudo: "{{ ipaclient_no_sudo }}" + all_ip_addresses: "{{ ipaclient_all_ip_addresses }}" + fixed_primary: "{{ ipassd_fixed_primary + | default(ipasssd_fixed_primary) }}" + permit: "{{ ipassd_permit | default(ipasssd_permit) }}" + enable_dns_updates: "{{ ipassd_enable_dns_updates + | default(ipasssd_enable_dns_updates) }}" + preserve_sssd: "{{ ipassd_preserve_sssd + | default(ipasssd_preserve_sssd) }}" + no_krb5_offline_passwords: + "{{ ipassd_no_krb5_offline_passwords + | default(ipasssd_no_krb5_offline_passwords) }}" + + - name: Install - Configure krb5 for IPA realm + ipaclient_setup_krb5: + realm: "{{ result_ipaclient_test.realm }}" + domain: "{{ result_ipaclient_test.domain }}" + servers: "{{ result_ipaclient_test.servers }}" + kdc: "{{ result_ipaclient_test.kdc }}" + dnsok: "{{ result_ipaclient_test.dnsok }}" + client_domain: "{{ result_ipaclient_test.client_domain }}" + hostname: "{{ result_ipaclient_test.hostname }}" + sssd: "{{ result_ipaclient_test.sssd }}" + force: "{{ ipaclient_force }}" + # on_master: "{{ ipaclient_on_master }}" + when: not ipaclient_on_master | bool + + - name: Install - IPA API calls for remaining enrollment parts + ipaclient_api: + servers: "{{ result_ipaclient_test.servers }}" + realm: "{{ result_ipaclient_test.realm }}" + hostname: "{{ result_ipaclient_test.hostname }}" + # debug: yes + register: result_ipaclient_api + + - name: Install - Fix IPA ca + ipaclient_fix_ca: + servers: "{{ result_ipaclient_test.servers }}" + realm: "{{ result_ipaclient_test.realm }}" + basedn: "{{ result_ipaclient_test.basedn }}" + allow_repair: "{{ ipaclient_allow_repair }}" + when: not ipaclient_on_master | bool and + result_ipaclient_test_keytab.krb5_keytab_ok and + not result_ipaclient_test_keytab.ca_crt_exists + + - name: Install - Create IPA NSS database + ipaclient_setup_nss: + servers: "{{ result_ipaclient_test.servers }}" + domain: "{{ result_ipaclient_test.domain }}" + realm: "{{ result_ipaclient_test.realm }}" + basedn: "{{ result_ipaclient_test.basedn }}" + hostname: "{{ result_ipaclient_test.hostname }}" + subject_base: "{{ result_ipaclient_api.subject_base }}" + principal: "{{ ipaadmin_principal | default(omit) }}" + mkhomedir: "{{ ipaclient_mkhomedir }}" + ca_enabled: "{{ result_ipaclient_api.ca_enabled }}" + on_master: "{{ ipaclient_on_master }}" + dnsok: "{{ result_ipaclient_test.dnsok }}" + enable_dns_updates: "{{ ipassd_enable_dns_updates + | default(ipasssd_enable_dns_updates) }}" + all_ip_addresses: "{{ ipaclient_all_ip_addresses }}" + ip_addresses: "{{ ipaclient_ip_addresses | default(omit) }}" + request_cert: "{{ ipaclient_request_cert }}" + preserve_sssd: "{{ ipassd_preserve_sssd + | default(ipasssd_preserve_sssd) }}" + no_ssh: "{{ ipaclient_no_ssh }}" + no_sshd: "{{ ipaclient_no_sshd }}" + no_sudo: "{{ ipaclient_no_sudo }}" + fixed_primary: "{{ ipassd_fixed_primary + | default(ipasssd_fixed_primary) }}" + permit: "{{ ipassd_permit | default(ipasssd_permit) }}" + no_krb5_offline_passwords: + "{{ ipassd_no_krb5_offline_passwords + | default(ipasssd_no_krb5_offline_passwords) }}" + no_dns_sshfp: "{{ ipaclient_no_dns_sshfp }}" + + - name: Install - Configure SSH and SSHD + ipaclient_setup_ssh: + servers: "{{ result_ipaclient_test.servers }}" + sssd: "{{ result_ipaclient_test.sssd }}" + no_ssh: "{{ ipaclient_no_ssh }}" + ssh_trust_dns: "{{ ipaclient_ssh_trust_dns }}" + no_sshd: "{{ ipaclient_no_sshd }}" + + - name: Install - Configure automount + ipaclient_setup_automount: + servers: "{{ result_ipaclient_test.servers }}" + sssd: "{{ result_ipaclient_test.sssd }}" + automount_location: "{{ ipaautomount_location | default(omit) }}" + + - name: Install - Configure firefox + ipaclient_setup_firefox: + firefox_dir: "{{ ipaclient_firefox_dir | default(omit) }}" + when: ipaclient_configure_firefox | bool + + - name: Install - Configure NIS + ipaclient_setup_nis: + domain: "{{ result_ipaclient_test.domain }}" + nisdomain: "{{ ipaclient_nisdomain | default(omit) }}" + when: not ipaclient_no_nisdomain | bool + + when: not (not ipaclient_on_master | bool and + not result_ipaclient_join.changed and + not ipaclient_allow_repair | bool + and (result_ipaclient_test_keytab.krb5_keytab_ok + or (result_ipaclient_join.already_joined is defined + and result_ipaclient_join.already_joined))) + + when: not ansible_check_mode and + not (result_ipaclient_test.client_already_configured and + not ipaclient_allow_repair | bool and not ipaclient_force_join | bool) + + always: + - name: Install - Restore original admin password if overwritten by OTP + no_log: yes + set_fact: + ipaadmin_password: "{{ ipaadmin_orig_password }}" + when: ipaclient_use_otp | bool and ipaadmin_orig_password is defined + + - name: Cleanup leftover ccache + file: + path: "/etc/ipa/.dns_ccache" + state: absent diff --git a/roles/ipaclient/tasks/main.yml b/roles/ipaclient/tasks/main.yml new file mode 100644 index 0000000..d8b3c03 --- /dev/null +++ b/roles/ipaclient/tasks/main.yml @@ -0,0 +1,18 @@ +--- +# tasks file for ipaclient + +- name: Import variables specific to distribution + include_vars: "{{ item }}" + with_first_found: + - "{{ role_path }}/vars/{{ ansible_distribution }}-{{ ansible_distribution_version }}.yml" + - "{{ role_path }}/vars/{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml" + - "{{ role_path }}/vars/{{ ansible_distribution }}.yml" + - "{{ role_path }}/vars/default.yml" + +- name: Install IPA client + include_tasks: install.yml + when: state|default('present') == 'present' + +- name: Uninstall IPA client + include_tasks: uninstall.yml + when: state|default('present') == 'absent' diff --git a/roles/ipaclient/tasks/python_2_3_test.yml b/roles/ipaclient/tasks/python_2_3_test.yml new file mode 100644 index 0000000..511dea4 --- /dev/null +++ b/roles/ipaclient/tasks/python_2_3_test.yml @@ -0,0 +1,18 @@ +--- +- block: + - name: Verify Python3 import + script: py3test.py + register: result_py3test + failed_when: False + changed_when: False + check_mode: no + + - name: Set python interpreter to 3 + set_fact: + ansible_python_interpreter: "/usr/bin/python3" + when: result_py3test.rc == 0 + + - name: Set python interpreter to 2 + set_fact: + ansible_python_interpreter: "/usr/bin/python2" + when: result_py3test.failed or result_py3test.rc != 0 diff --git a/roles/ipaclient/tasks/uninstall.yml b/roles/ipaclient/tasks/uninstall.yml new file mode 100644 index 0000000..095099a --- /dev/null +++ b/roles/ipaclient/tasks/uninstall.yml @@ -0,0 +1,20 @@ +--- +# tasks to uninstall IPA client + +# - name: Uninstall - Include Python2/3 import test +# import_tasks: "{{ role_path }}/tasks/python_2_3_test.yml" + +- name: Uninstall - Uninstall IPA client + command: > + /usr/sbin/ipa-client-install + --uninstall + -U + register: uninstall + # 2 means that uninstall failed because IPA client was not configured + failed_when: uninstall.rc != 0 and uninstall.rc != 2 + changed_when: uninstall.rc == 0 + +#- name: Remove IPA client package +# package: +# name: "{{ ipaclient_packages }}" +# state: absent diff --git a/roles/ipaclient/vars/CentOS-7.yml b/roles/ipaclient/vars/CentOS-7.yml new file mode 100644 index 0000000..51ab7bf --- /dev/null +++ b/roles/ipaclient/vars/CentOS-7.yml @@ -0,0 +1,4 @@ +# defaults file for ipaclient +# vars/rhel.yml +ipaclient_packages: [ "ipa-client", "libselinux-python" ] +#ansible_python_interpreter: '/usr/bin/python2' diff --git a/roles/ipaclient/vars/CentOS-8.yml b/roles/ipaclient/vars/CentOS-8.yml new file mode 120000 index 0000000..d49e1cd --- /dev/null +++ b/roles/ipaclient/vars/CentOS-8.yml @@ -0,0 +1 @@ +RedHat-8.yml \ No newline at end of file diff --git a/roles/ipaclient/vars/Debian.yml b/roles/ipaclient/vars/Debian.yml new file mode 100644 index 0000000..96d9c32 --- /dev/null +++ b/roles/ipaclient/vars/Debian.yml @@ -0,0 +1,2 @@ +# vars/Debian.yml +ipaclient_packages: [ "freeipa-client" ] diff --git a/roles/ipaclient/vars/Fedora-25.yml b/roles/ipaclient/vars/Fedora-25.yml new file mode 100644 index 0000000..1a62ffa --- /dev/null +++ b/roles/ipaclient/vars/Fedora-25.yml @@ -0,0 +1,2 @@ +ipaclient_packages: [ "ipa-client", "libselinux-python" ] +#ansible_python_interpreter: '/usr/bin/python2' \ No newline at end of file diff --git a/roles/ipaclient/vars/Fedora-26.yml b/roles/ipaclient/vars/Fedora-26.yml new file mode 100644 index 0000000..1a62ffa --- /dev/null +++ b/roles/ipaclient/vars/Fedora-26.yml @@ -0,0 +1,2 @@ +ipaclient_packages: [ "ipa-client", "libselinux-python" ] +#ansible_python_interpreter: '/usr/bin/python2' \ No newline at end of file diff --git a/roles/ipaclient/vars/RedHat-7.3.yml b/roles/ipaclient/vars/RedHat-7.3.yml new file mode 100644 index 0000000..2501eab --- /dev/null +++ b/roles/ipaclient/vars/RedHat-7.3.yml @@ -0,0 +1,4 @@ +# defaults file for ipaclient +# vars/rhel.yml +ipaclient_packages: [ "ipa-client", "ipa-admintools", "libselinux-python" ] +#ansible_python_interpreter: '/usr/bin/python2' diff --git a/roles/ipaclient/vars/RedHat-7.yml b/roles/ipaclient/vars/RedHat-7.yml new file mode 100644 index 0000000..51ab7bf --- /dev/null +++ b/roles/ipaclient/vars/RedHat-7.yml @@ -0,0 +1,4 @@ +# defaults file for ipaclient +# vars/rhel.yml +ipaclient_packages: [ "ipa-client", "libselinux-python" ] +#ansible_python_interpreter: '/usr/bin/python2' diff --git a/roles/ipaclient/vars/RedHat-8.yml b/roles/ipaclient/vars/RedHat-8.yml new file mode 100644 index 0000000..f2b883c --- /dev/null +++ b/roles/ipaclient/vars/RedHat-8.yml @@ -0,0 +1,3 @@ +# defaults file for ipaclient +# vars/RedHat-8.yml +ipaclient_packages: [ "@idm:DL1/client" ] diff --git a/roles/ipaclient/vars/Ubuntu.yml b/roles/ipaclient/vars/Ubuntu.yml new file mode 100644 index 0000000..ded2f5e --- /dev/null +++ b/roles/ipaclient/vars/Ubuntu.yml @@ -0,0 +1,2 @@ +# vars/Ubuntu.yml +ipaclient_packages: [ "freeipa-client" ] diff --git a/roles/ipaclient/vars/default.yml b/roles/ipaclient/vars/default.yml new file mode 100644 index 0000000..ff34748 --- /dev/null +++ b/roles/ipaclient/vars/default.yml @@ -0,0 +1,4 @@ +# defaults file for ipaclient +# vars/default.yml +ipaclient_packages: [ "ipa-client", "python3-libselinux" ] +#ansible_python_interpreter: '/usr/bin/python3' diff --git a/roles/ipareplica/README.md b/roles/ipareplica/README.md new file mode 100644 index 0000000..ef058c4 --- /dev/null +++ b/roles/ipareplica/README.md @@ -0,0 +1,246 @@ +ipareplica role +============== + +Description +----------- + +This role allows to configure a new IPA server that is a replica of the server. Once it has been created it is an exact copy of the original IPA server and is an equal master. +Changes made to any master are automatically replicated to other masters. + +This can be done in different ways using auto-discovery of the servers, domain and other settings or by specifying them. + +**Note**: The ansible playbooks and role require a configured ansible environment where the ansible nodes are reachable and are properly set up to have an IP address and a working package manager. + + +Features +-------- +* Replica deployment + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.6 and up are supported by the replica role. + + +Supported Distributions +----------------------- + +* RHEL/CentOS 7.6+ +* Fedora 26+ +* Ubuntu + + +Requirements +------------ + +**Controller** +* Ansible version: 2.8+ + +**Node** +* Supported FreeIPA version (see above) +* Supported distribution (needed for package installation only, see above) + + +Usage +===== + +Example inventory file with fixed principal using auto-discovery with DNS records: + + [ipareplicas] + ipareplica1.example.com + ipareplica2.example.com + + [ipareplicas:vars] + ipaadmin_principal=admin + +Example playbook to setup the IPA client(s) using principal from inventory file and password from an [Ansible Vault](http://docs.ansible.com/ansible/latest/playbooks_vault.html) file: + + - name: Playbook to configure IPA replicas + hosts: ipareplicas + become: true + vars_files: + - playbook_sensitive_data.yml + + roles: + - role: ipareplica + state: present + +Example playbook to unconfigure the IPA client(s) using principal and password from inventory file: + + - name: Playbook to unconfigure IPA replicas + hosts: ipareplicas + become: true + + roles: + - role: ipareplica + state: absent + +Example inventory file with fixed server, principal, password and domain: + + [ipaserver] + ipaserver.example.com + + [ipareplicas] + ipareplica1.example.com + ipareplica2.example.com + + [ipareplicas:vars] + ipaclient_domain=example.com + ipaadmin_principal=admin + ipaadmin_password=MySecretPassword123 + ipadm_password=MySecretPassword456 + +Example playbook to setup the IPA client(s) using principal and password from inventory file: + + - name: Playbook to configure IPA replicas with username/password + hosts: ipareplicas + become: true + + roles: + - role: ipareplica + state: present + + +Playbooks +========= + +The playbooks needed to deploy or undeploy a replica are part of the repository in the playbooks folder. There are also playbooks to deploy and undeploy clusters. +``` +install-replica.yml +uninstall-replica.yml +``` +Please remember to link or copy the playbooks to the base directory of ansible-freeipa if you want to use the roles within the source archive. + + +How to setup replicas +--------------------- + +```bash +ansible-playbook -v -i inventory/hosts install-replica.yml +``` +This will deploy the replicas defined in the inventory file. + + +Variables +========= + +Base Variables +-------------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaservers` | This group with the IPA master full qualified hostnames. (list of strings) | mostly +`ipareplicas` | Group of IPA replica hostnames. (list of strings) | yes +`ipaadmin_password` | The password for the IPA admin user (string) | mostly +`ipareplica_ip_addresses` | The list of master server IP addresses. (list of strings) | no +`ipareplica_domain` | The primary DNS domain of an existing IPA deployment. (string) | no +`ipaserver_realm` | The Kerberos realm of an existing IPA deployment. (string) | no +`ipaserver_hostname` | Fully qualified name of the server. (string) | no +`ipaadmin_principal` | The authorized kerberos principal used to join the IPA realm. (string) | no +`ipareplica_no_host_dns` | Do not use DNS for hostname lookup during installation. (bool, default: false) | no +`ipareplica_skip_conncheck` | Skip connection check to remote master. (bool, default: false) | no +`ipareplica_pki_config_override` | Path to ini file with config overrides. This is only usable with recent FreeIPA versions. (string) | no + +Server Vaiables +--------------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipadm_password` | The password for the Directory Manager. (string) | mostly +`ipareplica_setup_adtrust` | Configure AD trust capability. (bool, default: false) | no +`ipareplica_setup_ca` | Configure a dogtag CA. (bool, default: false) | no +`ipareplica_setup_kra` | Configure a dogtag KRA. (bool, default: false) | no +`ipareplica_setup_dns` | Configure bind with our zone. (bool, default: false) | no +`ipareplica_no_pkinit` | Disables pkinit setup steps. (bool, default: false) | no +`ipareplica_no_ui_redirect` | Do not automatically redirect to the Web UI. (bool, default: false) | no +`ipareplica_dirsrv_config_file` | The path to LDIF file that will be used to modify configuration of dse.ldif during installation of the directory server instance. (string)| no + +SSL certificate Variables +------------------------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipareplica_dirsrv_cert_files` | Files containing the Directory Server SSL certificate and private keys. (list of strings) | no +`ipareplica_http_cert_files` | Files containing the Apache Server SSL certificate and private key. (list of string) | no +`ipareplica_pkinit_cert_files` | Files containing the Kerberos KDC SSL certificate and private key. (list of string) | no +`ipareplica_dirsrv_pin` | The password to unlock the Directory Server private key. (string) | no +`ipareplica_http_pin` | The password to unlock the Apache Server private key. (string) | no +`ipareplica_pkinit_pin` | The password to unlock the Kerberos KDC private key. (string) | no +`ipareplica_dirsrv_cert_name` | Name of the Directory Server SSL certificate to install. (string) | no +`ipareplica_http_cert_name` | Name of the Apache Server SSL certificate to install. (string) | no +`ipareplica_pkinit_cert_name` | Name of the Kerberos KDC SSL certificate to install. (string) | no + +Client Variables +---------------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaclient_keytab` | Path to backed up keytab from previous enrollment. (string) | no +`ipaclient_mkhomedir` | Set to yes to configure PAM to create a users home directory if it does not exist. (string) | no +`ipaclient_force_join` | Force client enrollment even if already enrolled. (bool, default: false) | no +`ipaclient_ntp_servers` | The list defines the NTP servers to be used. (list of strings) | no +`ipaclient_ntp_pool` | The string value defines the ntp server pool to be used. (string) | no +`ipaclient_no_ntp` | The bool value defines if NTP will not be configured and enabled. (bool, default: false) | no +`ipaclient_ssh_trust_dns` | The bool value defines if OpenSSH client will be configured to trust DNS SSHFP records. (bool, default: false) | no +`ipaclient_no_ssh` | The bool value defines if OpenSSH client will be configured. (bool, default: false) | no +`ipaclient_no_sshd` | The bool value defines if OpenSSH server will be configured. (bool, default: false) | no +`ipaclient_no_sudo` | The bool value defines if SSSD will be configured as a data source for sudo. (bool, default: false) | no +`ipaclient_no_dns_sshfp` | The bool value defines if DNS SSHFP records will not be created automatically. (bool, default: false) | no + +Certificate system Variables +---------------------------- + +Variable | Description | Required +-------- | ----------- | -------- +~~`ipareplica_skip_schema_check`~~ | ~~Skip check for updated CA DS schema on the remote master. (bool, default: false)~~ | ~~no~~ + +DNS Variables +------------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipareplica_allow_zone_overlap` | Allow creation of (reverse) zone even if the zone is already resolvable. (bool, default: false) | no +`ipareplica_reverse_zones` | The reverse DNS zones to use. (list of strings) | no +`ipareplica_no_reverse` | Do not create reverse DNS zone. (bool, default: false) | no +`ipareplica_auto_reverse` | Try to resolve reverse records and reverse zones for server IP addresses. (bool, default: false) | no +`ipareplica_zonemgr` | The e-mail address of the DNS zone manager. (string, default: hostmaster@DOMAIN.) | no +`ipareplica_forwarders` | Add DNS forwarders to the DNS configuration. (list of strings) | no +`ipareplica_no_forwarders` | Do not add any DNS forwarders. Root DNS servers will be used instead. (bool, default: false) | no +`ipareplica_auto_forwarders` | Add DNS forwarders configured in /etc/resolv.conf to the list of forwarders used by IPA DNS. (bool, default: false) | no +`ipareplica_forward_policy` | DNS forwarding policy for global forwarders specified using other options. (choice: first,only) | no +`ipareplica_no_dnssec_validation` | Disable DNSSEC validation on this server. (bool, default: false) | no + +AD trust Variables +------------------ + +Variable | Description | Required +-------- | ----------- | -------- +~~`ipareplica_add_sids`~~ | ~~Add SIDs for existing users and groups as the final step. (bool, default: false)~~ | ~~no~~ +~~`ipareplica_add_agents`~~ | ~~Add IPA masters to a list of hosts allowed to serve information about users from trusted forests. (bool, default: false)~~ | ~~no~~ +`ipareplica_enable_compat`| Enables support for trusted domains users for old clients through Schema Compatibility plugin. (bool, default: false) | no +`ipareplica_netbios_name` | The NetBIOS name for the IPA domain. (string) | no +`ipareplica_rid_base` | First RID value of the local domain. (integer) | no +`ipareplica_secondary_rid_base` | Start value of the secondary RID range. (integer) | no + +Cluster Specific Variables +-------------------------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipareplica_servers` | Manually override list of servers for example in a cluster environment on a per replica basis. The list of servers is normally taken from from groups.ipaserver in cluster environments. (list of strings) | no +`ipaserver_domain` | Used if set in a cluster environment to overload `ipareplica_domain` | no + +Special Variables +----------------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipareplica_install_packages` | The bool value defines if the needed packages are installed on the node. (bool, default: true) | no +`ipareplica_setup_firewalld` | The value defines if the needed services will automatically be openen in the firewall managed by firewalld. (bool, default: true) | no + + +Authors +======= + +Thomas Woerner diff --git a/roles/ipareplica/defaults/main.yml b/roles/ipareplica/defaults/main.yml new file mode 100644 index 0000000..5eca590 --- /dev/null +++ b/roles/ipareplica/defaults/main.yml @@ -0,0 +1,42 @@ +--- +# defaults file for ipareplica + +### basic ### +ipareplica_no_host_dns: no +ipareplica_skip_conncheck: no +ipareplica_hidden_replica: no +### server ### +ipareplica_setup_adtrust: no +ipareplica_setup_ca: no +ipareplica_setup_kra: no +ipareplica_setup_dns: no +ipareplica_no_pkinit: no +ipareplica_no_ui_redirect: no +### client ### +ipaclient_mkhomedir: no +ipaclient_force_join: no +ipaclient_no_ntp: no +#ipaclient_ssh_trust_dns: no +#ipaclient_no_ssh: no +#ipaclient_no_sshd: no +#ipaclient_no_dns_sshfp: no +ipaclient_ssh_trust_dns: no +### certificate system ### +ipareplica_skip_schema_check: no +### dns ### +ipareplica_allow_zone_overlap: no +ipareplica_no_reverse: no +ipareplica_auto_reverse: no +ipareplica_no_forwarders: no +ipareplica_auto_forwarders: no +ipareplica_no_dnssec_validation: no +### ad trust ### +ipareplica_enable_compat: no +### uninstall ### +ipareplica_ignore_topology_disconnect: no +ipareplica_ignore_last_of_role: no +### additional ### +### packages ### +ipareplica_install_packages: yes +### firewalld ### +ipareplica_setup_firewalld: yes diff --git a/roles/ipareplica/files/py3test.py b/roles/ipareplica/files/py3test.py new file mode 100644 index 0000000..8c60ded --- /dev/null +++ b/roles/ipareplica/files/py3test.py @@ -0,0 +1,12 @@ +#!/usr/bin/python3 + +# Test ipaerver python3 binding +try: + from ipaserver.install.server.replicainstall import install_check +except ImportError: + from ipaserver.install.server.replicainstall import promote_check + +# Check ipapython version to be >= 4.6 +from ipapython.version import NUM_VERSION, VERSION +if NUM_VERSION < 40590: + raise Exception("ipa %s not usable with python3" % VERSION) diff --git a/roles/ipareplica/library/ipareplica_add_to_ipaservers.py b/roles/ipareplica/library/ipareplica_add_to_ipaservers.py new file mode 100644 index 0000000..6a85dc1 --- /dev/null +++ b/roles/ipareplica/library/ipareplica_add_to_ipaservers.py @@ -0,0 +1,147 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_add_to_ipaservers +short description: Add to ipaservers +description: + Add to ipaservers +options: + setup_kra: + description: Configure a dogtag KRA + required: no + config_master_host_name: + description: The config master_host_name setting + required: no + ccache: + description: The local ccache + required: no + installer_ccache: + description: The installer ccache setting + required: no + _top_dir: + description: The installer _top_dir setting + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os +import six + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, installer, paths, + gen_env_boostrap_finalize_core, constants, api_bootstrap_finalize, + gen_remote_api, api +) + +if six.PY3: + unicode = str + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # server + setup_kra=dict(required=True, type='bool'), + # additional + config_master_host_name=dict(required=True), + ccache=dict(required=True), + installer_ccache=dict(required=True), + _top_dir=dict(required=True), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + options = installer + # server + options.setup_kra = ansible_module.params.get('setup_kra') + # additional + config_master_host_name = ansible_module.params.get( + 'config_master_host_name') + ccache = ansible_module.params.get('ccache') + os.environ['KRB5CCNAME'] = ccache + options._ccache = ansible_module.params.get('installer_ccache') + # os.environ['KRB5CCNAME'] = ansible_module.params.get('installer_ccache') + options._top_dir = ansible_module.params.get('_top_dir') + + # init # + + ansible_log.debug("== INSTALLER ==") + + options = installer + + env = gen_env_boostrap_finalize_core(paths.ETC_IPA, + constants.DEFAULT_CONFIG) + api_bootstrap_finalize(env) + # config = gen_ReplicaConfig() + + remote_api = gen_remote_api(config_master_host_name, paths.ETC_IPA) + # installer._remote_api = remote_api + + conn = remote_api.Backend.ldap2 + ccache = os.environ['KRB5CCNAME'] + + ansible_log.debug("-- HOSTGROUP_ADD_MEMBER --") + try: + ansible_log.debug("-- CONNECT --") + conn.connect(ccache=installer._ccache) + remote_api.Command['hostgroup_add_member']( + u'ipaservers', + host=[unicode(api.env.host)], + ) + finally: + if conn.isconnected(): + ansible_log.debug("-- DISCONNECT --") + conn.disconnect() + os.environ['KRB5CCNAME'] = ccache + + # done # + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_create_ipa_conf.py b/roles/ipareplica/library/ipareplica_create_ipa_conf.py new file mode 100644 index 0000000..8a258f2 --- /dev/null +++ b/roles/ipareplica/library/ipareplica_create_ipa_conf.py @@ -0,0 +1,286 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_create_ipa_conf +short description: Create ipa.conf +description: + Create ipa.conf +options: + dm_password: + description: Directory Manager password + required: yes + password: + description: Admin user kerberos password + required: yes + ip_addresses: + description: List of Master Server IP Addresses + required: yes + domain: + description: Primary DNS domain of the IPA deployment + required: yes + realm: + description: Kerberos realm name of the IPA deployment + required: yes + hostname: + description: Fully qualified name of this host + required: yes + ca_cert_files: + description: + List of files containing CA certificates for the service certificate + files + required: yes + no_host_dns: + description: Do not use DNS for hostname lookup during installation + required: yes + setup_adtrust: + description: Configure AD trust capability + required: yes + setup_ca: + description: Configure a dogtag CA + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + setup_dns: + description: Configure bind with our zone + required: yes + dirsrv_cert_files: + description: + Files containing the Directory Server SSL certificate and private key + required: yes + force_join: + description: Force client enrollment even if already enrolled + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: no + server: + description: Fully qualified name of IPA server to enroll to + required: no + config_master_host_name: + description: The config master_host_name setting + required: no + config_ca_host_name: + description: The config ca_host_name setting + required: no + ccache: + description: The local ccache + required: no + installer_ccache: + description: The installer ccache setting + required: no + _ca_enabled: + description: The installer _ca_enabled setting + required: yes + _top_dir: + description: The installer _top_dir setting + required: no + _add_to_ipaservers: + description: The installer _add_to_ipaservers setting + required: no + _ca_subject: + description: The installer _ca_subject setting + required: no + _subject_base: + description: The installer _subject_base setting + required: no + master: + description: Master host name + required: yes + dirman_password: + description: Directory Manager (master) password + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, installer, DN, paths, + ansible_module_get_parsed_ip_addresses, sysrestore, + gen_env_boostrap_finalize_core, constants, api_bootstrap_finalize, + gen_ReplicaConfig, gen_remote_api, create_ipa_conf +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # basic + dm_password=dict(required=False, no_log=True), + password=dict(required=False, no_log=True), + ip_addresses=dict(required=False, type='list', default=[]), + domain=dict(required=False), + realm=dict(required=False), + hostname=dict(required=False), + ca_cert_files=dict(required=False, type='list', default=[]), + no_host_dns=dict(required=False, type='bool', default=False), + # server + setup_adtrust=dict(required=False, type='bool'), + setup_ca=dict(required=False, type='bool'), + setup_kra=dict(required=False, type='bool'), + setup_dns=dict(required=False, type='bool'), + # ssl certificate + dirsrv_cert_files=dict(required=False, type='list', default=[]), + # client + force_join=dict(required=False, type='bool'), + # certificate system + subject_base=dict(required=True), + # additional + server=dict(required=True), + config_master_host_name=dict(required=True), + config_ca_host_name=dict(required=True), + ccache=dict(required=True), + installer_ccache=dict(required=True), + _ca_enabled=dict(required=False, type='bool'), + _top_dir=dict(required=True), + _add_to_ipaservers=dict(required=True, type='bool'), + _ca_subject=dict(required=True), + _subject_base=dict(required=True), + master=dict(required=False, default=None), + + dirman_password=dict(required=True, no_log=True), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + options = installer + options.dm_password = ansible_module.params.get('dm_password') + options.password = options.dm_password + options.admin_password = ansible_module.params.get('password') + options.ip_addresses = ansible_module_get_parsed_ip_addresses( + ansible_module) + options.domain_name = ansible_module.params.get('domain') + options.realm_name = ansible_module.params.get('realm') + options.host_name = ansible_module.params.get('hostname') + options.ca_cert_files = ansible_module.params.get('ca_cert_files') + options.no_host_dns = ansible_module.params.get('no_host_dns') + # server + options.setup_adtrust = ansible_module.params.get('setup_adtrust') + options.setup_ca = ansible_module.params.get('setup_ca') + options.setup_kra = ansible_module.params.get('setup_kra') + options.setup_dns = ansible_module.params.get('setup_dns') + # ssl certificate + options.dirsrv_cert_files = ansible_module.params.get('dirsrv_cert_files') + # client + options.force_join = ansible_module.params.get('force_join') + # certificate system + options.external_ca = ansible_module.params.get('external_ca') + options.external_cert_files = ansible_module.params.get( + 'external_cert_files') + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + options.ca_subject = ansible_module.params.get('ca_subject') + # additional + # options._host_name_overridden = ansible_module.params.get( + # '_hostname_overridden') + options.server = ansible_module.params.get('server') + master_host_name = ansible_module.params.get('config_master_host_name') + ca_host_name = ansible_module.params.get('config_ca_host_name') + ccache = ansible_module.params.get('ccache') + os.environ['KRB5CCNAME'] = ccache + # os.environ['KRB5CCNAME'] = ansible_module.params.get('installer_ccache') + installer._ccache = ansible_module.params.get('installer_ccache') + ca_enabled = ansible_module.params.get('_ca_enabled') + + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + options._top_dir = ansible_module.params.get('_top_dir') + options._add_to_ipaservers = ansible_module.params.get( + '_add_to_ipaservers') + + options._ca_subject = ansible_module.params.get('_ca_subject') + options._subject_base = ansible_module.params.get('_subject_base') + master = ansible_module.params.get('master') + + dirman_password = ansible_module.params.get('dirman_password') + + # init # + + fstore = sysrestore.FileStore(paths.SYSRESTORE) + + # prepare (install prepare, install checks) # + + ansible_log.debug("== INSTALL ==") + + options = installer + promote = installer.promote + + env = gen_env_boostrap_finalize_core(paths.ETC_IPA, + constants.DEFAULT_CONFIG) + api_bootstrap_finalize(env) + config = gen_ReplicaConfig() + config.subject_base = options.subject_base + config.dirman_password = dirman_password + config.ca_host_name = ca_host_name + + remote_api = gen_remote_api(master_host_name, paths.ETC_IPA) + installer._remote_api = remote_api + + ccache = os.environ['KRB5CCNAME'] + + if promote: + ansible_log.debug("-- CREATE_IPA_CONF --") + # Create the management framework config file. Do this irregardless + # of the state of DS installation. Even if it fails, + # we need to have master-like configuration in order to perform a + # successful uninstallation + # The configuration creation has to be here otherwise previous call + # To config certmonger would try to connect to local server + create_ipa_conf(fstore, config, ca_enabled, master) + + # done # + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_custodia_import_dm_password.py b/roles/ipareplica/library/ipareplica_custodia_import_dm_password.py new file mode 100644 index 0000000..5b95a61 --- /dev/null +++ b/roles/ipareplica/library/ipareplica_custodia_import_dm_password.py @@ -0,0 +1,212 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_custodia_import_dm_password +short description: Import dm password into custodia +description: + Import dm password into custodia +options: + setup_ca: + description: Configure a dogtag CA + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + no_pkinit: + description: Disable pkinit setup steps + required: yes + no_ui_redirect: + description: Do not automatically redirect to the Web UI + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: no + ccache: + description: The local ccache + required: no + _ca_enabled: + description: The installer _ca_enabled setting + required: yes + _ca_file: + description: The installer _ca_file setting + required: yes + _kra_enabled: + description: The installer _kra_enabled setting + required: yes + _kra_host_name: + description: The installer _kra_host_name setting + required: yes + _top_dir: + description: The installer _top_dir setting + required: no + dirman_password: + description: Directory Manager (master) password + required: no + config_setup_ca: + description: The config setup_ca setting + required: no + config_master_host_name: + description: The config master_host_name setting + required: no + config_ca_host_name: + description: The config ca_host_name setting + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os +import inspect + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, installer, DN, paths, + gen_env_boostrap_finalize_core, constants, api_bootstrap_finalize, + gen_ReplicaConfig, gen_remote_api, redirect_stdout, custodiainstance +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # server + setup_ca=dict(required=False, type='bool'), + setup_kra=dict(required=False, type='bool'), + no_pkinit=dict(required=False, type='bool'), + no_ui_redirect=dict(required=False, type='bool'), + # certificate system + subject_base=dict(required=True), + # additional + ccache=dict(required=True), + _ca_enabled=dict(required=False, type='bool'), + _ca_file=dict(required=False), + _kra_enabled=dict(required=False, type='bool'), + _kra_host_name=dict(required=False), + _top_dir=dict(required=True), + dirman_password=dict(required=True, no_log=True), + config_setup_ca=dict(required=True, type='bool'), + config_master_host_name=dict(required=True), + config_ca_host_name=dict(required=True), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + options = installer + # server + options.setup_ca = ansible_module.params.get('setup_ca') + options.setup_kra = ansible_module.params.get('setup_kra') + options.no_pkinit = ansible_module.params.get('no_pkinit') + # certificate system + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + # additional + master_host_name = ansible_module.params.get('config_master_host_name') + ccache = ansible_module.params.get('ccache') + os.environ['KRB5CCNAME'] = ccache + # os.environ['KRB5CCNAME'] = ansible_module.params.get('installer_ccache') + # installer._ccache = ansible_module.params.get('installer_ccache') + ca_enabled = ansible_module.params.get('_ca_enabled') + kra_enabled = ansible_module.params.get('_kra_enabled') + kra_host_name = ansible_module.params.get('_kra_host_name') + options._top_dir = ansible_module.params.get('_top_dir') + dirman_password = ansible_module.params.get('dirman_password') + config_setup_ca = ansible_module.params.get('config_setup_ca') + config_ca_host_name = ansible_module.params.get('config_ca_host_name') + + # init # + + ansible_log.debug("== INSTALL ==") + + options = installer + + env = gen_env_boostrap_finalize_core(paths.ETC_IPA, + constants.DEFAULT_CONFIG) + api_bootstrap_finalize(env) + config = gen_ReplicaConfig() + config.dirman_password = dirman_password + config.setup_ca = config_setup_ca + config.master_host_name = master_host_name + config.ca_host_name = config_ca_host_name + config.subject_base = options.subject_base + config.promote = installer.promote + config.kra_enabled = kra_enabled + config.kra_host_name = kra_host_name + + remote_api = gen_remote_api(config.master_host_name, paths.ETC_IPA) + installer._remote_api = remote_api + + ccache = os.environ['KRB5CCNAME'] + + # do the work # + + with redirect_stdout(ansible_log): + if not hasattr(custodiainstance, "get_custodia_instance"): + custodia = custodiainstance.CustodiaInstance(config.host_name, + config.realm_name) + else: + if ca_enabled: + mode = custodiainstance.CustodiaModes.CA_PEER + else: + mode = custodiainstance.CustodiaModes.MASTER_PEER + custodia = custodiainstance.get_custodia_instance(config, mode) + + ansible_log.debug("-- CUSTODIA IMPORT DM PASSWORD --") + + argspec = inspect.getargspec(custodia.import_dm_password) + if "master_host_name" in argspec.args: + custodia.import_dm_password(config.master_host_name) + else: + custodia.import_dm_password() + + # done # + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_ds_apply_updates.py b/roles/ipareplica/library/ipareplica_ds_apply_updates.py new file mode 100644 index 0000000..e407176 --- /dev/null +++ b/roles/ipareplica/library/ipareplica_ds_apply_updates.py @@ -0,0 +1,222 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_ds_apply_updates +short description: DS apply updates +description: + DS apply updates +options: + setup_ca: + description: Configure a dogtag CA + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + no_pkinit: + description: Disable pkinit setup steps + required: yes + no_ui_redirect: + description: Do not automatically redirect to the Web UI + required: yes + dirsrv_config_file: + description: + The path to LDIF file that will be used to modify configuration of + dse.ldif during installation of the directory server instance + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: no + config_master_host_name: + description: The config master_host_name setting + required: no + ccache: + description: The local ccache + required: no + _ca_enabled: + description: The installer _ca_enabled setting + required: yes + _ca_file: + description: The installer _ca_file setting + required: yes + _dirsrv_pkcs12_info: + description: The installer _dirsrv_pkcs12_info setting + required: yes + _pkinit_pkcs12_info: + description: The installer _pkinit_pkcs12_info setting + required: yes + _top_dir: + description: The installer _top_dir setting + required: no + dirman_password: + description: Directory Manager (master) password + required: no + ds_ca_subject: + description: The ds.ca_subject setting + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, installer, DN, paths, + gen_env_boostrap_finalize_core, constants, api_bootstrap_finalize, + gen_ReplicaConfig, gen_remote_api, api, redirect_stdout, + replica_ds_init_info, dsinstance, upgradeinstance, installutils +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # server + setup_ca=dict(required=False, type='bool'), + setup_kra=dict(required=False, type='bool'), + no_pkinit=dict(required=False, type='bool'), + no_ui_redirect=dict(required=False, type='bool'), + dirsrv_config_file=dict(required=False), + # certificate system + subject_base=dict(required=True), + # additional + config_master_host_name=dict(required=True), + ccache=dict(required=True), + _ca_enabled=dict(required=False, type='bool'), + _ca_file=dict(required=False), + _dirsrv_pkcs12_info=dict(required=False), + _pkinit_pkcs12_info=dict(required=False), + _top_dir=dict(required=True), + dirman_password=dict(required=True, no_log=True), + ds_ca_subject=dict(required=True), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + options = installer + # server + options.setup_ca = ansible_module.params.get('setup_ca') + options.setup_kra = ansible_module.params.get('setup_kra') + options.no_pkinit = ansible_module.params.get('no_pkinit') + options.dirsrv_config_file = ansible_module.params.get( + 'dirsrv_config_file') + # certificate system + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + # additional + master_host_name = ansible_module.params.get('config_master_host_name') + ccache = ansible_module.params.get('ccache') + os.environ['KRB5CCNAME'] = ccache + # os.environ['KRB5CCNAME'] = ansible_module.params.get('installer_ccache') + # installer._ccache = ansible_module.params.get('installer_ccache') + ca_enabled = ansible_module.params.get('_ca_enabled') + installer._dirsrv_pkcs12_info = ansible_module.params.get( + '_dirsrv_pkcs12_info') + installer._pkinit_pkcs12_info = ansible_module.params.get( + '_pkinit_pkcs12_info') + options._top_dir = ansible_module.params.get('_top_dir') + dirman_password = ansible_module.params.get('dirman_password') + ds_ca_subject = ansible_module.params.get('ds_ca_subject') + + # init # + + ansible_log.debug("== INSTALL ==") + + options = installer + promote = installer.promote + + env = gen_env_boostrap_finalize_core(paths.ETC_IPA, + constants.DEFAULT_CONFIG) + api_bootstrap_finalize(env) + config = gen_ReplicaConfig() + config.dirman_password = dirman_password + config.subject_base = options.subject_base + + remote_api = gen_remote_api(master_host_name, paths.ETC_IPA) + + conn = remote_api.Backend.ldap2 + ccache = os.environ['KRB5CCNAME'] + + # There is a api.Backend.ldap2.connect call somewhere in ca, ds, dns or + # ntpinstance + api.Backend.ldap2.connect() + conn.connect(ccache=ccache) + + with redirect_stdout(ansible_log): + ds = replica_ds_init_info(ansible_log, + config, options, ca_enabled, + remote_api, ds_ca_subject, + ca_file=paths.IPA_CA_CRT, + promote=promote, + pkcs12_info=installer._dirsrv_pkcs12_info) + + ansible_log.debug("-- DS APPLY_UPDATES --") + + # Apply any LDAP updates. Needs to be done after the replica is + # synced-up + # service.print_msg("Applying LDAP updates") + # ds.apply_updates() + schema_files = dsinstance.get_all_external_schema_files( + paths.EXTERNAL_SCHEMA_DIR) + data_upgrade = upgradeinstance.IPAUpgrade(ds.realm, + schema_files=schema_files) + data_upgrade.set_output(ansible_log) + try: + data_upgrade.create_instance() + except Exception as e: + # very fatal errors only will raise exception + raise RuntimeError("Update failed: %s" % e) + installutils.store_version() + + # done # + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_ds_enable_ssl.py b/roles/ipareplica/library/ipareplica_ds_enable_ssl.py new file mode 100644 index 0000000..ead7376 --- /dev/null +++ b/roles/ipareplica/library/ipareplica_ds_enable_ssl.py @@ -0,0 +1,206 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_ds_enable_ssl +short description: DS enable SSL +description: + DS enable SSL +options: + setup_ca: + description: Configure a dogtag CA + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + no_pkinit: + description: Disable pkinit setup steps + required: yes + dirsrv_config_file: + description: + The path to LDIF file that will be used to modify configuration of + dse.ldif during installation of the directory server instance + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: no + config_master_host_name: + description: The config master_host_name setting + required: no + ccache: + description: The local ccache + required: no + _ca_enabled: + description: The installer _ca_enabled setting + required: yes + _ca_file: + description: The installer _ca_file setting + required: yes + _dirsrv_pkcs12_info: + description: The installer _dirsrv_pkcs12_info setting + required: yes + _pkinit_pkcs12_info: + description: The installer _pkinit_pkcs12_info setting + required: yes + _top_dir: + description: The installer _top_dir setting + required: no + dirman_password: + description: Directory Manager (master) password + required: no + ds_ca_subject: + description: The ds.ca_subject setting + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, installer, DN, paths, + gen_env_boostrap_finalize_core, constants, api_bootstrap_finalize, + gen_ReplicaConfig, gen_remote_api, api, redirect_stdout, + replica_ds_init_info +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # server + setup_ca=dict(required=False, type='bool'), + setup_kra=dict(required=False, type='bool'), + no_pkinit=dict(required=False, type='bool'), + dirsrv_config_file=dict(required=False), + # certificate system + subject_base=dict(required=True), + # additional + config_master_host_name=dict(required=True), + ccache=dict(required=True), + _ca_enabled=dict(required=False, type='bool'), + _ca_file=dict(required=False), + _dirsrv_pkcs12_info=dict(required=False), + _pkinit_pkcs12_info=dict(required=False), + _top_dir=dict(required=True), + dirman_password=dict(required=True, no_log=True), + ds_ca_subject=dict(required=True), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + options = installer + # server + options.setup_ca = ansible_module.params.get('setup_ca') + options.setup_kra = ansible_module.params.get('setup_kra') + options.no_pkinit = ansible_module.params.get('no_pkinit') + options.dirsrv_config_file = ansible_module.params.get( + 'dirsrv_config_file') + # certificate system + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + # additional + master_host_name = ansible_module.params.get('config_master_host_name') + ccache = ansible_module.params.get('ccache') + os.environ['KRB5CCNAME'] = ccache + # os.environ['KRB5CCNAME'] = ansible_module.params.get('installer_ccache') + # installer._ccache = ansible_module.params.get('installer_ccache') + ca_enabled = ansible_module.params.get('_ca_enabled') + options._dirsrv_pkcs12_info = ansible_module.params.get( + '_dirsrv_pkcs12_info') + options._pkinit_pkcs12_info = ansible_module.params.get( + '_pkinit_pkcs12_info') + options._top_dir = ansible_module.params.get('_top_dir') + dirman_password = ansible_module.params.get('dirman_password') + ds_ca_subject = ansible_module.params.get('ds_ca_subject') + + # init # + + ansible_log.debug("== INSTALL ==") + + options = installer + promote = installer.promote + + env = gen_env_boostrap_finalize_core(paths.ETC_IPA, + constants.DEFAULT_CONFIG) + api_bootstrap_finalize(env) + config = gen_ReplicaConfig() + config.dirman_password = dirman_password + config.subject_base = options.subject_base + + remote_api = gen_remote_api(master_host_name, paths.ETC_IPA) + # installer._remote_api = remote_api + + conn = remote_api.Backend.ldap2 + ccache = os.environ['KRB5CCNAME'] + + # There is a api.Backend.ldap2.connect call somewhere in ca, ds, dns or + # ntpinstance + api.Backend.ldap2.connect() + conn.connect(ccache=ccache) + + with redirect_stdout(ansible_log): + ds = replica_ds_init_info(ansible_log, + config, options, ca_enabled, + remote_api, ds_ca_subject, + ca_file=paths.IPA_CA_CRT, + promote=promote, + pkcs12_info=installer._dirsrv_pkcs12_info) + + ansible_log.debug("-- DS.ENABLE_SSL --") + + # we now need to enable ssl on the ds + ds.enable_ssl() + + # done # + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_enable_ipa.py b/roles/ipareplica/library/ipareplica_enable_ipa.py new file mode 100644 index 0000000..27ae285 --- /dev/null +++ b/roles/ipareplica/library/ipareplica_enable_ipa.py @@ -0,0 +1,174 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_enable_ipa +short description: Enable IPA +description: Enable IPA + Enable IPA +options: + hostname: + description: Fully qualified name of this host + required: yes + hidden_replica: + description: Install a hidden replica + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: no + ccache: + description: The local ccache + required: no + _top_dir: + description: The installer _top_dir setting + required: no + setup_ca: + description: Configure a dogtag CA + required: no + setup_kra: + description: Configure a dogtag KRA + required: no + config_master_host_name: + description: The config master_host_name setting + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, installer, DN, paths, + gen_env_boostrap_finalize_core, constants, api_bootstrap_finalize, + gen_ReplicaConfig, gen_remote_api, api, redirect_stdout, service, + find_providing_servers, services +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + hostname=dict(required=False), + hidden_replica=dict(required=False, type='bool', default=False), + # server + # certificate system + subject_base=dict(required=True), + # additional + ccache=dict(required=True), + _top_dir=dict(required=True), + setup_ca=dict(required=True, type='bool'), + setup_kra=dict(required=True, type='bool'), + config_master_host_name=dict(required=True), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + options = installer + options.host_name = ansible_module.params.get('hostname') + options.hidden_replica = ansible_module.params.get('hidden_replica') + # server + # certificate system + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + # additional + ccache = ansible_module.params.get('ccache') + os.environ['KRB5CCNAME'] = ccache + options._top_dir = ansible_module.params.get('_top_dir') + options.setup_ca = ansible_module.params.get('setup_ca') + options.setup_kra = ansible_module.params.get('setup_kra') + config_master_host_name = ansible_module.params.get( + 'config_master_host_name') + + # init # + + ansible_log.debug("== INSTALL ==") + + env = gen_env_boostrap_finalize_core(paths.ETC_IPA, + constants.DEFAULT_CONFIG) + api_bootstrap_finalize(env) + config = gen_ReplicaConfig() + + remote_api = gen_remote_api(config_master_host_name, paths.ETC_IPA) + installer._remote_api = remote_api + + ccache = os.environ['KRB5CCNAME'] + + api.Backend.ldap2.connect() + + with redirect_stdout(ansible_log): + if options.hidden_replica: + # Set services to hidden + service.hide_services(config.host_name) + else: + # Enable configured services + service.enable_services(config.host_name) + # update DNS SRV records. Although it's only really necessary in + # enabled-service case, also perform update in hidden replica case. + api.Command.dns_update_system_records() + ca_servers = find_providing_servers('CA', api.Backend.ldap2, api=api) + api.Backend.ldap2.disconnect() + + # Everything installed properly, activate ipa service. + services.knownservices.ipa.enable() + + # Print a warning if CA role is only installed on one server + if len(ca_servers) == 1: + msg = u''' + WARNING: The CA service is only installed on one server ({}). + It is strongly recommended to install it on another server. + Run ipa-ca-install(1) on another master to accomplish this. + '''.format(ca_servers[0]) + ansible_module.debug(msg) + + # done # + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_install_ca_certs.py b/roles/ipareplica/library/ipareplica_install_ca_certs.py new file mode 100644 index 0000000..84466e6 --- /dev/null +++ b/roles/ipareplica/library/ipareplica_install_ca_certs.py @@ -0,0 +1,303 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_install_ca_cert +short description: Install CA certs +description: + Install CA certs +options: + dm_password: + description: Directory Manager password + required: yes + password: + description: Admin user kerberos password + required: yes + ip_addresses: + description: List of Master Server IP Addresses + required: yes + domain: + description: Primary DNS domain of the IPA deployment + required: yes + realm: + description: Kerberos realm name of the IPA deployment + required: yes + hostname: + description: Fully qualified name of this host + required: yes + ca_cert_files: + description: + List of files containing CA certificates for the service certificate + files + required: yes + no_host_dns: + description: Do not use DNS for hostname lookup during installation + required: yes + setup_adtrust: + description: Configure AD trust capability + required: yes + setup_ca: + description: Configure a dogtag CA + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + setup_dns: + description: Configure bind with our zone + required: yes + dirsrv_cert_files: + description: + Files containing the Directory Server SSL certificate and private key + required: yes + force_join: + description: Force client enrollment even if already enrolled + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: no + server: + description: Fully qualified name of IPA server to enroll to + required: no + ccache: + description: The local ccache + required: no + installer_ccache: + description: The installer ccache setting + required: no + _top_dir: + description: The installer _top_dir setting + required: no + _add_to_ipaservers: + description: The installer _add_to_ipaservers setting + required: no + _ca_subject: + description: The installer _ca_subject setting + required: no + _subject_base: + description: The installer _subject_base setting + required: no + dirman_password: + description: Directory Manager (master) password + required: no + config_setup_ca: + description: The config setup_ca setting + required: no + config_master_host_name: + description: The config master_host_name setting + required: no + config_ca_host_name: + description: The config ca_host_name setting + required: no + config_ips: + description: The config ips setting + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, installer, DN, paths, + ansible_module_get_parsed_ip_addresses, + gen_env_boostrap_finalize_core, constants, api_bootstrap_finalize, + gen_ReplicaConfig, gen_remote_api, api, redirect_stdout, ipaldap, + install_ca_cert +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # basic + dm_password=dict(required=False, no_log=True), + password=dict(required=False, no_log=True), + ip_addresses=dict(required=False, type='list', default=[]), + domain=dict(required=False), + realm=dict(required=False), + hostname=dict(required=False), + ca_cert_files=dict(required=False, type='list', default=[]), + no_host_dns=dict(required=False, type='bool', default=False), + # server + setup_adtrust=dict(required=False, type='bool'), + setup_ca=dict(required=False, type='bool'), + setup_kra=dict(required=False, type='bool'), + setup_dns=dict(required=False, type='bool'), + # ssl certificate + dirsrv_cert_files=dict(required=False, type='list', default=[]), + # client + force_join=dict(required=False, type='bool'), + # certificate system + subject_base=dict(required=True), + # additional + server=dict(required=True), + ccache=dict(required=True), + installer_ccache=dict(required=True), + _top_dir=dict(required=True), + _add_to_ipaservers=dict(required=True, type='bool'), + _ca_subject=dict(required=True), + _subject_base=dict(required=True), + dirman_password=dict(required=True, no_log=True), + config_setup_ca=dict(required=True, type='bool'), + config_master_host_name=dict(required=True), + config_ca_host_name=dict(required=True), + config_ips=dict(required=False, type='list', default=[]), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + options = installer + # basic + options.dm_password = ansible_module.params.get('dm_password') + options.password = options.dm_password + options.admin_password = ansible_module.params.get('password') + options.ip_addresses = ansible_module_get_parsed_ip_addresses( + ansible_module) + options.domain_name = ansible_module.params.get('domain') + options.realm_name = ansible_module.params.get('realm') + options.host_name = ansible_module.params.get('hostname') + options.ca_cert_files = ansible_module.params.get('ca_cert_files') + options.no_host_dns = ansible_module.params.get('no_host_dns') + # server + options.setup_adtrust = ansible_module.params.get('setup_adtrust') + options.setup_ca = ansible_module.params.get('setup_ca') + options.setup_kra = ansible_module.params.get('setup_kra') + options.setup_dns = ansible_module.params.get('setup_dns') + # ssl certificate + options.dirsrv_cert_files = ansible_module.params.get('dirsrv_cert_files') + # client + options.force_join = ansible_module.params.get('force_join') + # certificate system + options.external_ca = ansible_module.params.get('external_ca') + options.external_cert_files = ansible_module.params.get( + 'external_cert_files') + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + options.ca_subject = ansible_module.params.get('ca_subject') + # additional + options.server = ansible_module.params.get('server') + ccache = ansible_module.params.get('ccache') + os.environ['KRB5CCNAME'] = ccache + # os.environ['KRB5CCNAME'] = ansible_module.params.get('installer_ccache') + installer._ccache = ansible_module.params.get('installer_ccache') + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + options._top_dir = ansible_module.params.get('_top_dir') + options._add_to_ipaservers = ansible_module.params.get( + '_add_to_ipaservers') + options._ca_subject = ansible_module.params.get('_ca_subject') + options._subject_base = ansible_module.params.get('_subject_base') + dirman_password = ansible_module.params.get('dirman_password') + config_setup_ca = ansible_module.params.get('config_setup_ca') + config_master_host_name = ansible_module.params.get( + 'config_master_host_name') + config_ca_host_name = ansible_module.params.get('config_ca_host_name') + config_ips = ansible_module_get_parsed_ip_addresses(ansible_module, + "config_ips") + + # init # + + ansible_log.debug("== INSTALLER ==") + + options = installer + promote = installer.promote + + env = gen_env_boostrap_finalize_core(paths.ETC_IPA, + constants.DEFAULT_CONFIG) + api_bootstrap_finalize(env) + config = gen_ReplicaConfig() + config.dirman_password = dirman_password + config.setup_ca = config_setup_ca + config.master_host_name = config_master_host_name + config.ca_host_name = config_ca_host_name + config.ips = config_ips + + remote_api = gen_remote_api(config.master_host_name, paths.ETC_IPA) + installer._remote_api = remote_api + + conn = remote_api.Backend.ldap2 + ccache = os.environ['KRB5CCNAME'] + + cafile = paths.IPA_CA_CRT + with redirect_stdout(ansible_log): + try: + ansible_log.debug("-- CONNECT --") + if promote: + conn.connect(ccache=ccache) + else: + # dmlvl 0 replica install should always use DM credentials + # to create remote LDAP connection. Since ACIs permitting hosts + # to manage their own services were added in 4.2 release, + # the master denies this operations. + conn.connect(bind_dn=ipaldap.DIRMAN_DN, cacert=cafile, + bind_pw=dirman_password) + + ansible_log.debug("-- INSTALL_CA_CERT --") + # Update and istall updated CA file + cafile = install_ca_cert(conn, api.env.basedn, api.env.realm, + cafile) + install_ca_cert(conn, api.env.basedn, api.env.realm, cafile, + destfile=paths.KDC_CA_BUNDLE_PEM) + install_ca_cert(conn, api.env.basedn, api.env.realm, cafile, + destfile=paths.CA_BUNDLE_PEM) + + finally: + if conn.isconnected(): + ansible_log.debug("-- DISCONNECT --") + conn.disconnect() + + # done # + + ansible_module.exit_json(changed=True, + config_master_host_name=config.master_host_name, + config_ca_host_name=config.ca_host_name) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_krb_enable_ssl.py b/roles/ipareplica/library/ipareplica_krb_enable_ssl.py new file mode 100644 index 0000000..6d09b6b --- /dev/null +++ b/roles/ipareplica/library/ipareplica_krb_enable_ssl.py @@ -0,0 +1,187 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_krb_enable_ssl +short description: KRB enable SSL +description: + KRB enable SSL +options: + setup_ca: + description: Configure a dogtag CA + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + no_pkinit: + description: Disable pkinit setup steps + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: no + config_master_host_name: + description: The config master_host_name setting + required: no + ccache: + description: The local ccache + required: no + _ca_enabled: + description: The installer _ca_enabled setting + required: yes + _ca_file: + description: The installer _ca_file setting + required: yes + _pkinit_pkcs12_info: + description: The installer _pkinit_pkcs12_info setting + required: yes + _top_dir: + description: The installer _top_dir setting + required: no + dirman_password: + description: Directory Manager (master) password + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, installer, DN, paths, sysrestore, + gen_env_boostrap_finalize_core, constants, api_bootstrap_finalize, + gen_ReplicaConfig, gen_remote_api, api, krbinstance, redirect_stdout +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # server + setup_ca=dict(required=False, type='bool'), + setup_kra=dict(required=False, type='bool'), + no_pkinit=dict(required=False, type='bool'), + # certificate system + subject_base=dict(required=True), + # additional + config_master_host_name=dict(required=True), + ccache=dict(required=True), + _ca_enabled=dict(required=False, type='bool'), + _ca_file=dict(required=False), + _pkinit_pkcs12_info=dict(required=False), + _top_dir=dict(required=True), + dirman_password=dict(required=True, no_log=True), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + options = installer + # server + options.setup_ca = ansible_module.params.get('setup_ca') + options.setup_kra = ansible_module.params.get('setup_kra') + options.no_pkinit = ansible_module.params.get('no_pkinit') + # certificate system + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + # additional + master_host_name = ansible_module.params.get('config_master_host_name') + ccache = ansible_module.params.get('ccache') + os.environ['KRB5CCNAME'] = ccache + # os.environ['KRB5CCNAME'] = ansible_module.params.get('installer_ccache') + # installer._ccache = ansible_module.params.get('installer_ccache') + options._pkinit_pkcs12_info = ansible_module.params.get( + '_pkinit_pkcs12_info') + options._top_dir = ansible_module.params.get('_top_dir') + dirman_password = ansible_module.params.get('dirman_password') + + # init # + + fstore = sysrestore.FileStore(paths.SYSRESTORE) + + ansible_log.debug("== INSTALL ==") + + options = installer + + env = gen_env_boostrap_finalize_core(paths.ETC_IPA, + constants.DEFAULT_CONFIG) + api_bootstrap_finalize(env) + config = gen_ReplicaConfig() + config.dirman_password = dirman_password + + remote_api = gen_remote_api(master_host_name, paths.ETC_IPA) + # installer._remote_api = remote_api + + conn = remote_api.Backend.ldap2 + ccache = os.environ['KRB5CCNAME'] + + # There is a api.Backend.ldap2.connect call somewhere in ca, ds, dns or + # ntpinstance + api.Backend.ldap2.connect() + conn.connect(ccache=ccache) + + # krb + krb = krbinstance.KrbInstance(fstore) + krb.set_output(ansible_log) + with redirect_stdout(ansible_log): + krb.init_info(api.env.realm, api.env.host, + setup_pkinit=not options.no_pkinit, + subject_base=options.subject_base) + krb.pkcs12_info = options._pkinit_pkcs12_info + krb.master_fqdn = master_host_name + + ansible_log.debug("-- KRB ENABLE_SSL --") + + # configure PKINIT now that all required services are in place + krb.enable_ssl() + + # done # + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_master_password.py b/roles/ipareplica/library/ipareplica_master_password.py new file mode 100644 index 0000000..875a23d --- /dev/null +++ b/roles/ipareplica/library/ipareplica_master_password.py @@ -0,0 +1,81 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-server-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_master_password +short description: Generate kerberos master password if not given +description: + Generate kerberos master password if not given +options: + master_password: + description: kerberos master password (normally autogenerated) + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +password: + description: The master password + returned: always +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + ipa_generate_password +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + # basic + master_password=dict(required=False, no_log=True), + ), + supports_check_mode=True, + ) + + module._ansible_debug = True + + master_password = module.params.get('master_password') + + if not master_password: + master_password = ipa_generate_password() + + module.exit_json(changed=True, + password=master_password) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_prepare.py b/roles/ipareplica/library/ipareplica_prepare.py new file mode 100644 index 0000000..3d9230b --- /dev/null +++ b/roles/ipareplica/library/ipareplica_prepare.py @@ -0,0 +1,2068 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_prepare +short description: Prepare ipa replica installation +description: + Prepare ipa replica installation: Create IPA configuration file, run install + checks again and also update the host name and the hosts file if needed. + The tests and also the results from ipareplica_test are needed. +ptions: + dm_password: + description: Directory Manager password + required: yes + password: + description: Admin user kerberos password + required: yes + ip_addresses: + description: List of Master Server IP Addresses + required: no + domain: + description: Primary DNS domain of the IPA deployment + required: yes + realm: + description: Kerberos realm name of the IPA deployment + required: yes + hostname: + description: Fully qualified name of this host + required: yes + ca_cert_files: + description: + List of files containing CA certificates for the service certificate + files + required: yes + no_host_dns: + description: Do not use DNS for hostname lookup during installation + required: yes + setup_adtrust: + description: Configure AD trust capability + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + setup_dns: + description: Configure bind with our zone + required: yes + external_ca: + description: External ca setting + required: yes + external_cert_files: + description: + File containing the IPA CA certificate and the external CA certificate + chain + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: yes + ca_subject: + description: The installer ca_subject setting + required: yes + reverse_zones: + description: The reverse DNS zones to use + required: yes + no_reverse: + description: Do not create new reverse DNS zone + required: yes + auto_reverse: + description: Create necessary reverse zones + required: yes + forwarders: + description: Add DNS forwarders + required: yes + no_forwarders: + description: Do not add any DNS forwarders, use root servers instead + required: yes + auto_forwarders: + description: Use DNS forwarders configured in /etc/resolv.conf + required: yes + forward_policy: + description: DNS forwarding policy for global forwarders + required: yes + enable_compat: + description: Enable support for trusted domains for old clients + required: yes + netbios_name: + description: NetBIOS name of the IPA domain + required: yes + rid_base: + description: Start value for mapping UIDs and GIDs to RIDs + required: yes + secondary_rid_base: + description: + Start value of the secondary range for mapping UIDs and GIDs to RIDs + required: yes + setup_ca: + description: Configure a dogtag CA + required: yes + _hostname_overridden: + description: The installer _hostname_overridden setting + required: yes + dm_password: + description: Directory Manager password + required: False + password: + description: Admin user kerberos password + required: False + ip_addresses: + description: List of Master Server IP Addresses + required: False + domain: + description: Primary DNS domain of the IPA deployment + required: False + realm: + description: Kerberos realm name of the IPA deployment + required: False + hostname: + description: Fully qualified name of this host + required: False + principal: + description: + User Principal allowed to promote replicas and join IPA realm + required: True + ca_cert_files: + description: + List of files containing CA certificates for the service certificate + files + required: False + no_host_dns: + description: Do not use DNS for hostname lookup during installation + required: False + setup_adtrust: + description: Configure AD trust capability + required: False + setup_ca: + description: Configure a dogtag CA + required: False + setup_kra: + description: Configure a dogtag KRA + required: False + setup_dns: + description: Configure bind with our zone + required: False + dirsrv_cert_files: + description: + Files containing the Directory Server SSL certificate and private key + required: False + dirsrv_cert_name: + description: Name of the Directory Server SSL certificate to install + required: False + dirsrv_pin: + description: The password to unlock the Directory Server private key + required: False + http_cert_files: + description: + File containing the Apache Server SSL certificate and private key + required: False + http_cert_name: + description: Name of the Apache Server SSL certificate to install + required: False + http_pin: + description: The password to unlock the Apache Server private key + required: False + pkinit_cert_files: + description: + File containing the Kerberos KDC SSL certificate and private key + required: False + pkinit_cert_name: + description: Name of the Kerberos KDC SSL certificate to install + required: False + pkinit_pin: + description: The password to unlock the Kerberos KDC private key + required: False + keytab: + description: Path to backed up keytab from previous enrollment + required: False + mkhomedir: + description: Create home directories for users on their first login + required: False + force_join: + description: Force client enrollment even if already enrolled + required: False + no_ntp: + description: Do not configure ntp + required: False + ssh_trust_dns: + description: Configure OpenSSH client to trust DNS SSHFP records + required: False + no_ssh: + description: Do not configure OpenSSH client + required: False + no_sshd: + description: Do not configure OpenSSH server + required: False + no_dns_sshfp: + description: Do not automatically create DNS SSHFP records + required: False + allow_zone_overlap: + description: Create DNS zone even if it already exists + required: False + reverse_zones: + description: The reverse DNS zones to use + required: False + no_reverse: + description: Do not create new reverse DNS zone + required: False + auto_reverse: + description: Create necessary reverse zones + required: False + forwarders: + description: Add DNS forwarders + required: False + no_forwarders: + description: Do not add any DNS forwarders, use root servers instead + required: False + auto_forwarders: + description: Use DNS forwarders configured in /etc/resolv.conf + required: False + forward_policy: + description: DNS forwarding policy for global forwarders + required: False + no_dnssec_validation: + description: Disable DNSSEC validation + required: False + enable_compat: + description: Enable support for trusted domains for old clients + required: False + netbios_name: + description: NetBIOS name of the IPA domain + required: False + rid_base: + description: Start value for mapping UIDs and GIDs to RIDs + required: False + secondary_rid_base: + description: + Start value of the secondary range for mapping UIDs and GIDs to RIDs + required: False + server: + description: Fully qualified name of IPA server to enroll to + required: True + skip_conncheck: + description: Skip connection check to remote master + required: False + dm_password: + description: Directory Manager password + required: True + password: + description: Admin user kerberos password + required: True + ip_addresses: + description: List of Master Server IP Addresses + required: True + domain: + description: Primary DNS domain of the IPA deployment + required: True + realm: + description: Kerberos realm name of the IPA deployment + required: True + hostname: + description: Fully qualified name of this host + required: True + principal: + description: + User Principal allowed to promote replicas and join IPA realm + required: False + ca_cert_files: + description: + List of files containing CA certificates for the service certificate + files + required: True + no_host_dns: + description: Do not use DNS for hostname lookup during installation + required: True + setup_adtrust: + description: Configure AD trust capability + required: True + setup_ca: + description: Configure a dogtag CA + required: True + setup_kra: + description: Configure a dogtag KRA + required: True + setup_dns: + description: Configure bind with our zone + required: True + dirsrv_cert_files: + description: + Files containing the Directory Server SSL certificate and private key + required: True + dirsrv_cert_name: + description: Name of the Directory Server SSL certificate to install + required: True + dirsrv_pin: + description: The password to unlock the Directory Server private key + required: True + http_cert_files: + description: + File containing the Apache Server SSL certificate and private key + required: True + http_cert_name: + description: Name of the Apache Server SSL certificate to install + required: True + http_pin: + description: The password to unlock the Apache Server private key + required: True + pkinit_cert_files: + description: + File containing the Kerberos KDC SSL certificate and private key + required: True + pkinit_cert_name: + description: Name of the Kerberos KDC SSL certificate to install + required: True + pkinit_pin: + description: The password to unlock the Kerberos KDC private key + required: True + keytab: + description: Path to backed up keytab from previous enrollment + required: True + mkhomedir: + description: Create home directories for users on their first login + required: True + force_join: + description: Force client enrollment even if already enrolled + required: True + no_ntp: + description: Do not configure ntp + required: True + ssh_trust_dns: + description: Configure OpenSSH client to trust DNS SSHFP records + required: True + no_ssh: + description: Do not configure OpenSSH client + required: True + no_sshd: + description: Do not configure OpenSSH server + required: True + no_dns_sshfp: + description: Do not automatically create DNS SSHFP records + required: True + allow_zone_overlap: + description: Create DNS zone even if it already exists + required: True + reverse_zones: + description: The reverse DNS zones to use + required: True + no_reverse: + description: Do not create new reverse DNS zone + required: True + auto_reverse: + description: Create necessary reverse zones + required: True + forwarders: + description: Add DNS forwarders + required: True + no_forwarders: + description: Do not add any DNS forwarders, use root servers instead + required: True + auto_forwarders: + description: Use DNS forwarders configured in /etc/resolv.conf + required: True + forward_policy: + description: DNS forwarding policy for global forwarders + required: True + no_dnssec_validation: + description: Disable DNSSEC validation + required: True + enable_compat: + description: Enable support for trusted domains for old clients + required: True + netbios_name: + description: NetBIOS name of the IPA domain + required: True + rid_base: + description: Start value for mapping UIDs and GIDs to RIDs + required: True + secondary_rid_base: + description: + Start value of the secondary range for mapping UIDs and GIDs to RIDs + required: True + server: + description: Fully qualified name of IPA server to enroll to + required: False + skip_conncheck: + description: Skip connection check to remote master + required: True + dm_password: + description: Directory Manager password + required: yes + password: + description: Admin user kerberos password + required: yes + ip_addresses: + description: List of Master Server IP Addresses + required: yes + domain: + description: Primary DNS domain of the IPA deployment + required: yes + realm: + description: Kerberos realm name of the IPA deployment + required: yes + hostname: + description: Fully qualified name of this host + required: yes + principal: + description: + User Principal allowed to promote replicas and join IPA realm + required: no + ca_cert_files: + description: + List of files containing CA certificates for the service certificate + files + required: yes + no_host_dns: + description: Do not use DNS for hostname lookup during installation + required: yes + setup_adtrust: + description: Configure AD trust capability + required: yes + setup_ca: + description: Configure a dogtag CA + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + setup_dns: + description: Configure bind with our zone + required: yes + dirsrv_cert_files: + description: + Files containing the Directory Server SSL certificate and private key + required: yes + dirsrv_cert_name: + description: Name of the Directory Server SSL certificate to install + required: yes + dirsrv_pin: + description: The password to unlock the Directory Server private key + required: yes + http_cert_files: + description: + File containing the Apache Server SSL certificate and private key + required: yes + http_cert_name: + description: Name of the Apache Server SSL certificate to install + required: yes + http_pin: + description: The password to unlock the Apache Server private key + required: yes + pkinit_cert_files: + description: + File containing the Kerberos KDC SSL certificate and private key + required: yes + pkinit_cert_name: + description: Name of the Kerberos KDC SSL certificate to install + required: yes + pkinit_pin: + description: The password to unlock the Kerberos KDC private key + required: yes + keytab: + description: Path to backed up keytab from previous enrollment + required: yes + mkhomedir: + description: Create home directories for users on their first login + required: yes + force_join: + description: Force client enrollment even if already enrolled + required: yes + no_ntp: + description: Do not configure ntp + required: yes + ssh_trust_dns: + description: Configure OpenSSH client to trust DNS SSHFP records + required: yes + no_ssh: + description: Do not configure OpenSSH client + required: yes + no_sshd: + description: Do not configure OpenSSH server + required: yes + no_dns_sshfp: + description: Do not automatically create DNS SSHFP records + required: yes + allow_zone_overlap: + description: Create DNS zone even if it already exists + required: yes + reverse_zones: + description: The reverse DNS zones to use + required: yes + no_reverse: + description: Do not create new reverse DNS zone + required: yes + auto_reverse: + description: Create necessary reverse zones + required: yes + forwarders: + description: Add DNS forwarders + required: yes + no_forwarders: + description: Do not add any DNS forwarders, use root servers instead + required: yes + auto_forwarders: + description: Use DNS forwarders configured in /etc/resolv.conf + required: yes + forward_policy: + description: DNS forwarding policy for global forwarders + required: yes + no_dnssec_validation: + description: Disable DNSSEC validation + required: yes + enable_compat: + description: Enable support for trusted domains for old clients + required: yes + netbios_name: + description: NetBIOS name of the IPA domain + required: yes + rid_base: + description: Start value for mapping UIDs and GIDs to RIDs + required: yes + secondary_rid_base: + description: + Start value of the secondary range for mapping UIDs and GIDs to RIDs + required: yes + server: + description: Fully qualified name of IPA server to enroll to + required: no + skip_conncheck: + description: Skip connection check to remote master + required: yes + dm_password: + description: Directory Manager password + required: yes + password: + description: Admin user kerberos password + required: yes + ip_addresses: + description: List of Master Server IP Addresses + required: yes + domain: + description: Primary DNS domain of the IPA deployment + required: yes + realm: + description: Kerberos realm name of the IPA deployment + required: yes + hostname: + description: Fully qualified name of this host + required: yes + principal: + description: + User Principal allowed to promote replicas and join IPA realm + required: no + ca_cert_files: + description: + List of files containing CA certificates for the service certificate + files + required: yes + no_host_dns: + description: Do not use DNS for hostname lookup during installation + required: yes + setup_adtrust: + description: Configure AD trust capability + required: yes + setup_ca: + description: Configure a dogtag CA + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + setup_dns: + description: Configure bind with our zone + required: yes + dirsrv_cert_files: + description: + Files containing the Directory Server SSL certificate and private key + required: yes + dirsrv_cert_name: + description: Name of the Directory Server SSL certificate to install + required: yes + dirsrv_pin: + description: The password to unlock the Directory Server private key + required: yes + http_cert_files: + description: + File containing the Apache Server SSL certificate and private key + required: yes + http_cert_name: + description: Name of the Apache Server SSL certificate to install + required: yes + http_pin: + description: The password to unlock the Apache Server private key + required: yes + pkinit_cert_files: + description: + File containing the Kerberos KDC SSL certificate and private key + required: yes + pkinit_cert_name: + description: Name of the Kerberos KDC SSL certificate to install + required: yes + pkinit_pin: + description: The password to unlock the Kerberos KDC private key + required: yes + keytab: + description: Path to backed up keytab from previous enrollment + required: yes + mkhomedir: + description: Create home directories for users on their first login + required: yes + force_join: + description: Force client enrollment even if already enrolled + required: yes + no_ntp: + description: Do not configure ntp + required: yes + ssh_trust_dns: + description: Configure OpenSSH client to trust DNS SSHFP records + required: yes + no_ssh: + description: Do not configure OpenSSH client + required: yes + no_sshd: + description: Do not configure OpenSSH server + required: yes + no_dns_sshfp: + description: Do not automatically create DNS SSHFP records + required: yes + allow_zone_overlap: + description: Create DNS zone even if it already exists + required: yes + reverse_zones: + description: The reverse DNS zones to use + required: yes + no_reverse: + description: Do not create new reverse DNS zone + required: yes + auto_reverse: + description: Create necessary reverse zones + required: yes + forwarders: + description: Add DNS forwarders + required: yes + no_forwarders: + description: Do not add any DNS forwarders, use root servers instead + required: yes + auto_forwarders: + description: Use DNS forwarders configured in /etc/resolv.conf + required: yes + forward_policy: + description: DNS forwarding policy for global forwarders + required: yes + no_dnssec_validation: + description: Disable DNSSEC validation + required: yes + enable_compat: + description: Enable support for trusted domains for old clients + required: yes + netbios_name: + description: NetBIOS name of the IPA domain + required: yes + rid_base: + description: Start value for mapping UIDs and GIDs to RIDs + required: yes + secondary_rid_base: + description: + Start value of the secondary range for mapping UIDs and GIDs to RIDs + required: yes + server: + description: Fully qualified name of IPA server to enroll to + required: no + skip_conncheck: + description: Skip connection check to remote master + required: yes + dm_password: + description: Directory Manager password + required: yes + password: + description: Admin user kerberos password + required: yes + ip_addresses: + description: List of Master Server IP Addresses + required: yes + domain: + description: Primary DNS domain of the IPA deployment + required: yes + realm: + description: Kerberos realm name of the IPA deployment + required: yes + hostname: + description: Fully qualified name of this host + required: yes + principal: + description: + User Principal allowed to promote replicas and join IPA realm + required: no + ca_cert_files: + description: + List of files containing CA certificates for the service certificate + files + required: yes + no_host_dns: + description: Do not use DNS for hostname lookup during installation + required: yes + setup_adtrust: + description: Configure AD trust capability + required: yes + setup_ca: + description: Configure a dogtag CA + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + setup_dns: + description: Configure bind with our zone + required: yes + dirsrv_cert_files: + description: + Files containing the Directory Server SSL certificate and private key + required: yes + dirsrv_cert_name: + description: Name of the Directory Server SSL certificate to install + required: yes + dirsrv_pin: + description: The password to unlock the Directory Server private key + required: yes + http_cert_files: + description: + File containing the Apache Server SSL certificate and private key + required: yes + http_cert_name: + description: Name of the Apache Server SSL certificate to install + required: yes + http_pin: + description: The password to unlock the Apache Server private key + required: yes + pkinit_cert_files: + description: + File containing the Kerberos KDC SSL certificate and private key + required: yes + pkinit_cert_name: + description: Name of the Kerberos KDC SSL certificate to install + required: yes + pkinit_pin: + description: The password to unlock the Kerberos KDC private key + required: yes + keytab: + description: Path to backed up keytab from previous enrollment + required: yes + mkhomedir: + description: Create home directories for users on their first login + required: yes + force_join: + description: Force client enrollment even if already enrolled + required: yes + no_ntp: + description: Do not configure ntp + required: yes + ssh_trust_dns: + description: Configure OpenSSH client to trust DNS SSHFP records + required: yes + no_ssh: + description: Do not configure OpenSSH client + required: yes + no_sshd: + description: Do not configure OpenSSH server + required: yes + no_dns_sshfp: + description: Do not automatically create DNS SSHFP records + required: yes + allow_zone_overlap: + description: Create DNS zone even if it already exists + required: yes + reverse_zones: + description: The reverse DNS zones to use + required: yes + no_reverse: + description: Do not create new reverse DNS zone + required: yes + auto_reverse: + description: Create necessary reverse zones + required: yes + forwarders: + description: Add DNS forwarders + required: yes + no_forwarders: + description: Do not add any DNS forwarders, use root servers instead + required: yes + auto_forwarders: + description: Use DNS forwarders configured in /etc/resolv.conf + required: yes + forward_policy: + description: DNS forwarding policy for global forwarders + required: yes + no_dnssec_validation: + description: Disable DNSSEC validation + required: yes + enable_compat: + description: Enable support for trusted domains for old clients + required: yes + netbios_name: + description: NetBIOS name of the IPA domain + required: yes + rid_base: + description: Start value for mapping UIDs and GIDs to RIDs + required: yes + secondary_rid_base: + description: + Start value of the secondary range for mapping UIDs and GIDs to RIDs + required: yes + server: + description: Fully qualified name of IPA server to enroll to + required: no + skip_conncheck: + description: Skip connection check to remote master + required: yes + dm_password: + description: Directory Manager password + required: yes + password: + description: Admin user kerberos password + required: yes + ip_addresses: + description: List of Master Server IP Addresses + required: yes + domain: + description: Primary DNS domain of the IPA deployment + required: yes + realm: + description: Kerberos realm name of the IPA deployment + required: yes + hostname: + description: Fully qualified name of this host + required: yes + principal: + description: + User Principal allowed to promote replicas and join IPA realm + required: no + ca_cert_files: + description: + List of files containing CA certificates for the service certificate + files + required: yes + no_host_dns: + description: Do not use DNS for hostname lookup during installation + required: yes + setup_adtrust: + description: Configure AD trust capability + required: yes + setup_ca: + description: Configure a dogtag CA + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + setup_dns: + description: Configure bind with our zone + required: yes + dirsrv_cert_files: + description: + Files containing the Directory Server SSL certificate and private key + required: yes + dirsrv_cert_name: + description: Name of the Directory Server SSL certificate to install + required: yes + dirsrv_pin: + description: The password to unlock the Directory Server private key + required: yes + http_cert_files: + description: + File containing the Apache Server SSL certificate and private key + required: yes + http_cert_name: + description: Name of the Apache Server SSL certificate to install + required: yes + http_pin: + description: The password to unlock the Apache Server private key + required: yes + pkinit_cert_files: + description: + File containing the Kerberos KDC SSL certificate and private key + required: yes + pkinit_cert_name: + description: Name of the Kerberos KDC SSL certificate to install + required: yes + pkinit_pin: + description: The password to unlock the Kerberos KDC private key + required: yes + keytab: + description: Path to backed up keytab from previous enrollment + required: yes + mkhomedir: + description: Create home directories for users on their first login + required: yes + force_join: + description: Force client enrollment even if already enrolled + required: yes + no_ntp: + description: Do not configure ntp + required: yes + ssh_trust_dns: + description: Configure OpenSSH client to trust DNS SSHFP records + required: yes + no_ssh: + description: Do not configure OpenSSH client + required: yes + no_sshd: + description: Do not configure OpenSSH server + required: yes + no_dns_sshfp: + description: Do not automatically create DNS SSHFP records + required: yes + allow_zone_overlap: + description: Create DNS zone even if it already exists + required: yes + reverse_zones: + description: The reverse DNS zones to use + required: yes + no_reverse: + description: Do not create new reverse DNS zone + required: yes + auto_reverse: + description: Create necessary reverse zones + required: yes + forwarders: + description: Add DNS forwarders + required: yes + no_forwarders: + description: Do not add any DNS forwarders, use root servers instead + required: yes + auto_forwarders: + description: Use DNS forwarders configured in /etc/resolv.conf + required: yes + forward_policy: + description: DNS forwarding policy for global forwarders + required: yes + no_dnssec_validation: + description: Disable DNSSEC validation + required: yes + enable_compat: + description: Enable support for trusted domains for old clients + required: yes + netbios_name: + description: NetBIOS name of the IPA domain + required: yes + rid_base: + description: Start value for mapping UIDs and GIDs to RIDs + required: yes + secondary_rid_base: + description: + Start value of the secondary range for mapping UIDs and GIDs to RIDs + required: yes + server: + description: Fully qualified name of IPA server to enroll to + required: no + skip_conncheck: + description: Skip connection check to remote master + required: yes + dm_password: + description: Directory Manager password + required: yes + password: + description: Admin user kerberos password + required: yes + ip_addresses: + description: List of Master Server IP Addresses + required: yes + domain: + description: Primary DNS domain of the IPA deployment + required: yes + realm: + description: Kerberos realm name of the IPA deployment + required: yes + hostname: + description: Fully qualified name of this host + required: yes + principal: + description: + User Principal allowed to promote replicas and join IPA realm + required: no + ca_cert_files: + description: + List of files containing CA certificates for the service certificate + files + required: yes + no_host_dns: + description: Do not use DNS for hostname lookup during installation + required: yes + setup_adtrust: + description: Configure AD trust capability + required: yes + setup_ca: + description: Configure a dogtag CA + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + setup_dns: + description: Configure bind with our zone + required: yes + dirsrv_cert_files: + description: + Files containing the Directory Server SSL certificate and private key + required: yes + dirsrv_cert_name: + description: Name of the Directory Server SSL certificate to install + required: yes + dirsrv_pin: + description: The password to unlock the Directory Server private key + required: yes + http_cert_files: + description: + File containing the Apache Server SSL certificate and private key + required: yes + http_cert_name: + description: Name of the Apache Server SSL certificate to install + required: yes + http_pin: + description: The password to unlock the Apache Server private key + required: yes + pkinit_cert_files: + description: + File containing the Kerberos KDC SSL certificate and private key + required: yes + pkinit_cert_name: + description: Name of the Kerberos KDC SSL certificate to install + required: yes + pkinit_pin: + description: The password to unlock the Kerberos KDC private key + required: yes + keytab: + description: Path to backed up keytab from previous enrollment + required: yes + mkhomedir: + description: Create home directories for users on their first login + required: yes + force_join: + description: Force client enrollment even if already enrolled + required: yes + no_ntp: + description: Do not configure ntp + required: yes + ssh_trust_dns: + description: Configure OpenSSH client to trust DNS SSHFP records + required: yes + no_ssh: + description: Do not configure OpenSSH client + required: yes + no_sshd: + description: Do not configure OpenSSH server + required: yes + no_dns_sshfp: + description: Do not automatically create DNS SSHFP records + required: yes + allow_zone_overlap: + description: Create DNS zone even if it already exists + required: yes + reverse_zones: + description: The reverse DNS zones to use + required: yes + no_reverse: + description: Do not create new reverse DNS zone + required: yes + auto_reverse: + description: Create necessary reverse zones + required: yes + forwarders: + description: Add DNS forwarders + required: yes + no_forwarders: + description: Do not add any DNS forwarders, use root servers instead + required: yes + auto_forwarders: + description: Use DNS forwarders configured in /etc/resolv.conf + required: yes + forward_policy: + description: DNS forwarding policy for global forwarders + required: yes + no_dnssec_validation: + description: Disable DNSSEC validation + required: yes + enable_compat: + description: Enable support for trusted domains for old clients + required: yes + netbios_name: + description: NetBIOS name of the IPA domain + required: yes + rid_base: + description: Start value for mapping UIDs and GIDs to RIDs + required: yes + secondary_rid_base: + description: + Start value of the secondary range for mapping UIDs and GIDs to RIDs + required: yes + server: + description: Fully qualified name of IPA server to enroll to + required: no + skip_conncheck: + description: Skip connection check to remote master + required: yes + dm_password: + description: Directory Manager password + required: yes + password: + description: Admin user kerberos password + required: yes + ip_addresses: + description: List of Master Server IP Addresses + required: yes + domain: + description: Primary DNS domain of the IPA deployment + required: yes + realm: + description: Kerberos realm name of the IPA deployment + required: yes + hostname: + description: Fully qualified name of this host + required: yes + principal: + description: + User Principal allowed to promote replicas and join IPA realm + required: no + ca_cert_files: + description: + List of files containing CA certificates for the service certificate + files + required: yes + no_host_dns: + description: Do not use DNS for hostname lookup during installation + required: yes + setup_adtrust: + description: Configure AD trust capability + required: yes + setup_ca: + description: Configure a dogtag CA + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + setup_dns: + description: Configure bind with our zone + required: yes + dirsrv_cert_files: + description: + Files containing the Directory Server SSL certificate and private key + required: yes + dirsrv_cert_name: + description: Name of the Directory Server SSL certificate to install + required: yes + dirsrv_pin: + description: The password to unlock the Directory Server private key + required: yes + http_cert_files: + description: + File containing the Apache Server SSL certificate and private key + required: yes + http_cert_name: + description: Name of the Apache Server SSL certificate to install + required: yes + http_pin: + description: The password to unlock the Apache Server private key + required: yes + pkinit_cert_files: + description: + File containing the Kerberos KDC SSL certificate and private key + required: yes + pkinit_cert_name: + description: Name of the Kerberos KDC SSL certificate to install + required: yes + pkinit_pin: + description: The password to unlock the Kerberos KDC private key + required: yes + keytab: + description: Path to backed up keytab from previous enrollment + required: yes + mkhomedir: + description: Create home directories for users on their first login + required: yes + force_join: + description: Force client enrollment even if already enrolled + required: yes + no_ntp: + description: Do not configure ntp + required: yes + ssh_trust_dns: + description: Configure OpenSSH client to trust DNS SSHFP records + required: yes + no_ssh: + description: Do not configure OpenSSH client + required: yes + no_sshd: + description: Do not configure OpenSSH server + required: yes + no_dns_sshfp: + description: Do not automatically create DNS SSHFP records + required: yes + allow_zone_overlap: + description: Create DNS zone even if it already exists + required: yes + reverse_zones: + description: The reverse DNS zones to use + required: yes + no_reverse: + description: Do not create new reverse DNS zone + required: yes + auto_reverse: + description: Create necessary reverse zones + required: yes + forwarders: + description: Add DNS forwarders + required: yes + no_forwarders: + description: Do not add any DNS forwarders, use root servers instead + required: yes + auto_forwarders: + description: Use DNS forwarders configured in /etc/resolv.conf + required: yes + forward_policy: + description: DNS forwarding policy for global forwarders + required: yes + no_dnssec_validation: + description: Disable DNSSEC validation + required: yes + enable_compat: + description: Enable support for trusted domains for old clients + required: yes + netbios_name: + description: NetBIOS name of the IPA domain + required: yes + rid_base: + description: Start value for mapping UIDs and GIDs to RIDs + required: yes + secondary_rid_base: + description: + Start value of the secondary range for mapping UIDs and GIDs to RIDs + required: yes + server: + description: Fully qualified name of IPA server to enroll to + required: no + skip_conncheck: + description: Skip connection check to remote master + required: yes + dm_password: + description: Directory Manager password + required: yes + password: + description: Admin user kerberos password + required: yes + ip_addresses: + description: List of Master Server IP Addresses + required: yes + domain: + description: Primary DNS domain of the IPA deployment + required: yes + realm: + description: Kerberos realm name of the IPA deployment + required: yes + hostname: + description: Fully qualified name of this host + required: yes + principal: + description: + User Principal allowed to promote replicas and join IPA realm + required: no + ca_cert_files: + description: + List of files containing CA certificates for the service certificate + files + required: yes + no_host_dns: + description: Do not use DNS for hostname lookup during installation + required: yes + setup_adtrust: + description: Configure AD trust capability + required: yes + setup_ca: + description: Configure a dogtag CA + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + setup_dns: + description: Configure bind with our zone + required: yes + dirsrv_cert_files: + description: + Files containing the Directory Server SSL certificate and private key + required: yes + dirsrv_cert_name: + description: Name of the Directory Server SSL certificate to install + required: yes + dirsrv_pin: + description: The password to unlock the Directory Server private key + required: yes + http_cert_files: + description: + File containing the Apache Server SSL certificate and private key + required: yes + http_cert_name: + description: Name of the Apache Server SSL certificate to install + required: yes + http_pin: + description: The password to unlock the Apache Server private key + required: yes + pkinit_cert_files: + description: + File containing the Kerberos KDC SSL certificate and private key + required: yes + pkinit_cert_name: + description: Name of the Kerberos KDC SSL certificate to install + required: yes + pkinit_pin: + description: The password to unlock the Kerberos KDC private key + required: yes + keytab: + description: Path to backed up keytab from previous enrollment + required: yes + mkhomedir: + description: Create home directories for users on their first login + required: yes + force_join: + description: Force client enrollment even if already enrolled + required: yes + no_ntp: + description: Do not configure ntp + required: yes + ssh_trust_dns: + description: Configure OpenSSH client to trust DNS SSHFP records + required: yes + no_ssh: + description: Do not configure OpenSSH client + required: yes + no_sshd: + description: Do not configure OpenSSH server + required: yes + no_dns_sshfp: + description: Do not automatically create DNS SSHFP records + required: yes + allow_zone_overlap: + description: Create DNS zone even if it already exists + required: yes + reverse_zones: + description: The reverse DNS zones to use + required: yes + no_reverse: + description: Do not create new reverse DNS zone + required: yes + auto_reverse: + description: Create necessary reverse zones + required: yes + forwarders: + description: Add DNS forwarders + required: yes + no_forwarders: + description: Do not add any DNS forwarders, use root servers instead + required: yes + auto_forwarders: + description: Use DNS forwarders configured in /etc/resolv.conf + required: yes + forward_policy: + description: DNS forwarding policy for global forwarders + required: yes + no_dnssec_validation: + description: Disable DNSSEC validation + required: yes + enable_compat: + description: Enable support for trusted domains for old clients + required: yes + netbios_name: + description: NetBIOS name of the IPA domain + required: yes + rid_base: + description: Start value for mapping UIDs and GIDs to RIDs + required: yes + secondary_rid_base: + description: + Start value of the secondary range for mapping UIDs and GIDs to RIDs + required: yes + server: + description: Fully qualified name of IPA server to enroll to + required: no + skip_conncheck: + description: Skip connection check to remote master + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os +import tempfile +import traceback +import six + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, options, installer, DN, paths, sysrestore, + ansible_module_get_parsed_ip_addresses, Env, ipautil, ipaldap, + installutils, ReplicaConfig, load_pkcs12, kinit_keytab, create_api, + rpc_client, check_remote_version, parse_version, check_remote_fips_mode, + ReplicationManager, promotion_check_ipa_domain, current_domain_level, + check_domain_level_is_supported, errors, ScriptError, + logger, check_dns_resolution, service, find_providing_server, ca, kra, + dns, no_matching_interface_for_ip_address_warning, adtrust, + constants, api, redirect_stdout, replica_conn_check, tasks +) + +if six.PY3: + unicode = str + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # basic + dm_password=dict(required=False, no_log=True), + password=dict(required=False, no_log=True), + ip_addresses=dict(required=False, type='list', default=[]), + domain=dict(required=False), + realm=dict(required=False), + hostname=dict(required=False), + principal=dict(required=True), + ca_cert_files=dict(required=False, type='list', default=[]), + no_host_dns=dict(required=False, type='bool', default=False), + # server + setup_adtrust=dict(required=False, type='bool'), + setup_ca=dict(required=False, type='bool'), + setup_kra=dict(required=False, type='bool'), + setup_dns=dict(required=False, type='bool'), + # ssl certificate + dirsrv_cert_files=dict(required=False, type='list', default=[]), + dirsrv_cert_name=dict(required=False), + dirsrv_pin=dict(required=False), + http_cert_files=dict(required=False, type='list', default=[]), + http_cert_name=dict(required=False), + http_pin=dict(required=False), + pkinit_cert_files=dict(required=False, type='list', default=[]), + pkinit_cert_name=dict(required=False), + pkinit_pin=dict(required=False), + # client + keytab=dict(required=False), + mkhomedir=dict(required=False, type='bool'), + force_join=dict(required=False, type='bool'), + no_ntp=dict(required=False, type='bool'), + ssh_trust_dns=dict(required=False, type='bool'), + no_ssh=dict(required=False, type='bool'), + no_sshd=dict(required=False, type='bool'), + no_dns_sshfp=dict(required=False, type='bool'), + # certificate system + # subject_base=dict(required=False), + # dns + allow_zone_overlap=dict(required=False, type='bool', + default=False), + reverse_zones=dict(required=False, type='list', default=[]), + no_reverse=dict(required=False, type='bool', default=False), + auto_reverse=dict(required=False, type='bool', default=False), + forwarders=dict(required=False, type='list', default=[]), + no_forwarders=dict(required=False, type='bool', default=False), + auto_forwarders=dict(required=False, type='bool', default=False), + forward_policy=dict(default=None, choices=['first', 'only']), + no_dnssec_validation=dict(required=False, type='bool', + default=False), + # ad trust + enable_compat=dict(required=False, type='bool', default=False), + netbios_name=dict(required=False), + rid_base=dict(required=False, type='int', default=1000), + secondary_rid_base=dict(required=False, type='int', + default=100000000), + # additional + server=dict(required=True), + skip_conncheck=dict(required=False, type='bool'), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + options.dm_password = ansible_module.params.get('dm_password') + options.password = options.dm_password + options.admin_password = ansible_module.params.get('password') + options.ip_addresses = ansible_module_get_parsed_ip_addresses( + ansible_module) + options.domain_name = ansible_module.params.get('domain') + options.realm_name = ansible_module.params.get('realm') + options.host_name = ansible_module.params.get('hostname') + options.principal = ansible_module.params.get('principal') + options.ca_cert_files = ansible_module.params.get('ca_cert_files') + options.no_host_dns = ansible_module.params.get('no_host_dns') + # server + options.setup_adtrust = ansible_module.params.get('setup_adtrust') + options.setup_ca = ansible_module.params.get('setup_ca') + options.setup_kra = ansible_module.params.get('setup_kra') + options.setup_dns = ansible_module.params.get('setup_dns') + # ssl certificate + options.dirsrv_cert_files = ansible_module.params.get('dirsrv_cert_files') + options.dirsrv_cert_name = ansible_module.params.get('dirsrv_cert_name') + options.dirsrv_pin = ansible_module.params.get('dirsrv_pin') + options.http_cert_files = ansible_module.params.get('http_cert_files') + options.http_cert_name = ansible_module.params.get('http_cert_name') + options.http_pin = ansible_module.params.get('http_pin') + options.pkinit_cert_files = ansible_module.params.get('pkinit_cert_files') + options.pkinit_cert_name = ansible_module.params.get('pkinit_cert_name') + options.pkinit_pin = ansible_module.params.get('pkinit_pin') + # client + options.keytab = ansible_module.params.get('keytab') + options.mkhomedir = ansible_module.params.get('mkhomedir') + options.force_join = ansible_module.params.get('force_join') + options.no_ntp = ansible_module.params.get('no_ntp') + options.ssh_trust_dns = ansible_module.params.get('ssh_trust_dns') + options.no_ssh = ansible_module.params.get('no_ssh') + options.no_sshd = ansible_module.params.get('no_sshd') + options.no_dns_sshfp = ansible_module.params.get('no_dns_sshfp') + # certificate system + options.external_ca = ansible_module.params.get('external_ca') + options.external_cert_files = ansible_module.params.get( + 'external_cert_files') + # options.subject_base = ansible_module.params.get('subject_base') + # options.ca_subject = ansible_module.params.get('ca_subject') + options.no_dnssec_validation = ansible_module.params.get( + 'no_dnssec_validation') + # dns + options.allow_zone_overlap = ansible_module.params.get( + 'allow_zone_overlap') + options.reverse_zones = ansible_module.params.get('reverse_zones') + options.no_reverse = ansible_module.params.get('no_reverse') + options.auto_reverse = ansible_module.params.get('auto_reverse') + options.forwarders = ansible_module.params.get('forwarders') + options.no_forwarders = ansible_module.params.get('no_forwarders') + options.auto_forwarders = ansible_module.params.get('auto_forwarders') + options.forward_policy = ansible_module.params.get('forward_policy') + options.no_dnssec_validation = ansible_module.params.get( + 'no_dnssec_validationdnssec_validation') + # ad trust + options.enable_compat = ansible_module.params.get('enable_compat') + options.netbios_name = ansible_module.params.get('netbios_name') + options.rid_base = ansible_module.params.get('rid_base') + options.secondary_rid_base = ansible_module.params.get( + 'secondary_rid_base') + + # additional + # options._host_name_overridden = ansible_module.params.get( + # '_hostname_overridden') + options.server = ansible_module.params.get('server') + options.skip_conncheck = ansible_module.params.get('skip_conncheck') + + # init # + + fstore = sysrestore.FileStore(paths.SYSRESTORE) + sstore = sysrestore.StateFile(paths.SYSRESTORE) + + # prepare (install prepare, install checks) # + + ########################################################################## + # replica promote_check ################################################## + ########################################################################## + + ansible_log.debug("== PROMOTE CHECK ==") + + # ansible_log.debug("-- NO_NTP --") # already done in test + + # check selinux status, http and DS ports, NTP conflicting services + # common_check(options.no_ntp) + + installer._enrollment_performed = False + installer._top_dir = tempfile.mkdtemp("ipa") + + # with ipautil.private_ccache(): + dir_path = tempfile.mkdtemp(prefix='krbcc') + os.environ['KRB5CCNAME'] = os.path.join(dir_path, 'ccache') + + ansible_log.debug("-- API --") + + env = Env() + env._bootstrap(context='installer', confdir=paths.ETC_IPA, log=None) + env._finalize_core(**dict(constants.DEFAULT_CONFIG)) + + # pylint: disable=no-member + xmlrpc_uri = 'https://{}/ipa/xml'.format(ipautil.format_netloc(env.host)) + if hasattr(ipaldap, "realm_to_ldapi_uri"): + realm_to_ldapi_uri = ipaldap.realm_to_ldapi_uri + else: + realm_to_ldapi_uri = installutils.realm_to_ldapi_uri + api.bootstrap(in_server=True, + context='installer', + confdir=paths.ETC_IPA, + ldap_uri=realm_to_ldapi_uri(env.realm), + xmlrpc_uri=xmlrpc_uri) + # pylint: enable=no-member + api.finalize() + + ansible_log.debug("-- REPLICA_CONFIG --") + + config = ReplicaConfig() + config.realm_name = api.env.realm + config.host_name = api.env.host + config.domain_name = api.env.domain + config.master_host_name = api.env.server + if not api.env.ca_host or api.env.ca_host == api.env.host: + # ca_host has not been configured explicitly, prefer source master + config.ca_host_name = api.env.server + else: + # default to ca_host from IPA config + config.ca_host_name = api.env.ca_host + config.kra_host_name = config.ca_host_name + config.ca_ds_port = 389 + config.setup_ca = options.setup_ca + config.setup_kra = options.setup_kra + config.dir = installer._top_dir + config.basedn = api.env.basedn + # config.hidden_replica = options.hidden_replica + + # load and check certificates # + + ansible_log.debug("-- CERT_FILES --") + + http_pkcs12_file = None + http_pkcs12_info = None + http_ca_cert = None + dirsrv_pkcs12_file = None + dirsrv_pkcs12_info = None + dirsrv_ca_cert = None + pkinit_pkcs12_file = None + pkinit_pkcs12_info = None + pkinit_ca_cert = None + + if options.http_cert_files: + ansible_log.debug("-- HTTP_CERT_FILES --") + if options.http_pin is None: + ansible_module.fail_json( + msg="Apache Server private key unlock password required") + http_pkcs12_file, http_pin, http_ca_cert = load_pkcs12( + cert_files=options.http_cert_files, + key_password=options.http_pin, + key_nickname=options.http_cert_name, + ca_cert_files=options.ca_cert_files, + host_name=config.host_name) + http_pkcs12_info = (http_pkcs12_file.name, http_pin) + + if options.dirsrv_cert_files: + ansible_log.debug("-- DIRSRV_CERT_FILES --") + if options.dirsrv_pin is None: + ansible_module.fail_json( + msg="Directory Server private key unlock password required") + dirsrv_pkcs12_file, dirsrv_pin, dirsrv_ca_cert = load_pkcs12( + cert_files=options.dirsrv_cert_files, + key_password=options.dirsrv_pin, + key_nickname=options.dirsrv_cert_name, + ca_cert_files=options.ca_cert_files, + host_name=config.host_name) + dirsrv_pkcs12_info = (dirsrv_pkcs12_file.name, dirsrv_pin) + + if options.pkinit_cert_files: + ansible_log.debug("-- PKINIT_CERT_FILES --") + if options.pkinit_pin is None: + ansible_module.fail_json( + msg="Kerberos KDC private key unlock password required") + pkinit_pkcs12_file, pkinit_pin, pkinit_ca_cert = load_pkcs12( + cert_files=options.pkinit_cert_files, + key_password=options.pkinit_pin, + key_nickname=options.pkinit_cert_name, + ca_cert_files=options.ca_cert_files, + realm_name=config.realm_name) + pkinit_pkcs12_info = (pkinit_pkcs12_file.name, pkinit_pin) + + if (options.http_cert_files and options.dirsrv_cert_files and + http_ca_cert != dirsrv_ca_cert): + ansible_module.fail_json( + msg="Apache Server SSL certificate and Directory " + "Server SSL certificate are not signed by the same" + " CA certificate") + + if (options.http_cert_files and + options.pkinit_cert_files and + http_ca_cert != pkinit_ca_cert): + ansible_module.fail_json( + msg="Apache Server SSL certificate and PKINIT KDC " + "certificate are not signed by the same CA " + "certificate") + + ansible_log.debug("-- FQDN --") + + installutils.verify_fqdn(config.host_name, options.no_host_dns) + installutils.verify_fqdn(config.master_host_name, options.no_host_dns) + + ansible_log.debug("-- KINIT_KEYTAB --") + + ccache = os.environ['KRB5CCNAME'] + kinit_keytab('host/{env.host}@{env.realm}'.format(env=api.env), + paths.KRB5_KEYTAB, + ccache) + + ansible_log.debug("-- CA_CRT --") + + cafile = paths.IPA_CA_CRT + if not os.path.isfile(cafile): + ansible_module.fail_json( + msg="CA cert file is not available! Please reinstall" + "the client and try again.") + + ansible_log.debug("-- REMOTE_API --") + + ldapuri = 'ldaps://%s' % ipautil.format_netloc(config.master_host_name) + xmlrpc_uri = 'https://{}/ipa/xml'.format( + ipautil.format_netloc(config.master_host_name)) + remote_api = create_api(mode=None) + remote_api.bootstrap(in_server=True, + context='installer', + confdir=paths.ETC_IPA, + ldap_uri=ldapuri, + xmlrpc_uri=xmlrpc_uri) + remote_api.finalize() + installer._remote_api = remote_api + + ansible_log.debug("-- RPC_CLIENT --") + + with rpc_client(remote_api) as client: + check_remote_version(client, parse_version(api.env.version)) + check_remote_fips_mode(client, api.env.fips_mode) + + conn = remote_api.Backend.ldap2 + replman = None + try: + ansible_log.debug("-- CONNECT --") + # Try out authentication + conn.connect(ccache=ccache) + replman = ReplicationManager(config.realm_name, + config.master_host_name, None) + + ansible_log.debug("-- CHECK IPA_DOMAIN --") + + promotion_check_ipa_domain(conn, remote_api.env.basedn) + + ansible_log.debug("-- CHECK DOMAIN_LEVEL --") + + # Make sure that domain fulfills minimal domain level + # requirement + domain_level = current_domain_level(remote_api) + check_domain_level_is_supported(domain_level) + if domain_level < constants.MIN_DOMAIN_LEVEL: + ansible_module.fail_json( + msg="Cannot promote this client to a replica. The domain " + "level " + "must be raised to {mindomainlevel} before the replica can be " + "installed".format( + mindomainlevel=constants.MIN_DOMAIN_LEVEL)) + + ansible_log.debug("-- CHECK AUTHORIZATION --") + + # Check authorization + result = remote_api.Command['hostgroup_find']( + cn=u'ipaservers', + host=[unicode(api.env.host)] + )['result'] + add_to_ipaservers = not result + + ansible_log.debug("-- ADD_TO_IPASERVERS --") + + if add_to_ipaservers: + if options.password and not options.admin_password: + raise errors.ACIError(info="Not authorized") + + if installer._ccache is None: + del os.environ['KRB5CCNAME'] + else: + os.environ['KRB5CCNAME'] = installer._ccache + + try: + installutils.check_creds(options, config.realm_name) + installer._ccache = os.environ.get('KRB5CCNAME') + finally: + os.environ['KRB5CCNAME'] = ccache + + conn.disconnect() + conn.connect(ccache=installer._ccache) + + try: + result = remote_api.Command['hostgroup_show']( + u'ipaservers', + all=True, + rights=True + )['result'] + + if 'w' not in result['attributelevelrights']['member']: + raise errors.ACIError(info="Not authorized") + finally: + ansible_log.debug("-- RECONNECT --") + conn.disconnect() + conn.connect(ccache=ccache) + + ansible_log.debug("-- CHECK FOR REPLICATION AGREEMENT --") + + # Check that we don't already have a replication agreement + if replman.get_replication_agreement(config.host_name): + msg = ("A replication agreement for this host already exists. " + "It needs to be removed.\n" + "Run this command:\n" + " %% ipa-replica-manage del {host} --force" + .format(host=config.host_name)) + raise ScriptError(msg, rval=3) + + ansible_log.debug("-- DETECT REPLICATION MANAGER GROUP --") + + # Detect if the other master can handle replication managers + # cn=replication managers,cn=sysaccounts,cn=etc,$SUFFIX + dn = DN(('cn', 'replication managers'), ('cn', 'sysaccounts'), + ('cn', 'etc'), ipautil.realm_to_suffix(config.realm_name)) + try: + conn.get_entry(dn) + except errors.NotFound: + msg = ("The Replication Managers group is not available in " + "the domain. Replica promotion requires the use of " + "Replication Managers to be able to replicate data. " + "Upgrade the peer master or use the ipa-replica-prepare " + "command on the master and use a prep file to install " + "this replica.") + logger.error("%s", msg) + raise ScriptError(msg, rval=3) + + ansible_log.debug("-- CHECK DNS_MASTERS --") + + dns_masters = remote_api.Object['dnsrecord'].get_dns_masters() + if dns_masters: + if not options.no_host_dns: + logger.debug('Check forward/reverse DNS resolution') + resolution_ok = ( + check_dns_resolution(config.master_host_name, + dns_masters) and + check_dns_resolution(config.host_name, dns_masters)) + if not resolution_ok and installer.interactive: + if not ipautil.user_input("Continue?", False): + raise ScriptError(rval=0) + else: + logger.debug('No IPA DNS servers, ' + 'skipping forward/reverse resolution check') + + ansible_log.debug("-- GET_IPA_CONFIG --") + + entry_attrs = conn.get_ipa_config() + subject_base = entry_attrs.get('ipacertificatesubjectbase', [None])[0] + if subject_base is not None: + config.subject_base = DN(subject_base) + + ansible_log.debug("-- SEARCH FOR CA --") + + # Find if any server has a CA + if not hasattr(service, "find_providing_server"): + _host = [config.ca_host_name] + else: + _host = config.ca_host_name + ca_host = find_providing_server('CA', conn, _host) + if ca_host is not None: + config.ca_host_name = ca_host + ca_enabled = True + if options.dirsrv_cert_files: + msg = ("Certificates could not be provided when " + "CA is present on some master.") + logger.error(msg) + raise ScriptError(msg, rval=3) + else: + if options.setup_ca: + msg = ("The remote master does not have a CA " + "installed, can't set up CA") + logger.error(msg) + raise ScriptError(msg, rval=3) + ca_enabled = False + if not options.dirsrv_cert_files: + msg = ("Cannot issue certificates: a CA is not " + "installed. Use the --http-cert-file, " + "--dirsrv-cert-file options to provide " + "custom certificates.") + logger.error(msg) + raise ScriptError(msg, rval=3) + + ansible_log.debug("-- SEARCH FOR KRA --") + + if not hasattr(service, "find_providing_server"): + _host = [config.kra_host_name] + else: + _host = config.kra_host_name + kra_host = find_providing_server('KRA', conn, _host) + if kra_host is not None: + config.kra_host_name = kra_host + kra_enabled = True + else: + if options.setup_kra: + msg = ("There is no active KRA server in the domain, " + "can't setup a KRA clone") + logger.error(msg) + raise ScriptError(msg, rval=3) + kra_enabled = False + + ansible_log.debug("-- CHECK CA --") + + if ca_enabled: + options.realm_name = config.realm_name + options.host_name = config.host_name + ca.install_check(False, config, options) + + ansible_log.debug(" ca.external_cert_file=%s" % + repr(ca.external_cert_file)) + ansible_log.debug(" ca.external_ca_file=%s" % + repr(ca.external_ca_file)) + + # TODO + # TODO + # Save global vars external_cert_file, external_ca_file for + # later use + # TODO + # TODO + + ansible_log.debug("-- CHECK KRA --") + + if kra_enabled: + try: + kra.install_check(remote_api, config, options) + except RuntimeError as e: + raise ScriptError(e) + + ansible_log.debug("-- CHECK DNS --") + + if options.setup_dns: + dns.install_check(False, remote_api, True, options, + config.host_name) + config.ips = dns.ip_addresses + else: + config.ips = installutils.get_server_ip_address( + config.host_name, not installer.interactive, + False, options.ip_addresses) + + # check addresses here, dns module is doing own check + no_matching_interface_for_ip_address_warning(config.ips) + + ansible_log.debug("-- CHECK ADTRUST --") + + if options.setup_adtrust: + adtrust.install_check(False, options, remote_api) + + except errors.ACIError: + logger.debug("%s", traceback.format_exc()) + ansible_module.fail_json( + msg=("\nInsufficient privileges to promote the server." + "\nPossible issues:" + "\n- A user has insufficient privileges" + "\n- This client has insufficient privileges " + "to become an IPA replica")) + except errors.LDAPError: + logger.debug("%s", traceback.format_exc()) + ansible_module.fail_json(msg="\nUnable to connect to LDAP server %s" % + config.master_host_name) + except ScriptError as e: + ansible_module.fail_json(msg=str(e)) + finally: + if replman and replman.conn: + ansible_log.debug("-- UNBIND REPLMAN--") + replman.conn.unbind() + if conn.isconnected(): + ansible_log.debug("-- DISCONNECT --") + conn.disconnect() + + ansible_log.debug("-- CHECK CONNECTION --") + + # check connection + if not options.skip_conncheck: + if add_to_ipaservers: + # use user's credentials when the server host is not ipaservers + if installer._ccache is None: + del os.environ['KRB5CCNAME'] + else: + os.environ['KRB5CCNAME'] = installer._ccache + + try: + with redirect_stdout(ansible_log): + replica_conn_check( + config.master_host_name, config.host_name, + config.realm_name, options.setup_ca, 389, + options.admin_password, principal=options.principal, + ca_cert_file=cafile) + except ScriptError as e: + ansible_module.fail_json(msg=str(e)) + finally: + if add_to_ipaservers: + os.environ['KRB5CCNAME'] = ccache + + if hasattr(tasks, "configure_pkcs11_modules"): + if tasks.configure_pkcs11_modules(fstore): + ansible_log.info("Disabled p11-kit-proxy") + + installer._ca_enabled = ca_enabled + installer._kra_enabled = kra_enabled + installer._ca_file = cafile + installer._fstore = fstore + installer._sstore = sstore + installer._config = config + installer._add_to_ipaservers = add_to_ipaservers + + # done # + + ansible_module.exit_json( + changed=True, + ccache=ccache, + installer_ccache=installer._ccache, + subject_base=str(config.subject_base), + forward_policy=options.forward_policy, + _ca_enabled=ca_enabled, + _ca_subject=str(options._ca_subject), + _subject_base=str(options._subject_base) if options._subject_base + is not None else None, + _kra_enabled=kra_enabled, + _ca_file=cafile, + _top_dir=installer._top_dir, + _add_to_ipaservers=add_to_ipaservers, + _dirsrv_pkcs12_info=dirsrv_pkcs12_info, + _dirsrv_ca_cert=dirsrv_ca_cert, + _http_pkcs12_info=http_pkcs12_info, + _http_ca_cert=http_ca_cert, + _pkinit_pkcs12_info=pkinit_pkcs12_info, + _pkinit_ca_cert=pkinit_ca_cert, + no_dnssec_validation=options.no_dnssec_validation, + config_setup_ca=config.setup_ca, + config_master_host_name=config.master_host_name, + config_ca_host_name=config.ca_host_name, + config_kra_host_name=config.kra_host_name, + config_ips=[str(ip) for ip in config.ips], + # ad trust + dns_ip_addresses=[str(ip) for ip in dns.ip_addresses], + dns_reverse_zones=dns.reverse_zones, + rid_base=options.rid_base, + secondary_rid_base=options.secondary_rid_base, + adtrust_netbios_name=adtrust.netbios_name, + adtrust_reset_netbios_name=adtrust.reset_netbios_name) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_promote_openldap_conf.py b/roles/ipareplica/library/ipareplica_promote_openldap_conf.py new file mode 100644 index 0000000..b68a5ec --- /dev/null +++ b/roles/ipareplica/library/ipareplica_promote_openldap_conf.py @@ -0,0 +1,145 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_promote_openldap_conf +short description: Promote openldap.conf +description: + Promote openldap.conf +options: + setup_kra: + description: Configure a dogtag KRA + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: no + ccache: + description: The local ccache + required: no + _top_dir: + description: The installer _top_dir setting + required: no + config_setup_ca: + description: The config setup_ca setting + required: no + config_master_host_name: + description: The config master_host_name setting + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, installer, DN, paths, + gen_env_boostrap_finalize_core, constants, api_bootstrap_finalize, + gen_ReplicaConfig, gen_remote_api, redirect_stdout, promote_openldap_conf +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # server + setup_kra=dict(required=False, type='bool'), + # certificate system + subject_base=dict(required=True), + # additional + ccache=dict(required=True), + _top_dir=dict(required=True), + config_setup_ca=dict(required=True, type='bool'), + config_master_host_name=dict(required=True), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + options = installer + # server + options.setup_kra = ansible_module.params.get('setup_kra') + # certificate system + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + # additional + ccache = ansible_module.params.get('ccache') + os.environ['KRB5CCNAME'] = ccache + options._top_dir = ansible_module.params.get('_top_dir') + config_setup_ca = ansible_module.params.get('config_setup_ca') + installer.setup_ca = config_setup_ca + config_master_host_name = ansible_module.params.get( + 'config_master_host_name') + + # init # + + ansible_log.debug("== INSTALL ==") + + env = gen_env_boostrap_finalize_core(paths.ETC_IPA, + constants.DEFAULT_CONFIG) + api_bootstrap_finalize(env) + config = gen_ReplicaConfig() + config.subject_base = options.subject_base + config.setup_ca = config_setup_ca + config.master_host_name = config_master_host_name + + remote_api = gen_remote_api(config.master_host_name, paths.ETC_IPA) + installer._remote_api = remote_api + + ccache = os.environ['KRB5CCNAME'] + + with redirect_stdout(ansible_log): + ansible_log.debug("-- PROMOTE OPENLDAP_CONF--") + + promote_openldap_conf(config.host_name, config.master_host_name) + + # done # + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_promote_sssd.py b/roles/ipareplica/library/ipareplica_promote_sssd.py new file mode 100644 index 0000000..a63f2e1 --- /dev/null +++ b/roles/ipareplica/library/ipareplica_promote_sssd.py @@ -0,0 +1,145 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_promote_sssd +short description: Promote sssd +description: + Promote sssd +options: + setup_kra: + description: Configure a dogtag KRA + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: no + ccache: + description: The local ccache + required: no + _top_dir: + description: The installer _top_dir setting + required: no + config_setup_ca: + description: The config setup_ca setting + required: no + config_master_host_name: + description: The config master_host_name setting + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, installer, DN, paths, + gen_env_boostrap_finalize_core, constants, api_bootstrap_finalize, + gen_ReplicaConfig, gen_remote_api, redirect_stdout, promote_sssd +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # server + setup_kra=dict(required=False, type='bool'), + # certificate system + subject_base=dict(required=True), + # additional + ccache=dict(required=True), + _top_dir=dict(required=True), + config_setup_ca=dict(required=True, type='bool'), + config_master_host_name=dict(required=True), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + options = installer + # server + options.setup_kra = ansible_module.params.get('setup_kra') + # certificate system + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + # additional + ccache = ansible_module.params.get('ccache') + os.environ['KRB5CCNAME'] = ccache + options._top_dir = ansible_module.params.get('_top_dir') + config_setup_ca = ansible_module.params.get('config_setup_ca') + installer.setup_ca = config_setup_ca + config_master_host_name = ansible_module.params.get( + 'config_master_host_name') + + # init # + + ansible_log.debug("== INSTALL ==") + + env = gen_env_boostrap_finalize_core(paths.ETC_IPA, + constants.DEFAULT_CONFIG) + api_bootstrap_finalize(env) + config = gen_ReplicaConfig() + config.subject_base = options.subject_base + config.setup_ca = config_setup_ca + config.master_host_name = config_master_host_name + + remote_api = gen_remote_api(config.master_host_name, paths.ETC_IPA) + installer._remote_api = remote_api + + ccache = os.environ['KRB5CCNAME'] + + with redirect_stdout(ansible_log): + ansible_log.debug("-- PROMOTE SSSD --") + + promote_sssd(config.host_name) + + # done # + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_restart_kdc.py b/roles/ipareplica/library/ipareplica_restart_kdc.py new file mode 100644 index 0000000..3eff24b --- /dev/null +++ b/roles/ipareplica/library/ipareplica_restart_kdc.py @@ -0,0 +1,177 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_restart_kdc +short description: Restart KDC +description: + Restart KDC +options: + setup_ca: + description: Configure a dogtag CA + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + no_pkinit: + description: Disable pkinit setup steps + required: yes + no_ui_redirect: + description: Do not automatically redirect to the Web UI + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: no + config_master_host_name: + description: The config master_host_name setting + required: no + ccache: + description: The local ccache + required: no + _ca_file: + description: The installer _ca_file setting + required: yes + _top_dir: + description: The installer _top_dir setting + required: no + dirman_password: + description: Directory Manager (master) password + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, installer, DN, paths, sysrestore, + gen_env_boostrap_finalize_core, constants, api_bootstrap_finalize, + gen_ReplicaConfig, gen_remote_api, api, redirect_stdout, service, + krbinstance +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # server + setup_ca=dict(required=False, type='bool'), + setup_kra=dict(required=False, type='bool'), + no_pkinit=dict(required=False, type='bool'), + no_ui_redirect=dict(required=False, type='bool'), + # certificate system + subject_base=dict(required=True), + # additional + config_master_host_name=dict(required=True), + ccache=dict(required=True), + _ca_file=dict(required=False), + _top_dir=dict(required=True), + dirman_password=dict(required=True, no_log=True), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + options = installer + # server + options.setup_ca = ansible_module.params.get('setup_ca') + options.setup_kra = ansible_module.params.get('setup_kra') + options.no_pkinit = ansible_module.params.get('no_pkinit') + # certificate system + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + # additional + master_host_name = ansible_module.params.get('config_master_host_name') + ccache = ansible_module.params.get('ccache') + os.environ['KRB5CCNAME'] = ccache + options._top_dir = ansible_module.params.get('_top_dir') + dirman_password = ansible_module.params.get('dirman_password') + + # init # + + fstore = sysrestore.FileStore(paths.SYSRESTORE) + + ansible_log.debug("== INSTALL ==") + + options = installer + + env = gen_env_boostrap_finalize_core(paths.ETC_IPA, + constants.DEFAULT_CONFIG) + api_bootstrap_finalize(env) + config = gen_ReplicaConfig() + config.dirman_password = dirman_password + + remote_api = gen_remote_api(master_host_name, paths.ETC_IPA) + + conn = remote_api.Backend.ldap2 + ccache = os.environ['KRB5CCNAME'] + + # There is a api.Backend.ldap2.connect call somewhere in ca, ds, dns or + # ntpinstance + api.Backend.ldap2.connect() + conn.connect(ccache=ccache) + + # krb + krb = krbinstance.KrbInstance(fstore) + krb.set_output(ansible_log) + with redirect_stdout(ansible_log): + krb.init_info(api.env.realm, api.env.host, + setup_pkinit=not options.no_pkinit, + subject_base=options.subject_base) + + ansible_log.debug("-- RESTART KDC --") + + service.print_msg("Restarting the KDC") + krb.restart() + + # done # + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_setup_adtrust.py b/roles/ipareplica/library/ipareplica_setup_adtrust.py new file mode 100644 index 0000000..d269a40 --- /dev/null +++ b/roles/ipareplica/library/ipareplica_setup_adtrust.py @@ -0,0 +1,178 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_setup_adtrust +short description: Setup adtrust +description: + Setup adtrust +options: + setup_kra: + description: Configure a dogtag KRA + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: no + enable_compat: + description: Enable support for trusted domains for old clients + required: yes + rid_base: + description: Start value for mapping UIDs and GIDs to RIDs + required: yes + secondary_rid_base: + description: + Start value of the secondary range for mapping UIDs and GIDs to RIDs + required: yes + adtrust_netbios_name: + description: The adtrust netbios_name setting + required: no + adtrust_reset_netbios_name: + description: The adtrust reset_netbios_name setting + required: no + ccache: + description: The local ccache + required: no + _top_dir: + description: The installer _top_dir setting + required: no + setup_ca: + description: Configure a dogtag CA + required: no + config_master_host_name: + description: The config master_host_name setting + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, installer, DN, paths, sysrestore, + gen_env_boostrap_finalize_core, constants, api_bootstrap_finalize, + gen_ReplicaConfig, gen_remote_api, api, redirect_stdout, adtrust +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # server + setup_kra=dict(required=False, type='bool'), + # certificate system + subject_base=dict(required=True), + # ad trust + enable_compat=dict(required=False, type='bool', default=False), + rid_base=dict(required=False, type='int'), + secondary_rid_base=dict(required=False, type='int'), + # additional + adtrust_netbios_name=dict(required=True), + adtrust_reset_netbios_name=dict(required=True, type='bool'), + # additional + ccache=dict(required=True), + _top_dir=dict(required=True), + setup_ca=dict(required=True), + config_master_host_name=dict(required=True), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + options = installer + # server + options.setup_kra = ansible_module.params.get('setup_kra') + # certificate system + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + # ad trust + options.enable_compat = ansible_module.params.get('enable_compat') + options.rid_base = ansible_module.params.get('rid_base') + options.secondary_rid_base = ansible_module.params.get( + 'secondary_rid_base') + # additional + ccache = ansible_module.params.get('ccache') + os.environ['KRB5CCNAME'] = ccache + options._top_dir = ansible_module.params.get('_top_dir') + options.setup_ca = ansible_module.params.get('setup_ca') + config_master_host_name = ansible_module.params.get( + 'config_master_host_name') + adtrust.netbios_name = ansible_module.params.get('adtrust_netbios_name') + adtrust.reset_netbios_name = ansible_module.params.get( + 'adtrust_reset_netbios_name') + + # init # + + fstore = sysrestore.FileStore(paths.SYSRESTORE) + + ansible_log.debug("== INSTALL ==") + + env = gen_env_boostrap_finalize_core(paths.ETC_IPA, + constants.DEFAULT_CONFIG) + api_bootstrap_finalize(env) + config = gen_ReplicaConfig() + config.subject_base = options.subject_base + config.master_host_name = config_master_host_name + + remote_api = gen_remote_api(config.master_host_name, paths.ETC_IPA) + installer._remote_api = remote_api + + ccache = os.environ['KRB5CCNAME'] + + api.Backend.ldap2.connect() + + with redirect_stdout(ansible_log): + ansible_log.debug("-- INSTALL ADTRUST --") + + adtrust.install(False, options, fstore, api) + + # done # + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_setup_ca.py b/roles/ipareplica/library/ipareplica_setup_ca.py new file mode 100644 index 0000000..a07c9e4 --- /dev/null +++ b/roles/ipareplica/library/ipareplica_setup_ca.py @@ -0,0 +1,257 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_setup_ca +short description: Setup CA +description: + Setup CA +options: + pki_config_override: + description: Path to ini file with config overrides + required: yes + setup_ca: + description: Configure a dogtag CA + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + no_pkinit: + description: Disable pkinit setup steps + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: no + ccache: + description: The local ccache + required: no + _ca_enabled: + description: The installer _ca_enabled setting + required: yes + _ca_file: + description: The installer _ca_file setting + required: yes + _kra_enabled: + description: The installer _kra_enabled setting + required: yes + _kra_host_name: + description: The installer _kra_host_name setting + required: yes + _dirsrv_pkcs12_info: + description: The installer _dirsrv_pkcs12_info setting + required: yes + _pkinit_pkcs12_info: + description: The installer _pkinit_pkcs12_info setting + required: yes + _top_dir: + description: The installer _top_dir setting + required: no + _ca_subject: + description: The installer _ca_subject setting + required: no + _subject_base: + description: The installer _subject_base setting + required: no + dirman_password: + description: Directory Manager (master) password + required: no + config_setup_ca: + description: The config setup_ca setting + required: no + config_master_host_name: + description: The config master_host_name setting + required: no + config_ca_host_name: + description: The config ca_host_name setting + required: no + config_ips: + description: The config ips setting + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, installer, DN, paths, + ansible_module_get_parsed_ip_addresses, + gen_env_boostrap_finalize_core, constants, api_bootstrap_finalize, + gen_ReplicaConfig, gen_remote_api, api, redirect_stdout, ca, + custodiainstance +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # basic + pki_config_override=dict(required=False), + # server + setup_ca=dict(required=False, type='bool'), + setup_kra=dict(required=False, type='bool'), + no_pkinit=dict(required=False, type='bool'), + # certificate system + subject_base=dict(required=True), + # additional + ccache=dict(required=True), + _ca_enabled=dict(required=False, type='bool'), + _ca_file=dict(required=False), + _kra_enabled=dict(required=False, type='bool'), + _kra_host_name=dict(required=False), + _dirsrv_pkcs12_info=dict(required=False), + _pkinit_pkcs12_info=dict(required=False), + _top_dir=dict(required=True), + _ca_subject=dict(required=True), + _subject_base=dict(required=True), + dirman_password=dict(required=True, no_log=True), + config_setup_ca=dict(required=True, type='bool'), + config_master_host_name=dict(required=True), + config_ca_host_name=dict(required=True), + config_ips=dict(required=False, type='list', default=[]), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + options = installer + # basic + options.pki_config_override = ansible_module.params.get( + 'pki_config_override') + # server + options.setup_ca = ansible_module.params.get('setup_ca') + options.setup_kra = ansible_module.params.get('setup_kra') + options.no_pkinit = ansible_module.params.get('no_pkinit') + # certificate system + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + # additional + ccache = ansible_module.params.get('ccache') + os.environ['KRB5CCNAME'] = ccache + # os.environ['KRB5CCNAME'] = ansible_module.params.get('installer_ccache') + # installer._ccache = ansible_module.params.get('installer_ccache') + ca_enabled = ansible_module.params.get('_ca_enabled') + kra_enabled = ansible_module.params.get('_kra_enabled') + kra_host_name = ansible_module.params.get('_kra_host_name') + installer._dirsrv_pkcs12_info = ansible_module.params.get( + '_dirsrv_pkcs12_info') + installer._pkinit_pkcs12_info = ansible_module.params.get( + '_pkinit_pkcs12_info') + options._top_dir = ansible_module.params.get('_top_dir') + options._ca_subject = ansible_module.params.get('_ca_subject') + if options._ca_subject is not None: + options._ca_subject = DN(options._ca_subject) + options._subject_base = ansible_module.params.get('_subject_base') + if options._subject_base is not None: + options._subject_base = DN(options._subject_base) + dirman_password = ansible_module.params.get('dirman_password') + config_setup_ca = ansible_module.params.get('config_setup_ca') + config_master_host_name = ansible_module.params.get( + 'config_master_host_name') + config_ca_host_name = ansible_module.params.get('config_ca_host_name') + config_ips = ansible_module_get_parsed_ip_addresses(ansible_module, + "config_ips") + + # init # + + ansible_log.debug("== INSTALL ==") + + options = installer + + env = gen_env_boostrap_finalize_core(paths.ETC_IPA, + constants.DEFAULT_CONFIG) + api_bootstrap_finalize(env) + + config = gen_ReplicaConfig() + config.dirman_password = dirman_password + config.setup_ca = config_setup_ca + config.master_host_name = config_master_host_name + config.ca_host_name = config_ca_host_name + config.ips = config_ips + config.promote = options.promote + config.kra_enabled = kra_enabled + config.kra_host_name = kra_host_name + + remote_api = gen_remote_api(config.master_host_name, paths.ETC_IPA) + options._remote_api = remote_api + + ccache = os.environ['KRB5CCNAME'] + + # There is a api.Backend.ldap2.connect call somewhere in ca, ds, dns or + # ntpinstance + api.Backend.ldap2.connect() + # conn.connect(ccache=ccache) + + ansible_log.debug("-- INSTALL CA --") + + with redirect_stdout(ansible_log): + options.realm_name = config.realm_name + options.domain_name = config.domain_name + options.host_name = config.host_name + options.dm_password = config.dirman_password + + if not hasattr(custodiainstance, "get_custodia_instance"): + ca.install(False, config, options) + else: + if kra_enabled: + # A KRA peer always provides a CA, too. + mode = custodiainstance.CustodiaModes.KRA_PEER + elif ca_enabled: + mode = custodiainstance.CustodiaModes.CA_PEER + else: + mode = custodiainstance.CustodiaModes.MASTER_PEER + custodia = custodiainstance.get_custodia_instance(config, mode) + + ca.install(False, config, options, custodia=custodia) + + # done # + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_setup_certmonger.py b/roles/ipareplica/library/ipareplica_setup_certmonger.py new file mode 100644 index 0000000..b889d10 --- /dev/null +++ b/roles/ipareplica/library/ipareplica_setup_certmonger.py @@ -0,0 +1,80 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_setup_certmonger +short description: Setup certmonger +description: + Setup certmonger +options: +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, redirect_stdout, configure_certmonger +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + with redirect_stdout(ansible_log): + ansible_log.debug("-- CONFIGURE_CERTMONGER --") + + # FIXME: allow to use passed in certs instead + configure_certmonger() + + # done # + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_setup_custodia.py b/roles/ipareplica/library/ipareplica_setup_custodia.py new file mode 100644 index 0000000..88da03b --- /dev/null +++ b/roles/ipareplica/library/ipareplica_setup_custodia.py @@ -0,0 +1,212 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_setup_custodia +short description: Setup custodia +description: + Setup custodia +options: + setup_ca: + description: Configure a dogtag CA + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + no_pkinit: + description: Disable pkinit setup steps + required: yes + no_ui_redirect: + description: Do not automatically redirect to the Web UI + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: no + config_master_host_name: + description: The config master_host_name setting + required: no + ccache: + description: The local ccache + required: no + _ca_enabled: + description: The installer _ca_enabled setting + required: yes + _ca_file: + description: The installer _ca_file setting + required: yes + _kra_enabled: + description: The installer _kra_enabled setting + required: yes + _kra_host_name: + description: The installer _kra_host_name setting + required: yes + _pkinit_pkcs12_info: + description: The installer _pkinit_pkcs12_info setting + required: yes + _top_dir: + description: The installer _top_dir setting + required: no + dirman_password: + description: Directory Manager (master) password + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, installer, DN, paths, + gen_env_boostrap_finalize_core, constants, api_bootstrap_finalize, + gen_ReplicaConfig, gen_remote_api, api, redirect_stdout, custodiainstance +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # server + setup_ca=dict(required=False, type='bool'), + setup_kra=dict(required=False, type='bool'), + no_pkinit=dict(required=False, type='bool'), + no_ui_redirect=dict(required=False, type='bool'), + # certificate system + subject_base=dict(required=True), + # additional + config_master_host_name=dict(required=True), + ccache=dict(required=True), + _ca_enabled=dict(required=False, type='bool'), + _ca_file=dict(required=False), + _kra_enabled=dict(required=False, type='bool'), + _kra_host_name=dict(required=False), + _pkinit_pkcs12_info=dict(required=False), + _top_dir=dict(required=True), + dirman_password=dict(required=True, no_log=True), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + options = installer + # server + options.setup_ca = ansible_module.params.get('setup_ca') + options.setup_kra = ansible_module.params.get('setup_kra') + options.no_pkinit = ansible_module.params.get('no_pkinit') + # certificate system + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + # additional + master_host_name = ansible_module.params.get('config_master_host_name') + ccache = ansible_module.params.get('ccache') + os.environ['KRB5CCNAME'] = ccache + # os.environ['KRB5CCNAME'] = ansible_module.params.get('installer_ccache') + # installer._ccache = ansible_module.params.get('installer_ccache') + ca_enabled = ansible_module.params.get('_ca_enabled') + kra_enabled = ansible_module.params.get('_kra_enabled') + kra_host_name = ansible_module.params.get('_kra_host_name') + options._pkinit_pkcs12_info = ansible_module.params.get( + '_pkinit_pkcs12_info') + options._top_dir = ansible_module.params.get('_top_dir') + dirman_password = ansible_module.params.get('dirman_password') + + # init # + + ansible_log.debug("== INSTALL ==") + + options = installer + promote = installer.promote + + env = gen_env_boostrap_finalize_core(paths.ETC_IPA, + constants.DEFAULT_CONFIG) + api_bootstrap_finalize(env) + config = gen_ReplicaConfig() + config.dirman_password = dirman_password + config.promote = installer.promote + config.kra_enabled = kra_enabled + config.kra_host_name = kra_host_name + + remote_api = gen_remote_api(master_host_name, paths.ETC_IPA) + + conn = remote_api.Backend.ldap2 + ccache = os.environ['KRB5CCNAME'] + + # There is a api.Backend.ldap2.connect call somewhere in ca, ds, dns or + # ntpinstance + api.Backend.ldap2.connect() + conn.connect(ccache=ccache) + + with redirect_stdout(ansible_log): + ansible_log.debug("-- INSTALL_CUSTODIA --") + + if not hasattr(custodiainstance, "get_custodia_instance"): + custodia = custodiainstance.CustodiaInstance(config.host_name, + config.realm_name) + if promote and \ + hasattr(custodiainstance.CustodiaInstance, "create_replica"): + ansible_log.debug("-- CUSTODIA CREATE_REPLICA --") + custodia.create_replica(config.master_host_name) + else: + ansible_log.debug("-- CUSTODIA CREATE_INSTANCE --") + custodia.create_instance() + else: + if kra_enabled: + # A KRA peer always provides a CA, too. + mode = custodiainstance.CustodiaModes.KRA_PEER + elif ca_enabled: + mode = custodiainstance.CustodiaModes.CA_PEER + else: + mode = custodiainstance.CustodiaModes.MASTER_PEER + custodia = custodiainstance.get_custodia_instance(config, mode) + custodia.create_instance() + + # done # + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_setup_dns.py b/roles/ipareplica/library/ipareplica_setup_dns.py new file mode 100644 index 0000000..34e5236 --- /dev/null +++ b/roles/ipareplica/library/ipareplica_setup_dns.py @@ -0,0 +1,191 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_setup_dns +short description: Setup DNS +description: + Setup DNS +options: + setup_kra: + description: Configure a dogtag KRA + required: yes + setup_dns: + description: Configure bind with our zone + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: no + zonemgr: + description: DNS zone manager e-mail address. Defaults to hostmaster@DOMAIN + required: yes + forwarders: + description: Add DNS forwarders + required: yes + forward_policy: + description: DNS forwarding policy for global forwarders + required: yes + no_dnssec_validation: + description: Disable DNSSEC validation + required: yes + dns_ip_addresses: + description: The dns ip_addresses setting + required: no + dns_reverse_zones: + description: The dns reverse_zones setting + required: no + ccache: + description: The local ccache + required: no + _top_dir: + description: The installer _top_dir setting + required: no + setup_ca: + description: Configure a dogtag CA + required: no + config_master_host_name: + description: The config master_host_name setting + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, installer, DN, paths, + gen_env_boostrap_finalize_core, constants, api_bootstrap_finalize, + gen_ReplicaConfig, gen_remote_api, api, redirect_stdout, dns, + ansible_module_get_parsed_ip_addresses +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # server + setup_kra=dict(required=False, type='bool'), + setup_dns=dict(required=False, type='bool'), + # certificate system + subject_base=dict(required=True), + # dns + zonemgr=dict(required=False), + forwarders=dict(required=False, type='list', default=[]), + forward_policy=dict(default=None, choices=['first', 'only']), + no_dnssec_validation=dict(required=False, type='bool', + default=False), + # additional + dns_ip_addresses=dict(required=True, type='list'), + dns_reverse_zones=dict(required=True, type='list'), + ccache=dict(required=True), + _top_dir=dict(required=True), + setup_ca=dict(required=True, type='bool'), + config_master_host_name=dict(required=True), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + options = installer + # server + options.setup_kra = ansible_module.params.get('setup_kra') + options.setup_dns = ansible_module.params.get('setup_dns') + # certificate system + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + # dns + options.zonemgr = ansible_module.params.get('zonemgr') + options.forwarders = ansible_module.params.get('forwarders') + options.forward_policy = ansible_module.params.get('forward_policy') + options.no_dnssec_validation = ansible_module.params.get( + 'no_dnssec_validationdnssec_validation') + # additional + dns.ip_addresses = ansible_module_get_parsed_ip_addresses( + ansible_module, 'dns_ip_addresses') + dns.reverse_zones = ansible_module.params.get('dns_reverse_zones') + ccache = ansible_module.params.get('ccache') + os.environ['KRB5CCNAME'] = ccache + options._top_dir = ansible_module.params.get('_top_dir') + options.setup_ca = ansible_module.params.get('setup_ca') + config_master_host_name = ansible_module.params.get( + 'config_master_host_name') + + # init # + + ansible_log.debug("== INSTALL ==") + + env = gen_env_boostrap_finalize_core(paths.ETC_IPA, + constants.DEFAULT_CONFIG) + api_bootstrap_finalize(env) + config = gen_ReplicaConfig() + config.subject_base = options.subject_base + config.master_host_name = config_master_host_name + + remote_api = gen_remote_api(config.master_host_name, paths.ETC_IPA) + installer._remote_api = remote_api + + ccache = os.environ['KRB5CCNAME'] + + # There is a api.Backend.ldap2.connect call somewhere in ca, ds, dns or + # ntpinstance + api.Backend.ldap2.connect() + + with redirect_stdout(ansible_log): + if options.setup_dns: + ansible_log.debug("-- INSTALL DNS --") + dns.install(False, True, options, api) + else: + ansible_log.debug("-- DNS UPDATE_SYSTEM_RECORDS --") + api.Command.dns_update_system_records() + + # done # + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_setup_ds.py b/roles/ipareplica/library/ipareplica_setup_ds.py new file mode 100644 index 0000000..c0e1565 --- /dev/null +++ b/roles/ipareplica/library/ipareplica_setup_ds.py @@ -0,0 +1,367 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_setup_ds +short description: Setup DS +description: + Setup DS +options: + dm_password: + description: Directory Manager password + required: yes + password: + description: Admin user kerberos password + required: yes + ip_addresses: + description: List of Master Server IP Addresses + required: yes + domain: + description: Primary DNS domain of the IPA deployment + required: yes + realm: + description: Kerberos realm name of the IPA deployment + required: yes + hostname: + description: Fully qualified name of this host + required: yes + ca_cert_files: + description: + List of files containing CA certificates for the service certificate + files + required: yes + no_host_dns: + description: Do not use DNS for hostname lookup during installation + required: yes + setup_adtrust: + description: Configure AD trust capability + required: yes + setup_ca: + description: Configure a dogtag CA + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + setup_dns: + description: Configure bind with our zone + required: yes + no_pkinit: + description: Disable pkinit setup steps + required: yes + dirsrv_config_file: + description: + The path to LDIF file that will be used to modify configuration of + dse.ldif during installation of the directory server instance + required: yes + dirsrv_cert_files: + description: + Files containing the Directory Server SSL certificate and private key + required: yes + force_join: + description: Force client enrollment even if already enrolled + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: no + server: + description: Fully qualified name of IPA server to enroll to + required: no + ccache: + description: The local ccache + required: no + installer_ccache: + description: The installer ccache setting + required: no + _ca_enabled: + description: The installer _ca_enabled setting + required: yes + _dirsrv_pkcs12_info: + description: The installer _dirsrv_pkcs12_info setting + required: yes + _top_dir: + description: The installer _top_dir setting + required: no + _add_to_ipaservers: + description: The installer _add_to_ipaservers setting + required: no + _ca_subject: + description: The installer _ca_subject setting + required: no + _subject_base: + description: The installer _subject_base setting + required: no + dirman_password: + description: Directory Manager (master) password + required: no + config_setup_ca: + description: The config setup_ca setting + required: no + config_master_host_name: + description: The config master_host_name setting + required: no + config_ca_host_name: + description: The config ca_host_name setting + required: no + config_ips: + description: The config ips setting + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os +import inspect + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, installer, DN, paths, sysrestore, + ansible_module_get_parsed_ip_addresses, + gen_env_boostrap_finalize_core, constants, api_bootstrap_finalize, + gen_ReplicaConfig, gen_remote_api, redirect_stdout, ipaldap, + install_replica_ds, install_dns_records, ntpinstance, ScriptError +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # basic + dm_password=dict(required=False, no_log=True), + password=dict(required=False, no_log=True), + ip_addresses=dict(required=False, type='list', default=[]), + domain=dict(required=False), + realm=dict(required=False), + hostname=dict(required=False), + ca_cert_files=dict(required=False, type='list', default=[]), + no_host_dns=dict(required=False, type='bool', default=False), + # server + setup_adtrust=dict(required=False, type='bool'), + setup_ca=dict(required=False, type='bool'), + setup_kra=dict(required=False, type='bool'), + setup_dns=dict(required=False, type='bool'), + no_pkinit=dict(required=False, type='bool', default=False), + dirsrv_config_file=dict(required=False), + # ssl certificate + dirsrv_cert_files=dict(required=False, type='list', default=[]), + # client + force_join=dict(required=False, type='bool'), + # certificate system + subject_base=dict(required=True), + # additional + server=dict(required=True), + ccache=dict(required=True), + installer_ccache=dict(required=True), + _ca_enabled=dict(required=False, type='bool'), + _dirsrv_pkcs12_info=dict(required=False), + _top_dir=dict(required=True), + _add_to_ipaservers=dict(required=True, type='bool'), + _ca_subject=dict(required=True), + _subject_base=dict(required=True), + dirman_password=dict(required=True, no_log=True), + config_setup_ca=dict(required=True, type='bool'), + config_master_host_name=dict(required=True), + config_ca_host_name=dict(required=True), + config_ips=dict(required=False, type='list', default=[]), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + options = installer + options.dm_password = ansible_module.params.get('dm_password') + options.password = options.dm_password + options.admin_password = ansible_module.params.get('password') + options.ip_addresses = ansible_module_get_parsed_ip_addresses( + ansible_module) + options.domain_name = ansible_module.params.get('domain') + options.realm_name = ansible_module.params.get('realm') + options.host_name = ansible_module.params.get('hostname') + options.ca_cert_files = ansible_module.params.get('ca_cert_files') + options.no_host_dns = ansible_module.params.get('no_host_dns') + # server + options.setup_adtrust = ansible_module.params.get('setup_adtrust') + options.setup_ca = ansible_module.params.get('setup_ca') + options.setup_kra = ansible_module.params.get('setup_kra') + options.setup_dns = ansible_module.params.get('setup_dns') + options.no_pkinit = ansible_module.params.get('no_pkinit') + options.dirsrv_config_file = ansible_module.params.get( + 'dirsrv_config_file') + # ssl certificate + options.dirsrv_cert_files = ansible_module.params.get('dirsrv_cert_files') + # client + options.force_join = ansible_module.params.get('force_join') + # certificate system + options.external_ca = ansible_module.params.get('external_ca') + options.external_cert_files = ansible_module.params.get( + 'external_cert_files') + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + options.ca_subject = ansible_module.params.get('ca_subject') + # additional + # options._host_name_overridden = ansible_module.params.get( + # '_hostname_overridden') + options.server = ansible_module.params.get('server') + master_host_name = ansible_module.params.get('config_master_host_name') + ccache = ansible_module.params.get('ccache') + os.environ['KRB5CCNAME'] = ccache + # os.environ['KRB5CCNAME'] = ansible_module.params.get('installer_ccache') + installer._ccache = ansible_module.params.get('installer_ccache') + ca_enabled = ansible_module.params.get('_ca_enabled') + + dirsrv_pkcs12_info = ansible_module.params.get('_dirsrv_pkcs12_info') + + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + options._top_dir = ansible_module.params.get('_top_dir') + options._add_to_ipaservers = ansible_module.params.get( + '_add_to_ipaservers') + + options._ca_subject = ansible_module.params.get('_ca_subject') + options._subject_base = ansible_module.params.get('_subject_base') + + dirman_password = ansible_module.params.get('dirman_password') + config_setup_ca = ansible_module.params.get('config_setup_ca') + config_master_host_name = ansible_module.params.get( + 'config_master_host_name') + config_ca_host_name = ansible_module.params.get('config_ca_host_name') + config_ips = ansible_module_get_parsed_ip_addresses(ansible_module, + "config_ips") + + # init # + + fstore = sysrestore.FileStore(paths.SYSRESTORE) + + ansible_log.debug("== INSTALL ==") + + options = installer + promote = installer.promote + + env = gen_env_boostrap_finalize_core(paths.ETC_IPA, + constants.DEFAULT_CONFIG) + api_bootstrap_finalize(env) + config = gen_ReplicaConfig() + config.subject_base = options.subject_base + config.dirman_password = dirman_password + config.setup_ca = config_setup_ca + config.master_host_name = config_master_host_name + config.ca_host_name = config_ca_host_name + config.ips = config_ips + config.promote = installer.promote + + remote_api = gen_remote_api(master_host_name, paths.ETC_IPA) + installer._remote_api = remote_api + + conn = remote_api.Backend.ldap2 + ccache = os.environ['KRB5CCNAME'] + + cafile = paths.IPA_CA_CRT + try: + ansible_log.debug("-- CONNECT --") + if promote: + conn.connect(ccache=ccache) + else: + # dmlvl 0 replica install should always use DM credentials + # to create remote LDAP connection. Since ACIs permitting hosts + # to manage their own services were added in 4.2 release, + # the master denies this operations. + conn.connect(bind_dn=ipaldap.DIRMAN_DN, cacert=cafile, + bind_pw=dirman_password) + + ansible_log.debug("-- CONFIGURE DIRSRV --") + # Configure dirsrv + with redirect_stdout(ansible_log): + argspec = inspect.getargspec(install_replica_ds) + if "promote" in argspec.args: + ds = install_replica_ds(config, options, ca_enabled, + remote_api, + ca_file=cafile, + promote=promote, + pkcs12_info=dirsrv_pkcs12_info) + else: + if "fstore" in argspec.args: + ds = install_replica_ds(config, options, ca_enabled, + remote_api, + ca_file=cafile, + pkcs12_info=dirsrv_pkcs12_info, + fstore=fstore) + else: + ds = install_replica_ds(config, options, ca_enabled, + remote_api, + ca_file=cafile, + pkcs12_info=dirsrv_pkcs12_info) + + ansible_log.debug("-- INSTALL DNS RECORDS --") + # Always try to install DNS records + argspec = inspect.getargspec(install_dns_records) + if "fstore" not in argspec.args: + install_dns_records(config, options, remote_api) + else: + install_dns_records(config, options, remote_api, fstore=fstore) + + # TODO: check if ntp needs to be enabled later on + + ansible_log.debug("-- NTP LDAP ENABLE --") + if ntpinstance is not None: + ntpinstance.ntp_ldap_enable(config.host_name, ds.suffix, + remote_api.env.realm) + + except (ScriptError, RuntimeError) as e: + ansible_module.fail_json(msg=str(e)) + finally: + if conn.isconnected(): + ansible_log.debug("-- DISCONNECT --") + conn.disconnect() + + # done # + + ansible_module.exit_json(changed=True, + ds_suffix=str(ds.suffix), + ds_ca_subject=str(ds.ca_subject)) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_setup_http.py b/roles/ipareplica/library/ipareplica_setup_http.py new file mode 100644 index 0000000..7b645f7 --- /dev/null +++ b/roles/ipareplica/library/ipareplica_setup_http.py @@ -0,0 +1,238 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_setup_http +short description: Setup HTTP +description: + Setup HTTP +options: + setup_ca: + description: Configure a dogtag CA + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + no_pkinit: + description: Disable pkinit setup steps + required: yes + no_ui_redirect: + description: Do not automatically redirect to the Web UI + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: no + config_master_host_name: + description: The config master_host_name setting + required: no + config_ca_host_name: + description: The config ca_host_name setting + required: no + ccache: + description: The local ccache + required: no + _ca_enabled: + description: The installer _ca_enabled setting + required: yes + _ca_file: + description: The installer _ca_file setting + required: yes + _http_pkcs12_info: + description: The installer _http_pkcs12_info setting + required: yes + _top_dir: + description: The installer _top_dir setting + required: no + dirman_password: + description: Directory Manager (master) password + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os +import inspect + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, installer, DN, paths, sysrestore, + gen_env_boostrap_finalize_core, constants, api_bootstrap_finalize, + gen_ReplicaConfig, gen_remote_api, api, redirect_stdout, create_ipa_conf, + install_http +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # server + setup_ca=dict(required=False, type='bool'), + setup_kra=dict(required=False, type='bool'), + no_pkinit=dict(required=False, type='bool'), + no_ui_redirect=dict(required=False, type='bool'), + # certificate system + subject_base=dict(required=True), + config_master_host_name=dict(required=True), + config_ca_host_name=dict(required=True), + ccache=dict(required=True), + _ca_enabled=dict(required=False, type='bool'), + _ca_file=dict(required=False), + _http_pkcs12_info=dict(required=False), + _top_dir=dict(required=True), + dirman_password=dict(required=True, no_log=True), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + options = installer + options.setup_ca = ansible_module.params.get('setup_ca') + options.setup_kra = ansible_module.params.get('setup_kra') + options.no_pkinit = ansible_module.params.get('no_pkinit') + options.no_ui_redirect = ansible_module.params.get('no_ui_redirect') + # certificate system + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + # additional + master_host_name = ansible_module.params.get('config_master_host_name') + ca_host_name = ansible_module.params.get('config_master_host_name') + ccache = ansible_module.params.get('ccache') + os.environ['KRB5CCNAME'] = ccache + # os.environ['KRB5CCNAME'] = ansible_module.params.get('installer_ccache') + # installer._ccache = ansible_module.params.get('installer_ccache') + ca_enabled = ansible_module.params.get('_ca_enabled') + http_pkcs12_info = ansible_module.params.get('_http_pkcs12_info') + options._top_dir = ansible_module.params.get('_top_dir') + dirman_password = ansible_module.params.get('dirman_password') + + # init # + + fstore = sysrestore.FileStore(paths.SYSRESTORE) + + ansible_log.debug("== INSTALL ==") + + promote = installer.promote + + env = gen_env_boostrap_finalize_core(paths.ETC_IPA, + constants.DEFAULT_CONFIG) + api_bootstrap_finalize(env) + config = gen_ReplicaConfig() + config.subject_base = options.subject_base + config.dirman_password = dirman_password + config.setup_ca = options.setup_ca + # config.master_host_name = master_host_name + config.ca_host_name = ca_host_name + config.promote = installer.promote + + remote_api = gen_remote_api(master_host_name, paths.ETC_IPA) + # installer._remote_api = remote_api + + conn = remote_api.Backend.ldap2 + ccache = os.environ['KRB5CCNAME'] + + # There is a api.Backend.ldap2.connect call somewhere in ca, ds, dns or + # ntpinstance + api.Backend.ldap2.connect() + conn.connect(ccache=ccache) + + cafile = paths.IPA_CA_CRT + with redirect_stdout(ansible_log): + ansible_log.debug("-- INSTALL_HTTP --") + + # We need to point to the master when certmonger asks for + # HTTP certificate. + # During http installation, the HTTP/hostname principal is created + # locally then the installer waits for the entry to appear on the + # master selected for the installation. + # In a later step, the installer requests a SSL certificate through + # Certmonger (and the op adds the principal if it does not exist yet). + # If xmlrpc_uri points to the soon-to-be replica, + # the httpd service is not ready yet to handle certmonger requests + # and certmonger tries to find another master. The master can be + # different from the one selected for the installation, and it is + # possible that the principal has not been replicated yet. This + # may lead to a replication conflict. + # This is why we need to force the use of the same master by + # setting xmlrpc_uri + create_ipa_conf(fstore, config, ca_enabled, + master=config.master_host_name) + + argspec = inspect.getargspec(install_http) + if "promote" in argspec.args: + install_http( + config, + auto_redirect=not options.no_ui_redirect, + promote=promote, + pkcs12_info=http_pkcs12_info, + ca_is_configured=ca_enabled, + ca_file=cafile) + else: + if "fstore" not in argspec.args: + install_http( + config, + auto_redirect=not options.no_ui_redirect, + pkcs12_info=http_pkcs12_info, + ca_is_configured=ca_enabled, + ca_file=cafile) + else: + install_http( + config, + auto_redirect=not options.no_ui_redirect, + pkcs12_info=http_pkcs12_info, + ca_is_configured=ca_enabled, + ca_file=cafile, + fstore=fstore) + + # Need to point back to ourself after the cert for HTTP is obtained + create_ipa_conf(fstore, config, ca_enabled) + + # done # + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_setup_kra.py b/roles/ipareplica/library/ipareplica_setup_kra.py new file mode 100644 index 0000000..86b077a --- /dev/null +++ b/roles/ipareplica/library/ipareplica_setup_kra.py @@ -0,0 +1,292 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_setup_kra +short description: Setup KRA +description: + Setup KRA +options: + dm_password: + description: Directory Manager password + required: yes + password: + description: Admin user kerberos password + required: yes + ip_addresses: + description: List of Master Server IP Addresses + required: yes + domain: + description: Primary DNS domain of the IPA deployment + required: yes + realm: + description: Kerberos realm name of the IPA deployment + required: yes + hostname: + description: Fully qualified name of this host + required: yes + ca_cert_files: + description: + List of files containing CA certificates for the service certificate + files + required: yes + no_host_dns: + description: Do not use DNS for hostname lookup during installation + required: yes + pki_config_override: + description: Path to ini file with config overrides + required: yes + setup_adtrust: + description: Configure AD trust capability + required: yes + setup_ca: + description: Configure a dogtag CA + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + setup_dns: + description: Configure bind with our zone + required: yes + dirsrv_cert_files: + description: + Files containing the Directory Server SSL certificate and private key + required: yes + force_join: + description: Force client enrollment even if already enrolled + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: no + server: + description: Fully qualified name of IPA server to enroll to + required: no + config_master_host_name: + description: The config master_host_name setting + required: no + ccache: + description: The local ccache + required: no + installer_ccache: + description: The installer ccache setting + required: no + _ca_enabled: + description: The installer _ca_enabled setting + required: yes + _kra_enabled: + description: The installer _kra_enabled setting + required: yes + _kra_host_name: + description: The installer _kra_host_name setting + required: yes + _top_dir: + description: The installer _top_dir setting + required: no + _add_to_ipaservers: + description: The installer _add_to_ipaservers setting + required: no + _ca_subject: + description: The installer _ca_subject setting + required: no + _subject_base: + description: The installer _subject_base setting + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, installer, DN, paths, + ansible_module_get_parsed_ip_addresses, + gen_env_boostrap_finalize_core, constants, api_bootstrap_finalize, + gen_ReplicaConfig, gen_remote_api, api, redirect_stdout, custodiainstance, + kra +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # basic + dm_password=dict(required=False, no_log=True), + password=dict(required=False, no_log=True), + ip_addresses=dict(required=False, type='list', default=[]), + domain=dict(required=False), + realm=dict(required=False), + hostname=dict(required=False), + ca_cert_files=dict(required=False, type='list', default=[]), + no_host_dns=dict(required=False, type='bool', default=False), + pki_config_override=dict(required=False), + # server + setup_adtrust=dict(required=False, type='bool'), + setup_ca=dict(required=False, type='bool'), + setup_kra=dict(required=False, type='bool'), + setup_dns=dict(required=False, type='bool'), + # ssl certificate + dirsrv_cert_files=dict(required=False, type='list', default=[]), + # client + force_join=dict(required=False, type='bool'), + # certificate system + subject_base=dict(required=True), + # additional + server=dict(required=True), + config_master_host_name=dict(required=True), + ccache=dict(required=True), + installer_ccache=dict(required=True), + _ca_enabled=dict(required=False, type='bool'), + _kra_enabled=dict(required=False, type='bool'), + _kra_host_name=dict(required=False), + _top_dir=dict(required=True), + _add_to_ipaservers=dict(required=True, type='bool'), + _ca_subject=dict(required=True), + _subject_base=dict(required=True), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + options = installer + options.dm_password = ansible_module.params.get('dm_password') + options.password = options.dm_password + options.admin_password = ansible_module.params.get('password') + options.ip_addresses = ansible_module_get_parsed_ip_addresses( + ansible_module) + options.domain_name = ansible_module.params.get('domain') + options.realm_name = ansible_module.params.get('realm') + options.host_name = ansible_module.params.get('hostname') + options.ca_cert_files = ansible_module.params.get('ca_cert_files') + options.no_host_dns = ansible_module.params.get('no_host_dns') + options.pki_config_override = ansible_module.params.get( + 'pki_config_override') + # server + options.setup_adtrust = ansible_module.params.get('setup_adtrust') + options.setup_ca = ansible_module.params.get('setup_ca') + options.setup_kra = ansible_module.params.get('setup_kra') + options.setup_dns = ansible_module.params.get('setup_dns') + # ssl certificate + options.dirsrv_cert_files = ansible_module.params.get('dirsrv_cert_files') + # client + options.force_join = ansible_module.params.get('force_join') + # certificate system + options.external_ca = ansible_module.params.get('external_ca') + options.external_cert_files = ansible_module.params.get( + 'external_cert_files') + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + options.ca_subject = ansible_module.params.get('ca_subject') + # dns + options.reverse_zones = ansible_module.params.get('reverse_zones') + options.no_reverse = ansible_module.params.get('no_reverse') + options.auto_reverse = ansible_module.params.get('auto_reverse') + options.forwarders = ansible_module.params.get('forwarders') + options.no_forwarders = ansible_module.params.get('no_forwarders') + options.auto_forwarders = ansible_module.params.get('auto_forwarders') + options.forward_policy = ansible_module.params.get('forward_policy') + # additional + options.server = ansible_module.params.get('server') + master_host_name = ansible_module.params.get('config_master_host_name') + ccache = ansible_module.params.get('ccache') + # os.environ['KRB5CCNAME'] = ccache + os.environ['KRB5CCNAME'] = ansible_module.params.get('installer_ccache') + installer._ccache = ansible_module.params.get('installer_ccache') + ca_enabled = ansible_module.params.get('_ca_enabled') + kra_enabled = ansible_module.params.get('_kra_enabled') + kra_host_name = ansible_module.params.get('_kra_host_name') + + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + options._top_dir = ansible_module.params.get('_top_dir') + options._add_to_ipaservers = ansible_module.params.get( + '_add_to_ipaservers') + + options._ca_subject = ansible_module.params.get('_ca_subject') + options._subject_base = ansible_module.params.get('_subject_base') + + # init # + + ansible_log.debug("== INSTALL ==") + + options = installer + + env = gen_env_boostrap_finalize_core(paths.ETC_IPA, + constants.DEFAULT_CONFIG) + api_bootstrap_finalize(env) + config = gen_ReplicaConfig() + config.subject_base = options.subject_base + config.promote = installer.promote + config.kra_enabled = kra_enabled + config.kra_host_name = kra_host_name + + remote_api = gen_remote_api(master_host_name, paths.ETC_IPA) + installer._remote_api = remote_api + + # ccache = os.environ['KRB5CCNAME'] + + with redirect_stdout(ansible_log): + ansible_log.debug("-- INSTALL KRA --") + + if not hasattr(custodiainstance, "get_custodia_instance"): + kra.install(api, config, options) + else: + if ca_enabled: + mode = custodiainstance.CustodiaModes.CA_PEER + else: + mode = custodiainstance.CustodiaModes.MASTER_PEER + custodia = custodiainstance.get_custodia_instance(config, mode) + + kra.install(api, config, options, custodia=custodia) + + # done # + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_setup_krb.py b/roles/ipareplica/library/ipareplica_setup_krb.py new file mode 100644 index 0000000..2d0180e --- /dev/null +++ b/roles/ipareplica/library/ipareplica_setup_krb.py @@ -0,0 +1,182 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_setup_krb +short description: Setup KRB +description: + Setup KRB +options: + setup_ca: + description: Configure a dogtag CA + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + no_pkinit: + description: Disable pkinit setup steps + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: no + config_master_host_name: + description: The config master_host_name setting + required: no + ccache: + description: The local ccache + required: no + _pkinit_pkcs12_info: + description: The installer _pkinit_pkcs12_info setting + required: yes + _top_dir: + description: The installer _top_dir setting + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os +import inspect + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, installer, DN, paths, sysrestore, + gen_env_boostrap_finalize_core, constants, api_bootstrap_finalize, + gen_ReplicaConfig, api, redirect_stdout, install_krb +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # server + setup_ca=dict(required=False, type='bool'), + setup_kra=dict(required=False, type='bool'), + no_pkinit=dict(required=False, type='bool'), + # certificate system + subject_base=dict(required=True), + # additional + config_master_host_name=dict(required=True), + ccache=dict(required=True), + _pkinit_pkcs12_info=dict(required=False), + _top_dir=dict(required=True), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + options = installer + # server + options.setup_ca = ansible_module.params.get('setup_ca') + options.setup_kra = ansible_module.params.get('setup_kra') + options.no_pkinit = ansible_module.params.get('no_pkinit') + # certificate system + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + # additional + config_master_host_name = ansible_module.params.get( + 'config_master_host_name') + ccache = ansible_module.params.get('ccache') + os.environ['KRB5CCNAME'] = ccache + installer._pkinit_pkcs12_info = ansible_module.params.get( + '_pkinit_pkcs12_info') + + options._top_dir = ansible_module.params.get('_top_dir') + + # init # + + fstore = sysrestore.FileStore(paths.SYSRESTORE) + + ansible_log.debug("== INSTALL ==") + + options = installer + promote = installer.promote + pkinit_pkcs12_info = installer._pkinit_pkcs12_info + + env = gen_env_boostrap_finalize_core(paths.ETC_IPA, + constants.DEFAULT_CONFIG) + api_bootstrap_finalize(env) + config = gen_ReplicaConfig() + config.master_host_name = config_master_host_name + config.subject_base = options.subject_base + + ccache = os.environ['KRB5CCNAME'] + + # There is a api.Backend.ldap2.connect call somewhere in ca, ds, dns or + # ntpinstance + api.Backend.ldap2.connect() + + ansible_log.debug("-- INSTALL_KRB --") + + with redirect_stdout(ansible_log): + argspec = inspect.getargspec(install_krb) + if "promote" in argspec.args: + krb = install_krb( + config, + setup_pkinit=not options.no_pkinit, + pkcs12_info=pkinit_pkcs12_info, + promote=promote) + else: + if "fstore" not in argspec.args: + krb = install_krb( + config, + setup_pkinit=not options.no_pkinit, + pkcs12_info=pkinit_pkcs12_info) + else: + krb = install_krb( + config, + setup_pkinit=not options.no_pkinit, + pkcs12_info=pkinit_pkcs12_info, + fstore=fstore) + + # done # + + ansible_module.exit_json(changed=True, + config_master_host_name=config.master_host_name) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_setup_otpd.py b/roles/ipareplica/library/ipareplica_setup_otpd.py new file mode 100644 index 0000000..4c5092d --- /dev/null +++ b/roles/ipareplica/library/ipareplica_setup_otpd.py @@ -0,0 +1,171 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_setup_otpd +short description: Setup OTPD +description: + Setup OTPD +options: + setup_ca: + description: Configure a dogtag CA + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + no_pkinit: + description: Disable pkinit setup steps + required: yes + no_ui_redirect: + description: Do not automatically redirect to the Web UI + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: no + config_master_host_name: + description: The config master_host_name setting + required: no + ccache: + description: The local ccache + required: no + _ca_file: + description: The installer _ca_file setting + required: yes + _top_dir: + description: The installer _top_dir setting + required: no + dirman_password: + description: Directory Manager (master) password + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, installer, DN, paths, + gen_env_boostrap_finalize_core, constants, api_bootstrap_finalize, + gen_ReplicaConfig, gen_remote_api, api, redirect_stdout, otpdinstance, + ipautil +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # server + setup_ca=dict(required=False, type='bool'), + setup_kra=dict(required=False, type='bool'), + no_pkinit=dict(required=False, type='bool'), + no_ui_redirect=dict(required=False, type='bool'), + # certificate system + subject_base=dict(required=True), + # additional + config_master_host_name=dict(required=True), + ccache=dict(required=True), + _ca_file=dict(required=False), + _top_dir=dict(required=True), + dirman_password=dict(required=True, no_log=True), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + options = installer + options.setup_ca = ansible_module.params.get('setup_ca') + options.setup_kra = ansible_module.params.get('setup_kra') + options.no_pkinit = ansible_module.params.get('no_pkinit') + # certificate system + options.subject_base = ansible_module.params.get('subject_base') + if options.subject_base is not None: + options.subject_base = DN(options.subject_base) + # additional + master_host_name = ansible_module.params.get('config_master_host_name') + ccache = ansible_module.params.get('ccache') + os.environ['KRB5CCNAME'] = ccache + # os.environ['KRB5CCNAME'] = ansible_module.params.get('installer_ccache') + # installer._ccache = ansible_module.params.get('installer_ccache') + options._top_dir = ansible_module.params.get('_top_dir') + dirman_password = ansible_module.params.get('dirman_password') + + # init # + + ansible_log.debug("== INSTALL ==") + + options = installer + + env = gen_env_boostrap_finalize_core(paths.ETC_IPA, + constants.DEFAULT_CONFIG) + api_bootstrap_finalize(env) + config = gen_ReplicaConfig() + config.dirman_password = dirman_password + + remote_api = gen_remote_api(master_host_name, paths.ETC_IPA) + + conn = remote_api.Backend.ldap2 + ccache = os.environ['KRB5CCNAME'] + + # There is a api.Backend.ldap2.connect call somewhere in ca, ds, dns or + # ntpinstance + api.Backend.ldap2.connect() + conn.connect(ccache=ccache) + + with redirect_stdout(ansible_log): + ansible_log.debug("-- INSTALL_OTPD --") + + otpd = otpdinstance.OtpdInstance() + otpd.set_output(ansible_log) + otpd.create_instance('OTPD', config.host_name, + ipautil.realm_to_suffix(config.realm_name)) + + # done # + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/library/ipareplica_test.py b/roles/ipareplica/library/ipareplica_test.py new file mode 100644 index 0000000..ec33cae --- /dev/null +++ b/roles/ipareplica/library/ipareplica_test.py @@ -0,0 +1,455 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipareplica_test +short description: IPA replica deployment tests +description: IPA replica deployment tests +options: + ip_addresses: + description: List of Master Server IP Addresses + required: yes + domain: + description: Primary DNS domain of the IPA deployment + required: yes + servers: + description: Fully qualified name of IPA servers to enroll to + required: yes + realm: + description: Kerberos realm name of the IPA deployment + required: yes + hostname: + description: Fully qualified name of this host + required: yes + ca_cert_files: + description: + List of files containing CA certificates for the service certificate + files + required: yes + hidden_replica: + description: Install a hidden replica + required: yes + setup_adtrust: + description: Configure AD trust capability + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + setup_dns: + description: Configure bind with our zone + required: yes + no_pkinit: + description: Disable pkinit setup steps + required: yes + dirsrv_config_file: + description: + The path to LDIF file that will be used to modify configuration of + dse.ldif during installation of the directory server instance + required: yes + dirsrv_cert_files: + description: + Files containing the Directory Server SSL certificate and private key + required: yes + http_cert_files: + description: + File containing the Apache Server SSL certificate and private key + required: yes + pkinit_cert_files: + description: + File containing the Kerberos KDC SSL certificate and private key + required: yes + no_ntp: + description: Do not configure ntp + required: yes + ntp_servers: + description: ntp servers to use + required: yes + ntp_pool: + description: ntp server pool to use + required: yes + no_reverse: + description: Do not create new reverse DNS zone + required: yes + auto_reverse: + description: Create necessary reverse zones + required: yes + forwarders: + description: Add DNS forwarders + required: yes + no_forwarders: + description: Do not add any DNS forwarders, use root servers instead + required: yes + auto_forwarders: + description: Use DNS forwarders configured in /etc/resolv.conf + required: yes + forward_policy: + description: DNS forwarding policy for global forwarders + required: yes + no_dnssec_validation: + description: Disable DNSSEC validation + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os +import inspect + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_replica import ( + AnsibleModuleLog, options, installer, paths, sysrestore, + ansible_module_get_parsed_ip_addresses, service, + redirect_stdout, create_ipa_conf, ipautil, + x509, validate_domain_name, common_check, + IPA_PYTHON_VERSION +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # basic + # dm_password=dict(required=False, no_log=True), + # password=dict(required=False, no_log=True), + ip_addresses=dict(required=False, type='list', default=[]), + domain=dict(required=False), + servers=dict(required=False, type='list', default=[]), + realm=dict(required=False), + hostname=dict(required=False), + ca_cert_files=dict(required=False, type='list', default=[]), + hidden_replica=dict(required=False, type='bool', default=False), + # server + setup_adtrust=dict(required=False, type='bool', default=False), + setup_kra=dict(required=False, type='bool', default=False), + setup_dns=dict(required=False, type='bool', default=False), + no_pkinit=dict(required=False, type='bool', default=False), + dirsrv_config_file=dict(required=False), + # ssl certificate + dirsrv_cert_files=dict(required=False, type='list', default=[]), + http_cert_files=dict(required=False, type='list', default=[]), + pkinit_cert_files=dict(required=False, type='list', default=[]), + # client + no_ntp=dict(required=False, type='bool', default=False), + ntp_servers=dict(required=False, type='list', default=[]), + ntp_pool=dict(required=False), + # dns + no_reverse=dict(required=False, type='bool', default=False), + auto_reverse=dict(required=False, type='bool', default=False), + forwarders=dict(required=False, type='list', default=[]), + no_forwarders=dict(required=False, type='bool', default=False), + auto_forwarders=dict(required=False, type='bool', default=False), + forward_policy=dict(default=None, choices=['first', 'only']), + no_dnssec_validation=dict(required=False, type='bool', + default=False), + ), + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # get parameters # + + # basic + # options.dm_password = ansible_module.params.get('dm_password') + # # options.password = ansible_module.params.get('password') + # options.password = options.dm_password + options.ip_addresses = ansible_module_get_parsed_ip_addresses( + ansible_module) + options.domain_name = ansible_module.params.get('domain') + options.servers = ansible_module.params.get('servers') + options.realm_name = ansible_module.params.get('realm') + options.host_name = ansible_module.params.get('hostname') + options.ca_cert_files = ansible_module.params.get('ca_cert_files') + options.hidden_replica = ansible_module.params.get('hidden_replica') + # server + options.setup_adtrust = ansible_module.params.get('setup_adtrust') + options.setup_kra = ansible_module.params.get('setup_kra') + options.setup_dns = ansible_module.params.get('setup_dns') + options.no_pkinit = ansible_module.params.get('no_pkinit') + options.dirsrv_config_file = ansible_module.params.get( + 'dirsrv_config_file') + # ssl certificate + options.dirsrv_cert_files = ansible_module.params.get('dirsrv_cert_files') + options.http_cert_files = ansible_module.params.get('http_cert_files') + options.pkinit_cert_files = ansible_module.params.get('pkinit_cert_files') + # client + options.no_ntp = ansible_module.params.get('no_ntp') + options.ntp_servers = ansible_module.params.get('ntp_servers') + options.ntp_pool = ansible_module.params.get('ntp_pool') + # dns + options.no_reverse = ansible_module.params.get('no_reverse') + options.auto_reverse = ansible_module.params.get('auto_reverse') + options.forwarders = ansible_module.params.get('forwarders') + options.no_forwarders = ansible_module.params.get('no_forwarders') + options.auto_forwarders = ansible_module.params.get('auto_forwarders') + options.forward_policy = ansible_module.params.get('forward_policy') + options.no_dnssec_validation = ansible_module.params.get( + 'no_dnssec_validation') + + ########################################################################## + # replica init ########################################################### + ########################################################################## + + if installer.servers: + installer.server = installer.servers[0] + else: + installer.server = None + # TODO: Kills ipa-client-install + # if installer.replica_file is None: + # installer.password = installer.admin_password + # else: + # installer.password = installer.dm_password + + # installer._ccache = os.environ.get('KRB5CCNAME') + + # If not defined, set domain from server name + if installer.domain_name is None and installer.server is not None: + installer.domain_name = installer.server[installer.server.find(".")+1:] + # If not defined, set realm from domain name + if installer.realm_name is None and installer.domain_name is not None: + installer.realm_name = installer.domain_name.upper() + + ########################################################################## + # other checks ########################################################### + ########################################################################## + + # version specific tests # + + # if options.setup_adtrust and not adtrust_imported: + # # if "adtrust" not in options._allow_missing: + # ansible_module.fail_json(msg="adtrust can not be imported") + # # else: + # # options.setup_adtrust = False + # # ansible_module.warn(msg="adtrust is not supported, disabling") + + # if options.setup_kra and not kra_imported: + # # if "kra" not in options._allow_missing: + # ansible_module.fail_json(msg="kra can not be imported") + # # else: + # # options.setup_kra = False + # # ansible_module.warn(msg="kra is not supported, disabling") + + if options.hidden_replica and not hasattr(service, "hide_services"): + ansible_module.fail_json( + msg="Hidden replica is not supported in this version.") + + # We need to point to the master in ipa default conf when certmonger + # asks for HTTP certificate in newer ipa versions. In these versions + # create_ipa_conf has the additional master argument. + change_master_for_certmonger = False + argspec = inspect.getargspec(create_ipa_conf) + if "master" in argspec.args: + change_master_for_certmonger = True + + # From ipa installer classes + + # pkinit is not supported on DL0, don't allow related options + if installer.replica_file is not None: + ansible_module.fail_json( + msg="Replica installation using a replica file is not supported") + + # If any of the key file options are selected, all are required. + cert_file_req = (installer.dirsrv_cert_files, installer.http_cert_files) + cert_file_opt = (installer.pkinit_cert_files,) + if not installer.no_pkinit: + cert_file_req += cert_file_opt + if installer.no_pkinit and installer.pkinit_cert_files: + ansible_module.fail_json( + msg="--no-pkinit and --pkinit-cert-file cannot be specified " + "together") + if any(cert_file_req + cert_file_opt) and not all(cert_file_req): + ansible_module.fail_json( + msg="--dirsrv-cert-file, --http-cert-file, and --pkinit-cert-file " + "or --no-pkinit are required if any key file options are used.") + + if not installer.setup_dns: + if installer.forwarders: + ansible_module.fail_json( + msg="You cannot specify a --forwarder option without the " + "--setup-dns option") + if installer.auto_forwarders: + ansible_module.fail_json( + msg="You cannot specify a --auto-forwarders option without " + "the --setup-dns option") + if installer.no_forwarders: + ansible_module.fail_json( + msg="You cannot specify a --no-forwarders option without the " + "--setup-dns option") + if installer.forward_policy: + ansible_module.fail_json( + msg="You cannot specify a --forward-policy option without the " + "--setup-dns option") + if installer.reverse_zones: + ansible_module.fail_json( + msg="You cannot specify a --reverse-zone option without the " + "--setup-dns option") + if installer.auto_reverse: + ansible_module.fail_json( + msg="You cannot specify a --auto-reverse option without the " + "--setup-dns option") + if installer.no_reverse: + ansible_module.fail_json( + msg="You cannot specify a --no-reverse option without the " + "--setup-dns option") + if installer.no_dnssec_validation: + ansible_module.fail_json( + msg="You cannot specify a --no-dnssec-validation option " + "without the --setup-dns option") + elif installer.forwarders and installer.no_forwarders: + ansible_module.fail_json( + msg="You cannot specify a --forwarder option together with " + "--no-forwarders") + elif installer.auto_forwarders and installer.no_forwarders: + ansible_module.fail_json( + msg="You cannot specify a --auto-forwarders option together with " + "--no-forwarders") + elif installer.reverse_zones and installer.no_reverse: + ansible_module.fail_json( + msg="You cannot specify a --reverse-zone option together with " + "--no-reverse") + elif installer.auto_reverse and installer.no_reverse: + ansible_module.fail_json( + msg="You cannot specify a --auto-reverse option together with " + "--no-reverse") + + # replica installers + if installer.servers and not installer.domain_name: + ansible_module.fail_json( + msg="The --server option cannot be used without providing " + "domain via the --domain option") + + if installer.setup_dns: + if (not installer.forwarders and + not installer.no_forwarders and + not installer.auto_forwarders): + ansible_module.fail_json( + msg="You must specify at least one of --forwarder, " + "--auto-forwarders, or --no-forwarders options") + + if installer.dirsrv_config_file is not None and \ + not os.path.exists(installer.dirsrv_config_file): + ansible_module.fail_json( + msg="File %s does not exist." % installer.dirsrv_config_file) + + if installer.ca_cert_files is not None: + if not isinstance(installer.ca_cert_files, list): + ansible_module.fail_json( + msg="Expected list, got {!r}".format(installer.ca_cert_files)) + for cert in installer.ca_cert_files: + if not os.path.exists(cert): + ansible_module.fail_json(msg="'%s' does not exist" % cert) + if not os.path.isfile(cert): + ansible_module.fail_json(msg="'%s' is not a file" % cert) + if not os.path.isabs(cert): + ansible_module.fail_json( + msg="'%s' is not an absolute file path" % cert) + + try: + x509.load_certificate_from_file(cert) + except Exception: + ansible_module.fail_json( + msg="'%s' is not a valid certificate file" % cert) + + if installer.ip_addresses is not None: + for value in installer.ip_addresses: + try: + ipautil.CheckedIPAddress(value) + except Exception as e: + ansible_module.fail_json( + msg="invalid IP address {0}: {1}".format( + value, e)) + + if installer.domain_name is not None: + validate_domain_name(installer.domain_name) + + ########################################################################## + # replica promote_check excerpts ######################################### + ########################################################################## + + # check selinux status, http and DS ports, NTP conflicting services + try: + with redirect_stdout(ansible_log): + common_check(options.no_ntp) + except Exception as msg: # ScriptError as msg: + _msg = str(msg) + if "server is already configured" in _msg: + ansible_module.exit_json(changed=False, + server_already_configured=True) + else: + ansible_module.fail_json(msg=_msg) + + # TODO: Check ntp_servers and ntp_pool + + # client enrolled? + + client_fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) + client_enrolled = client_fstore.has_files() + + if not client_enrolled: + # # One-step replica installation + # if options.dm_password and options.password: + # ansible_module.fail_json( + # msg="--password and --admin-password options are " + # "mutually exclusive") + pass + else: + # The NTP configuration can not be touched on pre-installed client: + if options.no_ntp or options.ntp_servers or options.ntp_pool: + ansible_module.fail_json( + msg="NTP configuration cannot be updated during promotion") + + # done # + + ansible_module.exit_json( + changed=False, + ipa_python_version=IPA_PYTHON_VERSION, + # basic + domain=options.domain_name, + realm=options.realm_name, + hostname=options.host_name, + # server + setup_adtrust=options.setup_adtrust, + setup_kra=options.setup_kra, + server=options.server, + # additional + client_enrolled=client_enrolled, + change_master_for_certmonger=change_master_for_certmonger, + ) + + +if __name__ == '__main__': + main() diff --git a/roles/ipareplica/meta/main.yml b/roles/ipareplica/meta/main.yml new file mode 100644 index 0000000..d796482 --- /dev/null +++ b/roles/ipareplica/meta/main.yml @@ -0,0 +1,20 @@ +dependencies: [] + +galaxy_info: + author: Thomas Woerner + description: A role to setup an IPA domain replica + company: Red Hat, Inc + license: GPLv3 + min_ansible_version: 2.8 + platforms: + - name: Fedora + versions: + - all + - name: EL + versions: + - 7 + - 8 + galaxy_tags: + - identity + - ipa + - freeipa diff --git a/roles/ipareplica/module_utils/ansible_ipa_replica.py b/roles/ipareplica/module_utils/ansible_ipa_replica.py new file mode 100644 index 0000000..ae05e45 --- /dev/null +++ b/roles/ipareplica/module_utils/ansible_ipa_replica.py @@ -0,0 +1,417 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-replica-install code +# +# Copyright (C) 2018 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +__all__ = ["contextlib", "dnsexception", "dnsresolver", "dnsreversename", + "parse_version", "IPAChangeConf", + "certstore", "sysrestore", "ipa_generate_password", "kinit_keytab", + "IPA_CA_TRUST_FLAGS", "EXTERNAL_CA_TRUST_FLAGS", "DN", + "ScriptError", "services", "tasks", "constants", "errors", "rpc", + "x509", "validate_domain_name", + "no_matching_interface_for_ip_address_warning", + "configure_krb5_conf", "purge_host_keytab", "adtrust", + "bindinstance", "ca", "certs", "dns", "httpinstance", "kra", + "otpdinstance", "custodiainstance", "service", "upgradeinstance", + "find_providing_servers", "find_providing_server", "load_pkcs12", + "is_ipa_configured", "ReplicationManager", "replica_conn_check", + "install_replica_ds", "install_krb", "install_ca_cert", + "install_http", "install_dns_records", "create_ipa_conf", + "check_dirsrv", "check_dns_resolution", "configure_certmonger", + "remove_replica_info_dir", "preserve_enrollment_state", + "uninstall_client", "promote_sssd", "promote_openldap_conf", + "rpc_client", "check_remote_fips_mode", "check_remote_version", + "common_check", "current_domain_level", + "check_domain_level_is_supported", "promotion_check_ipa_domain", + "SSSDConfig", "CalledProcessError", "timeconf", "ntpinstance", + "dnsname", "kernel_keyring", "krbinstance"] + +import sys +import logging +from contextlib import contextmanager as contextlib_contextmanager + + +from ipapython.version import NUM_VERSION, VERSION + +if NUM_VERSION < 30201: + # See ipapython/version.py + IPA_MAJOR, IPA_MINOR, IPA_RELEASE = [int(x) for x in VERSION.split(".", 2)] + IPA_PYTHON_VERSION = IPA_MAJOR*10000 + IPA_MINOR*100 + IPA_RELEASE +else: + IPA_PYTHON_VERSION = NUM_VERSION + + +if NUM_VERSION >= 40600: + # IPA version >= 4.6 + + import contextlib + + import dns.exception as dnsexception + import dns.name as dnsname + import dns.resolver as dnsresolver + import dns.reversename as dnsreversename + + from pkg_resources import parse_version + + from ipaclient.install.ipachangeconf import IPAChangeConf + from ipalib.install import certstore, sysrestore + from ipapython.ipautil import ipa_generate_password + from ipalib.install.kinit import kinit_keytab + from ipapython import ipaldap, ipautil, kernel_keyring + from ipapython.certdb import IPA_CA_TRUST_FLAGS, EXTERNAL_CA_TRUST_FLAGS + from ipapython.dn import DN + from ipapython.admintool import ScriptError + from ipapython.ipa_log_manager import standard_logging_setup + from ipaplatform import services + from ipaplatform.tasks import tasks + from ipaplatform.paths import paths + from ipalib import api, constants, create_api, errors, rpc, x509 + from ipalib.config import Env + from ipalib.util import ( + validate_domain_name, + no_matching_interface_for_ip_address_warning) + from ipaclient.install.client import configure_krb5_conf, purge_host_keytab + from ipaserver.install import ( + adtrust, bindinstance, ca, certs, dns, dsinstance, httpinstance, + installutils, kra, krbinstance, + otpdinstance, custodiainstance, service, upgradeinstance) + try: + from ipaserver.masters import ( + find_providing_servers, find_providing_server) + except ImportError: + from ipaserver.install.service import ( + find_providing_servers, find_providing_server) + from ipaserver.install.installutils import ( + ReplicaConfig, load_pkcs12, is_ipa_configured) + from ipaserver.install.replication import ( + ReplicationManager, replica_conn_check) + from ipaserver.install.server.replicainstall import ( + make_pkcs12_info, install_replica_ds, install_krb, install_ca_cert, + install_http, install_dns_records, create_ipa_conf, check_dirsrv, + check_dns_resolution, configure_certmonger, remove_replica_info_dir, + # common_cleanup, + preserve_enrollment_state, uninstall_client, + promote_sssd, promote_openldap_conf, rpc_client, + check_remote_fips_mode, check_remote_version, common_check, + current_domain_level, check_domain_level_is_supported, + # enroll_dl0_replica, + # ensure_enrolled, + promotion_check_ipa_domain + ) + import SSSDConfig + from subprocess import CalledProcessError + + try: + from ipaclient.install import timeconf + time_service = "chronyd" + ntpinstance = None + except ImportError: + try: + from ipaclient.install import ntpconf as timeconf + except ImportError: + from ipaclient import ntpconf as timeconf + from ipaserver.install import ntpinstance + time_service = "ntpd" + + +else: + # IPA version < 4.6 + + raise Exception("freeipa version '%s' is too old" % VERSION) + + +logger = logging.getLogger("ipa-server-install") +# logger.setLevel(logging.DEBUG) +standard_logging_setup( + paths.IPAREPLICA_INSTALL_LOG, verbose=False, debug=False, + filemode='a', console_format='%(message)s') + + +@contextlib_contextmanager +def redirect_stdout(f): + sys.stdout = f + try: + yield f + finally: + sys.stdout = sys.__stdout__ + + +class AnsibleModuleLog(): + def __init__(self, module): + self.module = module + _ansible_module_log = self + + class AnsibleLoggingHandler(logging.Handler): + def emit(self, record): + _ansible_module_log.write(self.format(record)) + + self.logging_handler = AnsibleLoggingHandler() + logger.setLevel(logging.DEBUG) + logger.root.addHandler(self.logging_handler) + + def close(self): + self.flush() + + def flush(self): + pass + + def log(self, msg): + # self.write(msg+"\n") + self.write(msg) + + def debug(self, msg): + self.module.debug(msg) + + def info(self, msg): + self.module.debug(msg) + + def write(self, msg): + self.module.debug(msg) + # self.module.warn(msg) + + +class installer_obj(object): + def __init__(self): + # CompatServerReplicaInstall + self.ca_cert_files = None + self.all_ip_addresses = False + self.no_wait_for_dns = True + self.nisdomain = None + self.no_nisdomain = False + self.no_sudo = False + self.request_cert = False + self.ca_file = None + self.zonemgr = None + self.replica_file = None + # ServerReplicaInstall + self.subject_base = None + self.ca_subject = None + # others + self._ccache = None + self.password = None + self.reverse_zones = [] + # def _is_promote(self): + # return self.replica_file is None + # self.skip_conncheck = False + self._replica_install = False + # self.dnssec_master = False # future unknown + # self.disable_dnssec_master = False # future unknown + # self.domainlevel = MAX_DOMAIN_LEVEL # deprecated + # self.domain_level = self.domainlevel # deprecated + self.interactive = False + self.unattended = not self.interactive + # self.promote = self.replica_file is None + self.promote = True + self.skip_schema_check = None + + # def __getattribute__(self, attr): + # value = super(installer_obj, self).__getattribute__(attr) + # if not attr.startswith("--") and not attr.endswith("--"): + # logger.debug( + # " <-- Accessing installer.%s (%s)" % (attr, repr(value))) + # return value + + def __getattr__(self, attr): + logger.info(" --> ADDING missing installer.%s", attr) + setattr(self, attr, None) + return getattr(self, attr) + + # def __setattr__(self, attr, value): + # logger.debug(" --> Setting installer.%s to %s" % (attr, repr(value))) + # return super(installer_obj, self).__setattr__(attr, value) + + def knobs(self): + for name in self.__dict__: + yield self, name + + +installer = installer_obj() +options = installer + +# DNSInstallInterface +options.dnssec_master = False +options.disable_dnssec_master = False +options.kasp_db_file = None +options.force = False + +# ServerMasterInstall +options.add_sids = False +options.add_agents = False + +# ServerReplicaInstall +options.subject_base = None +options.ca_subject = None + + +def gen_env_boostrap_finalize_core(etc_ipa, default_config): + env = Env() + # env._bootstrap(context='installer', confdir=paths.ETC_IPA, log=None) + # env._finalize_core(**dict(constants.DEFAULT_CONFIG)) + env._bootstrap(context='installer', confdir=etc_ipa, log=None) + env._finalize_core(**dict(default_config)) + return env + + +def api_bootstrap_finalize(env): + # pylint: disable=no-member + xmlrpc_uri = 'https://{}/ipa/xml'.format(ipautil.format_netloc(env.host)) + api.bootstrap(in_server=True, + context='installer', + confdir=paths.ETC_IPA, + ldap_uri=installutils.realm_to_ldapi_uri(env.realm), + xmlrpc_uri=xmlrpc_uri) + # pylint: enable=no-member + api.finalize() + + +def gen_ReplicaConfig(): + class ExtendedReplicaConfig(ReplicaConfig): + def __init__(self, top_dir=None): + super(ExtendedReplicaConfig, self).__init__(top_dir) + + # def __getattribute__(self, attr): + # value = super(ExtendedReplicaConfig, self).__getattribute__(attr) + # if attr not in ["__dict__", "knobs"]: + # logger.debug(" <== Accessing config.%s (%s)" % + # (attr, repr(value))) + # return value + + def __getattr__(self, attr): + logger.info(" ==> ADDING missing config.%s", attr) + setattr(self, attr, None) + return getattr(self, attr) + + # def __setattr__(self, attr, value): + # logger.debug(" ==> Setting config.%s to %s" % (attr, repr(value))) + # return super(ExtendedReplicaConfig, self).__setattr__(attr, value) + + def knobs(self): + for name in self.__dict__: + yield self, name + + # config = ReplicaConfig() + config = ExtendedReplicaConfig() + config.realm_name = api.env.realm + config.host_name = api.env.host + config.domain_name = api.env.domain + config.master_host_name = api.env.server + config.ca_host_name = api.env.ca_host + config.kra_host_name = config.ca_host_name + config.ca_ds_port = 389 + config.setup_ca = options.setup_ca + config.setup_kra = options.setup_kra + config.dir = options._top_dir + config.basedn = api.env.basedn + # config.subject_base = options.subject_base + + return config + + +def replica_ds_init_info(ansible_log, + config, options, ca_is_configured, remote_api, + ds_ca_subject, ca_file, + promote=False, pkcs12_info=None): + + dsinstance.check_ports() + + # if we have a pkcs12 file, create the cert db from + # that. Otherwise the ds setup will create the CA + # cert + if pkcs12_info is None: + pkcs12_info = make_pkcs12_info(config.dir, "dscert.p12", + "dirsrv_pin.txt") + + # during replica install, this gets invoked before local DS is + # available, so use the remote api. + # if ca_is_configured: + # ca_subject = ca.lookup_ca_subject(_api, config.subject_base) + # else: + # ca_subject = installutils.default_ca_subject_dn(config.subject_base) + ca_subject = ds_ca_subject + + ds = dsinstance.DsInstance( + config_ldif=options.dirsrv_config_file) + ds.set_output(ansible_log) + + # Source: ipaserver/install/dsinstance.py + + # idstart and idmax are configured so that the range is seen as + # depleted by the DNA plugin and the replica will go and get a + # new range from the master. + # This way all servers use the initially defined range by default. + idstart = 1101 + idmax = 1100 + + with redirect_stdout(ansible_log): + ds.init_info( + realm_name=config.realm_name, + fqdn=config.host_name, + domain_name=config.domain_name, + dm_password=config.dirman_password, + subject_base=config.subject_base, + ca_subject=ca_subject, + idstart=idstart, + idmax=idmax, + pkcs12_info=pkcs12_info, + ca_file=ca_file, + setup_pkinit=not options.no_pkinit, + ) + ds.master_fqdn = config.master_host_name + if ca_is_configured is not None: + ds.ca_is_configured = ca_is_configured + ds.promote = promote + ds.api = remote_api + + # from __setup_replica + + # Always connect to ds over ldapi + ldap_uri = ipaldap.get_ldap_uri(protocol='ldapi', realm=ds.realm) + conn = ipaldap.LDAPClient(ldap_uri) + conn.external_bind() + + return ds + + +def ansible_module_get_parsed_ip_addresses(ansible_module, + param='ip_addresses'): + ip_addrs = [] + for ip in ansible_module.params.get(param): + try: + ip_parsed = ipautil.CheckedIPAddress(ip) + except Exception as e: + ansible_module.fail_json(msg="Invalid IP Address %s: %s" % (ip, e)) + ip_addrs.append(ip_parsed) + return ip_addrs + + +def gen_remote_api(master_host_name, etc_ipa): + ldapuri = 'ldaps://%s' % ipautil.format_netloc(master_host_name) + xmlrpc_uri = 'https://{}/ipa/xml'.format( + ipautil.format_netloc(master_host_name)) + remote_api = create_api(mode=None) + remote_api.bootstrap(in_server=True, + context='installer', + confdir=etc_ipa, + ldap_uri=ldapuri, + xmlrpc_uri=xmlrpc_uri) + remote_api.finalize() + return remote_api diff --git a/roles/ipareplica/tasks/install.yml b/roles/ipareplica/tasks/install.yml new file mode 100644 index 0000000..98681e0 --- /dev/null +++ b/roles/ipareplica/tasks/install.yml @@ -0,0 +1,756 @@ +--- +# tasks file for ipareplica + +- block: + + - name: Install - Ensure IPA replica packages are installed + package: + name: "{{ ipareplica_packages }}" + state: present + + - name: Install - Ensure IPA replica packages for dns are installed + package: + name: "{{ ipareplica_packages_dns }}" + state: present + when: ipareplica_setup_dns | bool + + - name: Install - Ensure IPA replica packages for adtrust are installed + package: + name: "{{ ipareplica_packages_adtrust }}" + state: present + when: ipareplica_setup_adtrust | bool + + - name: Install - Ensure that firewall packages installed + package: + name: "{{ ipareplica_packages_firewalld }}" + state: present + when: ipareplica_setup_firewalld | bool + + - name: Firewalld service - Ensure that firewalld is running + systemd: + name: firewalld + enabled: yes + state: started + when: ipareplica_setup_firewalld | bool + + when: ipareplica_install_packages | bool + +#- name: Install - Include Python2/3 import test +# import_tasks: "{{ role_path }}/tasks/python_2_3_test.yml" + +- name: Install - Set ipareplica_servers + set_fact: + ipareplica_servers: "{{ groups['ipaservers'] | list }}" + when: groups.ipaservers is defined and ipareplica_servers is not defined + +- name: Install - Set default principal if no keytab is given + set_fact: + ipaadmin_principal: admin + when: ipaadmin_principal is undefined and ipaclient_keytab is undefined + +- name: Install - Replica installation test + ipareplica_test: + ### basic ### + # dm_password: "{{ ipadm_password | default(omit) }}" + # password: "{{ ipaadmin_password | default(omit) }}" + ip_addresses: "{{ ipareplica_ip_addresses | default([]) }}" + domain: "{{ ipareplica_domain | default(ipaserver_domain) | + default(omit) }}" + servers: "{{ ipareplica_servers | default(omit) }}" + realm: "{{ ipareplica_realm | default(omit) }}" + hostname: "{{ ipareplica_hostname | default(ansible_fqdn) }}" + ca_cert_files: "{{ ipareplica_ca_cert_files | default([]) }}" + hidden_replica: "{{ ipareplica_hidden_replica }}" + ### server ### + setup_adtrust: "{{ ipareplica_setup_adtrust }}" + setup_kra: "{{ ipareplica_setup_kra }}" + setup_dns: "{{ ipareplica_setup_dns }}" + no_pkinit: "{{ ipareplica_no_pkinit }}" + dirsrv_config_file: "{{ ipareplica_dirsrv_config_file | default(omit) }}" + ### ssl certificate ### + dirsrv_cert_files: "{{ ipareplica_dirsrv_cert_files | default([]) }}" + http_cert_files: "{{ ipareplica_http_cert_files | default([]) }}" + pkinit_cert_files: "{{ ipareplica_pkinit_cert_files | default([]) }}" + ### client ### + no_ntp: "{{ ipaclient_no_ntp }}" + ntp_servers: "{{ ipaclient_ntp_servers | default([]) }}" + ntp_pool: "{{ ipaclient_ntp_pool | default(omit) }}" + ### dns ### + no_reverse: "{{ ipareplica_no_reverse }}" + auto_reverse: "{{ ipareplica_auto_reverse }}" + forwarders: "{{ ipareplica_forwarders | default([]) }}" + no_forwarders: "{{ ipareplica_no_forwarders }}" + auto_forwarders: "{{ ipareplica_auto_forwarders }}" + forward_policy: "{{ ipareplica_forward_policy | default(omit) }}" + no_dnssec_validation: "{{ ipareplica_no_dnssec_validation }}" + register: result_ipareplica_test + +- block: + # This block is executed only when + # not ansible_check_mode and + # not (result_ipareplica_test.client_already_configured is defined or + # result_ipareplica_test.server_already_configured is defined) + + - name: Install - Setup client + include_role: + name: ipaclient + vars: + state: present + ipaclient_domain: "{{ result_ipareplica_test.domain | default(omit) }}" + ipaclient_realm: "{{ result_ipareplica_test.realm | default(omit) }}" + ipaclient_servers: "{{ ipareplica_servers | default(omit) }}" + ipaclient_hostname: "{{ result_ipareplica_test.hostname }}" + ipaclient_no_ntp: "{{ result_ipareplica_test.ipa_python_version + < 40690 }}" + ipaclient_install_packages: "{{ ipareplica_install_packages }}" + when: not result_ipareplica_test.client_enrolled + + - name: Install - Configure firewalld + command: > + firewall-cmd + --permanent + --add-service=freeipa-ldap + --add-service=freeipa-ldaps + {{ "--add-service=freeipa-trust" if result_ipareplica_test.setup_adtrust + else "" }} + {{ "--add-service=dns" if ipareplica_setup_dns | bool else "" }} + {{ "--add-service=ntp" if not ipaclient_no_ntp | bool else "" }} + when: ipareplica_setup_firewalld | bool + + - name: Install - Configure firewalld runtime + command: > + firewall-cmd + --add-service=freeipa-ldap + --add-service=freeipa-ldaps + {{ "--add-service=freeipa-trust" if result_ipareplica_test.setup_adtrust + else "" }} + {{ "--add-service=dns" if ipareplica_setup_dns | bool else "" }} + {{ "--add-service=ntp" if not ipaclient_no_ntp | bool else "" }} + when: ipareplica_setup_firewalld | bool + + - name: Install - Replica preparation + ipareplica_prepare: + ### basic ### + password: "{{ ipaadmin_password | default(omit) }}" + ip_addresses: "{{ ipareplica_ip_addresses | default([]) }}" + domain: "{{ result_ipareplica_test.domain }}" + realm: "{{ result_ipareplica_test.realm }}" + hostname: "{{ result_ipareplica_test.hostname }}" + principal: "{{ ipaadmin_principal | default(omit) }}" + ca_cert_files: "{{ ipareplica_ca_cert_files | default([]) }}" + no_host_dns: "{{ ipareplica_no_host_dns }}" + ### replica ### + setup_adtrust: "{{ result_ipareplica_test.setup_adtrust }}" + setup_ca: "{{ ipareplica_setup_ca }}" + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + setup_dns: "{{ ipareplica_setup_dns }}" + ### ssl certificate ### + dirsrv_cert_files: "{{ ipareplica_dirsrv_cert_files | default([]) }}" + dirsrv_cert_name: "{{ ipareplica_dirsrv_cert_name | default(omit) }}" + dirsrv_pin: "{{ ipareplica_dirsrv_pin | default(omit) }}" + http_cert_files: "{{ ipareplica_http_cert_files | default([]) }}" + http_cert_name: "{{ ipareplica_http_cert_name | default(omit) }}" + http_pin: "{{ ipareplica_http_pin | default(omit) }}" + pkinit_cert_files: "{{ ipareplica_pkinit_cert_files | default([]) }}" + pkinit_cert_name: "{{ ipareplica_pkinit_cert_name | default(omit) }}" + pkinit_pin: "{{ ipareplica_pkinit_pin | default(omit) }}" + ### client ### + keytab: "{{ ipaclient_keytab | default(omit) }}" + mkhomedir: "{{ ipaclient_mkhomedir | default(omit) }}" + force_join: "{{ ipaclient_force_join | default(omit) }}" + no_ntp: "{{ ipaclient_no_ntp | default(omit) }}" + ssh_trust_dns: "{{ ipaclient_ssh_trust_dns | default(omit) }}" + no_ssh: no + no_sshd: no + no_dns_sshfp: no + ### dns ### + allow_zone_overlap: "{{ ipareplica_allow_zone_overlap }}" + reverse_zones: "{{ ipareplica_reverse_zones | default([]) }}" + no_reverse: "{{ ipareplica_no_reverse }}" + auto_reverse: "{{ ipareplica_auto_reverse }}" + forwarders: "{{ ipareplica_forwarders | default([]) }}" + no_forwarders: "{{ ipareplica_no_forwarders }}" + auto_forwarders: "{{ ipareplica_auto_forwarders }}" + forward_policy: "{{ ipareplica_forward_policy | default(omit) }}" + no_dnssec_validation: "{{ ipareplica_no_dnssec_validation }}" + ### ad trust ### + enable_compat: "{{ ipareplica_enable_compat }}" + netbios_name: "{{ ipareplica_netbios_name | default(omit) }}" + rid_base: "{{ ipareplica_rid_base | default(omit) }}" + secondary_rid_base: "{{ ipareplica_secondary_rid_base | default(omit) }}" + ### additional ### + server: "{{ result_ipareplica_test.server }}" + skip_conncheck: "{{ ipareplica_skip_conncheck }}" + register: result_ipareplica_prepare + + - name: Install - Add to ipaservers + ipareplica_add_to_ipaservers: + ### server ### + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + ### additional ### + config_master_host_name: + "{{ result_ipareplica_prepare.config_master_host_name }}" + ccache: "{{ result_ipareplica_prepare.ccache }}" + installer_ccache: "{{ result_ipareplica_prepare.installer_ccache }}" + _top_dir: "{{ result_ipareplica_prepare._top_dir }}" + when: result_ipareplica_prepare._add_to_ipaservers + + - name: Install - Create dirman password + no_log: yes + ipareplica_master_password: + master_password: "{{ ipareplica_master_password | default(omit) }}" + register: result_ipareplica_master_password + + - name: Install - Set dirman password + no_log: yes + set_fact: + ipareplica_dirman_password: + "{{ result_ipareplica_master_password.password }}" + + - name: Install - Setup certmonger + ipareplica_setup_certmonger: + when: result_ipareplica_prepare._ca_enabled + + - name: Install - Install CA certs + ipareplica_install_ca_certs: + ### basic ### + dm_password: "{{ ipadm_password | default(omit) }}" + password: "{{ ipaadmin_password | default(omit) }}" + ip_addresses: "{{ ipareplica_ip_addresses | default([]) }}" + domain: "{{ result_ipareplica_test.domain }}" + realm: "{{ result_ipareplica_test.realm }}" + hostname: "{{ result_ipareplica_test.hostname }}" + ca_cert_files: "{{ ipareplica_ca_cert_files | default([]) }}" + no_host_dns: "{{ ipareplica_no_host_dns }}" + ### replica ### + setup_adtrust: "{{ result_ipareplica_test.setup_adtrust }}" + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + setup_dns: "{{ ipareplica_setup_dns }}" + ### ssl certificate ### + dirsrv_cert_files: "{{ ipareplica_dirsrv_cert_files | default([]) }}" + ### client ### + force_join: "{{ ipaclient_force_join }}" + ### ad trust ### + netbios_name: "{{ ipareplica_netbios_name | default(omit) }}" + rid_base: "{{ ipareplica_rid_base | default(omit) }}" + secondary_rid_base: "{{ ipareplica_secondary_rid_base | default(omit) }}" + ### additional ### + server: "{{ result_ipareplica_test.server }}" + ccache: "{{ result_ipareplica_prepare.ccache }}" + installer_ccache: "{{ result_ipareplica_prepare.installer_ccache }}" + subject_base: "{{ result_ipareplica_prepare.subject_base }}" + _top_dir: "{{ result_ipareplica_prepare._top_dir }}" + _add_to_ipaservers: "{{ result_ipareplica_prepare._add_to_ipaservers }}" + _ca_subject: "{{ result_ipareplica_prepare._ca_subject }}" + _subject_base: "{{ result_ipareplica_prepare._subject_base }}" + dirman_password: "{{ ipareplica_dirman_password }}" + config_setup_ca: "{{ result_ipareplica_prepare.config_setup_ca }}" + config_master_host_name: + "{{ result_ipareplica_prepare.config_master_host_name }}" + config_ca_host_name: "{{ result_ipareplica_prepare.config_ca_host_name }}" + config_ips: "{{ result_ipareplica_prepare.config_ips }}" + register: result_ipareplica_install_ca_certs + + - name: Install - Setup DS + ipareplica_setup_ds: + ### basic ### + dm_password: "{{ ipadm_password | default(omit) }}" + password: "{{ ipaadmin_password | default(omit) }}" + ip_addresses: "{{ ipareplica_ip_addresses | default([]) }}" + domain: "{{ result_ipareplica_test.domain }}" + realm: "{{ result_ipareplica_test.realm }}" + hostname: "{{ result_ipareplica_test.hostname }}" + ca_cert_files: "{{ ipareplica_ca_cert_files | default([]) }}" + no_host_dns: "{{ ipareplica_no_host_dns }}" + ### replica ### + setup_adtrust: "{{ result_ipareplica_test.setup_adtrust }}" + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + setup_dns: "{{ ipareplica_setup_dns }}" + no_pkinit: "{{ ipareplica_no_pkinit }}" + dirsrv_config_file: "{{ ipareplica_dirsrv_config_file | default(omit) }}" + ### ssl certificate ### + dirsrv_cert_files: "{{ ipareplica_dirsrv_cert_files | default([]) }}" + ### client ### + force_join: "{{ ipaclient_force_join }}" + ### ad trust ### + netbios_name: "{{ ipareplica_netbios_name | default(omit) }}" + rid_base: "{{ ipareplica_rid_base | default(omit) }}" + secondary_rid_base: "{{ ipareplica_secondary_rid_base | default(omit) }}" + ### additional ### + server: "{{ result_ipareplica_test.server }}" + ccache: "{{ result_ipareplica_prepare.ccache }}" + installer_ccache: "{{ result_ipareplica_prepare.installer_ccache }}" + _ca_enabled: "{{ result_ipareplica_prepare._ca_enabled }}" + _dirsrv_pkcs12_info: "{{ result_ipareplica_prepare._dirsrv_pkcs12_info }}" + subject_base: "{{ result_ipareplica_prepare.subject_base }}" + _top_dir: "{{ result_ipareplica_prepare._top_dir }}" + _add_to_ipaservers: "{{ result_ipareplica_prepare._add_to_ipaservers }}" + _ca_subject: "{{ result_ipareplica_prepare._ca_subject }}" + _subject_base: "{{ result_ipareplica_prepare._subject_base }}" + dirman_password: "{{ ipareplica_dirman_password }}" + config_setup_ca: "{{ result_ipareplica_prepare.config_setup_ca }}" + config_master_host_name: + "{{ result_ipareplica_install_ca_certs.config_master_host_name }}" + config_ca_host_name: "{{ result_ipareplica_prepare.config_ca_host_name }}" + config_ips: "{{ result_ipareplica_prepare.config_ips }}" + register: result_ipareplica_setup_ds + + - name: Install - Create IPA conf + ipareplica_create_ipa_conf: + ### basic ### + dm_password: "{{ ipadm_password | default(omit) }}" + password: "{{ ipaadmin_password | default(omit) }}" + ip_addresses: "{{ ipareplica_ip_addresses | default([]) }}" + domain: "{{ result_ipareplica_test.domain }}" + realm: "{{ result_ipareplica_test.realm }}" + hostname: "{{ result_ipareplica_test.hostname }}" + ca_cert_files: "{{ ipareplica_ca_cert_files | default([]) }}" + no_host_dns: "{{ ipareplica_no_host_dns }}" + ### replica ### + setup_adtrust: "{{ result_ipareplica_test.setup_adtrust }}" + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + setup_dns: "{{ ipareplica_setup_dns }}" + ### ssl certificate ### + dirsrv_cert_files: "{{ ipareplica_dirsrv_cert_files | default([]) }}" + ### client ### + force_join: "{{ ipaclient_force_join }}" + ### ad trust ### + netbios_name: "{{ ipareplica_netbios_name | default(omit) }}" + rid_base: "{{ ipareplica_rid_base | default(omit) }}" + secondary_rid_base: "{{ ipareplica_secondary_rid_base | default(omit) }}" + ### additional ### + server: "{{ result_ipareplica_test.server }}" + config_master_host_name: + "{{ result_ipareplica_install_ca_certs.config_master_host_name }}" + config_ca_host_name: "{{ result_ipareplica_prepare.config_ca_host_name }}" + ccache: "{{ result_ipareplica_prepare.ccache }}" + installer_ccache: "{{ result_ipareplica_prepare.installer_ccache }}" + _ca_enabled: "{{ result_ipareplica_prepare._ca_enabled }}" + subject_base: "{{ result_ipareplica_prepare.subject_base }}" + _top_dir: "{{ result_ipareplica_prepare._top_dir }}" + _add_to_ipaservers: "{{ result_ipareplica_prepare._add_to_ipaservers }}" + _ca_subject: "{{ result_ipareplica_prepare._ca_subject }}" + _subject_base: "{{ result_ipareplica_prepare._subject_base }}" + dirman_password: "{{ ipareplica_dirman_password }}" + + - name: Install - Setup KRB + ipareplica_setup_krb: + ### server ### + setup_ca: "{{ ipareplica_setup_ca }}" + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + no_pkinit: "{{ ipareplica_no_pkinit }}" + ### certificate system ### + subject_base: "{{ result_ipareplica_prepare.subject_base }}" + ### additional ### + config_master_host_name: + "{{ result_ipareplica_install_ca_certs.config_master_host_name }}" + ccache: "{{ result_ipareplica_prepare.ccache }}" + _pkinit_pkcs12_info: "{{ result_ipareplica_prepare._pkinit_pkcs12_info }}" + _top_dir: "{{ result_ipareplica_prepare._top_dir }}" + + # We need to point to the master in ipa default conf when certmonger + # asks for HTTP certificate in newer ipa versions. In these versions + # create_ipa_conf has the additional master argument. + - name: Install - Create override IPA conf + ipareplica_create_ipa_conf: + ### basic ### + dm_password: "{{ ipadm_password | default(omit) }}" + password: "{{ ipaadmin_password | default(omit) }}" + ip_addresses: "{{ ipareplica_ip_addresses | default([]) }}" + domain: "{{ result_ipareplica_test.domain }}" + realm: "{{ result_ipareplica_test.realm }}" + hostname: "{{ result_ipareplica_test.hostname }}" + ca_cert_files: "{{ ipareplica_ca_cert_files | default([]) }}" + no_host_dns: "{{ ipareplica_no_host_dns }}" + ### replica ### + setup_adtrust: "{{ result_ipareplica_test.setup_adtrust }}" + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + setup_dns: "{{ ipareplica_setup_dns }}" + ### ssl certificate ### + dirsrv_cert_files: "{{ ipareplica_dirsrv_cert_files | default([]) }}" + ### client ### + force_join: "{{ ipaclient_force_join }}" + ### ad trust ### + netbios_name: "{{ ipareplica_netbios_name | default(omit) }}" + rid_base: "{{ ipareplica_rid_base | default(omit) }}" + secondary_rid_base: "{{ ipareplica_secondary_rid_base | default(omit) }}" + ### additional ### + server: "{{ result_ipareplica_test.server }}" + config_master_host_name: + "{{ result_ipareplica_install_ca_certs.config_master_host_name }}" + config_ca_host_name: "{{ result_ipareplica_prepare.config_ca_host_name }}" + ccache: "{{ result_ipareplica_prepare.ccache }}" + installer_ccache: "{{ result_ipareplica_prepare.installer_ccache }}" + _ca_enabled: "{{ result_ipareplica_prepare._ca_enabled }}" + subject_base: "{{ result_ipareplica_prepare.subject_base }}" + _top_dir: "{{ result_ipareplica_prepare._top_dir }}" + _add_to_ipaservers: "{{ result_ipareplica_prepare._add_to_ipaservers }}" + _ca_subject: "{{ result_ipareplica_prepare._ca_subject }}" + _subject_base: "{{ result_ipareplica_prepare._subject_base }}" + dirman_password: "{{ ipareplica_dirman_password }}" + master: + "{{ result_ipareplica_install_ca_certs.config_master_host_name }}" + when: result_ipareplica_test.change_master_for_certmonger + + - name: Install - DS enable SSL + ipareplica_ds_enable_ssl: + ### server ### + setup_ca: "{{ ipareplica_setup_ca }}" + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + no_pkinit: "{{ ipareplica_no_pkinit }}" + dirsrv_config_file: "{{ ipareplica_dirsrv_config_file | default(omit) }}" + ### certificate system ### + subject_base: "{{ result_ipareplica_prepare.subject_base }}" + ### additional ### + config_master_host_name: + "{{ result_ipareplica_install_ca_certs.config_master_host_name }}" + ccache: "{{ result_ipareplica_prepare.ccache }}" + _ca_enabled: "{{ result_ipareplica_prepare._ca_enabled }}" + _ca_file: "{{ result_ipareplica_prepare._ca_file }}" + _pkinit_pkcs12_info: "{{ result_ipareplica_prepare._pkinit_pkcs12_info }}" + _top_dir: "{{ result_ipareplica_prepare._top_dir }}" + dirman_password: "{{ ipareplica_dirman_password }}" + ds_ca_subject: "{{ result_ipareplica_setup_ds.ds_ca_subject }}" + + - name: Install - Setup http + ipareplica_setup_http: + ### server ### + setup_ca: "{{ ipareplica_setup_ca }}" + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + no_pkinit: "{{ ipareplica_no_pkinit }}" + no_ui_redirect: "{{ ipareplica_no_ui_redirect }}" + ### certificate system ### + subject_base: "{{ result_ipareplica_prepare.subject_base }}" + ### additional ### + config_master_host_name: + "{{ result_ipareplica_install_ca_certs.config_master_host_name }}" + config_ca_host_name: "{{ result_ipareplica_prepare.config_ca_host_name }}" + ccache: "{{ result_ipareplica_prepare.ccache }}" + _ca_enabled: "{{ result_ipareplica_prepare._ca_enabled }}" + _ca_file: "{{ result_ipareplica_prepare._ca_file }}" + _http_pkcs12_info: "{{ result_ipareplica_prepare._http_pkcs12_info }}" + _top_dir: "{{ result_ipareplica_prepare._top_dir }}" + dirman_password: "{{ ipareplica_dirman_password }}" + + # Need to point back to ourself after the cert for HTTP is obtained + - name: Install - Create original IPA conf again + ipareplica_create_ipa_conf: + ### basic ### + dm_password: "{{ ipadm_password | default(omit) }}" + password: "{{ ipaadmin_password | default(omit) }}" + ip_addresses: "{{ ipareplica_ip_addresses | default([]) }}" + domain: "{{ result_ipareplica_test.domain }}" + realm: "{{ result_ipareplica_test.realm }}" + hostname: "{{ result_ipareplica_test.hostname }}" + ca_cert_files: "{{ ipareplica_ca_cert_files | default([]) }}" + no_host_dns: "{{ ipareplica_no_host_dns }}" + ### replica ### + setup_adtrust: "{{ result_ipareplica_test.setup_adtrust }}" + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + setup_dns: "{{ ipareplica_setup_dns }}" + ### ssl certificate ### + dirsrv_cert_files: "{{ ipareplica_dirsrv_cert_files | default([]) }}" + ### client ### + force_join: "{{ ipaclient_force_join }}" + ### ad trust ### + netbios_name: "{{ ipareplica_netbios_name | default(omit) }}" + rid_base: "{{ ipareplica_rid_base | default(omit) }}" + secondary_rid_base: "{{ ipareplica_secondary_rid_base | default(omit) }}" + ### additional ### + server: "{{ result_ipareplica_test.server }}" + config_master_host_name: + "{{ result_ipareplica_install_ca_certs.config_master_host_name }}" + config_ca_host_name: "{{ result_ipareplica_prepare.config_ca_host_name }}" + ccache: "{{ result_ipareplica_prepare.ccache }}" + installer_ccache: "{{ result_ipareplica_prepare.installer_ccache }}" + _ca_enabled: "{{ result_ipareplica_prepare._ca_enabled }}" + subject_base: "{{ result_ipareplica_prepare.subject_base }}" + _top_dir: "{{ result_ipareplica_prepare._top_dir }}" + _add_to_ipaservers: "{{ result_ipareplica_prepare._add_to_ipaservers }}" + _ca_subject: "{{ result_ipareplica_prepare._ca_subject }}" + _subject_base: "{{ result_ipareplica_prepare._subject_base }}" + dirman_password: "{{ ipareplica_dirman_password }}" + when: result_ipareplica_test.change_master_for_certmonger + + - name: Install - Setup otpd + ipareplica_setup_otpd: + ### server ### + setup_ca: "{{ ipareplica_setup_ca }}" + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + no_pkinit: "{{ ipareplica_no_pkinit }}" + no_ui_redirect: "{{ ipareplica_no_ui_redirect }}" + ### certificate system ### + subject_base: "{{ result_ipareplica_prepare.subject_base }}" + ### additional ### + config_master_host_name: + "{{ result_ipareplica_install_ca_certs.config_master_host_name }}" + ccache: "{{ result_ipareplica_prepare.ccache }}" + _ca_file: "{{ result_ipareplica_prepare._ca_file }}" + _top_dir: "{{ result_ipareplica_prepare._top_dir }}" + dirman_password: "{{ ipareplica_dirman_password }}" + + - name: Install - Setup custodia + ipareplica_setup_custodia: + ### server ### + setup_ca: "{{ ipareplica_setup_ca }}" + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + no_pkinit: "{{ ipareplica_no_pkinit }}" + no_ui_redirect: "{{ ipareplica_no_ui_redirect }}" + ### certificate system ### + subject_base: "{{ result_ipareplica_prepare.subject_base }}" + ### additional ### + config_master_host_name: + "{{ result_ipareplica_prepare.config_master_host_name }}" + ccache: "{{ result_ipareplica_prepare.ccache }}" + _ca_enabled: "{{ result_ipareplica_prepare._ca_enabled }}" + _kra_enabled: "{{ result_ipareplica_prepare._kra_enabled }}" + _kra_host_name: "{{ result_ipareplica_prepare.config_kra_host_name }}" + _ca_file: "{{ result_ipareplica_prepare._ca_file }}" + _pkinit_pkcs12_info: "{{ result_ipareplica_prepare._pkinit_pkcs12_info }}" + _top_dir: "{{ result_ipareplica_prepare._top_dir }}" + dirman_password: "{{ ipareplica_dirman_password }}" + + - name: Install - Setup CA + ipareplica_setup_ca: + ### server ### + setup_ca: "{{ ipareplica_setup_ca }}" + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + no_pkinit: "{{ ipareplica_no_pkinit }}" + pki_config_override: + "{{ ipareplica_pki_config_override | default(omit) }}" + ### certificate system ### + subject_base: "{{ result_ipareplica_prepare.subject_base }}" + ### additional ### + ccache: "{{ result_ipareplica_prepare.ccache }}" + _ca_enabled: "{{ result_ipareplica_prepare._ca_enabled }}" + _ca_file: "{{ result_ipareplica_prepare._ca_file }}" + _ca_subject: "{{ result_ipareplica_prepare._ca_subject }}" + _kra_enabled: "{{ result_ipareplica_prepare._kra_enabled }}" + _kra_host_name: "{{ result_ipareplica_prepare.config_kra_host_name }}" + _subject_base: "{{ result_ipareplica_prepare._subject_base }}" + _pkinit_pkcs12_info: "{{ result_ipareplica_prepare._pkinit_pkcs12_info }}" + _top_dir: "{{ result_ipareplica_prepare._top_dir }}" + dirman_password: "{{ ipareplica_dirman_password }}" + config_setup_ca: "{{ result_ipareplica_prepare.config_setup_ca }}" + config_master_host_name: + "{{ result_ipareplica_install_ca_certs.config_master_host_name }}" + config_ca_host_name: + "{{ result_ipareplica_install_ca_certs.config_ca_host_name }}" + config_ips: "{{ result_ipareplica_prepare.config_ips }}" + when: result_ipareplica_prepare._ca_enabled + + - name: Install - KRB enable SSL + ipareplica_krb_enable_ssl: + ### server ### + setup_ca: "{{ ipareplica_setup_ca }}" + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + no_pkinit: "{{ ipareplica_no_pkinit }}" + ### certificate system ### + subject_base: "{{ result_ipareplica_prepare.subject_base }}" + ### additional ### + config_master_host_name: + "{{ result_ipareplica_install_ca_certs.config_master_host_name }}" + ccache: "{{ result_ipareplica_prepare.ccache }}" + _ca_enabled: "{{ result_ipareplica_prepare._ca_enabled }}" + _ca_file: "{{ result_ipareplica_prepare._ca_file }}" + _pkinit_pkcs12_info: "{{ result_ipareplica_prepare._pkinit_pkcs12_info }}" + _top_dir: "{{ result_ipareplica_prepare._top_dir }}" + dirman_password: "{{ ipareplica_dirman_password }}" + + - name: Install - DS apply updates + ipareplica_ds_apply_updates: + ### server ### + setup_ca: "{{ ipareplica_setup_ca }}" + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + no_pkinit: "{{ ipareplica_no_pkinit }}" + no_ui_redirect: "{{ ipareplica_no_ui_redirect }}" + dirsrv_config_file: "{{ ipareplica_dirsrv_config_file | default(omit) }}" + ### certificate system ### + subject_base: "{{ result_ipareplica_prepare.subject_base }}" + ### additional ### + config_master_host_name: + "{{ result_ipareplica_install_ca_certs.config_master_host_name }}" + ccache: "{{ result_ipareplica_prepare.ccache }}" + _ca_enabled: "{{ result_ipareplica_prepare._ca_enabled }}" + _ca_file: "{{ result_ipareplica_prepare._ca_file }}" + _pkinit_pkcs12_info: "{{ result_ipareplica_prepare._pkinit_pkcs12_info }}" + _top_dir: "{{ result_ipareplica_prepare._top_dir }}" + dirman_password: "{{ ipareplica_dirman_password }}" + ds_ca_subject: "{{ result_ipareplica_setup_ds.ds_ca_subject }}" + + - name: Install - Setup kra + ipareplica_setup_kra: + ### basic ### + dm_password: "{{ ipadm_password | default(omit) }}" + password: "{{ ipaadmin_password | default(omit) }}" + ip_addresses: "{{ ipareplica_ip_addresses | default([]) }}" + domain: "{{ result_ipareplica_test.domain }}" + realm: "{{ result_ipareplica_test.realm }}" + hostname: "{{ result_ipareplica_test.hostname }}" + ca_cert_files: "{{ ipareplica_ca_cert_files | default([]) }}" + no_host_dns: "{{ ipareplica_no_host_dns }}" + pki_config_override: + "{{ ipareplica_pki_config_override | default(omit) }}" + ### replica ### + setup_adtrust: "{{ result_ipareplica_test.setup_adtrust }}" + setup_ca: "{{ ipareplica_setup_ca }}" + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + setup_dns: "{{ ipareplica_setup_dns }}" + ### ssl certificate ### + dirsrv_cert_files: "{{ ipareplica_dirsrv_cert_files | default([]) }}" + ### client ### + force_join: "{{ ipaclient_force_join }}" + ### certificate system ### + subject_base: "{{ result_ipareplica_prepare.subject_base }}" + ### additional ### + server: "{{ result_ipareplica_test.server }}" + config_master_host_name: + "{{ result_ipareplica_prepare.config_master_host_name }}" + ccache: "{{ result_ipareplica_prepare.ccache }}" + installer_ccache: "{{ result_ipareplica_prepare.installer_ccache }}" + _ca_enabled: "{{ result_ipareplica_prepare._ca_enabled }}" + _kra_enabled: "{{ result_ipareplica_prepare._kra_enabled }}" + _kra_host_name: "{{ result_ipareplica_prepare.config_kra_host_name }}" + _top_dir: "{{ result_ipareplica_prepare._top_dir }}" + _add_to_ipaservers: "{{ result_ipareplica_prepare._add_to_ipaservers }}" + _ca_subject: "{{ result_ipareplica_prepare._ca_subject }}" + _subject_base: "{{ result_ipareplica_prepare._subject_base }}" + when: result_ipareplica_test.setup_kra + + - name: Install - Restart KDC + ipareplica_restart_kdc: + ### server ### + setup_ca: "{{ ipareplica_setup_ca }}" + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + no_pkinit: "{{ ipareplica_no_pkinit }}" + no_ui_redirect: "{{ ipareplica_no_ui_redirect }}" + ### certificate system ### + subject_base: "{{ result_ipareplica_prepare.subject_base }}" + ### additional ### + config_master_host_name: + "{{ result_ipareplica_install_ca_certs.config_master_host_name }}" + ccache: "{{ result_ipareplica_prepare.ccache }}" + _ca_file: "{{ result_ipareplica_prepare._ca_file }}" + _top_dir: "{{ result_ipareplica_prepare._top_dir }}" + dirman_password: "{{ ipareplica_dirman_password }}" + + - name: Install - Custodia import dm password + ipareplica_custodia_import_dm_password: + ### server ### + setup_ca: "{{ ipareplica_setup_ca }}" + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + no_pkinit: "{{ ipareplica_no_pkinit }}" + no_ui_redirect: "{{ ipareplica_no_ui_redirect }}" + ### certificate system ### + subject_base: "{{ result_ipareplica_prepare.subject_base }}" + ### additional ### + config_master_host_name: + "{{ result_ipareplica_prepare.config_master_host_name }}" + config_ca_host_name: "{{ result_ipareplica_prepare.config_ca_host_name }}" + ccache: "{{ result_ipareplica_prepare.ccache }}" + _ca_enabled: "{{ result_ipareplica_prepare._ca_enabled }}" + _ca_file: "{{ result_ipareplica_prepare._ca_file }}" + _kra_enabled: "{{ result_ipareplica_prepare._kra_enabled }}" + _kra_host_name: "{{ result_ipareplica_prepare.config_kra_host_name }}" + _top_dir: "{{ result_ipareplica_prepare._top_dir }}" + dirman_password: "{{ ipareplica_dirman_password }}" + config_setup_ca: "{{ result_ipareplica_prepare.config_setup_ca }}" + + - name: Install - Promote SSSD + ipareplica_promote_sssd: + ### replica ### + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + ### certificate system ### + subject_base: "{{ result_ipareplica_prepare.subject_base }}" + ### additional ### + ccache: "{{ result_ipareplica_prepare.ccache }}" + _top_dir: "{{ result_ipareplica_prepare._top_dir }}" + config_setup_ca: "{{ result_ipareplica_prepare.config_setup_ca }}" + config_master_host_name: + "{{ result_ipareplica_prepare.config_master_host_name }}" + + - name: Install - Promote openldap.conf + ipareplica_promote_openldap_conf: + ### replica ### + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + ### certificate system ### + subject_base: "{{ result_ipareplica_prepare.subject_base }}" + ### additional ### + ccache: "{{ result_ipareplica_prepare.ccache }}" + _top_dir: "{{ result_ipareplica_prepare._top_dir }}" + config_setup_ca: "{{ result_ipareplica_prepare.config_setup_ca }}" + config_master_host_name: + "{{ result_ipareplica_prepare.config_master_host_name }}" + + - name: Install - Setup DNS + ipareplica_setup_dns: + ### server ### + setup_dns: "{{ ipareplica_setup_dns }}" + ### replica ### + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + ### certificate system ### + subject_base: "{{ result_ipareplica_prepare.subject_base }}" + ### dns ### + zonemgr: "{{ ipareplica_zonemgr | default(omit) }}" + forwarders: "{{ ipareplica_forwarders | default([]) }}" + forward_policy: "{{ result_ipareplica_prepare.forward_policy if + result_ipareplica_prepare.forward_policy is + not none else omit }}" + no_dnssec_validation: "{{ ipareplica_no_dnssec_validation }}" + ### additional ### + dns_ip_addresses: "{{ result_ipareplica_prepare.dns_ip_addresses }}" + dns_reverse_zones: "{{ result_ipareplica_prepare.dns_reverse_zones }}" + ccache: "{{ result_ipareplica_prepare.ccache }}" + _top_dir: "{{ result_ipareplica_prepare._top_dir }}" + setup_ca: "{{ result_ipareplica_prepare.config_setup_ca }}" + config_master_host_name: + "{{ result_ipareplica_prepare.config_master_host_name }}" + + - name: Install - Setup adtrust + ipareplica_setup_adtrust: + ### replica ### + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + ### certificate system ### + subject_base: "{{ result_ipareplica_prepare.subject_base }}" + ### ad trust ### + enable_compat: "{{ ipareplica_enable_compat }}" + rid_base: "{{ result_ipareplica_prepare.rid_base }}" + secondary_rid_base: "{{ result_ipareplica_prepare.secondary_rid_base }}" + ### additional ### + ccache: "{{ result_ipareplica_prepare.ccache }}" + _top_dir: "{{ result_ipareplica_prepare._top_dir }}" + setup_ca: "{{ result_ipareplica_prepare.config_setup_ca }}" + config_master_host_name: + "{{ result_ipareplica_prepare.config_master_host_name }}" + adtrust_netbios_name: + "{{ result_ipareplica_prepare.adtrust_netbios_name }}" + adtrust_reset_netbios_name: + "{{ result_ipareplica_prepare.adtrust_reset_netbios_name }}" + when: result_ipareplica_test.setup_adtrust + + - name: Install - Enable IPA + ipareplica_enable_ipa: + hostname: "{{ result_ipareplica_test.hostname }}" + hidden_replica: "{{ ipareplica_hidden_replica }}" + ### server ### + ### replica ### + setup_kra: "{{ result_ipareplica_test.setup_kra }}" + ### certificate system ### + subject_base: "{{ result_ipareplica_prepare.subject_base }}" + ### additional ### + ccache: "{{ result_ipareplica_prepare.ccache }}" + _top_dir: "{{ result_ipareplica_prepare._top_dir }}" + setup_ca: "{{ result_ipareplica_prepare.config_setup_ca }}" + config_master_host_name: + "{{ result_ipareplica_prepare.config_master_host_name }}" + register: result_ipareplica_enable_ipa + + - name: Install - Cleanup root IPA cache + file: + path: "/root/.ipa_cache" + state: absent + when: result_ipareplica_enable_ipa.changed + + when: not ansible_check_mode and + not (result_ipareplica_test.client_already_configured is defined or + result_ipareplica_test.server_already_configured is defined) diff --git a/roles/ipareplica/tasks/main.yml b/roles/ipareplica/tasks/main.yml new file mode 100644 index 0000000..0d9cd7a --- /dev/null +++ b/roles/ipareplica/tasks/main.yml @@ -0,0 +1,18 @@ +--- +# tasks file for ipareplica + +- name: Import variables specific to distribution + include_vars: "{{ item }}" + with_first_found: + - "vars/{{ ansible_distribution }}-{{ ansible_distribution_version }}.yml" + - "vars/{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml" + - "vars/{{ ansible_distribution }}.yml" + - "vars/default.yml" + +- name: Install IPA replica + include_tasks: install.yml + when: state|default('present') == 'present' + +- name: Uninstall IPA replica + include_tasks: uninstall.yml + when: state|default('present') == 'absent' diff --git a/roles/ipareplica/tasks/python_2_3_test.yml b/roles/ipareplica/tasks/python_2_3_test.yml new file mode 100644 index 0000000..d407932 --- /dev/null +++ b/roles/ipareplica/tasks/python_2_3_test.yml @@ -0,0 +1,23 @@ +--- +- block: + - name: Verify Python3 import + script: py3test.py + register: result_py3test + failed_when: False + changed_when: False + check_mode: no + + - name: Set python interpreter to 3 + set_fact: + ansible_python_interpreter: "/usr/bin/python3" + when: result_py3test.rc == 0 + + - name: Fail for IPA 4.5.90 + fail: msg="You need to install python2 bindings for ipa server usage" + when: result_py3test.rc != 0 and "not usable with python3" in + result_py3test.stdout + + - name: Set python interpreter to 2 + set_fact: + ansible_python_interpreter: "/usr/bin/python2" + when: result_py3test.failed or result_py3test.rc != 0 diff --git a/roles/ipareplica/tasks/uninstall.yml b/roles/ipareplica/tasks/uninstall.yml new file mode 100644 index 0000000..65068e6 --- /dev/null +++ b/roles/ipareplica/tasks/uninstall.yml @@ -0,0 +1,40 @@ +--- +# tasks to uninstall IPA replica + +# - name: Uninstall - Include Python2/3 import test +# import_tasks: "{{ role_path }}/tasks/python_2_3_test.yml" + +- name: Uninstall - Uninstall IPA replica + command: > + /usr/sbin/ipa-server-install + --uninstall + -U + {{ "--ignore-topology-disconnect" if + ipareplica_ignore_topology_disconnect | bool else "" }} + {{ "--ignore-last-of-role" if ipareplica_ignore_last_of_role | bool + else "" }} + register: result_uninstall + # 2 means that uninstall failed because IPA replica was not configured + failed_when: result_uninstall.rc != 0 and "'Env' object + has no attribute 'basedn'" not in result_uninstall.stderr + # IPA server is not configured on this system" not in + # result_uninstall.stdout_lines + changed_when: result_uninstall.rc == 0 + # until: result_uninstall.rc == 0 + retries: 2 + delay: 1 + +#- name: Uninstall - Remove all replication agreements and data about replica +# command: > +# /usr/sbin/ipa-replica-manage +# del +# {{ ipareplica_hostname | default(ansible_fqdn) }} +# --force +# --password={{ ipadm_password }} +# failed_when: False +# delegate_to: "{{ groups.ipaserver[0] | default(fail) }}" + +#- name: Remove IPA replica packages +# package: +# name: "{{ ipareplica_packages }}" +# state: absent diff --git a/roles/ipareplica/vars/CentOS-7.yml b/roles/ipareplica/vars/CentOS-7.yml new file mode 100644 index 0000000..614de3e --- /dev/null +++ b/roles/ipareplica/vars/CentOS-7.yml @@ -0,0 +1,6 @@ +# defaults file for ipareplica +# vars/RedHat-7.yml +ipareplica_packages: [ "ipa-server", "libselinux-python" ] +ipareplica_packages_dns: [ "ipa-server-dns" ] +ipareplica_packages_adtrust: [ "ipa-server-trust-ad" ] +ipareplica_packages_firewalld: [ "firewalld" ] \ No newline at end of file diff --git a/roles/ipareplica/vars/CentOS-8.yml b/roles/ipareplica/vars/CentOS-8.yml new file mode 120000 index 0000000..d49e1cd --- /dev/null +++ b/roles/ipareplica/vars/CentOS-8.yml @@ -0,0 +1 @@ +RedHat-8.yml \ No newline at end of file diff --git a/roles/ipareplica/vars/Fedora-25.yml b/roles/ipareplica/vars/Fedora-25.yml new file mode 100644 index 0000000..ce981ff --- /dev/null +++ b/roles/ipareplica/vars/Fedora-25.yml @@ -0,0 +1,6 @@ +# Fedora-25 defaults file for ipareplica +# vars/Fedora-25.yml +ipareplica_packages: [ "ipa-server", "libselinux-python" ] +ipareplica_packages_dns: [ "ipa-server-dns" ] +ipareplica_packages_adtrust: [ "ipa-server-trust-ad" ] +ipareplica_packages_firewalld: [ "firewalld" ] \ No newline at end of file diff --git a/roles/ipareplica/vars/Fedora-26.yml b/roles/ipareplica/vars/Fedora-26.yml new file mode 100644 index 0000000..5a65e43 --- /dev/null +++ b/roles/ipareplica/vars/Fedora-26.yml @@ -0,0 +1,6 @@ +# Fedora defaults file for ipareplica +# vars/Fedora-26.yml +ipareplica_packages: [ "ipa-server", "libselinux-python" ] +ipareplica_packages_dns: [ "ipa-server-dns" ] +ipareplica_packages_adtrust: [ "ipa-server-trust-ad" ] +ipareplica_packages_firewalld: [ "firewalld" ] \ No newline at end of file diff --git a/roles/ipareplica/vars/Fedora-27.yml b/roles/ipareplica/vars/Fedora-27.yml new file mode 100644 index 0000000..5d6f02a --- /dev/null +++ b/roles/ipareplica/vars/Fedora-27.yml @@ -0,0 +1,6 @@ +# Fedora defaults file for ipareplica +# vars/Fedora.yml +ipareplica_packages: [ "ipa-server", "libselinux-python" ] +ipareplica_packages_dns: [ "ipa-server-dns" ] +ipareplica_packages_adtrust: [ "ipa-server-trust-ad" ] +ipareplica_packages_firewalld: [ "firewalld" ] \ No newline at end of file diff --git a/roles/ipareplica/vars/Fedora.yml b/roles/ipareplica/vars/Fedora.yml new file mode 100644 index 0000000..f19fb99 --- /dev/null +++ b/roles/ipareplica/vars/Fedora.yml @@ -0,0 +1,6 @@ +# Fedora defaults file for ipareplica +# vars/Fedora.yml +ipareplica_packages: [ "freeipa-server", "python3-libselinux" ] +ipareplica_packages_dns: [ "freeipa-server-dns" ] +ipareplica_packages_adtrust: [ "freeipa-server-trust-ad" ] +ipareplica_packages_firewalld: [ "firewalld" ] \ No newline at end of file diff --git a/roles/ipareplica/vars/RedHat-7.3.yml b/roles/ipareplica/vars/RedHat-7.3.yml new file mode 100644 index 0000000..a0e7ffe --- /dev/null +++ b/roles/ipareplica/vars/RedHat-7.3.yml @@ -0,0 +1,6 @@ +# defaults file for ipareplica +# vars/RedHat-7.3.yml +ipareplica_packages: [ "ipa-server", "libselinux-python" ] +ipareplica_packages_dns: [ "ipa-server-dns" ] +ipareplica_packages_adtrust: [ "ipa-server-trust-ad" ] +ipareplica_packages_firewalld: [ "firewalld" ] \ No newline at end of file diff --git a/roles/ipareplica/vars/RedHat-7.yml b/roles/ipareplica/vars/RedHat-7.yml new file mode 100644 index 0000000..614de3e --- /dev/null +++ b/roles/ipareplica/vars/RedHat-7.yml @@ -0,0 +1,6 @@ +# defaults file for ipareplica +# vars/RedHat-7.yml +ipareplica_packages: [ "ipa-server", "libselinux-python" ] +ipareplica_packages_dns: [ "ipa-server-dns" ] +ipareplica_packages_adtrust: [ "ipa-server-trust-ad" ] +ipareplica_packages_firewalld: [ "firewalld" ] \ No newline at end of file diff --git a/roles/ipareplica/vars/RedHat-8.yml b/roles/ipareplica/vars/RedHat-8.yml new file mode 100644 index 0000000..0257302 --- /dev/null +++ b/roles/ipareplica/vars/RedHat-8.yml @@ -0,0 +1,6 @@ +# defaults file for ipareplica +# vars/RedHat-8.yml +ipareplica_packages: [ "@idm:DL1/server" ] +ipareplica_packages_dns: [ "@idm:DL1/dns" ] +ipareplica_packages_adtrust: [ "@idm:DL1/adtrust" ] +ipareplica_packages_firewalld: [ "firewalld" ] diff --git a/roles/ipareplica/vars/Ubuntu.yml b/roles/ipareplica/vars/Ubuntu.yml new file mode 100644 index 0000000..7cdabbf --- /dev/null +++ b/roles/ipareplica/vars/Ubuntu.yml @@ -0,0 +1,5 @@ +# vars/Ubuntu.yml +ipareplica_packages: [ "freeipa-server" ] +ipareplica_packages_dns: [ "freeipa-server-dns" ] +ipareplica_packages_adtrust: [ "freeipa-server-trust-ad" ] +ipareplica_packages_firewalld: [ "firewalld" ] diff --git a/roles/ipareplica/vars/default.yml b/roles/ipareplica/vars/default.yml new file mode 100644 index 0000000..ce7393a --- /dev/null +++ b/roles/ipareplica/vars/default.yml @@ -0,0 +1,6 @@ +# defaults file for ipareplica +# vars/default.yml +ipareplica_packages: [ "freeipa-server", "python3-libselinux" ] +ipareplica_packages_dns: [ "freeipa-server-dns" ] +ipareplica_packages_adtrust: [ "freeipa-server-trust-ad" ] +ipareplica_packages_firewalld: [ "firewalld" ] diff --git a/roles/ipaserver/README.md b/roles/ipaserver/README.md new file mode 100644 index 0000000..88afb3a --- /dev/null +++ b/roles/ipaserver/README.md @@ -0,0 +1,292 @@ +ipaserver role +============== + +Description +----------- + +This role allows to configure and IPA server. + +**Note**: The ansible playbooks and role require a configured ansible environment where the ansible nodes are reachable and are properly set up to have an IP address and a working package manager. + + +Features +-------- +* Server deployment + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.5 and up are supported by the server role. + + +Supported Distributions +----------------------- + +* RHEL/CentOS 7.6+ +* Fedora 26+ +* Ubuntu + + +Requirements +------------ + +**Controller** +* Ansible version: 2.8+ + +**Node** +* Supported FreeIPA version (see above) +* Supported distribution (needed for package installation only, see above) + + +Limitations +----------- + +External signed CA + +External signed CA is now supported. But the currently needed two step process is an issue for the processing in a simple playbook. + +Work is planned to have a new method to handle CSR for external signed CAs in a separate step before starting the server installation. + + +Usage +===== + +Example inventory file with fixed domain and realm, setting up of the DNS server and using forwarders from /etc/resolv.conf: + + [ipaserver] + ipaserver2.example.com + + [ipaserver:vars] + ipaserver_domain=example.com + ipaserver_realm=EXAMPLE.COM + ipaserver_setup_dns=yes + ipaserver_auto_forwarders=yes + +Example playbook to setup the IPA server using admin and dirman passwords from an [Ansible Vault](http://docs.ansible.com/ansible/latest/playbooks_vault.html) file: + + - name: Playbook to configure IPA server + hosts: ipaserver + become: true + vars_files: + - playbook_sensitive_data.yml + + roles: + - role: ipaserver + state: present + +Example playbook to unconfigure the IPA client(s) using principal and password from inventory file: + + - name: Playbook to unconfigure IPA server + hosts: ipaserver + become: true + + roles: + - role: ipaserver + state: absent + +Example inventory file with fixed domain, realm, admin and dirman passwords: + + [ipaserver] + ipaserver.example.com + + [ipaserver:vars] + ipaserver_domain=example.com + ipaserver_realm=EXAMPLE.COM + ipaadmin_password=MySecretPassword123 + ipadm_password=MySecretPassword234 + +Example playbook to setup the IPA server using admin and dirman passwords from inventory file: + + - name: Playbook to configure IPA server + hosts: ipaserver + become: true + + roles: + - role: ipaserver + state: present + +Example playbook to setup the IPA primary with external signed CA using the previous inventory file: + +Server installation step 1: Generate CSR, copy to controller as `-ipa.csr` + +```yaml +--- +- name: Playbook to configure IPA server step1 + hosts: ipaserver + become: true + vars: + ipaserver_external_ca: yes + + roles: + - role: ipaserver + state: present + + post_tasks: + - name: Copy CSR /root/ipa.csr from node to "{{ groups.ipaserver[0] + '-ipa.csr' }}" + fetch: + src: /root/ipa.csr + dest: "{{ groups.ipaserver[0] + '-ipa.csr' }}" + flat: yes +``` + +Sign with CA: This is up to you + +Server installation step 2: Copy `-chain.crt` to the IPA server and continue with installation of the primary. + +```yaml +- name: Playbook to configure IPA server step3 + hosts: ipaserver + become: true + vars: + ipaserver_external_cert_files: "/root/chain.crt" + + pre_tasks: + - name: Copy "{{ groups.ipaserver[0] + '-chain.crt' }}" to /root/chain.crt on node + copy: + src: "{{ groups.ipaserver[0] + '-chain.crt' }}" + dest: "/root/chain.crt" + force: yes + + roles: + - role: ipaserver + state: present +``` + +The files can also be copied automatically: Set `ipaserver_copy_csr_to_controller` to true in the server installation step 1 and set `ipaserver_external_cert_files_from_controller` to point to the `chain.crt` file in the server installation step 2. + + +Playbooks +========= + +The playbooks needed to deploy or undeploy a server are part of the repository in the playbooks folder. There are also playbooks to deploy and undeploy clusters. +``` +install-server.yml +uninstall-server.yml +``` +Please remember to link or copy the playbooks to the base directory of ansible-freeipa if you want to use the roles within the source archive. + + +How to setup a server +--------------------- + +```bash +ansible-playbook -v -i inventory/hosts install-server.yml +``` +This will deploy the server defined in the inventory file. + + +Variables +========= + +Base Variables +-------------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaserver` | This group with the single IPA server full qualified hostname. (list of strings) | yes +`ipadm_password` | The password for the Directory Manager. (string) | no +`ipaadmin_password` | The password for the IPA admin user (string) | no +`ipaserver_ip_addresses` | The list of master server IP addresses. (list of strings) | no +`ipaserver_domain` | The primary DNS domain of an existing IPA deployment. (string) | no +`ipaserver_realm` | The Kerberos realm of an existing IPA deployment. (string) | no +`ipaserver_hostname` | Fully qualified name of the server. (string) | no +`ipaserver_no_host_dns` | Do not use DNS for hostname lookup during installation. (bool, default: false) | no + +Server Variables +---------------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaserver_setup_adtrust` | Configure AD Trust capability. (bool, default: false) | no +`ipaserver_setup_kra` | Install and configure a KRA on this server. (bool, default: false) | no +`ipaserver_setup_dns` | Configure an integrated DNS server, create DNS zone specified by domain. (bool, default: false) | no +`ipaserver_idstart` | The starting user and group id number. (integer, default: random) | no +`ipaserver_idmax` | The maximum user and group id number. (integer, default: idstart+199999) | no +`ipaserver_no_hbac_allow` | Do not install allow_all HBAC rule. (bool) | no +`ipaserver_no_ui_redirect` | Do not automatically redirect to the Web UI. (bool) | no +`ipaserver_dirsrv_config_file` | The path to LDIF file that will be used to modify configuration of dse.ldif during installation. (string) | no +`ipaserver_pki_config_override` | Path to ini file with config overrides. This is only usable with recent FreeIPA versions. (string) | no + +SSL certificate Variables +------------------------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaserver_dirsrv_cert_files` | Files containing the Directory Server SSL certificate and private keys. (list of strings) | no +`ipaserver_http_cert_file` | File containing the Apache Server SSL certificate and private key. (string) | no +`ipaserver_pkinit_cert_file` | File containing the Kerberos KDC SSL certificate and private key. (string) | no +`ipaserver_dirsrv_pin` | The password to unlock the Directory Server private key. (string) | no +`ipaserver_http_pin` | The password to unlock the Apache Server private key. (string) | no +`ipaserver_pkinit_pin` | The password to unlock the Kerberos KDC private key. (string) | no +`ipaserver_dirsrv_cert_name` | Name of the Directory Server SSL certificate to install. (string) | no +`ipaserver_http_cert_name` | Name of the Apache Server SSL certificate to install. (string) | no +`ipaserver_pkinit_cert_name` | Name of the Kerberos KDC SSL certificate to install. (string) | no + +Client Variables +---------------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaclient_ntp_servers` | The list defines the NTP servers to be used. | no +`ipaclient_ntp_pool` | The string value defines the ntp server pool to be used. | no +`ipaclient_no_ntp` | The bool value defines if NTP will not be configured and enabled. `ipaclient_no_ntp` defaults to `no`. | no +`ipaclient_ssh_trust_dns` | The bool value defines if OpenSSH client will be configured to trust DNS SSHFP records. `ipaclient_ssh_trust_dns` defaults to `no`. | no +`ipaclient_no_ssh` | The bool value defines if OpenSSH client will be configured. `ipaclient_no_ssh` defaults to `no`. | no +`ipaclient_no_sshd` | The bool value defines if OpenSSH server will be configured. `ipaclient_no_sshd` defaults to `no`. | no +`ipaclient_no_sudo` | The bool value defines if SSSD will be configured as a data source for sudo. `ipaclient_no_sudo` defaults to `no`. | no +`ipaclient_no_dns_sshfp` | The bool value defines if DNS SSHFP records will not be created automatically. `ipaclient_no_dns_sshfp` defaults to `no`. | no + +Certificate system Variables +---------------------------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaserver_external_ca` | Generate a CSR for the IPA CA certificate to be signed by an external CA. (bool, default: false) | no +`ipaserver_external_ca_type` | Type of the external CA. (choice: generic,ms-cs) | no +`ipaserver_external_ca_profile` | Specify the certificate profile/template to use at the external CA. (string) | no +`ipaserver_external_cert_files` | Files containing the IPA CA certificates and the external CA certificate chains (list of string) | no +`ipaserver_subject_base` | The certificate subject base (default O=). RDNs are in LDAP order (most specific RDN first). (string) | no +`ipaserver_ca_subject` | The CA certificate subject DN (default CN=Certificate Authority,O=). RDNs are in LDAP order (most specific RDN first). (string) | no +`ipaserver_ca_signing_algorithm` | Signing algorithm of the IPA CA certificate. (choice: SHA1withRSA,SHA256withRSA,SHA512withRSA) | no + +DNS Variables +------------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaserver_allow_zone_overlap` | Allow creation of (reverse) zone even if the zone is already resolvable. (bool, default: false) | no +`ipaserver_reverse_zones` | The reverse DNS zones to use. (list of strings) | no +`ipaserver_no_reverse` | Do not create reverse DNS zone. (bool, default: false) | no +`ipaserver_auto_reverse` | Try to resolve reverse records and reverse zones for server IP addresses. (bool, default: false) | no +`ipaserver_zonemgr` | The e-mail address of the DNS zone manager. (string, default: hostmaster@DOMAIN.) | no +`ipaserver_forwarders` | Add DNS forwarders to the DNS configuration. (list of strings) | no +`ipaserver_no_forwarders` | Do not add any DNS forwarders. Root DNS servers will be used instead. (bool, default: false) | no +`ipaserver_auto_forwarders` | Add DNS forwarders configured in /etc/resolv.conf to the list of forwarders used by IPA DNS. (bool, default: false) | no +`ipaserver_forward_policy` | DNS forwarding policy for global forwarders specified using other options. (choice: first|only) | no +`ipaserver_no_dnssec_validation` | Disable DNSSEC validation on this server. (bool, default: false) | no + +AD trust Variables +------------------ + +Variable | Description | Required +-------- | ----------- | -------- +`ipaserver_enable_compat`| Enables support for trusted domains users for old clients through Schema Compatibility plugin. (bool, default: false) | no +`ipaserver_netbios_name` | The NetBIOS name for the IPA domain. (string) | no +`ipaserver_rid_base` | First RID value of the local domain. (integer) | no +`ipaserver_secondary_rid_base` | Start value of the secondary RID range. (integer) | no + +Special Variables +----------------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaserver_install_packages` | The bool value defines if the needed packages are installed on the node. (bool, default: true) | no +`ipaserver_setup_firewalld` | The value defines if the needed services will automatically be opened in the firewall managed by firewalld. (bool, default: true) | no +`ipaserver_external_cert_files_from_controller` | Files containing the IPA CA certificates and the external CA certificate chains on the controller that will be copied to the ipaserver host to `/root` folder. (list of string) | no +`ipaserver_copy_csr_to_controller` | Copy the generated CSR from the ipaserver to the controller as `"{{ inventory_hostname }}-ipa.csr"`. (bool) | no + +Authors +======= + +Thomas Woerner diff --git a/roles/ipaserver/defaults/main.yml b/roles/ipaserver/defaults/main.yml new file mode 100644 index 0000000..ed1364b --- /dev/null +++ b/roles/ipaserver/defaults/main.yml @@ -0,0 +1,43 @@ +--- +# defaults file for ipaserver + +### basic ### +ipaserver_no_host_dns: no +### server ### +ipaserver_setup_adtrust: no +ipaserver_setup_kra: no +ipaserver_setup_dns: no +ipaserver_no_hbac_allow: no +ipaserver_no_pkinit: no +ipaserver_no_ui_redirect: no +### ssl certificate ### +### client ### +ipaclient_mkhomedir: no +ipaclient_no_ntp: no +#ipaclient_ssh_trust_dns: no +#ipaclient_no_ssh: no +#ipaclient_no_sshd: no +#ipaclient_no_dns_sshfp: no +### certificate system ### +ipaserver_external_ca: no +### dns ### +ipaserver_allow_zone_overlap: no +ipaserver_no_reverse: no +ipaserver_auto_reverse: no +ipaserver_no_forwarders: no +ipaserver_auto_forwarders: no +ipaserver_no_dnssec_validation: no +### ad trust ### +ipaserver_enable_compat: no +ipaserver_setup_ca: yes +### packages ### +ipaserver_install_packages: yes +### firewalld ### +ipaserver_setup_firewalld: yes + +### additional ### +ipaserver_copy_csr_to_controller: no + +### uninstall ### +ipaserver_ignore_topology_disconnect: no +ipaserver_ignore_last_of_role: no diff --git a/roles/ipaserver/files/py3test.py b/roles/ipaserver/files/py3test.py new file mode 100644 index 0000000..8f5c2d8 --- /dev/null +++ b/roles/ipaserver/files/py3test.py @@ -0,0 +1,9 @@ +#!/usr/bin/python3 + +# Test ipaerver python3 binding +from ipaserver.install.server.install import install_check + +# Check ipapython version to be >= 4.6 +from ipapython.version import NUM_VERSION, VERSION +if NUM_VERSION < 40590: + raise Exception("ipa %s not usable with python3" % VERSION) diff --git a/roles/ipaserver/library/ipaserver_enable_ipa.py b/roles/ipaserver/library/ipaserver_enable_ipa.py new file mode 100644 index 0000000..5f50213 --- /dev/null +++ b/roles/ipaserver/library/ipaserver_enable_ipa.py @@ -0,0 +1,126 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-server-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaserver_enable_ipa +short description: Enable IPA +description: Enable IPA +options: + hostname: + description: Fully qualified name of this host + required: yes + setup_dns: + description: Configure bind with our zone + required: no + setup_ca: + description: Configure a dogtag CA + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_server import ( + AnsibleModuleLog, options, paths, api, sysrestore, tasks, + service, bindinstance, redirect_stdout, services +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + hostname=dict(required=False), + setup_dns=dict(required=True, type='bool'), + setup_ca=dict(required=True, type='bool'), + ), + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # set values ############################################################# + + options.host_name = ansible_module.params.get('hostname') + options.setup_dns = ansible_module.params.get('setup_dns') + options.setup_ca = ansible_module.params.get('setup_ca') + + # Configuration for ipalib, we will bootstrap and finalize later, after + # we are sure we have the configuration file ready. + cfg = dict( + context='installer', + confdir=paths.ETC_IPA, + in_server=True, + # make sure host name specified by user is used instead of default + host=options.host_name, + ) + if options.setup_ca: + # we have an IPA-integrated CA + cfg['ca_host'] = options.host_name + + api.bootstrap(**cfg) + api.finalize() + api.Backend.ldap2.connect() + + # setup ds ###################################################### + + fstore = sysrestore.FileStore(paths.SYSRESTORE) + + if hasattr(tasks, "configure_tmpfiles"): + # Make sure the files we crated in /var/run are recreated at startup + tasks.configure_tmpfiles() + + if hasattr(service, "enable_services"): + # Enable configured services and update DNS SRV records + service.enable_services(options.host_name) + api.Command.dns_update_system_records() + + if not options.setup_dns: + # After DNS and AD trust are configured and services are + # enabled, create a dummy instance to dump DNS configuration. + bind = bindinstance.BindInstance(fstore) + bind.create_file_with_system_records() + + with redirect_stdout(ansible_log): + services.knownservices.ipa.enable() + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaserver/library/ipaserver_load_cache.py b/roles/ipaserver/library/ipaserver_load_cache.py new file mode 100644 index 0000000..20d335c --- /dev/null +++ b/roles/ipaserver/library/ipaserver_load_cache.py @@ -0,0 +1,101 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaserver_load_cache +short description: Load cache file +description: Load cache file +options: + dm_password: + description: Directory Manager password + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_server import ( + options, paths, read_cache +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # basic + dm_password=dict(required=True, no_log=True), + ), + ) + + ansible_module._ansible_debug = True + + # set values ############################################################ + + # basic + options.dm_password = ansible_module.params.get('dm_password') + + # restore cache ######################################################### + + if os.path.isfile(paths.ROOT_IPA_CACHE): + if options.dm_password is None: + ansible_module.fail_json(msg="Directory Manager password required") + try: + cache_vars = read_cache(options.dm_password) + options.__dict__.update(cache_vars) + if cache_vars.get('external_ca', False): + options.external_ca = False + options.interactive = False + except Exception as e: + ansible_module.fail_json( + msg="Cannot process the cache file: %s" % str(e)) + + kwargs = {"changed": True} + for name in options.__dict__: + kwargs[name] = options.__dict__[name] + ansible_module.exit_json(**kwargs) + + # done ################################################################## + + ansible_module.exit_json(changed=False) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaserver/library/ipaserver_master_password.py b/roles/ipaserver/library/ipaserver_master_password.py new file mode 100644 index 0000000..0bb444e --- /dev/null +++ b/roles/ipaserver/library/ipaserver_master_password.py @@ -0,0 +1,97 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-server-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaserver_master_password +short description: Generate kerberos master password if not given +description: + Generate kerberos master password if not given +options: + dm_password: + description: Directory Manager password + required: no + master_password: + description: kerberos master password (normally autogenerated) + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +password: + description: The master password + returned: always +''' + +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_server import ( + options, paths, read_cache, ipa_generate_password +) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + # basic + dm_password=dict(required=True, no_log=True), + master_password=dict(required=False, no_log=True), + ), + supports_check_mode=True, + ) + + module._ansible_debug = True + + options.dm_password = module.params.get('dm_password') + options.master_password = module.params.get('master_password') + + # This will override any settings passed in on the cmdline + if os.path.isfile(paths.ROOT_IPA_CACHE): + # dm_password check removed, checked already + try: + cache_vars = read_cache(options.dm_password) + options.__dict__.update(cache_vars) + except Exception as e: + module.fail_json(msg="Cannot process the cache file: %s" % str(e)) + + if not options.master_password: + options.master_password = ipa_generate_password() + + module.exit_json(changed=True, + password=options.master_password) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaserver/library/ipaserver_prepare.py b/roles/ipaserver/library/ipaserver_prepare.py new file mode 100644 index 0000000..68dadc5 --- /dev/null +++ b/roles/ipaserver/library/ipaserver_prepare.py @@ -0,0 +1,418 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaserver_prepare +short description: Prepare IPA server deployment +description: Prepare IPA server deployment +options: + force: + description: Installer force parameter + required: yes + dm_password: + description: Directory Manager password + required: no + password: + description: Admin user kerberos password + required: no + ip_addresses: + description: List of Master Server IP Addresses + required: yes + domain: + description: Primary DNS domain of the IPA deployment + required: no + realm: + description: Kerberos realm name of the IPA deployment + required: no + hostname: + description: Fully qualified name of this host + required: yes + ca_cert_files: + description: + List of files containing CA certificates for the service certificate + files + required: yes + no_host_dns: + description: Do not use DNS for hostname lookup during installation + required: yes + setup_adtrust: + description: Configure AD trust capability + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + setup_dns: + description: Configure bind with our zone + required: yes + external_ca: + description: External ca setting + required: yes + external_ca_type: + description: Type of the external CA + required: yes + external_ca_profile: + description: + Specify the certificate profile/template to use at the external CA + required: yes + external_cert_files: + description: + File containing the IPA CA certificate and the external CA certificate + chain + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: yes + ca_subject: + description: The installer ca_subject setting + required: yes + allow_zone_overlap: + description: Create DNS zone even if it already exists + required: yes + reverse_zones: + description: The reverse DNS zones to use + required: yes + no_reverse: + description: Do not create new reverse DNS zone + required: yes + auto_reverse: + description: Create necessary reverse zones + required: yes + forwarders: + description: Add DNS forwarders + required: yes + no_forwarders: + description: Do not add any DNS forwarders, use root servers instead + required: yes + auto_forwarders: + description: Use DNS forwarders configured in /etc/resolv.conf + required: yes + forward_policy: + description: DNS forwarding policy for global forwarders + required: yes + no_dnssec_validation: + description: Disable DNSSEC validation + required: yes + enable_compat: + description: Enable support for trusted domains for old clients + required: yes + netbios_name: + description: NetBIOS name of the IPA domain + required: yes + rid_base: + description: Start value for mapping UIDs and GIDs to RIDs + required: yes + secondary_rid_base: + description: + Start value of the secondary range for mapping UIDs and GIDs to RIDs + required: yes + setup_ca: + description: Configure a dogtag CA + required: yes + _hostname_overridden: + description: The installer _hostname_overridden setting + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_server import ( + AnsibleModuleLog, options, sysrestore, paths, + ansible_module_get_parsed_ip_addresses, + redirect_stdout, adtrust, api, default_subject_base, + default_ca_subject_dn, ipautil, installutils, ca, kra, dns, + get_server_ip_address, no_matching_interface_for_ip_address_warning, + services, logger, tasks, update_hosts_file, ScriptError +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # basic + force=dict(required=False, type='bool', default=False), + dm_password=dict(required=True, no_log=True), + password=dict(required=True, no_log=True), + ip_addresses=dict(required=False, type='list', default=[]), + domain=dict(required=True), + realm=dict(required=True), + hostname=dict(required=False), + ca_cert_files=dict(required=False, type='list', default=[]), + no_host_dns=dict(required=False, type='bool', default=False), + # server + setup_adtrust=dict(required=False, type='bool', default=False), + setup_kra=dict(required=False, type='bool', default=False), + setup_dns=dict(required=False, type='bool', default=False), + # ssl certificate + # client + # certificate system + external_ca=dict(required=False, type='bool'), + external_ca_type=dict(required=False), + external_ca_profile=dict(required=False), + external_cert_files=dict(required=False, type='list', default=[]), + subject_base=dict(required=False), + ca_subject=dict(required=False), + # dns + allow_zone_overlap=dict(required=False, type='bool', + default=False), + reverse_zones=dict(required=False, type='list', default=[]), + no_reverse=dict(required=False, type='bool', default=False), + auto_reverse=dict(required=False, type='bool', default=False), + forwarders=dict(required=False, type='list', default=[]), + no_forwarders=dict(required=False, type='bool', default=False), + auto_forwarders=dict(required=False, type='bool', default=False), + forward_policy=dict(default=None, choices=['first', 'only']), + no_dnssec_validation=dict(required=False, type='bool', + default=False), + # ad trust + enable_compat=dict(required=False, type='bool', default=False), + netbios_name=dict(required=False), + rid_base=dict(required=False, type='int'), + secondary_rid_base=dict(required=False, type='int'), + + # additional + setup_ca=dict(required=False, type='bool', default=False), + _hostname_overridden=dict(required=False, type='bool', + default=False), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # initialize return values for flake ############################ + + # These are set by ca.install_check + options._subject_base = None + options._ca_subject = None + + # set values #################################################### + + options.force = ansible_module.params.get('force') + options.dm_password = ansible_module.params.get('dm_password') + options.admin_password = ansible_module.params.get('password') + options.ip_addresses = ansible_module_get_parsed_ip_addresses( + ansible_module) + options.domain_name = ansible_module.params.get('domain') + options.realm_name = ansible_module.params.get('realm') + options.host_name = ansible_module.params.get('hostname') + options.ca_cert_files = ansible_module.params.get('ca_cert_files') + options.no_host_dns = ansible_module.params.get('no_host_dns') + # server + options.setup_adtrust = ansible_module.params.get('setup_adtrust') + options.setup_kra = ansible_module.params.get('setup_kra') + options.setup_dns = ansible_module.params.get('setup_dns') + # options.no_pkinit = ansible_module.params.get('no_pkinit') + # ssl certificate + # options.dirsrv_cert_files = ansible_module.params.get( + # 'dirsrv_cert_files') + # client + # options.no_ntp = ansible_module.params.get('no_ntp') + # certificate system + options.external_ca = ansible_module.params.get('external_ca') + options.external_ca_type = ansible_module.params.get('external_ca_type') + options.external_ca_profile = ansible_module.params.get( + 'external_ca_profile') + options.external_cert_files = ansible_module.params.get( + 'external_cert_files') + options.subject_base = ansible_module.params.get('subject_base') + options.ca_subject = ansible_module.params.get('ca_subject') + # dns + options.allow_zone_overlap = ansible_module.params.get( + 'allow_zone_overlap') + options.reverse_zones = ansible_module.params.get('reverse_zones') + options.no_reverse = ansible_module.params.get('no_reverse') + options.auto_reverse = ansible_module.params.get('auto_reverse') + options.forwarders = ansible_module.params.get('forwarders') + options.no_forwarders = ansible_module.params.get('no_forwarders') + options.auto_forwarders = ansible_module.params.get('auto_forwarders') + options.forward_policy = ansible_module.params.get('forward_policy') + options.no_dnssec_validation = ansible_module.params.get( + 'no_dnssec_validation') + # ad trust + options.enable_compat = ansible_module.params.get('enable_compat') + options.netbios_name = ansible_module.params.get('netbios_name') + # additional + options.setup_ca = ansible_module.params.get('setup_ca') + options._host_name_overridden = ansible_module.params.get( + '_hostname_overridden') + options.kasp_db_file = None + + # init ################################################################## + + fstore = sysrestore.FileStore(paths.SYSRESTORE) + sstore = sysrestore.StateFile(paths.SYSRESTORE) + + # subject_base + if not options.subject_base: + options.subject_base = str(default_subject_base(options.realm_name)) + # set options.subject for old ipa releases + options.subject = options.subject_base + + if not options.ca_subject: + options.ca_subject = str(default_ca_subject_dn(options.subject_base)) + + try: + + # Configuration for ipalib, we will bootstrap and finalize later, after + # we are sure we have the configuration file ready. + cfg = dict( + context='installer', + confdir=paths.ETC_IPA, + in_server=True, + # make sure host name specified by user is used instead of default + host=options.host_name, + ) + if options.setup_ca: + # we have an IPA-integrated CA + cfg['ca_host'] = options.host_name + + # Create the management framework config file and finalize api + target_fname = paths.IPA_DEFAULT_CONF + fd = open(target_fname, "w") + fd.write("[global]\n") + fd.write("host=%s\n" % options.host_name) + fd.write("basedn=%s\n" % ipautil.realm_to_suffix(options.realm_name)) + fd.write("realm=%s\n" % options.realm_name) + fd.write("domain=%s\n" % options.domain_name) + fd.write("xmlrpc_uri=https://%s/ipa/xml\n" % + ipautil.format_netloc(options.host_name)) + fd.write("ldap_uri=ldapi://%%2fvar%%2frun%%2fslapd-%s.socket\n" % + installutils.realm_to_serverid(options.realm_name)) + if options.setup_ca: + fd.write("enable_ra=True\n") + fd.write("ra_plugin=dogtag\n") + fd.write("dogtag_version=10\n") + else: + fd.write("enable_ra=False\n") + fd.write("ra_plugin=none\n") + fd.write("mode=production\n") + fd.close() + + # Must be readable for everyone + os.chmod(target_fname, 0o644) + + api.bootstrap(**cfg) + api.finalize() + + if options.setup_ca: + with redirect_stdout(ansible_log): + ca.install_check(False, None, options) + if options.setup_kra: + with redirect_stdout(ansible_log): + kra.install_check(api, None, options) + + if options.setup_dns: + with redirect_stdout(ansible_log): + dns.install_check(False, api, False, options, + options.host_name) + ip_addresses = dns.ip_addresses + else: + ip_addresses = get_server_ip_address(options.host_name, + not options.interactive, + False, + options.ip_addresses) + + # check addresses here, dns module is doing own check + no_matching_interface_for_ip_address_warning(ip_addresses) + options.ip_addresses = ip_addresses + options.reverse_zones = dns.reverse_zones + + instance_name = "-".join(options.realm_name.split(".")) + dirsrv = services.knownservices.dirsrv + if options.external_cert_files \ + and dirsrv.is_installed(instance_name) \ + and not dirsrv.is_running(instance_name): + logger.debug('Starting Directory Server') + services.knownservices.dirsrv.start(instance_name) + + if options.setup_adtrust: + with redirect_stdout(ansible_log): + adtrust.install_check(False, options, api) + + _update_hosts_file = False + # options needs to update hosts file when DNS subsystem will be + # installed or custom addresses are used + if options.ip_addresses or options.setup_dns: + _update_hosts_file = True + + if options._host_name_overridden: + tasks.backup_hostname(fstore, sstore) + tasks.set_hostname(options.host_name) + + if _update_hosts_file: + update_hosts_file(ip_addresses, options.host_name, fstore) + + if hasattr(tasks, "configure_pkcs11_modules"): + if tasks.configure_pkcs11_modules(fstore): + ansible_log.info("Disabled p11-kit-proxy") + + except (RuntimeError, ValueError, ScriptError, + ipautil.CalledProcessError) as e: + ansible_module.fail_json(msg=str(e)) + + ansible_module.exit_json( + changed=True, + # basic + ip_addresses=[str(ip) for ip in ip_addresses], + # certificate system + subject_base=options.subject_base, + _subject_base=options._subject_base, + ca_subject=options.ca_subject, + _ca_subject=options._ca_subject, + # dns + reverse_zones=options.reverse_zones, + forward_policy=options.forward_policy, + forwarders=options.forwarders, + no_dnssec_validation=options.no_dnssec_validation, + # additional + dns_ip_addresses=[str(ip) for ip + in dns.ip_addresses], + dns_reverse_zones=dns.reverse_zones, + adtrust_netbios_name=adtrust.netbios_name, + adtrust_reset_netbios_name=adtrust.reset_netbios_name) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaserver/library/ipaserver_set_ds_password.py b/roles/ipaserver/library/ipaserver_set_ds_password.py new file mode 100644 index 0000000..da6ef68 --- /dev/null +++ b/roles/ipaserver/library/ipaserver_set_ds_password.py @@ -0,0 +1,201 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaserver_set_ds_password +short description: Set DS password +description: Set DS password +options: + dm_password: + description: Directory Manager password + required: no + password: + description: Admin user kerberos password + required: no + domain: + description: Primary DNS domain of the IPA deployment + required: no + realm: + description: Kerberos realm name of the IPA deployment + required: no + hostname: + description: Fully qualified name of this host + required: no + setup_ca: + description: Configure a dogtag CA + required: no + idstart: + description: The starting value for the IDs range (default random) + required: no + idmax: + description: The max value for the IDs range (default: idstart+199999) + required: no + no_hbac_allow: + description: Don't install allow_all HBAC rule + required: yes + no_pkinit: + description: Disable pkinit setup steps + required: yes + dirsrv_config_file: + description: + The path to LDIF file that will be used to modify configuration of + dse.ldif during installation of the directory server instance + required: yes + _dirsrv_pkcs12_info: + description: The installer _dirsrv_pkcs12_info setting + required: yes + dirsrv_cert_files: + description: + Files containing the Directory Server SSL certificate and private key + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: yes + ca_subject: + description: The installer ca_subject setting + required: yes + external_cert_files: + description: + File containing the IPA CA certificate and the external CA certificate + chain + required: yes + domainlevel: + description: The domain level + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_server import ( + MAX_DOMAIN_LEVEL, AnsibleModuleLog, options, sysrestore, paths, + api_Backend_ldap2, ds_init_info, redirect_stdout +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # basic + dm_password=dict(required=True, no_log=True), + password=dict(required=True, no_log=True), + domain=dict(required=True), + realm=dict(required=True), + hostname=dict(required=True), + # server + setup_ca=dict(required=True, type='bool'), + idstart=dict(required=True, type='int'), + idmax=dict(required=True, type='int'), + no_hbac_allow=dict(required=False, type='bool', default=False), + no_pkinit=dict(required=False, type='bool', default=False), + dirsrv_config_file=dict(required=False), + _dirsrv_pkcs12_info=dict(required=False), + # ssl certificate + dirsrv_cert_files=dict(required=False, type='list', default=[]), + subject_base=dict(required=False), + ca_subject=dict(required=False), + # certificate system + external_cert_files=dict(required=False, type='list', default=[]), + # additional + domainlevel=dict(required=False, type='int', + default=MAX_DOMAIN_LEVEL), + ), + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # set values #################################################### + + # basic + options.dm_password = ansible_module.params.get('dm_password') + options.admin_password = ansible_module.params.get('password') + options.domain_name = ansible_module.params.get('domain') + options.realm_name = ansible_module.params.get('realm') + options.host_name = ansible_module.params.get('hostname') + # server + options.setup_ca = ansible_module.params.get('setup_ca') + options.idstart = ansible_module.params.get('idstart') + options.idmax = ansible_module.params.get('idmax') + options.no_hbac_allow = ansible_module.params.get('no_hbac_allow') + options.no_pkinit = ansible_module.params.get('no_pkinit') + options.dirsrv_config_file = ansible_module.params.get( + 'dirsrv_config_file') + options._dirsrv_pkcs12_info = ansible_module.params.get( + '_dirsrv_pkcs12_info') + # ssl certificate + options.dirsrv_cert_files = ansible_module.params.get('dirsrv_cert_files') + options.subject_base = ansible_module.params.get('subject_base') + options.ca_subject = ansible_module.params.get('ca_subject') + # certificate system + options.external_cert_files = ansible_module.params.get( + 'external_cert_files') + # additional + options.domainlevel = ansible_module.params.get('domainlevel') + options.domain_level = options.domainlevel + + # init ########################################################## + + fstore = sysrestore.FileStore(paths.SYSRESTORE) + + api_Backend_ldap2(options.host_name, options.setup_ca, connect=True) + + ds = ds_init_info(ansible_log, fstore, + options.domainlevel, options.dirsrv_config_file, + options.realm_name, options.host_name, + options.domain_name, options.dm_password, + options.idstart, options.idmax, + options.subject_base, options.ca_subject, + options.no_hbac_allow, options._dirsrv_pkcs12_info, + options.no_pkinit) + + # set ds password ############################################### + + with redirect_stdout(ansible_log): + ds.change_admin_password(options.admin_password) + + # done ########################################################## + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaserver/library/ipaserver_setup_adtrust.py b/roles/ipaserver/library/ipaserver_setup_adtrust.py new file mode 100644 index 0000000..4838f2b --- /dev/null +++ b/roles/ipaserver/library/ipaserver_setup_adtrust.py @@ -0,0 +1,133 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaserver_setup_adtrust +short description: Setup trust ad +description: Setup trust ad +options: + hostname: + description: Fully qualified name of this host + required: yes + setup_ca: + description: Configure a dogtag CA + required: yes + setup_adtrust: + description: Configure AD trust capability + required: yes + enable_compat: + description: Enable support for trusted domains for old clients + required: yes + rid_base: + description: Start value for mapping UIDs and GIDs to RIDs + required: yes + secondary_rid_base: + description: + Start value of the secondary range for mapping UIDs and GIDs to RIDs + required: yes + adtrust_netbios_name: + description: The adtrust netbios_name setting + required: no + adtrust_reset_netbios_name: + description: The adtrust reset_netbios_name setting + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_server import ( + AnsibleModuleLog, options, sysrestore, paths, + api_Backend_ldap2, redirect_stdout, adtrust, api +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # basic + hostname=dict(required=False), + setup_ca=dict(required=False, type='bool', default=False), + setup_adtrust=dict(required=False, type='bool', default=False), + # ad trust + enable_compat=dict(required=False, type='bool', default=False), + rid_base=dict(required=False, type='int'), + secondary_rid_base=dict(required=False, type='int'), + # additional + adtrust_netbios_name=dict(required=True), + adtrust_reset_netbios_name=dict(required=True, type='bool'), + ), + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # set values #################################################### + + options.host_name = ansible_module.params.get('hostname') + options.setup_ca = ansible_module.params.get('setup_ca') + options.setup_adtrust = ansible_module.params.get('setup_adtrust') + # ad trust + options.enable_compat = ansible_module.params.get('enable_compat') + options.rid_base = ansible_module.params.get('rid_base') + options.secondary_rid_base = ansible_module.params.get( + 'secondary_rid_base') + # additional + adtrust.netbios_name = ansible_module.params.get('adtrust_netbios_name') + adtrust.reset_netbios_name = ansible_module.params.get( + 'adtrust_reset_netbios_name') + + # init ########################################################## + + fstore = sysrestore.FileStore(paths.SYSRESTORE) + + api_Backend_ldap2(options.host_name, options.setup_ca, connect=True) + + # setup ds ###################################################### + + with redirect_stdout(ansible_log): + adtrust.install(False, options, fstore, api) + + # done ########################################################## + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaserver/library/ipaserver_setup_ca.py b/roles/ipaserver/library/ipaserver_setup_ca.py new file mode 100644 index 0000000..374b20b --- /dev/null +++ b/roles/ipaserver/library/ipaserver_setup_ca.py @@ -0,0 +1,354 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaserver_setup_ca +short description: Setup CA +description: Setup CA +options: + dm_password: + description: Directory Manager password + required: no + password: + description: Admin user kerberos password + required: no + master_password: + description: kerberos master password (normally autogenerated) + required: no + ip_addresses: + description: List of Master Server IP Addresses + required: yes + domain: + description: Primary DNS domain of the IPA deployment + required: no + realm: + description: Kerberos realm name of the IPA deployment + required: no + hostname: + description: Fully qualified name of this host + required: yes + no_host_dns: + description: Do not use DNS for hostname lookup during installation + required: yes + pki_config_override: + description: Path to ini file with config overrides + required: yes + setup_adtrust: + description: Configure AD trust capability + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + setup_dns: + description: Configure bind with our zone + required: yes + setup_ca: + description: Configure a dogtag CA + required: yes + idstart: + description: The starting value for the IDs range (default random) + required: no + idmax: + description: The max value for the IDs range (default: idstart+199999) + required: no + no_hbac_allow: + description: Don't install allow_all HBAC rule + required: yes + no_pkinit: + description: Disable pkinit setup steps + required: yes + dirsrv_config_file: + description: + The path to LDIF file that will be used to modify configuration of + dse.ldif during installation of the directory server instance + required: yes + dirsrv_cert_files: + description: + Files containing the Directory Server SSL certificate and private key + required: yes + _dirsrv_pkcs12_info: + description: The installer _dirsrv_pkcs12_info setting + required: yes + external_ca: + description: External ca setting + required: yes + external_ca_type: + description: Type of the external CA + required: yes + external_ca_profile: + description: + Specify the certificate profile/template to use at the external CA + required: yes + external_cert_files: + description: + File containing the IPA CA certificate and the external CA certificate + chain + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: yes + _subject_base: + description: The installer _subject_base setting + required: yes + ca_subject: + description: The installer ca_subject setting + required: yes + _ca_subject: + description: The installer _ca_subject setting + required: yes + ca_signing_algorithm: + description: Signing algorithm of the IPA CA certificate + required: yes + reverse_zones: + description: The reverse DNS zones to use + required: yes + no_reverse: + description: Do not create new reverse DNS zone + required: yes + auto_forwarders: + description: Use DNS forwarders configured in /etc/resolv.conf + required: yes + domainlevel: + description: The domain level + required: yes + _http_ca_cert: + description: The installer _http_ca_cert setting + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_server import ( + AnsibleModuleLog, options, sysrestore, paths, + ansible_module_get_parsed_ip_addresses, + api_Backend_ldap2, redirect_stdout, ca, installutils, ds_init_info, + custodiainstance, write_cache, x509 +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # basic + dm_password=dict(required=True, no_log=True), + password=dict(required=True, no_log=True), + master_password=dict(required=True, no_log=True), + ip_addresses=dict(required=False, type='list', default=[]), + domain=dict(required=True), + realm=dict(required=True), + hostname=dict(required=False), + no_host_dns=dict(required=False, type='bool', default=False), + pki_config_override=dict(required=False), + # server + setup_adtrust=dict(required=False, type='bool', default=False), + setup_kra=dict(required=False, type='bool', default=False), + setup_dns=dict(required=False, type='bool', default=False), + setup_ca=dict(required=False, type='bool', default=False), + idstart=dict(required=True, type='int'), + idmax=dict(required=True, type='int'), + no_hbac_allow=dict(required=False, type='bool', default=False), + no_pkinit=dict(required=False, type='bool', default=False), + dirsrv_config_file=dict(required=False), + dirsrv_cert_files=dict(required=False, type='list'), + _dirsrv_pkcs12_info=dict(required=False), + # certificate system + external_ca=dict(required=False, type='bool', default=False), + external_ca_type=dict(required=False), + external_ca_profile=dict(required=False), + external_cert_files=dict(required=False, type='list', + default=None), + subject_base=dict(required=False), + _subject_base=dict(required=False), + ca_subject=dict(required=False), + _ca_subject=dict(required=False), + ca_signing_algorithm=dict(required=False), + # dns + reverse_zones=dict(required=False, type='list', default=[]), + no_reverse=dict(required=False, type='bool', default=False), + auto_forwarders=dict(required=False, type='bool', default=False), + # additional + domainlevel=dict(required=False, type='int'), + _http_ca_cert=dict(required=False), + ), + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # set values ############################################################ + + # basic + options.dm_password = ansible_module.params.get('dm_password') + options.admin_password = ansible_module.params.get('password') + options.master_password = ansible_module.params.get('master_password') + options.ip_addresses = ansible_module_get_parsed_ip_addresses( + ansible_module) + options.domain_name = ansible_module.params.get('domain') + options.realm_name = ansible_module.params.get('realm') + options.host_name = ansible_module.params.get('hostname') + options.no_host_dns = ansible_module.params.get('no_host_dns') + options.pki_config_override = ansible_module.params.get( + 'pki_config_override') + # server + options.setup_adtrust = ansible_module.params.get('setup_adtrust') + options.setup_kra = ansible_module.params.get('setup_kra') + options.setup_dns = ansible_module.params.get('setup_dns') + options.setup_ca = ansible_module.params.get('setup_ca') + options.idstart = ansible_module.params.get('idstart') + options.idmax = ansible_module.params.get('idmax') + options.no_hbac_allow = ansible_module.params.get('no_hbac_allow') + options.no_pkinit = ansible_module.params.get('no_pkinit') + options.dirsrv_config_file = ansible_module.params.get( + 'dirsrv_config_file') + options.dirsrv_cert_files = ansible_module.params.get('dirsrv_cert_files') + options._dirsrv_pkcs12_info = ansible_module.params.get( + '_dirsrv_pkcs12_info') + # certificate system + options.external_ca = ansible_module.params.get('external_ca') + options.external_ca_type = ansible_module.params.get('external_ca_type') + options.external_ca_profile = ansible_module.params.get( + 'external_ca_profile') + options.external_cert_files = ansible_module.params.get( + 'external_cert_files') + options.subject_base = ansible_module.params.get('subject_base') + options._subject_base = ansible_module.params.get('_subject_base') + options.ca_subject = ansible_module.params.get('ca_subject') + options._ca_subject = ansible_module.params.get('_ca_subject') + options.ca_signing_algorithm = ansible_module.params.get( + 'ca_signing_algorithm') + # dns + options.reverse_zones = ansible_module.params.get('reverse_zones') + options.no_reverse = ansible_module.params.get('no_reverse') + options.auto_forwarders = ansible_module.params.get('auto_forwarders') + # additional + options.domainlevel = ansible_module.params.get('domainlevel') + options._http_ca_cert = ansible_module.params.get('_http_ca_cert') + # tions._update_hosts_file = ansible_module.params.get( + # 'update_hosts_file') + + # init ################################################################# + + options.promote = False # first master, no promotion + + # Repeat from ca.install_check + # ca.external_cert_file and ca.external_ca_file need to be set + if options.external_cert_files: + ca.external_cert_file, ca.external_ca_file = \ + installutils.load_external_cert( + options.external_cert_files, options._ca_subject) + + fstore = sysrestore.FileStore(paths.SYSRESTORE) + + api_Backend_ldap2(options.host_name, options.setup_ca, connect=True) + + ds = ds_init_info(ansible_log, fstore, + options.domainlevel, options.dirsrv_config_file, + options.realm_name, options.host_name, + options.domain_name, options.dm_password, + options.idstart, options.idmax, + options.subject_base, options.ca_subject, + options.no_hbac_allow, options._dirsrv_pkcs12_info, + options.no_pkinit) + + # setup CA ############################################################## + + if hasattr(custodiainstance, "get_custodia_instance"): + if hasattr(custodiainstance.CustodiaModes, "FIRST_MASTER"): + mode = custodiainstance.CustodiaModes.FIRST_MASTER + else: + mode = custodiainstance.CustodiaModes.MASTER_PEER + custodia = custodiainstance.get_custodia_instance(options, mode) + custodia.set_output(ansible_log) + with redirect_stdout(ansible_log): + custodia.create_instance() + + if options.setup_ca: + if not options.external_cert_files and options.external_ca: + # stage 1 of external CA installation + cache_vars = {n: options.__dict__[n] for o, n in options.knobs() + if n in options.__dict__} + write_cache(cache_vars) + + try: + with redirect_stdout(ansible_log): + if hasattr(custodiainstance, "get_custodia_instance"): + ca.install_step_0(False, None, options, custodia=custodia) + else: + ca.install_step_0(False, None, options) + except SystemExit: + ansible_module.exit_json(changed=True, + csr_generated=True) + else: + # Put the CA cert where other instances expect it + x509.write_certificate(options._http_ca_cert, paths.IPA_CA_CRT) + os.chmod(paths.IPA_CA_CRT, 0o444) + + if not options.no_pkinit: + x509.write_certificate(options._http_ca_cert, + paths.KDC_CA_BUNDLE_PEM) + else: + with open(paths.KDC_CA_BUNDLE_PEM, 'w'): + pass + os.chmod(paths.KDC_CA_BUNDLE_PEM, 0o444) + + x509.write_certificate(options._http_ca_cert, paths.CA_BUNDLE_PEM) + os.chmod(paths.CA_BUNDLE_PEM, 0o444) + + with redirect_stdout(ansible_log): + # we now need to enable ssl on the ds + ds.enable_ssl() + + if options.setup_ca: + with redirect_stdout(ansible_log): + if hasattr(custodiainstance, "get_custodia_instance"): + ca.install_step_1(False, None, options, custodia=custodia) + else: + ca.install_step_1(False, None, options) + + ansible_module.exit_json(changed=True, + csr_generated=False) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaserver/library/ipaserver_setup_custodia.py b/roles/ipaserver/library/ipaserver_setup_custodia.py new file mode 100644 index 0000000..2514e06 --- /dev/null +++ b/roles/ipaserver/library/ipaserver_setup_custodia.py @@ -0,0 +1,111 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaserver_setup_custodia +short description: Setup custodia +description: Setup custodia +options: + realm: + description: Kerberos realm name of the IPA deployment + required: no + hostname: + description: Fully qualified name of this host + required: yes + setup_ca: + description: Configure a dogtag CA + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_server import ( + AnsibleModuleLog, options, + api_Backend_ldap2, + custodiainstance, redirect_stdout +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # basic + realm=dict(required=True), + hostname=dict(required=False), + setup_ca=dict(required=False, type='bool', default=False), + ), + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # set values ############################################################ + + options.realm_name = ansible_module.params.get('realm') + options.host_name = ansible_module.params.get('hostname') + options.setup_ca = ansible_module.params.get('setup_ca') + options.promote = False + + # init ################################################################## + + api_Backend_ldap2(options.host_name, options.setup_ca, connect=True) + + # setup custodia ######################################################## + + if hasattr(custodiainstance, "get_custodia_instance"): + if hasattr(custodiainstance.CustodiaModes, "FIRST_MASTER"): + mode = custodiainstance.CustodiaModes.FIRST_MASTER + else: + mode = custodiainstance.CustodiaModes.MASTER_PEER + custodia = custodiainstance.get_custodia_instance(options, mode) + else: + custodia = custodiainstance.CustodiaInstance(options.host_name, + options.realm_name) + custodia.set_output(ansible_log) + with redirect_stdout(ansible_log): + custodia.create_instance() + + # done ################################################################## + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaserver/library/ipaserver_setup_dns.py b/roles/ipaserver/library/ipaserver_setup_dns.py new file mode 100644 index 0000000..ae1429a --- /dev/null +++ b/roles/ipaserver/library/ipaserver_setup_dns.py @@ -0,0 +1,169 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaserver_setup_dns +short description: Setup DNS +description: Setup DNS +options: + ip_addresses: + description: List of Master Server IP Addresses + required: yes + domain: + description: Primary DNS domain of the IPA deployment + required: no + realm: + description: Kerberos realm name of the IPA deployment + required: no + hostname: + description: Fully qualified name of this host + required: no + setup_dns: + description: Configure bind with our zone + required: no + setup_ca: + description: Configure a dogtag CA + required: no + zonemgr: + description: DNS zone manager e-mail address. Defaults to hostmaster@DOMAIN + required: yes + forwarders: + description: Add DNS forwarders + required: no + forward_policy: + description: DNS forwarding policy for global forwarders + required: yes + no_dnssec_validation: + description: Disable DNSSEC validation + required: yes + dns_ip_addresses: + description: The dns ip_addresses setting + required: no + dns_reverse_zones: + description: The dns reverse_zones setting + required: no +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_server import ( + AnsibleModuleLog, options, paths, dns, + ansible_module_get_parsed_ip_addresses, sysrestore, api_Backend_ldap2, + redirect_stdout, bindinstance +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # basic + ip_addresses=dict(required=False, type='list', default=[]), + domain=dict(required=True), + realm=dict(required=True), + hostname=dict(required=True), + # server + setup_dns=dict(required=True, type='bool'), + setup_ca=dict(required=True, type='bool'), + # dns + zonemgr=dict(required=False), + forwarders=dict(required=True, type='list'), + forward_policy=dict(default='first', choices=['first', 'only']), + no_dnssec_validation=dict(required=False, type='bool', + default=False), + # additional + dns_ip_addresses=dict(required=True, type='list'), + dns_reverse_zones=dict(required=True, type='list'), + ), + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # set values ############################################################ + + # basic + options.ip_addresses = ansible_module_get_parsed_ip_addresses( + ansible_module) + options.domain_name = ansible_module.params.get('domain') + options.realm_name = ansible_module.params.get('realm') + options.host_name = ansible_module.params.get('hostname') + # server + options.setup_dns = ansible_module.params.get('setup_dns') + options.setup_ca = ansible_module.params.get('setup_ca') + # dns + options.zonemgr = ansible_module.params.get('zonemgr') + options.forwarders = ansible_module.params.get('forwarders') + options.forward_policy = ansible_module.params.get('forward_policy') + options.no_dnssec_validation = ansible_module.params.get( + 'no_dnssec_validation') + # additional + dns.ip_addresses = ansible_module_get_parsed_ip_addresses( + ansible_module, 'dns_ip_addresses') + dns.reverse_zones = ansible_module.params.get('dns_reverse_zones') + + # init ################################################################## + + fstore = sysrestore.FileStore(paths.SYSRESTORE) + + api_Backend_ldap2(options.host_name, options.setup_ca, connect=True) + + # setup dns ############################################################# + + with redirect_stdout(ansible_log): + if options.setup_dns: + dns.install(False, False, options) + else: + # Create a BIND instance + bind = bindinstance.BindInstance(fstore) + bind.set_output(ansible_log) + bind.setup(options.host_name, options.ip_addresses, + options.realm_name, + options.domain_name, (), 'first', (), + zonemgr=options.zonemgr, + no_dnssec_validation=options.no_dnssec_validation) + bind.create_file_with_system_records() + + # done ################################################################## + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaserver/library/ipaserver_setup_ds.py b/roles/ipaserver/library/ipaserver_setup_ds.py new file mode 100644 index 0000000..5ce35f4 --- /dev/null +++ b/roles/ipaserver/library/ipaserver_setup_ds.py @@ -0,0 +1,230 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaserver_setup_ds +short description: Configure directory server +description: Configure directory server +options: + dm_password: + description: Directory Manager password + required: no + password: + description: Admin user kerberos password + required: no + domain: + description: Primary DNS domain of the IPA deployment + required: no + realm: + description: Kerberos realm name of the IPA deployment + required: no + hostname: + description: Fully qualified name of this host + required: yes + idstart: + description: The starting value for the IDs range (default random) + required: no + idmax: + description: The max value for the IDs range (default: idstart+199999) + required: no + no_hbac_allow: + description: Don't install allow_all HBAC rule + required: yes + no_pkinit: + description: Disable pkinit setup steps + required: yes + dirsrv_config_file: + description: + The path to LDIF file that will be used to modify configuration of + dse.ldif during installation of the directory server instance + required: yes + dirsrv_cert_files: + description: + Files containing the Directory Server SSL certificate and private key + required: yes + _dirsrv_pkcs12_info: + description: The installer _dirsrv_pkcs12_info setting + required: yes + external_cert_files: + description: + File containing the IPA CA certificate and the external CA certificate + chain + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: yes + ca_subject: + description: The installer ca_subject setting + required: yes + setup_ca: + description: Configure a dogtag CA + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_server import ( + AnsibleModuleLog, options, sysrestore, paths, + api_Backend_ldap2, redirect_stdout, api, NUM_VERSION, tasks, + dsinstance, ntpinstance, IPAAPI_USER +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # basic + dm_password=dict(required=True, no_log=True), + password=dict(required=True, no_log=True), + domain=dict(required=True), + realm=dict(required=True), + hostname=dict(required=False), + # server + idstart=dict(required=True, type='int'), + idmax=dict(required=True, type='int'), + no_hbac_allow=dict(required=False, type='bool', default=False), + no_pkinit=dict(required=False, type='bool', default=False), + dirsrv_config_file=dict(required=False), + # ssl certificate + dirsrv_cert_files=dict(required=False, type='list', default=[]), + _dirsrv_pkcs12_info=dict(required=False), + # certificate system + external_cert_files=dict(required=False, type='list', default=[]), + subject_base=dict(required=False), + ca_subject=dict(required=False), + + # additional + setup_ca=dict(required=False, type='bool', default=False), + ), + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # set values ############################################################ + + # basic + options.dm_password = ansible_module.params.get('dm_password') + options.domain_name = ansible_module.params.get('domain') + options.realm_name = ansible_module.params.get('realm') + options.host_name = ansible_module.params.get('hostname') + # server + options.idstart = ansible_module.params.get('idstart') + options.idmax = ansible_module.params.get('idmax') + options.no_pkinit = ansible_module.params.get('no_pkinit') + options.no_hbac_allow = ansible_module.params.get('no_hbac_allow') + options.dirsrv_config_file = ansible_module.params.get( + 'dirsrv_config_file') + options._dirsrv_pkcs12_info = ansible_module.params.get( + '_dirsrv_pkcs12_info') + # ssl certificate + options.dirsrv_cert_files = ansible_module.params.get('dirsrv_cert_files') + # certificate system + options.external_cert_files = ansible_module.params.get( + 'external_cert_files') + options.subject_base = ansible_module.params.get('subject_base') + options.ca_subject = ansible_module.params.get('ca_subject') + + # additional + options.setup_ca = ansible_module.params.get('setup_ca') + + # init ################################################################## + + fstore = sysrestore.FileStore(paths.SYSRESTORE) + + # api Backend connect only if external_cert_files is not set + api_Backend_ldap2(options.host_name, options.setup_ca, connect=False) + + # setup DS ############################################################## + + # Make sure tmpfiles dir exist before installing components + if NUM_VERSION == 40504: + tasks.create_tmpfiles_dirs(IPAAPI_USER) + elif 40500 <= NUM_VERSION <= 40503: + tasks.create_tmpfiles_dirs() + + # Create a directory server instance + if not options.external_cert_files: + ds = dsinstance.DsInstance(fstore=fstore, + domainlevel=options.domainlevel, + config_ldif=options.dirsrv_config_file) + ds.set_output(ansible_log) + + if options.dirsrv_cert_files: + _dirsrv_pkcs12_info = options._dirsrv_pkcs12_info + else: + _dirsrv_pkcs12_info = None + + with redirect_stdout(ansible_log): + ds.create_instance(options.realm_name, options.host_name, + options.domain_name, + options.dm_password, _dirsrv_pkcs12_info, + idstart=options.idstart, idmax=options.idmax, + subject_base=options.subject_base, + ca_subject=options.ca_subject, + hbac_allow=not options.no_hbac_allow, + setup_pkinit=not options.no_pkinit) + if not options.dirsrv_cert_files and NUM_VERSION < 40690: + ntpinstance.ntp_ldap_enable(options.host_name, ds.suffix, + options.realm_name) + + else: + api.Backend.ldap2.connect() + + ds = dsinstance.DsInstance(fstore=fstore, + domainlevel=options.domainlevel) + ds.set_output(ansible_log) + + with redirect_stdout(ansible_log): + ds.init_info( + options.realm_name, options.host_name, options.domain_name, + options.dm_password, + options.subject_base, options.ca_subject, 1101, 1100, None, + setup_pkinit=not options.no_pkinit) + + # done ################################################################## + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaserver/library/ipaserver_setup_http.py b/roles/ipaserver/library/ipaserver_setup_http.py new file mode 100644 index 0000000..a29a044 --- /dev/null +++ b/roles/ipaserver/library/ipaserver_setup_http.py @@ -0,0 +1,325 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaserver_setup_http +short description: Setup HTTP +description: Setup HTTP +options: + dm_password: + description: Directory Manager password + required: no + password: + description: Admin user kerberos password + required: no + master_password: + description: kerberos master password (normally autogenerated) + required: no + domain: + description: Primary DNS domain of the IPA deployment + required: no + realm: + description: Kerberos realm name of the IPA deployment + required: no + hostname: + description: Fully qualified name of this host + required: yes + ip_addresses: + description: List of Master Server IP Addresses + required: yes + reverse_zones: + description: The reverse DNS zones to use + required: yes + http_cert_files: + description: + File containing the Apache Server SSL certificate and private key + required: yes + setup_adtrust: + description: Configure AD trust capability + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + setup_dns: + description: Configure bind with our zone + required: yes + setup_ca: + description: Configure a dogtag CA + required: yes + no_host_dns: + description: Do not use DNS for hostname lookup during installation + required: yes + no_pkinit: + description: Disable pkinit setup steps + required: yes + no_hbac_allow: + description: Don't install allow_all HBAC rule + required: yes + no_ui_redirect: + description: Do not automatically redirect to the Web UI + required: yes + external_cert_files: + description: + File containing the IPA CA certificate and the external CA certificate + chain + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: yes + _subject_base: + description: The installer _subject_base setting + required: yes + ca_subject: + description: The installer ca_subject setting + required: yes + _ca_subject: + description: The installer _ca_subject setting + required: yes + idstart: + description: The starting value for the IDs range (default random) + required: no + idmax: + description: The max value for the IDs range (default: idstart+199999) + required: no + domainlevel: + description: The domain level + required: yes + dirsrv_config_file: + description: + The path to LDIF file that will be used to modify configuration of + dse.ldif during installation of the directory server instance + required: yes + dirsrv_cert_files: + description: + Files containing the Directory Server SSL certificate and private key + required: yes + no_reverse: + description: Do not create new reverse DNS zone + required: yes + auto_forwarders: + description: Use DNS forwarders configured in /etc/resolv.conf + required: yes + _dirsrv_pkcs12_info: + description: The installer _dirsrv_pkcs12_info setting + required: yes + _http_pkcs12_info: + description: The installer _http_pkcs12_info setting + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_server import ( + AnsibleModuleLog, options, sysrestore, paths, + ansible_module_get_parsed_ip_addresses, + api_Backend_ldap2, redirect_stdout, ds_init_info, + krbinstance, httpinstance, ca, service, tasks +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # basic + dm_password=dict(required=True, no_log=True), + password=dict(required=True, no_log=True), + master_password=dict(required=True, no_log=True), + domain=dict(required=True), + realm=dict(required=True), + hostname=dict(required=False), + + ip_addresses=dict(required=False, type='list', default=[]), + reverse_zones=dict(required=False, type='list', default=[]), + http_cert_files=dict(required=False, type='list', default=[]), + + setup_adtrust=dict(required=False, type='bool', default=False), + setup_kra=dict(required=False, type='bool', default=False), + setup_dns=dict(required=False, type='bool', default=False), + setup_ca=dict(required=False, type='bool', default=False), + + no_host_dns=dict(required=False, type='bool', default=False), + no_pkinit=dict(required=False, type='bool', default=False), + no_hbac_allow=dict(required=False, type='bool', default=False), + + no_ui_redirect=dict(required=False, type='bool', default=False), + + external_cert_files=dict(required=False, type='list', default=[]), + subject_base=dict(required=False), + _subject_base=dict(required=False), + ca_subject=dict(required=False), + _ca_subject=dict(required=False), + + idstart=dict(required=True, type='int'), + idmax=dict(required=True, type='int'), + domainlevel=dict(required=False, type='int'), + dirsrv_config_file=dict(required=False), + dirsrv_cert_files=dict(required=False, type='list', default=[]), + + no_reverse=dict(required=False, type='bool', default=False), + auto_forwarders=dict(required=False, type='bool', default=False), + + # _update_hosts_file=dict(required=False, type='bool', + # default=False), + _dirsrv_pkcs12_info=dict(required=False), + _http_pkcs12_info=dict(required=False), + ), + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # set values ############################################################ + + options.dm_password = ansible_module.params.get('dm_password') + options.admin_password = ansible_module.params.get('password') + options.master_password = ansible_module.params.get('master_password') + options.domain_name = ansible_module.params.get('domain') + options.realm_name = ansible_module.params.get('realm') + options.host_name = ansible_module.params.get('hostname') + + options.ip_addresses = ansible_module_get_parsed_ip_addresses( + ansible_module) + options.reverse_zones = ansible_module.params.get('reverse_zones') + options.http_cert_files = ansible_module.params.get('http_cert_files') + + options.setup_adtrust = ansible_module.params.get('setup_adtrust') + options.setup_kra = ansible_module.params.get('setup_kra') + options.setup_dns = ansible_module.params.get('setup_dns') + options.setup_ca = ansible_module.params.get('setup_ca') + + options.no_host_dns = ansible_module.params.get('no_host_dns') + options.no_pkinit = ansible_module.params.get('no_pkinit') + options.no_hbac_allow = ansible_module.params.get('no_hbac_allow') + options.no_ui_redirect = ansible_module.params.get('no_ui_redirect') + + options.external_cert_files = ansible_module.params.get( + 'external_cert_files') + options.subject_base = ansible_module.params.get('subject_base') + options._subject_base = ansible_module.params.get('_subject_base') + options.ca_subject = ansible_module.params.get('ca_subject') + options._ca_subject = ansible_module.params.get('_ca_subject') + + options.no_reverse = ansible_module.params.get('no_reverse') + options.auto_forwarders = ansible_module.params.get('auto_forwarders') + + options.idstart = ansible_module.params.get('idstart') + options.idmax = ansible_module.params.get('idmax') + options.domainlevel = ansible_module.params.get('domainlevel') + options.dirsrv_config_file = ansible_module.params.get( + 'dirsrv_config_file') + options.dirsrv_cert_files = ansible_module.params.get('dirsrv_cert_files') + + # options._update_hosts_file = ansible_module.params.get( + # '_update_hosts_file') + options._dirsrv_pkcs12_info = ansible_module.params.get( + '_dirsrv_pkcs12_info') + options._http_pkcs12_info = ansible_module.params.get( + '_http_pkcs12_info') + + # init ################################################################## + + fstore = sysrestore.FileStore(paths.SYSRESTORE) + + api_Backend_ldap2(options.host_name, options.setup_ca, connect=True) + + ds = ds_init_info(ansible_log, fstore, + options.domainlevel, options.dirsrv_config_file, + options.realm_name, options.host_name, + options.domain_name, options.dm_password, + options.idstart, options.idmax, + options.subject_base, options.ca_subject, + options.no_hbac_allow, options._dirsrv_pkcs12_info, + options.no_pkinit) + + # krb + krb = krbinstance.KrbInstance(fstore) + krb.set_output(ansible_log) + with redirect_stdout(ansible_log): + krb.init_info(options.realm_name, options.host_name, + setup_pkinit=not options.no_pkinit, + subject_base=options.subject_base) + + # setup HTTP ############################################################ + + # Create a HTTP instance + http = httpinstance.HTTPInstance(fstore) + http.set_output(ansible_log) + with redirect_stdout(ansible_log): + if options.http_cert_files: + http.create_instance( + options.realm_name, options.host_name, options.domain_name, + options.dm_password, + pkcs12_info=options._http_pkcs12_info, + subject_base=options.subject_base, + auto_redirect=not options.no_ui_redirect, + ca_is_configured=options.setup_ca) + else: + http.create_instance( + options.realm_name, options.host_name, options.domain_name, + options.dm_password, + subject_base=options.subject_base, + auto_redirect=not options.no_ui_redirect, + ca_is_configured=options.setup_ca) + if hasattr(paths, "CACHE_IPA_SESSIONS"): + tasks.restore_context(paths.CACHE_IPA_SESSIONS) + + ca.set_subject_base_in_config(options.subject_base) + + # configure PKINIT now that all required services are in place + krb.enable_ssl() + + # Apply any LDAP updates. Needs to be done after the configuration file + # is created. DS is restarted in the process. + service.print_msg("Applying LDAP updates") + ds.apply_updates() + + # Restart krb after configurations have been changed + service.print_msg("Restarting the KDC") + krb.restart() + + # done ################################################################## + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaserver/library/ipaserver_setup_kra.py b/roles/ipaserver/library/ipaserver_setup_kra.py new file mode 100644 index 0000000..ebf03b0 --- /dev/null +++ b/roles/ipaserver/library/ipaserver_setup_kra.py @@ -0,0 +1,125 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaserver_setup_kra +short description: Setup KRA +description: Setup KRA +options: + dm_password: + description: Directory Manager password + required: no + hostname: + description: Fully qualified name of this host + required: no + setup_ca: + description: Configure a dogtag CA + required: no + setup_kra: + description: Configure a dogtag KRA + required: no + realm: + description: Kerberos realm name of the IPA deployment + required: no + pki_config_override: + description: Path to ini file with config overrides + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_server import ( + AnsibleModuleLog, options, + api_Backend_ldap2, redirect_stdout, api, custodiainstance, kra +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # basic + dm_password=dict(required=True, no_log=True), + hostname=dict(required=True), + setup_ca=dict(required=True, type='bool'), + setup_kra=dict(required=True, type='bool'), + realm=dict(required=True), + pki_config_override=dict(required=False), + ), + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # set values #################################################### + + options.dm_password = ansible_module.params.get('dm_password') + options.host_name = ansible_module.params.get('hostname') + options.setup_ca = ansible_module.params.get('setup_ca') + options.setup_kra = ansible_module.params.get('setup_kra') + options.realm_name = ansible_module.params.get('realm') + options.pki_config_override = ansible_module.params.get( + 'pki_config_override') + options.promote = False # first master, no promotion + + # init ########################################################## + + api_Backend_ldap2(options.host_name, options.setup_ca, connect=True) + + # setup kra ##################################################### + + with redirect_stdout(ansible_log): + if hasattr(custodiainstance, "get_custodia_instance"): + if hasattr(custodiainstance.CustodiaModes, "FIRST_MASTER"): + mode = custodiainstance.CustodiaModes.FIRST_MASTER + else: + mode = custodiainstance.CustodiaModes.MASTER_PEER + custodia = custodiainstance.get_custodia_instance(options, mode) + + kra.install(api, None, options, custodia=custodia) + else: + kra.install(api, None, options) + + # done ########################################################## + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaserver/library/ipaserver_setup_krb.py b/roles/ipaserver/library/ipaserver_setup_krb.py new file mode 100644 index 0000000..8690edd --- /dev/null +++ b/roles/ipaserver/library/ipaserver_setup_krb.py @@ -0,0 +1,236 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaserver_setup_krb +short description: Setup KRB +description: Setup KRB +options: + dm_password: + description: Directory Manager password + required: no + password: + description: Admin user kerberos password + required: no + master_password: + description: kerberos master password (normally autogenerated) + required: no + domain: + description: Primary DNS domain of the IPA deployment + required: no + realm: + description: Kerberos realm name of the IPA deployment + required: no + hostname: + description: Fully qualified name of this host + required: yes + ip_addresses: + description: List of Master Server IP Addresses + required: yes + reverse_zones: + description: The reverse DNS zones to use + required: yes + setup_adtrust: + description: Configure AD trust capability + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + setup_dns: + description: Configure bind with our zone + required: yes + setup_ca: + description: Configure a dogtag CA + required: yes + no_host_dns: + description: Do not use DNS for hostname lookup during installation + required: yes + no_pkinit: + description: Disable pkinit setup steps + required: yes + no_hbac_allow: + description: Don't install allow_all HBAC rule + required: yes + external_cert_files: + description: + File containing the IPA CA certificate and the external CA certificate + chain + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: yes + ca_subject: + description: The installer ca_subject setting + required: yes + idstart: + description: The starting value for the IDs range (default random) + required: no + idmax: + description: The max value for the IDs range (default: idstart+199999) + required: no + no_reverse: + description: Do not create new reverse DNS zone + required: yes + auto_forwarders: + description: Use DNS forwarders configured in /etc/resolv.conf + required: yes + _pkinit_pkcs12_info: + description: The installer _pkinit_pkcs12_info setting + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_server import ( + AnsibleModuleLog, options, sysrestore, paths, + ansible_module_get_parsed_ip_addresses, + api_Backend_ldap2, redirect_stdout, krbinstance +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # basic + dm_password=dict(required=True, no_log=True), + password=dict(required=True, no_log=True), + master_password=dict(required=True, no_log=True), + domain=dict(required=True), + realm=dict(required=True), + hostname=dict(required=False), + + ip_addresses=dict(required=False, type='list', default=[]), + reverse_zones=dict(required=False, type='list', default=[]), + + setup_adtrust=dict(required=False, type='bool', default=False), + setup_kra=dict(required=False, type='bool', default=False), + setup_dns=dict(required=False, type='bool', default=False), + setup_ca=dict(required=False, type='bool', default=False), + + no_host_dns=dict(required=False, type='bool', default=False), + no_pkinit=dict(required=False, type='bool', default=False), + no_hbac_allow=dict(required=False, type='bool', default=False), + + external_cert_files=dict(required=False, type='list', default=[]), + subject_base=dict(required=False), + ca_subject=dict(required=False), + + idstart=dict(required=True, type='int'), + idmax=dict(required=True, type='int'), + + no_reverse=dict(required=False, type='bool', default=False), + auto_forwarders=dict(required=False, type='bool', default=False), + + _pkinit_pkcs12_info=dict(required=False), + ), + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # set values ############################################################ + + options.dm_password = ansible_module.params.get('dm_password') + options.admin_password = ansible_module.params.get('password') + options.master_password = ansible_module.params.get('master_password') + options.domain_name = ansible_module.params.get('domain') + options.realm_name = ansible_module.params.get('realm') + options.host_name = ansible_module.params.get('hostname') + + options.ip_addresses = ansible_module_get_parsed_ip_addresses( + ansible_module) + options.reverse_zones = ansible_module.params.get('reverse_zones') + + options.setup_adtrust = ansible_module.params.get('setup_adtrust') + options.setup_kra = ansible_module.params.get('setup_kra') + options.setup_dns = ansible_module.params.get('setup_dns') + options.setup_ca = ansible_module.params.get('setup_ca') + + options.no_host_dns = ansible_module.params.get('no_host_dns') + options.no_pkinit = ansible_module.params.get('no_pkinit') + options.no_hbac_allow = ansible_module.params.get('no_hbac_allow') + + options.external_cert_files = ansible_module.params.get( + 'external_cert_files') + options.subject_base = ansible_module.params.get('subject_base') + options.ca_subject = ansible_module.params.get('ca_subject') + + options.no_reverse = ansible_module.params.get('no_reverse') + options.auto_forwarders = ansible_module.params.get('auto_forwarders') + + options.idstart = ansible_module.params.get('idstart') + options.idmax = ansible_module.params.get('idmax') + + options._pkinit_pkcs12_info = ansible_module.params.get( + '_pkinit_pkcs12_info') + + # options._update_hosts_file = ansible_module.params.get( + # 'update_hosts_file') + + # init ################################################################## + + fstore = sysrestore.FileStore(paths.SYSRESTORE) + + api_Backend_ldap2(options.host_name, options.setup_ca, connect=True) + + # setup KRB ############################################################# + + krb = krbinstance.KrbInstance(fstore) + krb.set_output(ansible_log) + with redirect_stdout(ansible_log): + if not options.external_cert_files: + krb.create_instance(options.realm_name, options.host_name, + options.domain_name, + options.dm_password, options.master_password, + setup_pkinit=not options.no_pkinit, + pkcs12_info=options._pkinit_pkcs12_info, + subject_base=options.subject_base) + else: + krb.init_info(options.realm_name, options.host_name, + setup_pkinit=not options.no_pkinit, + subject_base=options.subject_base) + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaserver/library/ipaserver_setup_ntp.py b/roles/ipaserver/library/ipaserver_setup_ntp.py new file mode 100644 index 0000000..2e635ab --- /dev/null +++ b/roles/ipaserver/library/ipaserver_setup_ntp.py @@ -0,0 +1,121 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaserver_setup_ntp +short description: Setup NTP +description: Setup NTP +options: + ntp_servers: + description: ntp servers to use + required: yes + ntp_pool: + description: ntp server pool to use + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import inspect + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_server import ( + AnsibleModuleLog, options, sysrestore, paths, + redirect_stdout, time_service, sync_time, ntpinstance, timeconf +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + ntp_servers=dict(required=False, type='list', default=None), + ntp_pool=dict(required=False, default=None), + ), + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # set values ############################################################ + + options.ntp_servers = ansible_module.params.get('ntp_servers') + options.ntp_pool = ansible_module.params.get('ntp_pool') + + # init ########################################################## + + fstore = sysrestore.FileStore(paths.SYSRESTORE) + sstore = sysrestore.StateFile(paths.SYSRESTORE) + + # setup NTP ##################################################### + + if time_service == "chronyd": + # We have to sync time before certificate handling on master. + # As chrony configuration is moved from client here, unconfiguration of + # chrony will be handled here in uninstall() method as well by invoking + # the ipa-server-install --uninstall + ansible_module.log("Synchronizing time") + + argspec = inspect.getargspec(sync_time) + if "options" not in argspec.args: + synced_ntp = sync_time(options.ntp_servers, options.ntp_pool, + fstore, sstore) + else: + synced_ntp = sync_time(options, fstore, sstore) + if not synced_ntp: + ansible_module.log( + "Warning: IPA was unable to sync time with chrony!") + ansible_module.log( + " Time synchronization is required for IPA " + "to work correctly") + else: + # Configure ntpd + timeconf.force_ntpd(sstore) + ntp = ntpinstance.NTPInstance(fstore) + ntp.set_output(ansible_log) + with redirect_stdout(ansible_log): + if not ntp.is_configured(): + ntp.create_instance() + + # done ########################################################## + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaserver/library/ipaserver_setup_otpd.py b/roles/ipaserver/library/ipaserver_setup_otpd.py new file mode 100644 index 0000000..12a8756 --- /dev/null +++ b/roles/ipaserver/library/ipaserver_setup_otpd.py @@ -0,0 +1,102 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +from __future__ import print_function + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaserver_setup_otpd +short description: Setup OTPD +description: Setup OTPD +options: + realm: + description: Kerberos realm name of the IPA deployment + required: no + hostname: + description: Fully qualified name of this host + required: yes + setup_ca: + description: Configure a dogtag CA + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_server import ( + AnsibleModuleLog, options, + api_Backend_ldap2, redirect_stdout, otpdinstance, ipautil +) + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # basic + realm=dict(required=True), + hostname=dict(required=False), + setup_ca=dict(required=False, type='bool', default=False), + ), + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # set values #################################################### + + options.realm_name = ansible_module.params.get('realm') + options.host_name = ansible_module.params.get('hostname') + options.setup_ca = ansible_module.params.get('setup_ca') + + # init ########################################################## + + api_Backend_ldap2(options.host_name, options.setup_ca, connect=True) + + # setup ds ###################################################### + + otpd = otpdinstance.OtpdInstance() + otpd.set_output(ansible_log) + with redirect_stdout(ansible_log): + otpd.create_instance('OTPD', options.host_name, + ipautil.realm_to_suffix(options.realm_name)) + + # done ########################################################## + + ansible_module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaserver/library/ipaserver_test.py b/roles/ipaserver/library/ipaserver_test.py new file mode 100644 index 0000000..5668231 --- /dev/null +++ b/roles/ipaserver/library/ipaserver_test.py @@ -0,0 +1,1052 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'supported_by': 'community', + 'status': ['preview'], +} + +DOCUMENTATION = ''' +--- +module: ipaserver_test +short description: IPA server test +description: IPA server test +options: + force: + description: Installer force parameter + required: yes + dm_password: + description: Directory Manager password + required: no + password: + description: Admin user kerberos password + required: no + master_password: + description: kerberos master password (normally autogenerated) + required: yes + domain: + description: Primary DNS domain of the IPA deployment + required: yes + realm: + description: Kerberos realm name of the IPA deployment + required: yes + hostname: + description: Fully qualified name of this host + required: yes + ca_cert_files: + description: + List of files containing CA certificates for the service certificate + files + required: yes + no_host_dns: + description: Do not use DNS for hostname lookup during installation + required: yes + pki_config_override: + description: Path to ini file with config overrides + required: yes + setup_adtrust: + description: Configure AD trust capability + required: yes + setup_kra: + description: Configure a dogtag KRA + required: yes + setup_dns: + description: Configure bind with our zone + required: yes + idstart: + description: The starting value for the IDs range (default random) + required: yes + idmax: + description: The max value for the IDs range (default: idstart+199999) + required: yes + no_pkinit: + description: Disable pkinit setup steps + required: yes + dirsrv_config_file: + description: + The path to LDIF file that will be used to modify configuration of + dse.ldif during installation of the directory server instance + required: yes + dirsrv_cert_files: + description: + Files containing the Directory Server SSL certificate and private key + required: yes + http_cert_files: + description: + File containing the Apache Server SSL certificate and private key + required: yes + pkinit_cert_files: + description: + File containing the Kerberos KDC SSL certificate and private key + required: yes + dirsrv_pin: + description: The password to unlock the Directory Server private key + required: yes + http_pin: + description: The password to unlock the Apache Server private key + required: yes + pkinit_pin: + description: The password to unlock the Kerberos KDC private key + required: yes + dirsrv_cert_name: + description: Name of the Directory Server SSL certificate to install + required: yes + http_cert_name: + description: Name of the Apache Server SSL certificate to install + required: yes + pkinit_cert_name: + description: Name of the Kerberos KDC SSL certificate to install + required: yes + ntp_servers: + description: ntp servers to use + required: yes + ntp_pool: + description: ntp server pool to use + required: yes + no_ntp: + description: Do not configure ntp + required: yes + external_ca: + description: External ca setting + required: yes + external_ca_type: + description: Type of the external CA + required: yes + external_ca_profile: + description: + Specify the certificate profile/template to use at the external CA + required: yes + external_cert_files: + description: + File containing the IPA CA certificate and the external CA certificate + chain + required: yes + subject_base: + description: + The certificate subject base (default O=). + RDNs are in LDAP order (most specific RDN first). + required: yes + ca_subject: + description: The installer ca_subject setting + required: yes + allow_zone_overlap: + description: Create DNS zone even if it already exists + required: yes + reverse_zones: + description: The reverse DNS zones to use + required: yes + no_reverse: + description: Do not create new reverse DNS zone + required: yes + auto_reverse: + description: Create necessary reverse zones + required: yes + zonemgr: + description: DNS zone manager e-mail address. Defaults to hostmaster@DOMAIN + required: yes + forwarders: + description: Add DNS forwarders + required: yes + no_forwarders: + description: Do not add any DNS forwarders, use root servers instead + required: yes + auto_forwarders: + description: Use DNS forwarders configured in /etc/resolv.conf + required: yes + forward_policy: + description: DNS forwarding policy for global forwarders + required: yes + no_dnssec_validation: + description: Disable DNSSEC validation + required: yes + enable_compat: + description: Enable support for trusted domains for old clients + required: yes + netbios_name: + description: NetBIOS name of the IPA domain + required: yes + rid_base: + description: Start value for mapping UIDs and GIDs to RIDs + required: yes + secondary_rid_base: + description: + Start value of the secondary range for mapping UIDs and GIDs to RIDs + required: yes +author: + - Thomas Woerner +''' + +EXAMPLES = ''' +''' + +RETURN = ''' +''' + +import os +import sys +import six +import inspect +import random + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.ansible_ipa_server import ( + AnsibleModuleLog, options, adtrust_imported, kra_imported, PKIIniLoader, + MIN_DOMAIN_LEVEL, MAX_DOMAIN_LEVEL, check_zone_overlap, + redirect_stdout, validate_dm_password, validate_admin_password, + NUM_VERSION, is_ipa_configured, sysrestore, paths, bindinstance, + read_cache, ca, tasks, check_ldap_conf, timeconf, httpinstance, + check_dirsrv, ScriptError, get_fqdn, verify_fqdn, BadHostError, + validate_domain_name, load_pkcs12, IPA_PYTHON_VERSION +) + +if six.PY3: + unicode = str + + +def main(): + ansible_module = AnsibleModule( + argument_spec=dict( + # basic + force=dict(required=False, type='bool', default=False), + dm_password=dict(required=True, no_log=True), + password=dict(required=True, no_log=True), + master_password=dict(required=False, no_log=True), + domain=dict(required=False), + realm=dict(required=False), + hostname=dict(required=False), + ca_cert_files=dict(required=False, type='list', default=[]), + no_host_dns=dict(required=False, type='bool', default=False), + pki_config_override=dict(required=False), + # server + setup_adtrust=dict(required=False, type='bool', default=False), + setup_kra=dict(required=False, type='bool', default=False), + setup_dns=dict(required=False, type='bool', default=False), + idstart=dict(required=False, type='int'), + idmax=dict(required=False, type='int'), + # no_hbac_allow + no_pkinit=dict(required=False, type='bool', default=False), + # no_ui_redirect + dirsrv_config_file=dict(required=False), + # ssl certificate + dirsrv_cert_files=dict(required=False, type='list', default=None), + http_cert_files=dict(required=False, type='list', defaullt=None), + pkinit_cert_files=dict(required=False, type='list', default=None), + dirsrv_pin=dict(required=False), + http_pin=dict(required=False), + pkinit_pin=dict(required=False), + dirsrv_cert_name=dict(required=False), + http_cert_name=dict(required=False), + pkinit_cert_name=dict(required=False), + # client + # mkhomedir + ntp_servers=dict(required=False, type='list', default=None), + ntp_pool=dict(required=False, default=None), + no_ntp=dict(required=False, type='bool', default=False), + # ssh_trust_dns + # no_ssh + # no_sshd + # no_dns_sshfp + # certificate system + external_ca=dict(required=False, type='bool', default=False), + external_ca_type=dict(required=False), + external_ca_profile=dict(required=False), + external_cert_files=dict(required=False, type='list', + default=None), + subject_base=dict(required=False), + ca_subject=dict(required=False), + # ca_signing_algorithm + # dns + allow_zone_overlap=dict(required=False, type='bool', + default=False), + reverse_zones=dict(required=False, type='list', default=[]), + no_reverse=dict(required=False, type='bool', default=False), + auto_reverse=dict(required=False, type='bool', default=False), + zonemgr=dict(required=False), + forwarders=dict(required=False, type='list', default=[]), + no_forwarders=dict(required=False, type='bool', default=False), + auto_forwarders=dict(required=False, type='bool', default=False), + forward_policy=dict(default=None, choices=['first', 'only']), + no_dnssec_validation=dict(required=False, type='bool', + default=False), + # ad trust + enable_compat=dict(required=False, type='bool', default=False), + netbios_name=dict(required=False), + rid_base=dict(required=False, type='int', default=1000), + secondary_rid_base=dict(required=False, type='int', + default=100000000), + # additional + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + ansible_log = AnsibleModuleLog(ansible_module) + + # set values ############################################################ + + # basic + options.force = ansible_module.params.get('force') + options.dm_password = ansible_module.params.get('dm_password') + options.admin_password = ansible_module.params.get('password') + options.master_password = ansible_module.params.get('master_password') + options.domain_name = ansible_module.params.get('domain') + options.realm_name = ansible_module.params.get('realm') + options.host_name = ansible_module.params.get('hostname') + options.ca_cert_files = ansible_module.params.get('ca_cert_files') + options.no_host_dns = ansible_module.params.get('no_host_dns') + options.pki_config_override = ansible_module.params.get( + 'pki_config_override') + # server + options.setup_adtrust = ansible_module.params.get('setup_adtrust') + options.setup_dns = ansible_module.params.get('setup_dns') + options.setup_kra = ansible_module.params.get('setup_kra') + options.idstart = ansible_module.params.get('idstart') + options.idmax = ansible_module.params.get('idmax') + # no_hbac_allow + options.no_pkinit = ansible_module.params.get('no_pkinit') + # no_ui_redirect + options.dirsrv_config_file = ansible_module.params.get( + 'dirsrv_config_file') + # ssl certificate + options.dirsrv_cert_files = ansible_module.params.get('dirsrv_cert_files') + options.http_cert_files = ansible_module.params.get('http_cert_files') + options.pkinit_cert_files = ansible_module.params.get('pkinit_cert_files') + options.dirsrv_pin = ansible_module.params.get('dirsrv_pin') + options.http_pin = ansible_module.params.get('http_pin') + options.pkinit_pin = ansible_module.params.get('pkinit_pin') + options.dirsrv_cert_name = ansible_module.params.get('dirsrv_cert_name') + options.http_cert_name = ansible_module.params.get('http_cert_name') + options.pkinit_cert_name = ansible_module.params.get('pkinit_cert_name') + # client + # mkhomedir + options.ntp_servers = ansible_module.params.get('ntp_servers') + options.ntp_pool = ansible_module.params.get('ntp_pool') + options.no_ntp = ansible_module.params.get('no_ntp') + # ssh_trust_dns + # no_ssh + # no_sshd + # no_dns_sshfp + # certificate system + options.external_ca = ansible_module.params.get('external_ca') + options.external_ca_type = ansible_module.params.get('external_ca_type') + options.external_ca_profile = ansible_module.params.get( + 'external_ca_profile') + options.external_cert_files = ansible_module.params.get( + 'external_cert_files') + options.subject_base = ansible_module.params.get('subject_base') + options.ca_subject = ansible_module.params.get('ca_subject') + # ca_signing_algorithm + # dns + options.allow_zone_overlap = ansible_module.params.get( + 'allow_zone_overlap') + options.reverse_zones = ansible_module.params.get('reverse_zones') + options.no_reverse = ansible_module.params.get('no_reverse') + options.auto_reverse = ansible_module.params.get('auto_reverse') + options.zonemgr = ansible_module.params.get('zonemgr') + options.forwarders = ansible_module.params.get('forwarders') + options.no_forwarders = ansible_module.params.get('no_forwarders') + options.auto_forwarders = ansible_module.params.get('auto_forwarders') + options.forward_policy = ansible_module.params.get('forward_policy') + options.no_dnssec_validation = ansible_module.params.get( + 'no_dnssec_validation') + # ad trust + options.enable_compat = ansible_module.params.get('enable_compat') + options.netbios_name = ansible_module.params.get('netbios_name') + options.rid_base = ansible_module.params.get('rid_base') + options.secondary_rid_base = ansible_module.params.get( + 'secondary_rid_base') + + # additional + options.kasp_db_file = None + + # version specific ###################################################### + + if options.setup_adtrust and not adtrust_imported: + # if "adtrust" not in options._allow_missing: + ansible_module.fail_json(msg="adtrust can not be imported") + # else: + # options.setup_adtrust = False + # ansible_module.warn(msg="adtrust is not supported, disabling") + + if options.setup_kra and not kra_imported: + # if "kra" not in options._allow_missing: + ansible_module.fail_json(msg="kra can not be imported") + # else: + # options.setup_kra = False + # ansible_module.warn(msg="kra is not supported, disabling") + + if options.pki_config_override is not None: + if PKIIniLoader is None: + ansible_module.warn("The use of pki_config_override is not " + "supported for this IPA version") + else: + # From DogtagInstallInterface @pki_config_override.validator + try: + PKIIniLoader.verify_pki_config_override( + options.pki_config_override) + except ValueError as e: + ansible_module.fail_json( + msg="pki_config_override: %s" % str(e)) + + # default values ######################################################## + + # idstart and idmax + if options.idstart is None: + options.idstart = random.randint(1, 10000) * 200000 + if options.idmax is None or options.idmax == 0: + options.idmax = options.idstart + 199999 + + # ServerInstallInterface.__init__ ####################################### + try: + self = options + + # If any of the key file options are selected, all are required. + cert_file_req = (self.dirsrv_cert_files, self.http_cert_files) + cert_file_opt = (self.pkinit_cert_files,) + if not self.no_pkinit: + cert_file_req += cert_file_opt + if self.no_pkinit and self.pkinit_cert_files: + raise RuntimeError( + "--no-pkinit and --pkinit-cert-file cannot be specified " + "together" + ) + if any(cert_file_req + cert_file_opt) and not all(cert_file_req): + raise RuntimeError( + "--dirsrv-cert-file, --http-cert-file, and --pkinit-cert-file " + "or --no-pkinit are required if any key file options are used." + ) + + if not self.interactive: + if self.dirsrv_cert_files and self.dirsrv_pin is None: + raise RuntimeError( + "You must specify --dirsrv-pin with --dirsrv-cert-file") + if self.http_cert_files and self.http_pin is None: + raise RuntimeError( + "You must specify --http-pin with --http-cert-file") + if self.pkinit_cert_files and self.pkinit_pin is None: + raise RuntimeError( + "You must specify --pkinit-pin with --pkinit-cert-file") + + if not self.setup_dns: + if self.forwarders: + raise RuntimeError( + "You cannot specify a --forwarder option without the " + "--setup-dns option") + if self.auto_forwarders: + raise RuntimeError( + "You cannot specify a --auto-forwarders option without " + "the --setup-dns option") + if self.no_forwarders: + raise RuntimeError( + "You cannot specify a --no-forwarders option without the " + "--setup-dns option") + if self.forward_policy: + raise RuntimeError( + "You cannot specify a --forward-policy option without the " + "--setup-dns option") + if self.reverse_zones: + raise RuntimeError( + "You cannot specify a --reverse-zone option without the " + "--setup-dns option") + if self.auto_reverse: + raise RuntimeError( + "You cannot specify a --auto-reverse option without the " + "--setup-dns option") + if self.no_reverse: + raise RuntimeError( + "You cannot specify a --no-reverse option without the " + "--setup-dns option") + if self.no_dnssec_validation: + raise RuntimeError( + "You cannot specify a --no-dnssec-validation option " + "without the --setup-dns option") + elif self.forwarders and self.no_forwarders: + raise RuntimeError( + "You cannot specify a --forwarder option together with " + "--no-forwarders") + elif self.auto_forwarders and self.no_forwarders: + raise RuntimeError( + "You cannot specify a --auto-forwarders option together with " + "--no-forwarders") + elif self.reverse_zones and self.no_reverse: + raise RuntimeError( + "You cannot specify a --reverse-zone option together with " + "--no-reverse") + elif self.auto_reverse and self.no_reverse: + raise RuntimeError( + "You cannot specify a --auto-reverse option together with " + "--no-reverse") + + if not self.setup_adtrust: + if self.add_agents: + raise RuntimeError( + "You cannot specify an --add-agents option without the " + "--setup-adtrust option") + + if self.enable_compat: + raise RuntimeError( + "You cannot specify an --enable-compat option without the " + "--setup-adtrust option") + + if self.netbios_name: + raise RuntimeError( + "You cannot specify a --netbios-name option without the " + "--setup-adtrust option") + + if self.no_msdcs: + raise RuntimeError( + "You cannot specify a --no-msdcs option without the " + "--setup-adtrust option") + + if not hasattr(self, 'replica_install'): + if self.external_cert_files and self.dirsrv_cert_files: + raise RuntimeError( + "Service certificate file options cannot be used with the " + "external CA options.") + + if self.external_ca_type and not self.external_ca: + raise RuntimeError( + "You cannot specify --external-ca-type without " + "--external-ca") + + if self.external_ca_profile and not self.external_ca: + raise RuntimeError( + "You cannot specify --external-ca-profile without " + "--external-ca") + + if self.uninstalling: + if (self.realm_name or self.admin_password or + self.master_password): + raise RuntimeError( + "In uninstall mode, -a, -r and -P options are not " + "allowed") + elif not self.interactive: + if (not self.realm_name or not self.dm_password or + not self.admin_password): + raise RuntimeError( + "In unattended mode you need to provide at least -r, " + "-p and -a options") + if self.setup_dns: + if (not self.forwarders and + not self.no_forwarders and + not self.auto_forwarders): + raise RuntimeError( + "You must specify at least one of --forwarder, " + "--auto-forwarders, or --no-forwarders options") + + any_ignore_option_true = any( + [self.ignore_topology_disconnect, self.ignore_last_of_role]) + if any_ignore_option_true and not self.uninstalling: + raise RuntimeError( + "'--ignore-topology-disconnect/--ignore-last-of-role' " + "options can be used only during uninstallation") + + if self.idmax < self.idstart: + raise RuntimeError( + "idmax (%s) cannot be smaller than idstart (%s)" % + (self.idmax, self.idstart)) + else: + # replica installers + if self.servers and not self.domain_name: + raise RuntimeError( + "The --server option cannot be used without providing " + "domain via the --domain option") + + if self.setup_dns: + if (not self.forwarders and + not self.no_forwarders and + not self.auto_forwarders): + raise RuntimeError( + "You must specify at least one of --forwarder, " + "--auto-forwarders, or --no-forwarders options") + + except RuntimeError as e: + ansible_module.fail_json(msg=e) + + # ####################################################################### + + # If any of the key file options are selected, all are required. + cert_file_req = (options.dirsrv_cert_files, options.http_cert_files) + cert_file_opt = (options.pkinit_cert_files,) + if not options.no_pkinit: + cert_file_req += cert_file_opt + if options.no_pkinit and options.pkinit_cert_files: + ansible_module.fail_json( + msg="no-pkinit and pkinit-cert-file cannot be specified together" + ) + if any(cert_file_req + cert_file_opt) and not all(cert_file_req): + ansible_module.fail_json( + msg="dirsrv-cert-file, http-cert-file, and pkinit-cert-file " + "or no-pkinit are required if any key file options are used." + ) + + if not options.interactive: + if options.dirsrv_cert_files and options.dirsrv_pin is None: + ansible_module.fail_json( + msg="You must specify dirsrv-pin with dirsrv-cert-file") + if options.http_cert_files and options.http_pin is None: + ansible_module.fail_json( + msg="You must specify http-pin with http-cert-file") + if options.pkinit_cert_files and options.pkinit_pin is None: + ansible_module.fail_json( + msg="You must specify pkinit-pin with pkinit-cert-file") + + if not options.setup_dns: + # lists + for x in ["forwarders", "reverse_zones"]: + if len(getattr(options, x)) > 1: + ansible_module.fail_json( + msg="You cannot specify %s without setting setup-dns" % x) + # bool and str values + for x in ["auto_forwarders", "no_forwarders", + "auto_reverse", "no_reverse", "no_dnssec_validation", + "forward_policy"]: + if getattr(options, x): + ansible_module.fail_json( + msg="You cannot specify %s without setting setup-dns" % x) + + elif len(options.forwarders) > 0 and options.no_forwarders: + ansible_module.fail_json( + msg="You cannot specify forwarders together with no-forwarders") + elif options.auto_forwarders and options.no_forwarders: + ansible_module.fail_json( + msg="You cannot specify auto-forwarders together with " + "no-forwarders") + elif len(options.reverse_zones) > 0 and options.no_reverse: + ansible_module.fail_json( + msg="You cannot specify reverse-zones together with no-reverse") + elif options.auto_reverse and options.no_reverse: + ansible_module.fail_json( + msg="You cannot specify auto-reverse together with no-reverse") + + if not hasattr(self, 'replica_install'): + if options.external_cert_files and options.dirsrv_cert_files: + ansible_module.fail_json( + msg="Service certificate file options cannot be used with the " + "external CA options.") + + if options.external_ca_type and not options.external_ca: + ansible_module.fail_json( + msg="You cannot specify external-ca-type without external-ca") + + # if options.uninstalling: + # if (options.realm_name or options.admin_password or + # options.master_password): + # ansible_module.fail_json( + # msg="In uninstall mode, -a, -r and -P options are not " + # "allowed") + # elif not options.interactive: + # if (not options.realm_name or not options.dm_password or + # not options.admin_password): + # ansible_module.fail_json(msg= + # "In unattended mode you need to provide at least -r, " + # "-p and -a options") + # if options.setup_dns: + # if (not options.forwarders and + # not options.no_forwarders and + # not options.auto_forwarders): + # ansible_module.fail_json(msg= + # "You must specify at least one of --forwarder, " + # "--auto-forwarders, or --no-forwarders options") + if (not options.realm_name or not options.dm_password or + not options.admin_password): + ansible_module.fail_json( + msg="You need to provide at least realm_name, dm_password " + "and admin_password") + if options.setup_dns: + if len(options.forwarders) < 1 and not options.no_forwarders and \ + not options.auto_forwarders: + ansible_module.fail_json( + msg="You must specify at least one of forwarders, " + "auto-forwarders or no-forwarders") + + # any_ignore_option_true = any( + # [options.ignore_topology_disconnect, options.ignore_last_of_role]) + # if any_ignore_option_true and not options.uninstalling: + # ansible_module.fail_json( + # msg="ignore-topology-disconnect and ignore-last-of-role " + # "can be used only during uninstallation") + + if options.idmax < options.idstart: + ansible_module.fail_json( + msg="idmax (%s) cannot be smaller than idstart (%s)" % + (options.idmax, options.idstart)) + + # validation ############################################################# + + if options.dm_password is None: + ansible_module.fail_json(msg="Directory Manager password required") + + if options.admin_password is None: + ansible_module.fail_json(msg="IPA admin password required") + + # validation ############################################################ + + # domain_level + if options.domain_level < MIN_DOMAIN_LEVEL: + ansible_module.fail_json( + msg="Domain Level cannot be lower than %d" % MIN_DOMAIN_LEVEL) + elif options.domain_level > MAX_DOMAIN_LEVEL: + ansible_module.fail_json( + msg="Domain Level cannot be higher than %d" % MAX_DOMAIN_LEVEL) + + # dirsrv_config_file + if options.dirsrv_config_file is not None: + if not os.path.exists(options.dirsrv_config_file): + ansible_module.fail_json( + msg="File %s does not exist." % options.dirsrv_config_file) + + # domain_name + # Validation is done later on in ipaserver_prepare dns.install_check + + # dm_password + with redirect_stdout(ansible_log): + validate_dm_password(options.dm_password) + + # admin_password + with redirect_stdout(ansible_log): + validate_admin_password(options.admin_password) + + # pkinit is not supported on DL0, don't allow related options + + """ + # replica install: if not options.replica_file is None: + if (not options._replica_install and \ + not options.domain_level > DOMAIN_LEVEL_0) or \ + (options._replica_install and options.replica_file is not None): + if (options.no_pkinit or options.pkinit_cert_files is not None or + options.pkinit_pin is not None): + ansible_module.fail_json( + msg="pkinit on domain level 0 is not supported. Please " + "don't use any pkinit-related options.") + options.no_pkinit = True + """ + + if options.setup_dns: + if len(options.forwarders) < 1 and not options.no_forwarders and \ + not options.auto_forwarders: + ansible_module.fail_json( + msg="You must specify at least one of forwarders, " + "auto-forwarders or no-forwarders") + + if NUM_VERSION >= 40200 and options.master_password and \ + not options.external_cert_files: + ansible_module.warn( + "Specifying kerberos master-password is deprecated") + + options._installation_cleanup = True + if not options.external_ca and not options.external_cert_files and \ + is_ipa_configured(): + options._installation_cleanup = False + ansible_module.log( + "IPA server is already configured on this system. If you want " + "to reinstall the IPA server, please uninstall it first.") + ansible_module.exit_json(changed=False, + server_already_configured=True) + + client_fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE) + if client_fstore.has_files(): + options._installation_cleanup = False + ansible_module.log( + "IPA client is already configured on this system. " + "Please uninstall it before configuring the IPA server.") + ansible_module.exit_json(changed=False, + client_already_configured=True) + + # validate reverse_zones + if not options.allow_zone_overlap: + for zone in options.reverse_zones: + with redirect_stdout(ansible_log): + check_zone_overlap(zone) + + # validate zonemgr + if options.zonemgr: + if six.PY3: + with redirect_stdout(ansible_log): + bindinstance.validate_zonemgr_str(options.zonemgr) + else: + try: + # IDNA support requires unicode + encoding = getattr(sys.stdin, 'encoding', None) + if encoding is None: + encoding = 'utf-8' + value = options.zonemgr + if not isinstance(value, unicode): + value = options.zonemgr.decode(encoding) + else: + value = options.zonemgr + with redirect_stdout(ansible_log): + bindinstance.validate_zonemgr_str(value) + except ValueError as e: + # FIXME we can do this in better way + # https://fedorahosted.org/freeipa/ticket/4804 + # decode to proper stderr encoding + stderr_encoding = getattr(sys.stderr, 'encoding', None) + if stderr_encoding is None: + stderr_encoding = 'utf-8' + error = unicode(e).encode(stderr_encoding) + ansible_module.fail_json(msg=error) + + # external cert file paths are absolute + if options.external_cert_files: + for path in options.external_cert_files: + if not os.path.isabs(path): + ansible_module.fail_json( + msg="External cert file '%s' must use an absolute " + "path" % path) + + options.setup_ca = True + # We only set up the CA if the PKCS#12 options are not given. + if options.dirsrv_cert_files and len(options.dirsrv_cert_files) > 0: + options.setup_ca = False + else: + options.setup_ca = True + + if not options.setup_ca and options.ca_subject: + ansible_module.fail_json( + msg="--ca-subject cannot be used with CA-less installation") + if not options.setup_ca and options.subject_base: + ansible_module.fail_json( + msg="--subject-base cannot be used with CA-less installation") + if not options.setup_ca and options.setup_kra: + ansible_module.fail_json( + msg="--setup-kra cannot be used with CA-less installation") + + # This will override any settings passed in on the cmdline + if os.path.isfile(paths.ROOT_IPA_CACHE): + # dm_password check removed, checked already + try: + cache_vars = read_cache(options.dm_password) + options.__dict__.update(cache_vars) + if cache_vars.get('external_ca', False): + options.external_ca = False + options.interactive = False + except Exception as e: + ansible_module.fail_json( + msg="Cannot process the cache file: %s" % str(e)) + + # ca_subject + if options.ca_subject: + ca.subject_validator(ca.VALID_SUBJECT_ATTRS, options.ca_subject) + + # IPv6 and SELinux check + + tasks.check_ipv6_stack_enabled() + tasks.check_selinux_status() + if check_ldap_conf is not None: + check_ldap_conf() + + _installation_cleanup = True + if not options.external_ca and not options.external_cert_files and \ + is_ipa_configured(): + _installation_cleanup = False + ansible_module.fail_json( + msg="IPA server is already configured on this system.") + + if not options.no_ntp: + try: + timeconf.check_timedate_services() + except timeconf.NTPConflictingService as e: + ansible_module.log( + "WARNING: conflicting time&date synchronization service " + "'%s' will be disabled in favor of chronyd" % + e.conflicting_service) + except timeconf.NTPConfigurationError: + pass + + if hasattr(httpinstance, "httpd_443_configured"): + # Check to see if httpd is already configured to listen on 443 + if httpinstance.httpd_443_configured(): + ansible_module.fail_json( + msg="httpd is already configured to listen on 443.") + + if not options.external_cert_files: + # Make sure the 389-ds ports are available + try: + check_dirsrv(True) + except ScriptError as e: + ansible_module.fail_json(msg=e) + + # check bind packages are installed + if options.setup_dns: + # Don't require an external DNS to say who we are if we are + # setting up a local DNS server. + options.no_host_dns = True + + # host name + if options.host_name: + host_default = options.host_name + else: + host_default = get_fqdn() + + try: + verify_fqdn(host_default, options.no_host_dns) + host_name = host_default + except BadHostError as e: + ansible_module.fail_json(msg=e) + + host_name = host_name.lower() + + if not options.domain_name: + domain_name = host_name[host_name.find(".")+1:] + try: + validate_domain_name(domain_name) + except ValueError as e: + ansible_module.fail_json( + msg="Invalid domain name: %s" % unicode(e)) + else: + domain_name = options.domain_name + + domain_name = domain_name.lower() + + if not options.realm_name: + realm_name = domain_name.upper() + else: + realm_name = options.realm_name.upper() + + argspec = inspect.getargspec(validate_domain_name) + if "entity" in argspec.args: + # NUM_VERSION >= 40690: + try: + validate_domain_name(realm_name, entity="realm") + except ValueError as e: + raise ScriptError("Invalid realm name: {}".format(unicode(e))) + + if not options.setup_adtrust: + # If domain name and realm does not match, IPA server will not be able + # to establish trust with Active Directory. Fail. + + if domain_name.upper() != realm_name: + ansible_module.warn( + "Realm name does not match the domain name: " + "You will not be able to establish trusts with Active " + "Directory.") + + # Do not ask for time source + # if not options.no_ntp and not options.unattended and not ( + # options.ntp_servers or options.ntp_pool): + # options.ntp_servers, options.ntp_pool = timeconf.get_time_source() + + ######################################################################### + + http_pkcs12_file = None + http_pkcs12_info = None + http_ca_cert = None + dirsrv_pkcs12_file = None + dirsrv_pkcs12_info = None + dirsrv_ca_cert = None + pkinit_pkcs12_file = None + pkinit_pkcs12_info = None + pkinit_ca_cert = None + + if options.http_cert_files: + if options.http_pin is None: + ansible_module.fail_json( + msg="Apache Server private key unlock password required") + http_pkcs12_file, http_pin, http_ca_cert = load_pkcs12( + cert_files=options.http_cert_files, + key_password=options.http_pin, + key_nickname=options.http_cert_name, + ca_cert_files=options.ca_cert_files, + host_name=host_name) + http_pkcs12_info = (http_pkcs12_file.name, http_pin) + + if options.dirsrv_cert_files: + if options.dirsrv_pin is None: + ansible_module.fail_json( + msg="Directory Server private key unlock password required") + dirsrv_pkcs12_file, dirsrv_pin, dirsrv_ca_cert = load_pkcs12( + cert_files=options.dirsrv_cert_files, + key_password=options.dirsrv_pin, + key_nickname=options.dirsrv_cert_name, + ca_cert_files=options.ca_cert_files, + host_name=host_name) + dirsrv_pkcs12_info = (dirsrv_pkcs12_file.name, dirsrv_pin) + + if options.pkinit_cert_files: + if options.pkinit_pin is None: + ansible_module.fail_json( + msg="Kerberos KDC private key unlock password required") + pkinit_pkcs12_file, pkinit_pin, pkinit_ca_cert = load_pkcs12( + cert_files=options.pkinit_cert_files, + key_password=options.pkinit_pin, + key_nickname=options.pkinit_cert_name, + ca_cert_files=options.ca_cert_files, + realm_name=realm_name) + pkinit_pkcs12_info = (pkinit_pkcs12_file.name, pkinit_pin) + + if options.http_cert_files and options.dirsrv_cert_files and \ + http_ca_cert != dirsrv_ca_cert: + ansible_module.fail_json( + msg="Apache Server SSL certificate and Directory Server SSL " + "certificate are not signed by the same CA certificate") + + if options.http_cert_files and options.pkinit_cert_files and \ + http_ca_cert != pkinit_ca_cert: + ansible_module.fail_json( + msg="Apache Server SSL certificate and PKINIT KDC " + "certificate are not signed by the same CA certificate") + + # done ################################################################## + + ansible_module.exit_json(changed=False, + ipa_python_version=IPA_PYTHON_VERSION, + # basic + domain=options.domain_name, + realm=realm_name, + hostname=host_name, + _hostname_overridden=bool(options.host_name), + no_host_dns=options.no_host_dns, + # server + setup_adtrust=options.setup_adtrust, + setup_kra=options.setup_kra, + setup_ca=options.setup_ca, + idstart=options.idstart, + idmax=options.idmax, + no_pkinit=options.no_pkinit, + # ssl certificate + _dirsrv_pkcs12_info=dirsrv_pkcs12_info, + _dirsrv_ca_cert=dirsrv_ca_cert, + _http_pkcs12_info=http_pkcs12_info, + _http_ca_cert=http_ca_cert, + _pkinit_pkcs12_info=pkinit_pkcs12_info, + _pkinit_ca_cert=pkinit_ca_cert, + # certificate system + external_ca=options.external_ca, + external_ca_type=options.external_ca_type, + external_ca_profile=options.external_ca_profile, + # ad trust + rid_base=options.rid_base, + secondary_rid_base=options.secondary_rid_base, + # client + ntp_servers=options.ntp_servers, + ntp_pool=options.ntp_pool, + # additional + _installation_cleanup=_installation_cleanup, + domainlevel=options.domainlevel) + + +if __name__ == '__main__': + main() diff --git a/roles/ipaserver/meta/main.yml b/roles/ipaserver/meta/main.yml new file mode 100644 index 0000000..99abca3 --- /dev/null +++ b/roles/ipaserver/meta/main.yml @@ -0,0 +1,20 @@ +dependencies: [] + +galaxy_info: + author: Thomas Woerner + description: A role to setup an iPA domain server + company: Red Hat, Inc + license: GPLv3 + min_ansible_version: 2.8 + platforms: + - name: Fedora + versions: + - all + - name: EL + versions: + - 7 + - 8 + galaxy_tags: + - identity + - ipa + - freeipa diff --git a/roles/ipaserver/module_utils/ansible_ipa_server.py b/roles/ipaserver/module_utils/ansible_ipa_server.py new file mode 100644 index 0000000..aad8f1d --- /dev/null +++ b/roles/ipaserver/module_utils/ansible_ipa_server.py @@ -0,0 +1,321 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Based on ipa-client-install code +# +# Copyright (C) 2017 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +__all__ = ["IPAChangeConf", "certmonger", "sysrestore", "root_logger", + "ipa_generate_password", "run", "ScriptError", "services", + "tasks", "errors", "x509", "DOMAIN_LEVEL_0", "MIN_DOMAIN_LEVEL", + "validate_domain_name", + "no_matching_interface_for_ip_address_warning", + "check_zone_overlap", "timeconf", "ntpinstance", "adtrust", + "bindinstance", "ca", "dns", "httpinstance", "installutils", + "kra", "krbinstance", "otpdinstance", "custodiainstance", + "replication", "service", "sysupgrade", "IPA_MODULES", + "BadHostError", "get_fqdn", "get_server_ip_address", + "is_ipa_configured", "load_pkcs12", "read_password", "verify_fqdn", + "update_hosts_file", "check_dirsrv", "validate_admin_password", + "validate_dm_password", "read_cache", "write_cache", + "adtrustinstance", "IPAAPI_USER", "sync_time", "PKIIniLoader", + "default_subject_base", "default_ca_subject_dn", + "check_ldap_conf"] + +import sys +import logging +from contextlib import contextmanager as contextlib_contextmanager + + +from ipapython.version import NUM_VERSION, VERSION + +if NUM_VERSION < 30201: + # See ipapython/version.py + IPA_MAJOR, IPA_MINOR, IPA_RELEASE = [int(x) for x in VERSION.split(".", 2)] + IPA_PYTHON_VERSION = IPA_MAJOR*10000 + IPA_MINOR*100 + IPA_RELEASE +else: + IPA_PYTHON_VERSION = NUM_VERSION + + +if NUM_VERSION >= 40500: + # IPA version >= 4.5 + + from ipaclient.install.ipachangeconf import IPAChangeConf + from ipalib.install import certmonger, sysrestore + from ipapython import ipautil + from ipapython.ipa_log_manager import standard_logging_setup + try: + from ipapython.ipa_log_manager import root_logger + except ImportError: + root_logger = None + from ipapython.ipautil import ( + ipa_generate_password, run) + from ipapython.admintool import ScriptError + from ipaplatform import services + from ipaplatform.paths import paths + from ipaplatform.tasks import tasks + from ipalib import api, errors, x509 + from ipalib.constants import DOMAIN_LEVEL_0, MIN_DOMAIN_LEVEL, \ + MAX_DOMAIN_LEVEL + try: + from ipalib.constants import IPAAPI_USER + except ImportError: + IPAAPI_USER = None + from ipalib.util import ( + validate_domain_name, + no_matching_interface_for_ip_address_warning, + ) + from ipapython.dnsutil import check_zone_overlap + from ipapython.dn import DN + try: + from ipaclient.install import timeconf + from ipaclient.install.client import sync_time + time_service = "chronyd" + ntpinstance = None + except ImportError: + try: + from ipaclient.install import ntpconf as timeconf + except ImportError: + from ipaclient import ntpconf as timeconf + from ipaserver.install import ntpinstance + time_service = "ntpd" + sync_time = None + from ipaserver.install import ( + adtrust, bindinstance, ca, dns, dsinstance, + httpinstance, installutils, kra, krbinstance, + otpdinstance, custodiainstance, replication, service, + sysupgrade) + adtrust_imported = True + kra_imported = True + from ipaserver.install.installutils import ( + IPA_MODULES, BadHostError, get_fqdn, get_server_ip_address, + is_ipa_configured, load_pkcs12, read_password, verify_fqdn, + update_hosts_file) + from ipaserver.install.server.install import ( + check_dirsrv, validate_admin_password, validate_dm_password, + read_cache, write_cache) + try: + from ipaserver.install.dogtaginstance import PKIIniLoader + except ImportError: + PKIIniLoader = None + try: + from ipaserver.install.installutils import default_subject_base + except ImportError: + def default_subject_base(realm_name): + return DN(('O', realm_name)) + try: + from ipaserver.install.installutils import default_ca_subject_dn + except ImportError: + def default_ca_subject_dn(subject_base): + return DN(('CN', 'Certificate Authority'), subject_base) + + try: + from ipaserver.install import adtrustinstance + _server_trust_ad_installed = True + except ImportError: + _server_trust_ad_installed = False + + try: + from ipaclient.install.client import check_ldap_conf + except ImportError: + check_ldap_conf = None + +else: + # IPA version < 4.5 + + raise Exception("freeipa version '%s' is too old" % VERSION) + + +logger = logging.getLogger("ipa-server-install") +# logger.setLevel(logging.DEBUG) +standard_logging_setup( + paths.IPASERVER_INSTALL_LOG, verbose=False, debug=False, + filemode='a', console_format='%(message)s') + + +@contextlib_contextmanager +def redirect_stdout(f): + sys.stdout = f + try: + yield f + finally: + sys.stdout = sys.__stdout__ + + +class AnsibleModuleLog(): + def __init__(self, module): + self.module = module + _ansible_module_log = self + + class AnsibleLoggingHandler(logging.Handler): + def emit(self, record): + _ansible_module_log.write(self.format(record)) + + self.logging_handler = AnsibleLoggingHandler() + logger.setLevel(logging.DEBUG) + logger.root.addHandler(self.logging_handler) + + def close(self): + self.flush() + + def flush(self): + pass + + def log(self, msg): + # self.write(msg+"\n") + self.write(msg) + + def debug(self, msg): + self.module.debug(msg) + + def info(self, msg): + self.module.debug(msg) + + def write(self, msg): + self.module.debug(msg) + # self.module.warn(msg) + + +class options_obj(object): + def __init__(self): + self._replica_install = False + self.dnssec_master = False # future unknown + self.disable_dnssec_master = False # future unknown + self.domainlevel = MAX_DOMAIN_LEVEL # deprecated + self.domain_level = self.domainlevel # deprecated + self.interactive = False + self.unattended = not self.interactive + + # def __getattribute__(self, attr): + # logger.info(" <-- Accessing options.%s" % attr) + # return super(options_obj, self).__getattribute__(attr) + + # def __getattr__(self, attr): + # logger.info(" --> Adding missing options.%s" % attr) + # setattr(self, attr, None) + # return getattr(self, attr) + + def knobs(self): + for name in self.__dict__: + yield self, name + + +options = options_obj() +installer = options + +# ServerMasterInstall +options.add_sids = True +options.add_agents = False + + +# Installable +options.uninstalling = False + +# ServerInstallInterface +options.description = "Server" + +options.kinit_attempts = 1 +options.fixed_primary = True +options.permit = False +options.enable_dns_updates = False +options.no_krb5_offline_passwords = False +options.preserve_sssd = False +options.no_sssd = False + +# ServerMasterInstall +options.force_join = False +options.servers = None +options.no_wait_for_dns = True +options.host_password = None +options.keytab = None +options.setup_ca = True +# always run sidgen task and do not allow adding agents on first master +options.add_sids = True +options.add_agents = False + +# ADTrustInstallInterface +# no_msdcs is deprecated +options.no_msdcs = False + +# For pylint +options.external_cert_files = None +options.dirsrv_cert_files = None + +# Uninstall +options.ignore_topology_disconnect = False +options.ignore_last_of_role = False + + +def api_Backend_ldap2(host_name, setup_ca, connect=False): + # we are sure we have the configuration file ready. + cfg = dict(context='installer', confdir=paths.ETC_IPA, in_server=True, + host=host_name) + if setup_ca: + # we have an IPA-integrated CA + cfg['ca_host'] = host_name + + api.bootstrap(**cfg) + api.finalize() + if connect: + api.Backend.ldap2.connect() + + +def ds_init_info(ansible_log, fstore, domainlevel, dirsrv_config_file, + realm_name, host_name, domain_name, dm_password, + idstart, idmax, subject_base, ca_subject, + no_hbac_allow, dirsrv_pkcs12_info, no_pkinit): + + if not options.external_cert_files: + ds = dsinstance.DsInstance(fstore=fstore, domainlevel=domainlevel, + config_ldif=dirsrv_config_file) + ds.set_output(ansible_log) + + if options.dirsrv_cert_files: + _dirsrv_pkcs12_info = dirsrv_pkcs12_info + else: + _dirsrv_pkcs12_info = None + + with redirect_stdout(ansible_log): + ds.init_info(realm_name, host_name, domain_name, dm_password, + subject_base, ca_subject, idstart, idmax, + # hbac_allow=not no_hbac_allow, + _dirsrv_pkcs12_info, setup_pkinit=not no_pkinit) + else: + ds = dsinstance.DsInstance(fstore=fstore, domainlevel=domainlevel) + ds.set_output(ansible_log) + + with redirect_stdout(ansible_log): + ds.init_info(realm_name, host_name, domain_name, dm_password, + subject_base, ca_subject, 1101, 1100, None, + setup_pkinit=not no_pkinit) + + return ds + + +def ansible_module_get_parsed_ip_addresses(ansible_module, + param='ip_addresses'): + ip_addrs = [] + for ip in ansible_module.params.get(param): + try: + ip_parsed = ipautil.CheckedIPAddress(ip) + except Exception as e: + ansible_module.fail_json(msg="Invalid IP Address %s: %s" % (ip, e)) + ip_addrs.append(ip_parsed) + return ip_addrs diff --git a/roles/ipaserver/tasks/copy_external_cert.yml b/roles/ipaserver/tasks/copy_external_cert.yml new file mode 100644 index 0000000..aaebd2b --- /dev/null +++ b/roles/ipaserver/tasks/copy_external_cert.yml @@ -0,0 +1,12 @@ +- name: Install - Initialize ipaserver_external_cert_files + set_fact: + ipaserver_external_cert_files: [] + when: ipaserver_external_cert_files is undefined +- name: Install - Copy "{{ item }}" "{{ inventory_hostname }}':/root/'{{ item }}" + copy: + src: "{{ item }}" + dest: "/root/{{ item }}" + force: yes +- name: Install - Extend ipaserver_external_cert_files with "/root/{{ item }}" + set_fact: + ipaserver_external_cert_files: "{{ ipaserver_external_cert_files }} + [ '/root/{{ item }}' ]" diff --git a/roles/ipaserver/tasks/install.yml b/roles/ipaserver/tasks/install.yml new file mode 100644 index 0000000..85df9a7 --- /dev/null +++ b/roles/ipaserver/tasks/install.yml @@ -0,0 +1,454 @@ +--- +# tasks file for ipaserver + +- block: + - name: Install - Ensure that IPA server packages are installed + package: + name: "{{ ipaserver_packages }}" + state: present + + - name: Install - Ensure that IPA server packages for dns are installed + package: + name: "{{ ipaserver_packages_dns }}" + state: present + when: ipaserver_setup_dns | bool + + - name: Install - Ensure that IPA server packages for adtrust are installed + package: + name: "{{ ipaserver_packages_adtrust }}" + state: present + when: ipaserver_setup_adtrust | bool + + - name: Install - Ensure that firewall packages installed + package: + name: "{{ ipaserver_packages_firewalld }}" + state: present + when: ipaserver_setup_firewalld | bool + + - name: Firewalld service - Ensure that firewalld is running + systemd: + name: firewalld + enabled: yes + state: started + when: ipaserver_setup_firewalld | bool + + when: ipaserver_install_packages | bool + +#- name: Install - Include Python2/3 import test +# import_tasks: "{{ role_path }}/tasks/python_2_3_test.yml" + +- include_tasks: "{{ role_path }}/tasks/copy_external_cert.yml" + with_items: "{{ ipaserver_external_cert_files_from_controller }}" + when: ipaserver_external_cert_files_from_controller is defined and + ipaserver_external_cert_files_from_controller|length > 0 and + not ipaserver_external_cert_files is defined + +- name: Install - Server installation test + ipaserver_test: + ### basic ### + dm_password: "{{ ipadm_password }}" + password: "{{ ipaadmin_password }}" + master_password: "{{ ipaserver_master_password | default(omit) }}" + domain: "{{ ipaserver_domain | default(omit) }}" + realm: "{{ ipaserver_realm | default(omit) }}" + hostname: "{{ ipaserver_hostname | default(ansible_fqdn) }}" + ca_cert_files: "{{ ipaserver_ca_cert_files | default(omit) }}" + no_host_dns: "{{ ipaserver_no_host_dns }}" + pki_config_override: "{{ ipaserver_pki_config_override | default(omit) }}" + ### server ### + setup_adtrust: "{{ ipaserver_setup_adtrust }}" + setup_kra: "{{ ipaserver_setup_kra }}" + setup_dns: "{{ ipaserver_setup_dns }}" + idstart: "{{ ipaserver_idstart | default(omit) }}" + idmax: "{{ ipaserver_idmax | default(omit) }}" + # no_hbac_allow: "{{ ipaserver_no_hbac_allow }}" + no_pkinit: "{{ ipaserver_no_pkinit }}" + # no_ui_redirect: "{{ ipaserver_no_ui_redirect }}" + dirsrv_config_file: "{{ ipaserver_dirsrv_config_file | default(omit) }}" + ### ssl certificate ### + dirsrv_cert_files: "{{ ipaserver_dirsrv_cert_files | default(omit) }}" + dirsrv_cert_name: "{{ ipaserver_dirsrv_cert_name | default(omit) }}" + dirsrv_pin: "{{ ipaserver_dirsrv_pin | default(omit) }}" + http_cert_files: "{{ ipaserver_http_cert_files | default(omit) }}" + http_cert_name: "{{ ipaserver_http_cert_name | default(omit) }}" + http_pin: "{{ ipaserver_http_pin | default(omit) }}" + pkinit_cert_files: "{{ ipaserver_pkinit_cert_files | default(omit) }}" + pkinit_cert_name: "{{ ipaserver_pkinit_cert_name | default(omit) }}" + pkinit_pin: "{{ ipaserver_pkinit_pin | default(omit) }}" + ### client ### + # mkhomedir + ntp_servers: "{{ ipaclient_ntp_servers | default(omit) }}" + ntp_pool: "{{ ipaclient_ntp_pool | default(omit) }}" + no_ntp: "{{ ipaclient_no_ntp }}" + # ssh_trust_dns + # no_ssh + # no_sshd + # no_dns_sshfp + ### certificate system ### + external_ca: "{{ ipaserver_external_ca }}" + external_ca_type: "{{ ipaserver_external_ca_type | default(omit) }}" + external_ca_profile: "{{ ipaserver_external_ca_profile | default(omit) }}" + external_cert_files: "{{ ipaserver_external_cert_files | default(omit) }}" + subject_base: "{{ ipaserver_subject_base | default(omit) }}" + ca_subject: "{{ ipaserver_ca_subject | default(omit) }}" + # ca_signing_algorithm + ### dns ### + allow_zone_overlap: "{{ ipaserver_allow_zone_overlap }}" + reverse_zones: "{{ ipaserver_reverse_zones | default([]) }}" + no_reverse: "{{ ipaserver_no_reverse }}" + auto_reverse: "{{ ipaserver_auto_reverse }}" + zonemgr: "{{ ipaserver_zonemgr | default(omit) }}" + forwarders: "{{ ipaserver_forwarders | default([]) }}" + no_forwarders: "{{ ipaserver_no_forwarders }}" + auto_forwarders: "{{ ipaserver_auto_forwarders }}" + forward_policy: "{{ ipaserver_forward_policy | default(omit) }}" + no_dnssec_validation: "{{ ipaserver_no_dnssec_validation }}" + ### ad trust ### + enable_compat: "{{ ipaserver_enable_compat }}" + netbios_name: "{{ ipaserver_netbios_name | default(omit) }}" + rid_base: "{{ ipaserver_rid_base | default(omit) }}" + secondary_rid_base: "{{ ipaserver_secondary_rid_base | default(omit) }}" + + ### additional ### + register: result_ipaserver_test + +- block: + # This block is executed only when + # not ansible_check_mode and + # not (not result_ipaserver_test.changed and + # (result_ipaserver_test.client_already_configured is defined or + # result_ipaserver_test.server_already_configured is defined) + + - block: + - name: Install - Master password creation + no_log: yes + ipaserver_master_password: + dm_password: "{{ ipadm_password }}" + master_password: "{{ ipaserver_master_password | default(omit) }}" + register: result_ipaserver_master_password + + - name: Install - Use new master password + no_log: yes + set_fact: + ipaserver_master_password: + "{{ result_ipaserver_master_password.password }}" + + when: ipaserver_master_password is undefined + + - name: Install - Server preparation + ipaserver_prepare: + ### basic ### + dm_password: "{{ ipadm_password }}" + password: "{{ ipaadmin_password }}" + ip_addresses: "{{ ipaserver_ip_addresses | default([]) }}" + domain: "{{ result_ipaserver_test.domain }}" + realm: "{{ result_ipaserver_test.realm }}" + hostname: "{{ result_ipaserver_test.hostname }}" + no_host_dns: "{{ result_ipaserver_test.no_host_dns }}" + ### server ### + setup_adtrust: "{{ ipaserver_setup_adtrust }}" + setup_kra: "{{ ipaserver_setup_kra }}" + setup_dns: "{{ ipaserver_setup_dns }}" + ### certificate system ### + external_ca: "{{ ipaserver_external_ca }}" + external_ca_type: "{{ ipaserver_external_ca_type | default(omit) }}" + external_ca_profile: + "{{ ipaserver_external_ca_profile | default(omit) }}" + external_cert_files: + "{{ ipaserver_external_cert_files | default(omit) }}" + subject_base: "{{ ipaserver_subject_base | default(omit) }}" + ca_subject: "{{ ipaserver_ca_subject | default(omit) }}" + ### dns ### + allow_zone_overlap: "{{ ipaserver_allow_zone_overlap }}" + reverse_zones: "{{ ipaserver_reverse_zones | default([]) }}" + no_reverse: "{{ ipaserver_no_reverse }}" + auto_reverse: "{{ ipaserver_auto_reverse }}" + zonemgr: "{{ ipaserver_zonemgr | default(omit) }}" + forwarders: "{{ ipaserver_forwarders | default([]) }}" + no_forwarders: "{{ ipaserver_no_forwarders }}" + auto_forwarders: "{{ ipaserver_auto_forwarders }}" + forward_policy: "{{ ipaserver_forward_policy | default(omit) }}" + no_dnssec_validation: "{{ ipaserver_no_dnssec_validation }}" + ### ad trust ### + enable_compat: "{{ ipaserver_enable_compat }}" + netbios_name: "{{ ipaserver_netbios_name | default(omit) }}" + rid_base: "{{ ipaserver_rid_base | default(omit) }}" + secondary_rid_base: "{{ ipaserver_secondary_rid_base | default(omit) }}" + ### additional ### + setup_ca: "{{ result_ipaserver_test.setup_ca }}" + _hostname_overridden: "{{ result_ipaserver_test._hostname_overridden }}" + register: result_ipaserver_prepare + + - name: Install - Setup NTP + ipaserver_setup_ntp: + ntp_servers: "{{ result_ipaserver_test.ntp_servers | default(omit) }}" + ntp_pool: "{{ result_ipaserver_test.ntp_pool | default(omit) }}" + when: not ipaclient_no_ntp | bool and (ipaserver_external_cert_files + is undefined or ipaserver_external_cert_files|length < 1) + + - name: Install - Setup DS + ipaserver_setup_ds: + dm_password: "{{ ipadm_password }}" + password: "{{ ipaadmin_password }}" + # master_password: "{{ ipaserver_master_password }}" + domain: "{{ result_ipaserver_test.domain }}" + realm: "{{ result_ipaserver_test.realm | default(omit) }}" + hostname: "{{ result_ipaserver_test.hostname }}" + # ip_addresses: "{{ result_ipaserver_prepare.ip_addresses }}" + # reverse_zones: "{{ result_ipaserver_prepare.reverse_zones }}" + # setup_adtrust: "{{ result_ipaserver_test.setup_adtrust }}" + # setup_kra: "{{ result_ipaserver_test.setup_kra }}" + # setup_dns: "{{ ipaserver_setup_dns }}" + setup_ca: "{{ result_ipaserver_test.setup_ca }}" + # no_host_dns: "{{ result_ipaserver_test.no_host_dns }}" + dirsrv_config_file: "{{ ipaserver_dirsrv_config_file | default(omit) }}" + dirsrv_cert_files: "{{ ipaserver_dirsrv_cert_files | default(omit) }}" + _dirsrv_pkcs12_info: "{{ result_ipaserver_test._dirsrv_pkcs12_info }}" + external_cert_files: + "{{ ipaserver_external_cert_files | default(omit) }}" + subject_base: "{{ result_ipaserver_prepare.subject_base }}" + ca_subject: "{{ result_ipaserver_prepare.ca_subject }}" + # no_reverse: "{{ ipaserver_no_reverse }}" + # auto_forwarders: "{{ ipaserver_auto_forwarders }}" + no_pkinit: "{{ result_ipaserver_test.no_pkinit }}" + no_hbac_allow: "{{ ipaserver_no_hbac_allow }}" + idstart: "{{ result_ipaserver_test.idstart }}" + idmax: "{{ result_ipaserver_test.idmax }}" + + - name: Install - Setup KRB + ipaserver_setup_krb: + dm_password: "{{ ipadm_password }}" + password: "{{ ipaadmin_password }}" + master_password: "{{ ipaserver_master_password }}" + domain: "{{ result_ipaserver_test.domain }}" + realm: "{{ result_ipaserver_test.realm }}" + hostname: "{{ result_ipaserver_test.hostname }}" + # ip_addresses: "{{ result_ipaserver_prepare.ip_addresses }}" + reverse_zones: "{{ result_ipaserver_prepare.reverse_zones }}" + setup_adtrust: "{{ result_ipaserver_test.setup_adtrust }}" + setup_kra: "{{ result_ipaserver_test.setup_kra }}" + setup_dns: "{{ ipaserver_setup_dns }}" + setup_ca: "{{ result_ipaserver_test.setup_ca }}" + no_host_dns: "{{ result_ipaserver_test.no_host_dns }}" + external_cert_files: + "{{ ipaserver_external_cert_files | default(omit) }}" + subject_base: "{{ result_ipaserver_prepare.subject_base }}" + ca_subject: "{{ result_ipaserver_prepare.ca_subject }}" + no_reverse: "{{ ipaserver_no_reverse }}" + auto_forwarders: "{{ ipaserver_auto_forwarders }}" + no_pkinit: "{{ result_ipaserver_test.no_pkinit }}" + no_hbac_allow: "{{ ipaserver_no_hbac_allow }}" + idstart: "{{ result_ipaserver_test.idstart }}" + idmax: "{{ result_ipaserver_test.idmax }}" + _pkinit_pkcs12_info: "{{ result_ipaserver_test._pkinit_pkcs12_info }}" + + - name: Install - Setup custodia + ipaserver_setup_custodia: + realm: "{{ result_ipaserver_test.realm }}" + hostname: "{{ result_ipaserver_test.hostname }}" + setup_ca: "{{ result_ipaserver_test.setup_ca }}" + + - name: Install - Setup CA + ipaserver_setup_ca: + dm_password: "{{ ipadm_password }}" + password: "{{ ipaadmin_password }}" + master_password: "{{ ipaserver_master_password }}" + # ip_addresses: "{{ result_ipaserver_prepare.ip_addresses }}" + domain: "{{ result_ipaserver_test.domain }}" + realm: "{{ result_ipaserver_test.realm }}" + hostname: "{{ result_ipaserver_test.hostname }}" + no_host_dns: "{{ result_ipaserver_test.no_host_dns }}" + pki_config_override: "{{ ipaserver_pki_config_override | + default(omit) }}" + setup_adtrust: "{{ result_ipaserver_test.setup_adtrust }}" + setup_kra: "{{ result_ipaserver_test.setup_kra }}" + setup_dns: "{{ ipaserver_setup_dns }}" + setup_ca: "{{ result_ipaserver_test.setup_ca }}" + idstart: "{{ result_ipaserver_test.idstart }}" + idmax: "{{ result_ipaserver_test.idmax }}" + no_hbac_allow: "{{ ipaserver_no_hbac_allow }}" + no_pkinit: "{{ result_ipaserver_test.no_pkinit }}" + dirsrv_config_file: "{{ ipaserver_dirsrv_config_file | default(omit) }}" + dirsrv_cert_files: "{{ ipaserver_dirsrv_cert_files | default([]) }}" + _dirsrv_pkcs12_info: "{{ result_ipaserver_test._dirsrv_pkcs12_info }}" + external_ca: "{{ ipaserver_external_ca }}" + external_ca_type: "{{ ipaserver_external_ca_type | default(omit) }}" + external_ca_profile: + "{{ ipaserver_external_ca_profile | default(omit) }}" + external_cert_files: + "{{ ipaserver_external_cert_files | default(omit) }}" + subject_base: "{{ result_ipaserver_prepare.subject_base }}" + _subject_base: "{{ result_ipaserver_prepare._subject_base }}" + ca_subject: "{{ result_ipaserver_prepare.ca_subject }}" + _ca_subject: "{{ result_ipaserver_prepare._ca_subject }}" + ca_signing_algorithm: "{{ ipaserver_ca_signing_algorithm | + default(omit) }}" + reverse_zones: "{{ result_ipaserver_prepare.reverse_zones }}" + no_reverse: "{{ ipaserver_no_reverse }}" + auto_forwarders: "{{ ipaserver_auto_forwarders }}" + register: result_ipaserver_setup_ca + + - name: Copy /root/ipa.csr to "{{ inventory_hostname }}-ipa.csr" + fetch: + src: /root/ipa.csr + dest: "{{ inventory_hostname }}-ipa.csr" + flat: yes + when: result_ipaserver_setup_ca.csr_generated | bool and + ipaserver_copy_csr_to_controller | bool + + - block: + - name: Install - Setup otpd + ipaserver_setup_otpd: + realm: "{{ result_ipaserver_test.realm }}" + hostname: "{{ result_ipaserver_test.hostname }}" + setup_ca: "{{ result_ipaserver_test.setup_ca }}" + + - name: Install - Setup HTTP + ipaserver_setup_http: + dm_password: "{{ ipadm_password }}" + password: "{{ ipaadmin_password }}" + master_password: "{{ ipaserver_master_password }}" + domain: "{{ result_ipaserver_test.domain }}" + realm: "{{ result_ipaserver_test.realm }}" + hostname: "{{ result_ipaserver_test.hostname }}" + # ip_addresses: "{{ result_ipaserver_prepare.ip_addresses }}" + reverse_zones: "{{ result_ipaserver_prepare.reverse_zones }}" + setup_adtrust: "{{ result_ipaserver_test.setup_adtrust }}" + setup_kra: "{{ result_ipaserver_test.setup_kra }}" + setup_dns: "{{ ipaserver_setup_dns }}" + setup_ca: "{{ result_ipaserver_test.setup_ca }}" + no_host_dns: "{{ result_ipaserver_test.no_host_dns }}" + dirsrv_cert_files: "{{ ipaserver_dirsrv_cert_files | default([]) }}" + external_cert_files: + "{{ ipaserver_external_cert_files | default(omit) }}" + subject_base: "{{ result_ipaserver_prepare.subject_base }}" + _subject_base: "{{ result_ipaserver_prepare._subject_base }}" + ca_subject: "{{ result_ipaserver_prepare.ca_subject }}" + _ca_subject: "{{ result_ipaserver_prepare._ca_subject }}" + no_reverse: "{{ ipaserver_no_reverse }}" + auto_forwarders: "{{ ipaserver_auto_forwarders }}" + no_pkinit: "{{ result_ipaserver_test.no_pkinit }}" + no_hbac_allow: "{{ ipaserver_no_hbac_allow }}" + idstart: "{{ result_ipaserver_test.idstart }}" + idmax: "{{ result_ipaserver_test.idmax }}" + http_cert_files: "{{ ipaserver_http_cert_files | default([]) }}" + no_ui_redirect: "{{ ipaserver_no_ui_redirect }}" + _http_pkcs12_info: "{{ result_ipaserver_test._http_pkcs12_info }}" + + - name: Install - Setup KRA + ipaserver_setup_kra: + hostname: "{{ result_ipaserver_test.hostname }}" + setup_ca: "{{ result_ipaserver_test.setup_ca }}" + dm_password: "{{ ipadm_password }}" + setup_kra: "{{ result_ipaserver_test.setup_kra }}" + realm: "{{ result_ipaserver_test.realm }}" + pki_config_override: "{{ ipaserver_pki_config_override | + default(omit) }}" + when: result_ipaserver_test.setup_kra | bool + + - name: Install - Setup DNS + ipaserver_setup_dns: + ip_addresses: "{{ ipaserver_ip_addresses | default([]) }}" + domain: "{{ result_ipaserver_test.domain }}" + realm: "{{ result_ipaserver_test.realm }}" + hostname: "{{ result_ipaserver_test.hostname }}" + setup_ca: "{{ result_ipaserver_test.setup_ca }}" + setup_dns: "{{ ipaserver_setup_dns }}" + forwarders: "{{ result_ipaserver_prepare.forwarders }}" + forward_policy: "{{ result_ipaserver_prepare.forward_policy }}" + zonemgr: "{{ ipaserver_zonemgr | default(omit) }}" + no_dnssec_validation: "{{ result_ipaserver_prepare.no_dnssec_validation }}" + ### additional ### + dns_ip_addresses: "{{ result_ipaserver_prepare.dns_ip_addresses }}" + dns_reverse_zones: "{{ result_ipaserver_prepare.dns_reverse_zones }}" + when: ipaserver_setup_dns | bool + + - name: Install - Setup ADTRUST + ipaserver_setup_adtrust: + hostname: "{{ result_ipaserver_test.hostname }}" + setup_ca: "{{ result_ipaserver_test.setup_ca }}" + setup_adtrust: "{{ result_ipaserver_test.setup_adtrust }}" + ### ad trust ### + enable_compat: "{{ ipaserver_enable_compat }}" + rid_base: "{{ result_ipaserver_test.rid_base }}" + secondary_rid_base: "{{ result_ipaserver_test.secondary_rid_base }}" + ### additional ### + adtrust_netbios_name: "{{ result_ipaserver_prepare.adtrust_netbios_name }}" + adtrust_reset_netbios_name: + "{{ result_ipaserver_prepare.adtrust_reset_netbios_name }}" + when: result_ipaserver_test.setup_adtrust + + - name: Install - Set DS password + ipaserver_set_ds_password: + dm_password: "{{ ipadm_password }}" + password: "{{ ipaadmin_password }}" + domain: "{{ result_ipaserver_test.domain }}" + realm: "{{ result_ipaserver_test.realm }}" + hostname: "{{ result_ipaserver_test.hostname }}" + setup_ca: "{{ result_ipaserver_test.setup_ca }}" + subject_base: "{{ result_ipaserver_prepare.subject_base }}" + ca_subject: "{{ result_ipaserver_prepare.ca_subject }}" + no_pkinit: "{{ result_ipaserver_test.no_pkinit }}" + no_hbac_allow: "{{ ipaserver_no_hbac_allow }}" + idstart: "{{ result_ipaserver_test.idstart }}" + idmax: "{{ result_ipaserver_test.idmax }}" + dirsrv_config_file: "{{ ipaserver_dirsrv_config_file | default(omit) }}" + _dirsrv_pkcs12_info: "{{ result_ipaserver_test._dirsrv_pkcs12_info }}" + + - name: Install - Setup client + include_role: + name: ipaclient + vars: + state: present + ipaclient_on_master: yes + ipaclient_domain: "{{ result_ipaserver_test.domain }}" + ipaclient_realm: "{{ result_ipaserver_test.realm }}" + ipaclient_servers: ["{{ result_ipaserver_test.hostname }}"] + ipaclient_hostname: "{{ result_ipaserver_test.hostname }}" + ipaclient_no_ntp: + "{{ 'true' if result_ipaserver_test.ipa_python_version >= 40690 + else 'false' }}" + ipaclient_install_packages: "{{ ipaserver_install_packages }}" + + - name: Install - Enable IPA + ipaserver_enable_ipa: + hostname: "{{ result_ipaserver_test.hostname }}" + setup_dns: "{{ ipaserver_setup_dns }}" + setup_ca: "{{ result_ipaserver_test.setup_ca }}" + register: result_ipaserver_enable_ipa + + - name: Install - Cleanup root IPA cache + file: + path: "/root/.ipa_cache" + state: absent + when: result_ipaserver_enable_ipa.changed + + - name: Install - Configure firewalld + command: > + firewall-cmd + --permanent + --add-service=freeipa-ldap + --add-service=freeipa-ldaps + {{ "--add-service=freeipa-trust" if ipaserver_setup_adtrust | bool + else "" }} + {{ "--add-service=dns" if ipaserver_setup_dns | bool else "" }} + {{ "--add-service=ntp" if not ipaclient_no_ntp | bool else "" }} + when: ipaserver_setup_firewalld | bool + + - name: Install - Configure firewalld runtime + command: > + firewall-cmd + --add-service=freeipa-ldap + --add-service=freeipa-ldaps + {{ "--add-service=freeipa-trust" if ipaserver_setup_adtrust | bool + else "" }} + {{ "--add-service=dns" if ipaserver_setup_dns | bool else "" }} + {{ "--add-service=ntp" if not ipaclient_no_ntp | bool else "" }} + when: ipaserver_setup_firewalld | bool + + when: not result_ipaserver_setup_ca.csr_generated | bool + + when: not ansible_check_mode and not + (not result_ipaserver_test.changed and + (result_ipaserver_test.client_already_configured is defined or + result_ipaserver_test.server_already_configured is defined)) diff --git a/roles/ipaserver/tasks/main.yml b/roles/ipaserver/tasks/main.yml new file mode 100644 index 0000000..6ae77ae --- /dev/null +++ b/roles/ipaserver/tasks/main.yml @@ -0,0 +1,18 @@ +--- +# tasks file for ipaserver + +- name: Import variables specific to distribution + include_vars: "{{ item }}" + with_first_found: + - "vars/{{ ansible_distribution }}-{{ ansible_distribution_version }}.yml" + - "vars/{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml" + - "vars/{{ ansible_distribution }}.yml" + - "vars/default.yml" + +- name: Install IPA server + include_tasks: install.yml + when: state|default('present') == 'present' + +- name: Uninstall IPA server + include_tasks: uninstall.yml + when: state|default('present') == 'absent' diff --git a/roles/ipaserver/tasks/python_2_3_test.yml b/roles/ipaserver/tasks/python_2_3_test.yml new file mode 100644 index 0000000..c8b2009 --- /dev/null +++ b/roles/ipaserver/tasks/python_2_3_test.yml @@ -0,0 +1,23 @@ +--- +- block: + - name: Verify Python3 import + script: py3test.py + register: result_py3test + failed_when: False + changed_when: False + check_mode: no + + - name: Set python interpreter to 3 + set_fact: + ansible_python_interpreter: "/usr/bin/python3" + when: result_py3test.rc == 0 + + - name: Fail for IPA 4.5.90 + fail: msg="You need to install python2 bindings for ipa server usage" + when: result_py3test.rc != 0 and "not usable with python3" + in result_py3test.stdout + + - name: Set python interpreter to 2 + set_fact: + ansible_python_interpreter: "/usr/bin/python2" + when: result_py3test.failed or result_py3test.rc != 0 diff --git a/roles/ipaserver/tasks/uninstall.yml b/roles/ipaserver/tasks/uninstall.yml new file mode 100644 index 0000000..d018429 --- /dev/null +++ b/roles/ipaserver/tasks/uninstall.yml @@ -0,0 +1,23 @@ +--- +# tasks to uninstall IPA server + +# - name: Uninstall - Include Python2/3 import test +# import: "{{ role_path }}/tasks/python_2_3_test.yml" + +- name: Uninstall - Uninstall IPA server + command: > + /usr/sbin/ipa-server-install + --uninstall + -U + {{ '--ignore-topology-disconnect' if ipaserver_ignore_topology_disconnect + | bool else '' }} + {{ '--ignore-last-of-role' if ipaserver_ignore_last_of_role | bool else ''}} + register: uninstall + # 1 means that uninstall failed because IPA server was not configured + failed_when: uninstall.rc != 0 and uninstall.rc != 1 + changed_when: uninstall.rc == 0 + +#- name: Remove IPA server packages +# package: +# name: "{{ ipaserver_packages }}" +# state: absent diff --git a/roles/ipaserver/vars/CentOS-7.yml b/roles/ipaserver/vars/CentOS-7.yml new file mode 100644 index 0000000..1186375 --- /dev/null +++ b/roles/ipaserver/vars/CentOS-7.yml @@ -0,0 +1,6 @@ +# defaults file for ipaserver +# vars/rhel.yml +ipaserver_packages: [ "ipa-server", "libselinux-python" ] +ipaserver_packages_dns: [ "ipa-server-dns" ] +ipaserver_packages_adtrust: [ "ipa-server-trust-ad" ] +ipaserver_packages_firewalld: [ "firewalld" ] \ No newline at end of file diff --git a/roles/ipaserver/vars/CentOS-8.yml b/roles/ipaserver/vars/CentOS-8.yml new file mode 120000 index 0000000..d49e1cd --- /dev/null +++ b/roles/ipaserver/vars/CentOS-8.yml @@ -0,0 +1 @@ +RedHat-8.yml \ No newline at end of file diff --git a/roles/ipaserver/vars/Fedora-25.yml b/roles/ipaserver/vars/Fedora-25.yml new file mode 100644 index 0000000..374056c --- /dev/null +++ b/roles/ipaserver/vars/Fedora-25.yml @@ -0,0 +1,4 @@ +ipaserver_packages: [ "ipa-server", "libselinux-python" ] +ipaserver_packages_dns: [ "ipa-server-dns" ] +ipaserver_packages_adtrust: [ "ipa-server-trust-ad" ] +ipaserver_packages_firewalld: [ "firewalld" ] \ No newline at end of file diff --git a/roles/ipaserver/vars/Fedora-26.yml b/roles/ipaserver/vars/Fedora-26.yml new file mode 100644 index 0000000..374056c --- /dev/null +++ b/roles/ipaserver/vars/Fedora-26.yml @@ -0,0 +1,4 @@ +ipaserver_packages: [ "ipa-server", "libselinux-python" ] +ipaserver_packages_dns: [ "ipa-server-dns" ] +ipaserver_packages_adtrust: [ "ipa-server-trust-ad" ] +ipaserver_packages_firewalld: [ "firewalld" ] \ No newline at end of file diff --git a/roles/ipaserver/vars/Fedora-27.yml b/roles/ipaserver/vars/Fedora-27.yml new file mode 100644 index 0000000..b8bfb57 --- /dev/null +++ b/roles/ipaserver/vars/Fedora-27.yml @@ -0,0 +1,4 @@ +ipaserver_packages: [ "ipa-server", "libselinux-python" ] +ipaserver_packages_dns: [ "ipa-server-dns" ] +ipaserver_packages_adtrust: [ "ipa-server-trust-ad" ] +ipaserver_packages_firewalld: [ "firewalld" ] diff --git a/roles/ipaserver/vars/Fedora.yml b/roles/ipaserver/vars/Fedora.yml new file mode 100644 index 0000000..55a3838 --- /dev/null +++ b/roles/ipaserver/vars/Fedora.yml @@ -0,0 +1,4 @@ +ipaserver_packages: [ "freeipa-server", "python3-libselinux" ] +ipaserver_packages_dns: [ "freeipa-server-dns" ] +ipaserver_packages_adtrust: [ "freeipa-server-trust-ad" ] +ipaserver_packages_firewalld: [ "firewalld" ] \ No newline at end of file diff --git a/roles/ipaserver/vars/RedHat-7.3.yml b/roles/ipaserver/vars/RedHat-7.3.yml new file mode 100644 index 0000000..1186375 --- /dev/null +++ b/roles/ipaserver/vars/RedHat-7.3.yml @@ -0,0 +1,6 @@ +# defaults file for ipaserver +# vars/rhel.yml +ipaserver_packages: [ "ipa-server", "libselinux-python" ] +ipaserver_packages_dns: [ "ipa-server-dns" ] +ipaserver_packages_adtrust: [ "ipa-server-trust-ad" ] +ipaserver_packages_firewalld: [ "firewalld" ] \ No newline at end of file diff --git a/roles/ipaserver/vars/RedHat-7.yml b/roles/ipaserver/vars/RedHat-7.yml new file mode 100644 index 0000000..1186375 --- /dev/null +++ b/roles/ipaserver/vars/RedHat-7.yml @@ -0,0 +1,6 @@ +# defaults file for ipaserver +# vars/rhel.yml +ipaserver_packages: [ "ipa-server", "libselinux-python" ] +ipaserver_packages_dns: [ "ipa-server-dns" ] +ipaserver_packages_adtrust: [ "ipa-server-trust-ad" ] +ipaserver_packages_firewalld: [ "firewalld" ] \ No newline at end of file diff --git a/roles/ipaserver/vars/RedHat-8.yml b/roles/ipaserver/vars/RedHat-8.yml new file mode 100644 index 0000000..7f5ae46 --- /dev/null +++ b/roles/ipaserver/vars/RedHat-8.yml @@ -0,0 +1,6 @@ +# defaults file for ipaserver +# vars/RedHat-8.yml +ipaserver_packages: [ "@idm:DL1/server" ] +ipaserver_packages_dns: [ "@idm:DL1/dns" ] +ipaserver_packages_adtrust: [ "@idm:DL1/adtrust" ] +ipaserver_packages_firewalld: [ "firewalld" ] diff --git a/roles/ipaserver/vars/Ubuntu.yml b/roles/ipaserver/vars/Ubuntu.yml new file mode 100644 index 0000000..d0e01ea --- /dev/null +++ b/roles/ipaserver/vars/Ubuntu.yml @@ -0,0 +1,5 @@ +# vars/Ubuntu.yml +ipaserver_packages: [ "freeipa-server" ] +ipaserver_packages_dns: [ "freeipa-server-dns" ] +ipaserver_packages_adtrust: [ "freeipa-server-trust-ad" ] +ipaserver_packages_firewalld: [ "firewalld" ] diff --git a/roles/ipaserver/vars/default.yml b/roles/ipaserver/vars/default.yml new file mode 100644 index 0000000..9f6d58a --- /dev/null +++ b/roles/ipaserver/vars/default.yml @@ -0,0 +1,6 @@ +# defaults file for ipaserver +# vars/default.yml +ipaserver_packages: [ "ipa-server", "python3-libselinux" ] +ipaserver_packages_dns: [ "ipa-server-dns" ] +ipaserver_packages_adtrust: [ "freeipa-server-trust-ad" ] +ipaserver_packages_firewalld: [ "firewalld" ] diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b254a8e --- /dev/null +++ b/setup.cfg @@ -0,0 +1,22 @@ +[metadata] +name = ansible-freeipa +summary = Ansible roles for FreeIPA +description-file = + README.md +home-page = https://github.com/freeipa/ansible-freeipa +classifier = + Intended Audience :: Developers + Intended Audience :: System Administrators + Operating System :: POSIX :: Linux + +[pbr] +warnerrors = True + +[wheel] +universal = 1 + +[files] +data_files = + /usr/share/ansible/roles/ipaclient = roles/ipaclient/* + /usr/share/ansible/roles/ipaserver = roles/ipaserver/* + /usr/share/ansible/roles/ipareplica = roles/ipareplica/* diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..33111c9 --- /dev/null +++ b/setup.py @@ -0,0 +1,13 @@ +import setuptools + +# In python < 2.7.4, a lazy loading of package `pbr` will break +# setuptools if some other modules registered functions in `atexit`. +# solution from: http://bugs.python.org/issue15881#msg170215 +try: + import multiprocessing # noqa +except ImportError: + pass + +setuptools.setup( + setup_requires=['pbr>=2.0.0'], + pbr=True) diff --git a/site.yml b/site.yml new file mode 100644 index 0000000..18a4255 --- /dev/null +++ b/site.yml @@ -0,0 +1,8 @@ +--- +- name: Playbook to install IPA clients + hosts: ipaclients + become: true + + roles: + - role: ipaclient + state: present diff --git a/tests/external-signed-ca-with-automatic-copy/external-ca.sh b/tests/external-signed-ca-with-automatic-copy/external-ca.sh new file mode 100644 index 0000000..9bf6f1c --- /dev/null +++ b/tests/external-signed-ca-with-automatic-copy/external-ca.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +master=$1 +if [ -z "$master" ]; then + echo "ERROR: master is not set" + echo + echo "usage: $0 master-fqdn domain" + exit 0; +fi + +PASSWORD="SomeCApassword" +DBDIR="${master}-nssdb" +PWDFILE="$DBDIR/pwdfile.txt" +NOISE="$DBDIR/noise.txt" + +domain=$2 +if [ -z "$domain" ]; then + echo "ERROR: domain is not set" + echo + echo "usage: $0 master-fqdn domain" + exit 0; +fi + +if [ ! -f "${master}-ipa.csr" ]; then + echo "ERROR: ${master}-ipa.csr missing" + exit 1; +fi + +ROOT_KEY_ID=0x$(dd if=/dev/urandom bs=20 count=1 | xxd -p) +IPA_CA_KEY_ID=0x$(dd if=/dev/urandom bs=20 count=1 | xxd -p) + +# Prepare a new NSS database to serve us as an external CA +rm -rf "$DBDIR" +mkdir "$DBDIR" +echo "$PASSWORD" > "$PWDFILE" +dd count=10 bs=1024 if=/dev/random of="$NOISE" 2>/dev/null +certutil -N -d "$DBDIR" -f "$PWDFILE" + +# Generate a CA certificate +echo -e "0\n1\n5\n6\n9\ny\ny\n\ny\n${ROOT_KEY_ID}\nn\n" \ + | certutil -d "$DBDIR" -f "$PWDFILE" -S -z "$NOISE" -n ca -x -t C,C,C \ + -s "CN=PRIMARY,O=$domain" -x -1 -2 --extSKID -m 1 + +# Change the form of the CSR from PEM to DER for the NSS database +openssl req -outform der -in "${master}-ipa.csr" -out "$DBDIR/req.csr" + +# Sign the certificate request +echo -e "0\n1\n5\n6\n9\ny\ny\n\ny\ny\n${ROOT_KEY_ID}\n\n\nn\n${IPA_CA_KEY_ID}\nn\n" \ + | certutil -d "$DBDIR" -f "$PWDFILE" -C -z "$NOISE" -c ca \ + -i "$DBDIR/req.csr" -o "$DBDIR/external.cer" -1 -2 -3 --extSKID -m 2 + +openssl x509 -inform der -in "$DBDIR/external.cer" -out "$DBDIR/external.pem" + +# Export the NSS CA certificate and add it to a chain file +certutil -L -n ca -d "$DBDIR" -a > "$DBDIR/ca.crt" +openssl x509 -text -in "$DBDIR/external.pem" > "$DBDIR/chain.crt" +openssl x509 -text -in "$DBDIR/ca.crt" >> "$DBDIR/chain.crt" + +cp "$DBDIR/chain.crt" "${master}-chain.crt" diff --git a/tests/external-signed-ca-with-automatic-copy/install-server-with-external-ca-with-automatic-copy.yml b/tests/external-signed-ca-with-automatic-copy/install-server-with-external-ca-with-automatic-copy.yml new file mode 100644 index 0000000..e856fb4 --- /dev/null +++ b/tests/external-signed-ca-with-automatic-copy/install-server-with-external-ca-with-automatic-copy.yml @@ -0,0 +1,36 @@ +--- +- name: Playbook to configure IPA server step1 + hosts: ipaserver + become: true + vars: + ipaserver_external_ca: yes + ipaserver_copy_csr_to_controller: yes + + roles: + - role: ipaserver + state: present + +- name: Create CA, get /root/ipa.csr signed by your CA, .. + hosts: localhost + gather_facts: false + + tasks: + - name: Run external-ca.sh + command: > + /bin/bash + external-ca.sh + "{{ groups.ipaserver[0] }}" + "{{ ipaserver_domain | default(groups.ipaserver[0].split('.')[1:] | join ('.')) }}" + args: + chdir: "{{ playbook_dir }}" + +- name: Playbook to configure IPA server step2 + hosts: ipaserver + become: true + vars: + ipaserver_external_cert_files_from_controller: "{{ groups.ipaserver[0] + '-chain.crt' }}" + #ipaserver_external_ca_file: "{{ groups.ipaserver[0] + '-cacert.asc' }}" + + roles: + - role: ipaserver + state: present diff --git a/tests/external-signed-ca-with-automatic-copy/inventory b/tests/external-signed-ca-with-automatic-copy/inventory new file mode 100644 index 0000000..e7e6b7e --- /dev/null +++ b/tests/external-signed-ca-with-automatic-copy/inventory @@ -0,0 +1,8 @@ +[ipaserver] +ipaserver.test.local + +[ipaserver:vars] +ipaadmin_password=SomeADMINpassword +ipadm_password=SomeDMpassword +ipaserver_domain=test.local +ipaserver_realm=TEST.LOCAL diff --git a/tests/external-signed-ca-with-manual-copy/external-ca.sh b/tests/external-signed-ca-with-manual-copy/external-ca.sh new file mode 120000 index 0000000..de59ac8 --- /dev/null +++ b/tests/external-signed-ca-with-manual-copy/external-ca.sh @@ -0,0 +1 @@ +../external-signed-ca-with-automatic-copy/external-ca.sh \ No newline at end of file diff --git a/tests/external-signed-ca-with-manual-copy/install-server-with-external-ca-with-manual-copy.yml b/tests/external-signed-ca-with-manual-copy/install-server-with-external-ca-with-manual-copy.yml new file mode 100644 index 0000000..33b466c --- /dev/null +++ b/tests/external-signed-ca-with-manual-copy/install-server-with-external-ca-with-manual-copy.yml @@ -0,0 +1,49 @@ +--- +- name: Playbook to configure IPA server step1 + hosts: ipaserver + become: true + vars: + ipaserver_external_ca: yes + + roles: + - role: ipaserver + state: present + + post_tasks: + - name: Copy CSR /root/ipa.csr from node to "{{ groups.ipaserver[0] + '-ipa.csr' }}" + fetch: + src: /root/ipa.csr + dest: "{{ groups.ipaserver[0] + '-ipa.csr' }}" + flat: yes + +- name: Get /root/ipa.csr, create CA, sign with our CA and copy to node + hosts: localhost + gather_facts: false + + tasks: + - name: Run external-ca.sh + command: > + /bin/bash + external-ca.sh + "{{ groups.ipaserver[0] }}" + "{{ ipaserver_domain | default(groups.ipaserver[0].split('.')[1:] | join ('.')) }}" + args: + chdir: "{{ playbook_dir }}" + +- name: Playbook to configure IPA server step2 + hosts: ipaserver + become: true + vars: + ipaserver_external_cert_files: "/root/chain.crt" + #ipaserver_external_ca_file: "cacert.asc" + + pre_tasks: + - name: Copy "{{ groups.ipaserver[0] + '-chain.crt' }}" to /root/chain.crt on node + copy: + src: "{{ groups.ipaserver[0] + '-chain.crt' }}" + dest: "/root/chain.crt" + force: yes + + roles: + - role: ipaserver + state: present diff --git a/tests/external-signed-ca-with-manual-copy/inventory b/tests/external-signed-ca-with-manual-copy/inventory new file mode 100644 index 0000000..e7e6b7e --- /dev/null +++ b/tests/external-signed-ca-with-manual-copy/inventory @@ -0,0 +1,8 @@ +[ipaserver] +ipaserver.test.local + +[ipaserver:vars] +ipaadmin_password=SomeADMINpassword +ipadm_password=SomeDMpassword +ipaserver_domain=test.local +ipaserver_realm=TEST.LOCAL diff --git a/tests/group/test_group.yml b/tests/group/test_group.yml new file mode 100644 index 0000000..5fc63dd --- /dev/null +++ b/tests/group/test_group.yml @@ -0,0 +1,175 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure users user1, user2 and user3 are absent + ipauser: + ipaadmin_password: MyPassword123 + name: user1,user2,user3 + state: absent + + - name: Ensure group group3, group2 and group1 are absent + ipagroup: + ipaadmin_password: MyPassword123 + name: group3,group2,group1 + state: absent + + - name: Ensure users user1..user3 are present + ipauser: + ipaadmin_password: MyPassword123 + users: + - name: user1 + first: user1 + last: Last + - name: user2 + first: user2 + last: Last + - name: user3 + first: user3 + last: Last + register: result + failed_when: not result.changed + + - name: Ensure group1 is present + ipagroup: + ipaadmin_password: MyPassword123 + name: group1 + register: result + failed_when: not result.changed + + - name: Ensure group1 is present again + ipagroup: + ipaadmin_password: MyPassword123 + name: group1 + register: result + failed_when: result.changed + + - name: Ensure group2 is present + ipagroup: + ipaadmin_password: MyPassword123 + name: group2 + register: result + failed_when: not result.changed + + - name: Ensure group2 is present again + ipagroup: + ipaadmin_password: MyPassword123 + name: group2 + register: result + failed_when: result.changed + + - name: Ensure group3 is present + ipagroup: + ipaadmin_password: MyPassword123 + name: group3 + register: result + failed_when: not result.changed + + - name: Ensure group3 is present again + ipagroup: + ipaadmin_password: MyPassword123 + name: group3 + register: result + failed_when: result.changed + + - name: Ensure groups group2 and group3 are present in group group1 + ipagroup: + ipaadmin_password: MyPassword123 + name: group1 + group: + - group2 + - group3 + action: member + register: result + failed_when: not result.changed + + - name: Ensure groups group2 and group3 are present in group group1 again + ipagroup: + ipaadmin_password: MyPassword123 + name: group1 + group: + - group2 + - group3 + action: member + register: result + failed_when: result.changed + + - name: Ensure group3 ia present in group group1 + ipagroup: + ipaadmin_password: MyPassword123 + name: group1 + group: + - group3 + action: member + register: result + failed_when: result.changed + + - name: Ensure users user1, user2 and user3 are present in group group1 + ipagroup: + ipaadmin_password: MyPassword123 + name: group1 + user: + - user1 + - user2 + - user3 + action: member + register: result + failed_when: not result.changed + + - name: Ensure users user1, user2 and user3 are present in group group1 again + ipagroup: + ipaadmin_password: MyPassword123 + name: group1 + user: + - user1 + - user2 + - user3 + action: member + register: result + failed_when: result.changed + + #- ipagroup: + # ipaadmin_password: MyPassword123 + # name: group1 + # user: + # - user7 + # action: member + + - name: Ensure user user7 is absent in group group1 + ipagroup: + ipaadmin_password: MyPassword123 + name: group1 + user: + - user7 + action: member + state: absent + register: result + failed_when: result.changed + + - name: Ensure group group4 is absent + ipagroup: + ipaadmin_password: MyPassword123 + name: group4 + state: absent + register: result + failed_when: result.changed + + - name: Ensure group group3, group2 and group1 are absent + ipagroup: + ipaadmin_password: MyPassword123 + name: group3,group2,group1 + state: absent + register: result + failed_when: not result.changed + + - name: Ensure users user1, user2 and user3 are absent + ipauser: + ipaadmin_password: MyPassword123 + name: user1,user2,user3 + state: absent + register: result + failed_when: not result.changed + diff --git a/tests/hbacrule/test_hbacrule.yml b/tests/hbacrule/test_hbacrule.yml new file mode 100644 index 0000000..a5615cc --- /dev/null +++ b/tests/hbacrule/test_hbacrule.yml @@ -0,0 +1,338 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure HBAC Rule allhosts is absent + ipahbacrule: + ipaadmin_password: MyPassword123 + name: allhosts,sshd-pinky,loginRule + state: absent + + - name: User pinky absent + ipauser: + ipaadmin_password: MyPassword123 + name: pinky + state: absent + + - name: User group login absent + ipagroup: + ipaadmin_password: MyPassword123 + name: login + state: absent + + - name: User pinky present + ipauser: + ipaadmin_password: MyPassword123 + name: pinky + uid: 10001 + gid: 100 + phone: "+555123457" + email: pinky@acme.com + principalexpiration: "20220119235959" + #passwordexpiration: "2022-01-19 23:59:59" + first: pinky + last: Acme + register: result + failed_when: not result.changed + + - name: User group login present + ipagroup: + ipaadmin_password: MyPassword123 + name: login + register: result + failed_when: not result.changed + + - name: Ensure HBAC Rule allhosts is present + ipahbacrule: + ipaadmin_password: MyPassword123 + name: allhosts + usercategory: all + register: result + failed_when: not result.changed + + - name: Ensure HBAC Rule allhosts is present again + ipahbacrule: + ipaadmin_password: MyPassword123 + name: allhosts + usercategory: all + register: result + failed_when: result.changed + + - name: Ensure host "{{ groups.ipaserver[0] }}" is present in HBAC Rule allhosts + ipahbacrule: + ipaadmin_password: MyPassword123 + name: allhosts + host: "{{ groups.ipaserver[0] }}" + action: member + register: result + failed_when: not result.changed + + - name: Ensure host "{{ groups.ipaserver[0] }}" is present in HBAC Rule allhosts again + ipahbacrule: + ipaadmin_password: MyPassword123 + name: allhosts + host: "{{ groups.ipaserver[0] }}" + action: member + register: result + failed_when: result.changed + + - name: Ensure HBAC Rule sshd-pinky is present + ipahbacrule: + ipaadmin_password: MyPassword123 + name: sshd-pinky + hostcategory: all + register: result + failed_when: not result.changed + + - name: Ensure HBAC Rule sshd-pinky is present again + ipahbacrule: + ipaadmin_password: MyPassword123 + name: sshd-pinky + hostcategory: all + register: result + failed_when: result.changed + + - name: Ensure user pinky is present in HBAC Rule sshd-pinky + ipahbacrule: + ipaadmin_password: MyPassword123 + name: sshd-pinky + user: pinky + action: member + register: result + failed_when: not result.changed + + - name: Ensure user pinky is present in HBAC Rule sshd-pinky again + ipahbacrule: + ipaadmin_password: MyPassword123 + name: sshd-pinky + user: pinky + action: member + register: result + failed_when: result.changed + + - name: Ensure HBAC service sshd is present in HBAC Rule sshd-pinky + ipahbacrule: + ipaadmin_password: MyPassword123 + name: sshd-pinky + hbacsvc: sshd + action: member + register: result + failed_when: not result.changed + + - name: Ensure HBAC service sshd is present in HBAC Rule sshd-pinky again + ipahbacrule: + ipaadmin_password: MyPassword123 + name: sshd-pinky + hbacsvc: sshd + action: member + register: result + failed_when: result.changed + + - name: Ensure HBAC Rule loginRule is present with HBAC service sshd + ipahbacrule: + ipaadmin_password: MyPassword123 + name: loginRule + group: login + register: result + failed_when: not result.changed + + - name: Ensure HBAC Rule loginRule is present with HBAC service sshd again + ipahbacrule: + ipaadmin_password: MyPassword123 + name: loginRule + group: login + register: result + failed_when: result.changed + + - name: Ensure user pinky is present in HBAC Rule loginRule + ipahbacrule: + ipaadmin_password: MyPassword123 + name: loginRule + user: pinky + action: member + register: result + failed_when: not result.changed + + - name: Ensure user pinky is present in HBAC Rule loginRule again + ipahbacrule: + ipaadmin_password: MyPassword123 + name: loginRule + user: pinky + action: member + register: result + failed_when: result.changed + + - name: Ensure user pinky is absent in HBAC Rule loginRule + ipahbacrule: + ipaadmin_password: MyPassword123 + name: loginRule + user: pinky + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Ensure user pinky is absent in HBAC Rule loginRule again + ipahbacrule: + ipaadmin_password: MyPassword123 + name: loginRule + user: pinky + action: member + state: absent + register: result + failed_when: result.changed + + - name: Ensure HBAC Rule loginRule is absent + ipahbacrule: + ipaadmin_password: MyPassword123 + name: loginRule + state: absent + register: result + failed_when: not result.changed + + - name: Ensure HBAC Rule loginRule is absent again + ipahbacrule: + ipaadmin_password: MyPassword123 + name: loginRule + state: absent + register: result + failed_when: result.changed + + - name: Ensure HBAC service sshd is absent in HBAC Rule sshd-pinky + ipahbacrule: + ipaadmin_password: MyPassword123 + name: sshd-pinky + hbacsvc: sshd + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Ensure HBAC service sshd is absent in HBAC Rule sshd-pinky again + ipahbacrule: + ipaadmin_password: MyPassword123 + name: sshd-pinky + hbacsvc: sshd + action: member + state: absent + register: result + failed_when: result.changed + + - name: Ensure user pinky is absent in HBAC Rule sshd-pinky + ipahbacrule: + ipaadmin_password: MyPassword123 + name: sshd-pinky + user: pinky + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Ensure user pinky is absent in HBAC Rule sshd-pinky again + ipahbacrule: + ipaadmin_password: MyPassword123 + name: sshd-pinky + user: pinky + action: member + state: absent + register: result + failed_when: result.changed + + - name: Ensure HBAC Rule sshd-pinky is disabled + ipahbacrule: + ipaadmin_password: MyPassword123 + name: sshd-pinky + state: disabled + register: result + failed_when: not result.changed + + - name: Ensure HBAC Rule sshd-pinky is disabled again + ipahbacrule: + ipaadmin_password: MyPassword123 + name: sshd-pinky + state: disabled + register: result + failed_when: result.changed + + - name: Ensure HBAC Rule sshd-pinky is enabled + ipahbacrule: + ipaadmin_password: MyPassword123 + name: sshd-pinky + state: enabled + register: result + failed_when: not result.changed + + - name: Ensure HBAC Rule sshd-pinky is enabled again + ipahbacrule: + ipaadmin_password: MyPassword123 + name: sshd-pinky + state: enabled + register: result + failed_when: result.changed + + - name: Ensure HBAC Rule sshd-pinky is absent + ipahbacrule: + ipaadmin_password: MyPassword123 + name: sshd-pinky + state: absent + register: result + failed_when: not result.changed + + - name: Ensure HBAC Rule sshd-pinky is absent again + ipahbacrule: + ipaadmin_password: MyPassword123 + name: sshd-pinky + state: absent + register: result + failed_when: result.changed + + - name: Ensure host "{{ groups.ipaserver[0] }}" is absent in HBAC Rule allhosts + ipahbacrule: + ipaadmin_password: MyPassword123 + name: allhosts + host: "{{ groups.ipaserver[0] }}" + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Ensure host "{{ groups.ipaserver[0] }}" is absent in HBAC Rule allhosts again + ipahbacrule: + ipaadmin_password: MyPassword123 + name: allhosts + host: "{{ groups.ipaserver[0] }}" + action: member + state: absent + register: result + failed_when: result.changed + + - name: Ensure HBAC Rule allhosts is absent + ipahbacrule: + ipaadmin_password: MyPassword123 + name: allhosts + state: absent + register: result + failed_when: not result.changed + + - name: Ensure HBAC Rule allhosts is absent again + ipahbacrule: + ipaadmin_password: MyPassword123 + name: allhosts + state: absent + register: result + failed_when: result.changed + + - name: User pinky absent + ipauser: + ipaadmin_password: MyPassword123 + name: pinky + state: absent + + - name: User group login absent + ipagroup: + ipaadmin_password: MyPassword123 + name: login + state: absent diff --git a/tests/hbacsvc/test_hbacsvc.yml b/tests/hbacsvc/test_hbacsvc.yml new file mode 100644 index 0000000..d919b66 --- /dev/null +++ b/tests/hbacsvc/test_hbacsvc.yml @@ -0,0 +1,58 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure HBAC Service for http is absent + ipahbacsvc: + ipaadmin_password: MyPassword123 + name: http,tftp + state: absent + + - name: Ensure HBAC Service for http is present + ipahbacsvc: + ipaadmin_password: MyPassword123 + name: http + register: result + failed_when: not result.changed + + - name: Ensure HBAC Service for http is present again + ipahbacsvc: + ipaadmin_password: MyPassword123 + name: http + register: result + failed_when: result.changed + + - name: Ensure HBAC Service for tftp is present + ipahbacsvc: + ipaadmin_password: MyPassword123 + name: tftp + description: TFTP service + register: result + failed_when: not result.changed + + - name: Ensure HBAC Service for tftp is present again + ipahbacsvc: + ipaadmin_password: MyPassword123 + name: tftp + description: TFTP service + register: result + failed_when: result.changed + + - name: Ensure HBAC Services for http and tftp are absent + ipahbacsvc: + ipaadmin_password: MyPassword123 + name: http,tftp + state: absent + register: result + failed_when: not result.changed + + - name: Ensure HBAC Services for http and tftp are absent again + ipahbacsvc: + ipaadmin_password: MyPassword123 + name: http,tftp + state: absent + register: result + failed_when: result.changed diff --git a/tests/hbacsvcgroup/test_hbacsvcgroup.yml b/tests/hbacsvcgroup/test_hbacsvcgroup.yml new file mode 100644 index 0000000..9a659f1 --- /dev/null +++ b/tests/hbacsvcgroup/test_hbacsvcgroup.yml @@ -0,0 +1,91 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure HBAC Service Group login is absent + ipahbacsvcgroup: + ipaadmin_password: MyPassword123 + name: login + state: absent + + - name: Ensure HBAC Service for sshd is present + ipahbacsvc: + ipaadmin_password: MyPassword123 + name: login + + - name: Ensure HBAC Service Group login is present + ipahbacsvcgroup: + ipaadmin_password: MyPassword123 + name: login + register: result + failed_when: not result.changed + + - name: Ensure HBAC Service Group login is present again + ipahbacsvcgroup: + ipaadmin_password: MyPassword123 + name: login + register: result + failed_when: result.changed + + - name: Ensure HBAC Service sshd is present in HBAC Service Group login + ipahbacsvcgroup: + ipaadmin_password: MyPassword123 + name: login + hbacsvc: + - sshd + action: member + register: result + failed_when: not result.changed + + - name: Ensure HBAC Service sshd is present in HBAC Service Group login again + ipahbacsvcgroup: + ipaadmin_password: MyPassword123 + name: login + hbacsvc: + - sshd + action: member + register: result + failed_when: result.changed + + - name: Ensure HBAC Services sshd and foo are absent in HBAC Service Group login + ipahbacsvcgroup: + ipaadmin_password: MyPassword123 + name: login + hbacsvc: + - sshd + - foo + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Ensure HBAC Services sshd and foo are absent in HBAC Service Group login again + ipahbacsvcgroup: + ipaadmin_password: MyPassword123 + name: login + hbacsvc: + - sshd + - foo + action: member + state: absent + register: result + failed_when: result.changed + + - name: Ensure HBAC Service Group login is absent + ipahbacsvcgroup: + ipaadmin_password: MyPassword123 + name: login + state: absent + register: result + failed_when: not result.changed + + - name: Ensure HBAC Service Group login is absent again + ipahbacsvcgroup: + ipaadmin_password: MyPassword123 + name: login + state: absent + register: result + failed_when: result.changed diff --git a/tests/host/certificate/cert1.der b/tests/host/certificate/cert1.der new file mode 100644 index 0000000..334511f Binary files /dev/null and b/tests/host/certificate/cert1.der differ diff --git a/tests/host/certificate/cert1.pem b/tests/host/certificate/cert1.pem new file mode 100644 index 0000000..6fb9d2e --- /dev/null +++ b/tests/host/certificate/cert1.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIC/zCCAeegAwIBAgIUZGHLaSYg1myp6EI4VGWSC27vOrswDQYJKoZIhvcNAQEL +BQAwDzENMAsGA1UEAwwEdGVzdDAeFw0xOTEwMTQxNjI4MzVaFw0yMDEwMTMxNjI4 +MzVaMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQDER/lB8wUAmPTSwSc/NOXNlzdpPOQDSwrhKH6XsqZF4KpQoSY/nmCjAhJm +OVpOUo4K2fGRZ0yAH9fkGv6yJP6c7IAFjLeec7GPHVwN4bZrP1DXfTAmfmXhcRQb +CYkV+wmq8Puzw/+xA9EJrrodnJPPsE6E8HnSVLF6Ys9+cJMJ7HuwOI+wYt3gkmsp +sir1tccmf4x1PP+yHJWdcXyetlFRcmZ8gspjqOR2jb89xSQsh8gcyDW6rPNlSTzY +Z2FmNtjES6ZhCsYL31fQbF2QglidlLGpAlvHUUS+xCigW73cvhFPMWXcfO51Mr15 +RcgYTckY+7QZ2nYqplRBoDlQl6DnAgMBAAGjUzBRMB0GA1UdDgQWBBTPG99XVRdx +pOXMZo3Nhy+ldnf13TAfBgNVHSMEGDAWgBTPG99XVRdxpOXMZo3Nhy+ldnf13TAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAjWTcnIl2mpNbfHAN8 +DB4Kk+RNRmhsH0y+r/47MXVTMMMToCfofeNY3Jeohu+2lIXMPQfTvXUbDTkNAGsG +Lv6LtQEUfSREqgk1eY7bT9BFfpH1uV2ZFhCO9jBA+E4bf55Kx7bgUNG31ykBshOs +OblOJM1lS/0q4TWHAxrsU2PNwPi8X0ten+eGeB8aRshxS17Ij2cH0fdAMmSA+jMA +vTIZl853Bxe0HuozauKwOFWL4qHm61c4O/j1mQCLqJKYfJ9mBDWFQLszd/tF+ePK +iNhZCQly60F8Lumn2CDZj5UIkl8wk9Wls5n1BIQs+M8AN65NAdv7+js8jKUKCuyj +i8r3 +-----END CERTIFICATE----- diff --git a/tests/host/certificate/cert2.der b/tests/host/certificate/cert2.der new file mode 100644 index 0000000..a3ba5a5 Binary files /dev/null and b/tests/host/certificate/cert2.der differ diff --git a/tests/host/certificate/cert2.pem b/tests/host/certificate/cert2.pem new file mode 100644 index 0000000..4a243b2 --- /dev/null +++ b/tests/host/certificate/cert2.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIC/zCCAeegAwIBAgIUAWE1vaA+mZd3nwZqwWH64EbHvR0wDQYJKoZIhvcNAQEL +BQAwDzENMAsGA1UEAwwEdGVzdDAeFw0xOTEwMTQxNjI4NDVaFw0yMDEwMTMxNjI4 +NDVaMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCWzJibKtN8Zf7LgandINhFonx99AKi44iaZkrlMKEObE6Faf8NTUbUgK3V +fJNYmCbA1baLVJ0YZJijJ7S/4o7h7eeqcJVXJkEhWNTimWXNW/YCzTHe3SSapnSY +OKmdHHRClplysL8OyyEG7pbX/aB9iAfFb/+vUFCX5sMwFFrYxOimKJ9Pc/NRFtdv +1wNw1rqWKF1ZzagWRlG4QgzRGwQ4quc7yO98TKikj2OPiIt7Zd46hbqQxmgGBtCk +VOZIhxu77OmNrFsXmM4rZZpmqh0UdqcpwkRojVnGXmNqeMCd6dNTnLhr9wukUYw0 +KgE57zCDVr9Ix+p/dA5R1mG4RJ2XAgMBAAGjUzBRMB0GA1UdDgQWBBSbuiH2lNVr +ID3yt1SsFwtOFKOnpTAfBgNVHSMEGDAWgBSbuiH2lNVrID3yt1SsFwtOFKOnpTAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBCVWd293wWyohFqMFM +HRBBg97T2Uc1yeT0dMH4BpuOaCqQp4q5ep+uLcXEI6+3mEwm8pa/ULQCD8yLLdot +IWlG3+h/4boFpdiPFcBDgT8kGe+0KOzB8Nt7E13QYOu12MNi10qwGrjKhdhu1xBe +4fpY5VCetVU1OLyuTsUyucQsFrtZI0SR83h+blbyoMZ7IhMngCfGUe1bnYeWnLbp +FbigKfPuVDWsMH2kgj05EAd5EgHkWbX8QA8hmcmDKfNT3YZM8kiGQwmFrnQdq8bN +0uHR8Nz+24cbmdbHcD65wlDW6GmYxi8mW+V6bAqn9pir/J14r4YFnqMGgjmdt81t +scJV +-----END CERTIFICATE----- diff --git a/tests/host/certificate/cert3.der b/tests/host/certificate/cert3.der new file mode 100644 index 0000000..783830e Binary files /dev/null and b/tests/host/certificate/cert3.der differ diff --git a/tests/host/certificate/cert3.pem b/tests/host/certificate/cert3.pem new file mode 100644 index 0000000..b2a6a99 --- /dev/null +++ b/tests/host/certificate/cert3.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIC/zCCAeegAwIBAgIUTC33WUoYGFoIVGMwgjbc5J6xCyowDQYJKoZIhvcNAQEL +BQAwDzENMAsGA1UEAwwEdGVzdDAeFw0xOTEwMTQxNjI4NTJaFw0yMDEwMTMxNjI4 +NTJaMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQDCA+6P2eieXHaVJivtWif7SntjjkJm0juRKRRGsT3wt+zCZqoDe8zylTBN +0mse/POWXdC+zXRMC2X/c4V10kgrvWbnNdFdUFfBUphiXSoqnUYHZ6Ta+b4UTzC2 +tECSUEnSCz9n1ofHnyqDyT9FELzVkRkQqexD+BFgZTF39R4q8BA4bWKQy94Kgvb+ +IP77+ou4fhkBLI1MX5nkWa3Oyu4TMzT/tqgPE70hk8wQzUU2aiwJ7IsmnWE6Ysk7 +c4DYMJQF/51bi2ByZWERNjyBY6L+ZV90aL4UFR9O+Pw9HatfHVBRdmzSkKJOr9iu +4summWgH0QYDmbkdhGwYvup0EmEfAgMBAAGjUzBRMB0GA1UdDgQWBBSJCQ8ho0Pp +e0khVhgiMqsvlgxIjzAfBgNVHSMEGDAWgBSJCQ8ho0Ppe0khVhgiMqsvlgxIjzAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAILLPnau32r/YoOVCV +WQotGtySy36aFlHa3T8IkSpatNCPIf3U0FWS6TVYBwY0PBfdqWBkvCuJTupLh0OE +P4TCsDa5pJGOK7blyfiAfcHajqyouACSVNlG63EPvB63h4H4F4HJnhDd4z7pVC/W +PB8w5GTBJNjELmeWfH7nj7lu8UkOdLhzTKL40RPs0k4l09yYBmZqqExxGsSfvRBQ +crwlAsvQ0E/cTNGbyzOKs3SbOM2WEHye6xNEsey01icYcjfjqvEd6mw3+WOUeJAu +DH9/EOloFM2iz5Xp31Ig3WT0RVy+lMriG9GesPpFBs2xp9wQCXLNIkpbHKyYs3vo +MyBH +-----END CERTIFICATE----- diff --git a/tests/host/certificate/private1.key b/tests/host/certificate/private1.key new file mode 100644 index 0000000..cb76298 --- /dev/null +++ b/tests/host/certificate/private1.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDER/lB8wUAmPTS +wSc/NOXNlzdpPOQDSwrhKH6XsqZF4KpQoSY/nmCjAhJmOVpOUo4K2fGRZ0yAH9fk +Gv6yJP6c7IAFjLeec7GPHVwN4bZrP1DXfTAmfmXhcRQbCYkV+wmq8Puzw/+xA9EJ +rrodnJPPsE6E8HnSVLF6Ys9+cJMJ7HuwOI+wYt3gkmspsir1tccmf4x1PP+yHJWd +cXyetlFRcmZ8gspjqOR2jb89xSQsh8gcyDW6rPNlSTzYZ2FmNtjES6ZhCsYL31fQ +bF2QglidlLGpAlvHUUS+xCigW73cvhFPMWXcfO51Mr15RcgYTckY+7QZ2nYqplRB +oDlQl6DnAgMBAAECggEBALtKTj6urHxId3xPEKsQR6Noglgp4Qx/Y687W6hWsLAG +051CV+PmtSF2DaZ7XX9U6PLTydzL68RqHjArzhKgmE+WoAYrot5QWQJNqpQYZ19o +uDQW4YYpn/+BTgUKkUNnGm+BqTt8b5QyJxoNHsy4ppZMDnBtomCfrgYxGPr2YmfZ +Ee7oEEf1xIU2maE5Nxv97lNR2Xvm2R4F8lzRcHmvejLYNiqZ2Ag4ijnKVTpoEMUy +afl5LNSzGJETXsv+LtaJGEr6x8/IesVSCdyX2LZeAyQPLKwb3YQDkQree54vwS7p +cVmQdx6fLTYV1tOSXUEC2ibInO188kGA198HSqSgHJkCgYEA7zhL+6tYZdXoMzTH +hXHLYGHmQsQXxleH5uciz4q+en7do6BFB2DqIgLTpcD/H8XMDlg9WO7756H1zqvb +6IOkqwsrro/fsgb6FrmjXl8zlkwT3pTNJfmBydRf7Qk2woCRPUoLZBRAumNL8RSx +Xm1/DbPbTR3jjVNH9dPb3Efd0qUCgYEA0gyesMgwDzjsXpPUsuWTMBMziy0KRFNT +lCMCI5DVpy/XnptyLdkY93jvmq+VWbily4KlOYbfYJ/16xeNZ7aNOMnC6z4z9p9+ +w3E9q5xKJcAJP5kN/WnjBwErveDK9r1YSj8RJpvapJFqjxA5WVTwADtyBhgNS4Og +mXPPBleMC5sCgYEA0Yw/AvXVOV9nR3O0UvCbdpJLYbDkIpoKMfnGRIcE08jN3cdG +sG/0qFZRj6C/2tUpKmehVYYCo6T77U4eFE88r5fZa9Ab45a4+68hrEk4py99ODyg +d+NYDbQ7Uyf/D+IPV+DEmaYkDSFuJIA73ruL0DT8pVDJQ8LwBibPMObDKQECgYBa +aUYxD6noE3diaj1GV5zYN5ubD2L47+jsvXjhOClOkkA8K+qko2qksrBno6YkfV8X +zv8xWMVzgMbIT1X1S1VUGTxGJ3sUb6iPlYGXCWm9AAC7GDU2W8p1rGJYk5apR+zl +4GmQdctRxKnaNICK3A2F/BBjYRzv4RNSmc+Fik9kewKBgHsCF3uEP7ONvmEjYLQ4 +7+6fZ+m4BXKeU/kKQoEXSjSFn0dBIHo+2yuafSUz04VJCVXUic3c47kHwVtgX5lu +jEUL1jgK4aBbl9cvywupHBf3spAP89aocgFiC9uUJzp3u39U0LpgXY7Z+1lUsCL+ +VG2oGh0KVgazjUzmbTf9ZcLp +-----END PRIVATE KEY----- diff --git a/tests/host/certificate/private2.key b/tests/host/certificate/private2.key new file mode 100644 index 0000000..59d83b9 --- /dev/null +++ b/tests/host/certificate/private2.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWzJibKtN8Zf7L +gandINhFonx99AKi44iaZkrlMKEObE6Faf8NTUbUgK3VfJNYmCbA1baLVJ0YZJij +J7S/4o7h7eeqcJVXJkEhWNTimWXNW/YCzTHe3SSapnSYOKmdHHRClplysL8OyyEG +7pbX/aB9iAfFb/+vUFCX5sMwFFrYxOimKJ9Pc/NRFtdv1wNw1rqWKF1ZzagWRlG4 +QgzRGwQ4quc7yO98TKikj2OPiIt7Zd46hbqQxmgGBtCkVOZIhxu77OmNrFsXmM4r +ZZpmqh0UdqcpwkRojVnGXmNqeMCd6dNTnLhr9wukUYw0KgE57zCDVr9Ix+p/dA5R +1mG4RJ2XAgMBAAECggEAS5nXCDO4Qy1/R9eBqXLF+mMztpGWoMMhwQZ3ld+DXw+9 +bfVuAOU1FWRNwjHqTQg6pYJ/Oer5tzj3rRRC8dBLgckb078Nn9t125oFYHU3LHVm +KJFm5yxHJaE94vLFVhbl0lxeIbmqj2gW7rq+tRpaU5TXEIzNyr6hKQZv5LLPuMx6 +MiBrSpkCwfPf9psv6k2GIGqE1JuY99dNqdEUi8UQryNMzV4pthUmVybO8NPxUY8M +s/VAbG1Hy9tgInR3wRgTjEc2ejUJrTziiqiZarZtCp+JSZufYakDU9yZbu9v4Oz9 +ityPdApkW8CuZnJcUDAtdgtKMhWyBPnWcrUgkbV0AQKBgQDGY1saiI9M7VlleyDc +QNVXpPCmOpDLso5X3hZrrHDgDIGkvXa026Q5ufkdxkybRYJeOCdYzIM/iXSJlgNe +R2a+aoAsePfEVFAe96ZgzrLrBq7lGvcPXGpT6GTVl0d0CwN/vG1Tzk89Hq3xIBbh +NTlM+j2ot66xgekIsE0v5Pi41wKBgQDCl14mgaui4DqYFYlI/ckI00r/X0/0HIhf +kf/Ck/pkF89IeOAK+O4GOfVoMk3vi1gDYgiz6G7h+sUsFTOYKuP9io/vX0pIFNOA +NPgaVtRKitiepNo4vwc+/PRmxvf2XXFXFRSiYf0jDzruvE3yDzWwX9P1nQFBQoPj +r8g/6+7pQQKBgDXHnVzWBDLQbNmLxV6v3KXDutD1M2dk4h2DwQQzXO3/te1YxyNE +H4LenV+q7/1vnGW6R0BVQIcq1gKuPf+Cz6Fy8Ygcyt3YFVgvvlSj8/CugR7ubmcl +oFVavGsCdYZJrgsko2aCmQxykqi5EDrA2OW7OJfSI3NPSkLmuCXxplNFAoGBALHD +D5pDqOTAzCY0vlY0qNrsEr4ZdvO8wQP1XtyEzB919MDy01CSuPZtKfeGxNWIyN1G +SEb5lZnQuSCdOaXPwLjURMralQQmKlQbj26YVZTHJD5AwK1ILTloYWgmaUzhbfGs +a04wD8xgVGjVEquHI3e9AueEBypztgJgiaGDSZxBAoGADpxUn3L6lJrPyOd3IJrj +ypU/EfvY7Qd5pRTrJd9tObbi8zF1sWi/FcQNgoZP7oz/aklFfq8WWwJbe0fL1Wk/ +MeVHj8JEc/dh1ISgbHYdBgegvS6L30RcNRUJWANYcifEQPlSHTzYXviQ8tEOCq+S +/TPqxnd2CkT6w3bSCJbxKVM= +-----END PRIVATE KEY----- diff --git a/tests/host/certificate/private3.key b/tests/host/certificate/private3.key new file mode 100644 index 0000000..f426162 --- /dev/null +++ b/tests/host/certificate/private3.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDCA+6P2eieXHaV +JivtWif7SntjjkJm0juRKRRGsT3wt+zCZqoDe8zylTBN0mse/POWXdC+zXRMC2X/ +c4V10kgrvWbnNdFdUFfBUphiXSoqnUYHZ6Ta+b4UTzC2tECSUEnSCz9n1ofHnyqD +yT9FELzVkRkQqexD+BFgZTF39R4q8BA4bWKQy94Kgvb+IP77+ou4fhkBLI1MX5nk +Wa3Oyu4TMzT/tqgPE70hk8wQzUU2aiwJ7IsmnWE6Ysk7c4DYMJQF/51bi2ByZWER +NjyBY6L+ZV90aL4UFR9O+Pw9HatfHVBRdmzSkKJOr9iu4summWgH0QYDmbkdhGwY +vup0EmEfAgMBAAECggEAdEgFAGSbHebPD79sDnq9gcf3QgjuVU/lcbAMPf5W4GJr +3WvItAPMJwwxgkL9/vmeSN37kY/0BuvB+yPStnYM2WJQPX0s+V+A6RZGzJWIAzh1 +01RUIwYR3XxE9wv7s3W5eNFS9DpI8OS9h3TjndJVSy8Gtc0SFP6l839S8dGQfiyN +eMqV16M+TM0LwyEogvGa79l4HjMIcorypCXg8NVcDaxNJYnAcegoRwdhGsb4jKG6 +kB+Z4dfAKLu7OFT6/20Q0QUdA/PrdBRAFt8KPhrrweKaAApVtrU0OHNs9ULFHXnu +kSVKZ+UTCUGWxMd4lJw5XZQ4FqUdb8Sxt8TUuvRioQKBgQDy6zY1EiqsDZE+sJbd +/jnUWn+I4/xR5y4KmbEx76dWL39TooyiHYKABJQ9BgvBIXi95AXaRodwn12DhaW7 +VW0m4RgJH/FNZoxc9xOE2+EPr2pQGn6bvJK9IjsITDoAmDbguzMZ+TCDGZqIYiIE +GTcgeW7NOBYM2Qy8Ufqe9zfV0QKBgQDMdo426TPxdU6Gb8AVOFFuXlL6Py0Sxk0q +pEAhyEzCKV9HM1eX8aDrJ5++lFiMwlhkYRrWVBENPyhPgWo9sMATJM0AIsBKTSyg +rVuqlaU8e+Pqyl8ZMOZ6uMq3zLts/Vp1sX5yU8vw5FqMddMas6SMpIoPEIAiJlse +CujyJ29T7wKBgQCiQnry+C+IvYdHWK1tm2MFdW27Ao6IJuOaMQ8rS+l6qD9kni9S +GmQRHv3lxSQU3UbJkIZYRsQxdkIAmEUb3PQMBE8JyUxlZxpa/q8LD9RFpeZdm1T2 +sf9SVosX/9K+ku4VLvXzY4AEEhYnA2W1VyJ7jqF0cwJHkrPvFtNRW9DwAQKBgCRi +6NYu1DahSLM2Cfn8xskccinkulHABpWTG3KnoblgAXu7UFhTAO84Yv5YihWqtG5Q +taT02v//gF39yvlljhkaEH14sb3HVCzYDRsjfH9yENKE5z2lbS7j2fexsJ0pzUJq +rvULopyhFtguU75Jv/vjgEpEBnmNV+PVzzTg/bfzAoGAGz11E33qpZjVw6becf9w +U8qnPfncIqSCg0fWNnsYwD56vI9L2ExCZG//SOUZ54b8GW+RaTFDHOlIE8dRAhrF +M5QnEjm2S+wVPJz7gKQ3cVART8EPi/Q6BT7YIgNIhemq+AwW5xMhZcBiA3vRI/Eu +vi807exD569efFLa9uspI8o= +-----END PRIVATE KEY----- diff --git a/tests/host/certificate/test_host_certificate.yml b/tests/host/certificate/test_host_certificate.yml new file mode 100644 index 0000000..7607006 --- /dev/null +++ b/tests/host/certificate/test_host_certificate.yml @@ -0,0 +1,110 @@ +# +# Generate self-signed certificates using openssl: +# +# openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout private1.key -out cert1.pem -subj '/CN=test' +# openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout private2.key -out cert2.pem -subj '/CN=test' +# openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout private3.key -out cert3.pem -subj '/CN=test' +# +# Convert the certificate do DER for easier handling through CLI +# +# openssl x509 -outform der -in cert1.pem -out cert1.der +# openssl x509 -outform der -in cert2.pem -out cert2.der +# openssl x509 -outform der -in cert3.pem -out cert3.der +# +# Use base64: +# +# base64 cert1.der -w5000 +# base64 cert2.der -w5000 +# base64 cert3.der -w5000 +# +--- +- name: Test host certificates + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Get Domain from server name + set_fact: + ipaserver_domain: "{{ groups.ipaserver[0].split('.')[1:] | join ('.') }}" + when: ipaserver_domain is not defined + + - name: Host test absent + ipahost: + ipaadmin_password: SomeADMINpassword + name: "{{ 'test.' + ipaserver_domain }}" + state: absent + + - name: Host test present + ipahost: + ipaadmin_password: SomeADMINpassword + name: "{{ 'test.' + ipaserver_domain }}" + force: yes + register: result + failed_when: not result.changed + + - name: Host test cert members present + ipahost: + ipaadmin_password: SomeADMINpassword + name: "{{ 'test.' + ipaserver_domain }}" + certificate: + - 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 + action: member + register: result + failed_when: not result.changed + + - name: Host test cert members present again + ipahost: + ipaadmin_password: SomeADMINpassword + name: "{{ 'test.' + ipaserver_domain }}" + certificate: + - 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 + action: member + register: result + failed_when: result.changed + + - name: Host test cert members absent + ipahost: + ipaadmin_password: SomeADMINpassword + name: "{{ 'test.' + ipaserver_domain }}" + certificate: + - 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 + state: absent + action: member + register: result + failed_when: not result.changed + + - name: Host test cert members absent again + ipahost: + ipaadmin_password: SomeADMINpassword + name: "{{ 'test.' + ipaserver_domain }}" + certificate: + - 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 + state: absent + action: member + register: result + failed_when: result.changed + + - name: Host test absent + ipahost: + ipaadmin_password: SomeADMINpassword + name: "{{ 'test.' + ipaserver_domain }}" + state: absent + register: result + failed_when: not result.changed + + - name: Host test absent again + ipahost: + ipaadmin_password: SomeADMINpassword + name: "{{ 'test.' + ipaserver_domain }}" + state: absent + register: result + failed_when: result.changed diff --git a/tests/host/certificate/test_hosts_certificate.yml b/tests/host/certificate/test_hosts_certificate.yml new file mode 100644 index 0000000..853762c --- /dev/null +++ b/tests/host/certificate/test_hosts_certificate.yml @@ -0,0 +1,109 @@ +# +# Generate self-signed certificates using openssl: +# +# openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout private1.key -out cert1.pem -subj '/CN=test' +# openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout private2.key -out cert2.pem -subj '/CN=test' +# openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout private3.key -out cert3.pem -subj '/CN=test' +# +# Convert the certificate do DER for easier handling through CLI +# +# openssl x509 -outform der -in cert1.pem -out cert1.der +# openssl x509 -outform der -in cert2.pem -out cert2.der +# openssl x509 -outform der -in cert3.pem -out cert3.der +# +# Use base64: +# +# base64 cert1.der -w5000 +# base64 cert2.der -w5000 +# base64 cert3.der -w5000 +# +--- +- name: Test host certificates + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Get Domain from server name + set_fact: + ipaserver_domain: "{{ groups.ipaserver[0].split('.')[1:] | join ('.') }}" + when: ipaserver_domain is not defined + + - name: Host test absent + ipahost: + ipaadmin_password: SomeADMINpassword + hosts: + - name: "{{ 'test.' + ipaserver_domain }}" + state: absent + + - name: Host test present + ipahost: + ipaadmin_password: SomeADMINpassword + hosts: + - name: "{{ 'test.' + ipaserver_domain }}" + force: yes + register: result + failed_when: not result.changed + + - name: Host test cert members present + ipahost: + ipaadmin_password: SomeADMINpassword + hosts: + - name: "{{ 'test.' + ipaserver_domain }}" + certificate: + - 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 + action: member + register: result + failed_when: not result.changed + + - name: Host test cert members present again + ipahost: + ipaadmin_password: SomeADMINpassword + hosts: + - name: "{{ 'test.' + ipaserver_domain }}" + certificate: + - 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 + action: member + register: result + failed_when: result.changed + + - name: Host test cert members absent + ipahost: + ipaadmin_password: SomeADMINpassword + hosts: + - name: "{{ 'test.' + ipaserver_domain }}" + certificate: + - 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 + state: absent + action: member + #register: result + #failed_when: not result.changed + + - name: Host test cert members absent again + ipahost: + ipaadmin_password: SomeADMINpassword + hosts: + - name: "{{ 'test.' + ipaserver_domain }}" + certificate: + - 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 + state: absent + action: member + register: result + failed_when: result.changed + + - name: Host test absent + ipahost: + ipaadmin_password: SomeADMINpassword + hosts: + - name: "{{ 'test.' + ipaserver_domain }}" + state: absent + register: result + failed_when: not result.changed diff --git a/tests/host/test_host.yml b/tests/host/test_host.yml new file mode 100644 index 0000000..1a555a1 --- /dev/null +++ b/tests/host/test_host.yml @@ -0,0 +1,218 @@ +--- +- name: Test host + hosts: ipaserver + become: true + + tasks: + - name: Get Domain from server name + set_fact: + ipaserver_domain: "{{ groups.ipaserver[0].split('.')[1:] | join ('.') }}" + when: ipaserver_domain is not defined + + - name: Set host1_fqdn .. host6_fqdn + set_fact: + host1_fqdn: "{{ 'host1.' + ipaserver_domain }}" + host2_fqdn: "{{ 'host2.' + ipaserver_domain }}" + host3_fqdn: "{{ 'host3.' + ipaserver_domain }}" + host4_fqdn: "{{ 'host4.' + ipaserver_domain }}" + host5_fqdn: "{{ 'host5.' + ipaserver_domain }}" + host6_fqdn: "{{ 'host6.' + ipaserver_domain }}" + + - name: Host absent + ipahost: + ipaadmin_password: MyPassword123 + name: + - "{{ host1_fqdn }}" + - "{{ host2_fqdn }}" + - "{{ host3_fqdn }}" + - "{{ host4_fqdn }}" + - "{{ host5_fqdn }}" + - "{{ host6_fqdn }}" + update_dns: yes + state: absent + + - name: Get IPv4 address prefix from server node + set_fact: + ipv4_prefix: "{{ ansible_default_ipv4.address.split('.')[:-1] | + join('.') }}" + + - name: Host "{{ host1_fqdn }}" present + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + ip_address: "{{ ipv4_prefix + '.201' }}" + update_dns: yes + reverse: no + register: result + failed_when: not result.changed + + - name: Host "{{ host1_fqdn }}" present again + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + ip_address: "{{ ipv4_prefix + '.201' }}" + update_dns: yes + reverse: no + register: result + failed_when: result.changed + + - name: Host "{{ host2_fqdn }}" present + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host2_fqdn }}" + ip_address: "{{ ipv4_prefix + '.202' }}" + update_dns: yes + reverse: no + register: result + failed_when: not result.changed + + - name: Host "{{ host2_fqdn }}" present again + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host2_fqdn }}" + ip_address: "{{ ipv4_prefix + '.202' }}" + update_dns: yes + reverse: no + register: result + failed_when: result.changed + + - name: Host "{{ host3_fqdn }}" present + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host3_fqdn }}" + ip_address: "{{ ipv4_prefix + '.203' }}" + update_dns: yes + reverse: no + register: result + failed_when: not result.changed + + - name: Host "{{ host3_fqdn }}" present again + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host3_fqdn }}" + ip_address: "{{ ipv4_prefix + '.203' }}" + update_dns: yes + reverse: no + register: result + failed_when: result.changed + + - name: Host "{{ host4_fqdn }}" present + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host4_fqdn }}" + ip_address: "{{ ipv4_prefix + '.204' }}" + update_dns: yes + reverse: no + register: result + failed_when: not result.changed + + - name: Host "{{ host4_fqdn }}" present again + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host4_fqdn }}" + ip_address: "{{ ipv4_prefix + '.204' }}" + update_dns: yes + reverse: no + register: result + failed_when: result.changed + + - name: Host "{{ host5_fqdn }}" present + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host5_fqdn }}" + ip_address: "{{ ipv4_prefix + '.205' }}" + update_dns: yes + reverse: no + register: result + failed_when: not result.changed + + - name: Host "{{ host5_fqdn }}" present again + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + ip_address: "{{ ipv4_prefix + '.205' }}" + update_dns: yes + reverse: no + register: result + failed_when: result.changed + + - name: Host "{{ host6_fqdn }}" present + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host6_fqdn }}" + ip_address: "{{ ipv4_prefix + '.206' }}" + update_dns: yes + reverse: no + register: result + failed_when: not result.changed + + - name: Host "{{ host6_fqdn }}" present again + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host6_fqdn }}" + ip_address: "{{ ipv4_prefix + '.206' }}" + update_dns: yes + reverse: no + register: result + failed_when: result.changed + + # disabled can only be checked with enabled hosts, all hosts above are + # not enabled. + #- name: Hosts host1..host6 disabled + # ipahost: + # ipaadmin_password: MyPassword123 + # name: + # - "{{ host1_fqdn }}" + # - "{{ host2_fqdn }}" + # - "{{ host3_fqdn }}" + # - "{{ host4_fqdn }}" + # - "{{ host5_fqdn }}" + # - "{{ host6_fqdn }}" + # state: disabled + # register: result + # failed_when: not result.changed + # + #- name: Hosts host1..host6 disabled again + # ipahost: + # ipaadmin_password: MyPassword123 + # name: + # - "{{ host1_fqdn }}" + # - "{{ host2_fqdn }}" + # - "{{ host3_fqdn }}" + # - "{{ host4_fqdn }}" + # - "{{ host5_fqdn }}" + # - "{{ host6_fqdn }}" + # state: disabled + # register: result + # failed_when: result.changed + + - name: Hosts host1..host6 absent + ipahost: + ipaadmin_password: MyPassword123 + name: + - "{{ host1_fqdn }}" + - "{{ host2_fqdn }}" + - "{{ host3_fqdn }}" + - "{{ host4_fqdn }}" + - "{{ host5_fqdn }}" + - "{{ host6_fqdn }}" + update_dns: yes + state: absent + register: result + failed_when: not result.changed + + - name: Hosts host1..host6 absent again + ipahost: + ipaadmin_password: MyPassword123 + name: + - "{{ host1_fqdn }}" + - "{{ host2_fqdn }}" + - "{{ host3_fqdn }}" + - "{{ host4_fqdn }}" + - "{{ host5_fqdn }}" + - "{{ host6_fqdn }}" + update_dns: yes + state: absent + register: result + failed_when: result.changed + diff --git a/tests/host/test_host_allow_create_keytab.yml b/tests/host/test_host_allow_create_keytab.yml new file mode 100644 index 0000000..eb7b776 --- /dev/null +++ b/tests/host/test_host_allow_create_keytab.yml @@ -0,0 +1,278 @@ +--- +- name: Test host allow_create_keytab + hosts: ipaserver + become: true + + tasks: + - name: Get Domain from server name + set_fact: + ipaserver_domain: "{{ groups.ipaserver[0].split('.')[1:] | join ('.') }}" + when: ipaserver_domain is not defined + + - name: Get Realm from server name + set_fact: + ipaserver_realm: "{{ groups.ipaserver[0].split('.')[1:] | join ('.') | upper }}" + when: ipaserver_realm is not defined + + - name: Set host1_fqdn .. host3_fqdn + set_fact: + host1_fqdn: "{{ 'host1.' + ipaserver_domain }}" + host2_fqdn: "{{ 'host2.' + ipaserver_domain }}" + host3_fqdn: "{{ 'host3.' + ipaserver_domain }}" + + - name: Host host1..., host2... and host3... absent + ipahost: + ipaadmin_password: MyPassword123 + name: + - "{{ host1_fqdn }}" + - "{{ host2_fqdn }}" + - "{{ host3_fqdn }}" + state: absent + + - name: Ensure host-groups hostgroup1 and hostgroup2 absent + ipahostgroup: + ipaadmin_password: MyPassword123 + name: hostgroup1,hostgroup2 + state: absent + + - name: Ensure users user1 and user2 absent + ipauser: + ipaadmin_password: MyPassword123 + users: + - name: user1 + - name: user2 + state: absent + + - name: Ensure group1 and group2 absent + ipagroup: + ipaadmin_password: MyPassword123 + name: group1,group2 + state: absent + + - name: Host host2... and host3... present + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host2_fqdn }}" + force: yes + - name: "{{ host3_fqdn }}" + force: yes + register: result + failed_when: not result.changed + + - name: Ensure host-group hostgroup1 present + ipahostgroup: + ipaadmin_password: MyPassword123 + name: hostgroup1 + state: present + register: result + failed_when: not result.changed + + - name: Ensure host-group hostgroup2 present + ipahostgroup: + ipaadmin_password: MyPassword123 + name: hostgroup2 + state: present + register: result + failed_when: not result.changed + + - name: Ensure users user1 and user2 present + ipauser: + ipaadmin_password: MyPassword123 + users: + - name: user1 + first: First1 + last: Last1 + - name: user2 + first: First2 + last: Last2 + register: result + failed_when: not result.changed + + - name: Ensure group1 present + ipagroup: + ipaadmin_password: MyPassword123 + name: group1 + register: result + failed_when: not result.changed + + - name: Ensure group2 present + ipagroup: + ipaadmin_password: MyPassword123 + name: group2 + register: result + failed_when: not result.changed + + - name: Host host1... present with allow_create_keytab users,groups,hosts and hostgroups + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + allow_create_keytab_user: + - user1 + - user2 + allow_create_keytab_group: + - group1 + - group2 + allow_create_keytab_host: + - "{{ host2_fqdn }}" + - "{{ host3_fqdn }}" + allow_create_keytab_hostgroup: + - hostgroup1 + - hostgroup2 + force: yes + register: result + failed_when: not result.changed + + - name: Host host1... present with allow_create_keytab users,groups,hosts and hostgroups again + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + allow_create_keytab_user: + - user1 + - user2 + allow_create_keytab_group: + - group1 + - group2 + allow_create_keytab_host: + - "{{ host2_fqdn }}" + - "{{ host3_fqdn }}" + allow_create_keytab_hostgroup: + - hostgroup1 + - hostgroup2 + force: yes + register: result + failed_when: result.changed + + - name: Host host1... absent + ipahost: + ipaadmin_password: MyPassword123 + name: + - "{{ host1_fqdn }}" + state: absent + + - name: Host host1... present + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + force: yes + register: result + failed_when: not result.changed + + - name: Host host1... ensure allow_create_keytab users,groups,hosts and hostgroups present + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + allow_create_keytab_user: + - user1 + - user2 + allow_create_keytab_group: + - group1 + - group2 + allow_create_keytab_host: + - "{{ host2_fqdn }}" + - "{{ host3_fqdn }}" + allow_create_keytab_hostgroup: + - hostgroup1 + - hostgroup2 + action: member + register: result + failed_when: not result.changed + + - name: Host host1... ensure allow_create_keytab users,groups,hosts and hostgroups present again + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + allow_create_keytab_user: + - user1 + - user2 + allow_create_keytab_group: + - group1 + - group2 + allow_create_keytab_host: + - "{{ host2_fqdn }}" + - "{{ host3_fqdn }}" + allow_create_keytab_hostgroup: + - hostgroup1 + - hostgroup2 + action: member + register: result + failed_when: result.changed + + - name: Host host1... ensure allow_create_keytab users,groups,hosts and hostgroups absent + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + allow_create_keytab_user: + - user1 + - user2 + allow_create_keytab_group: + - group1 + - group2 + allow_create_keytab_host: + - "{{ host2_fqdn }}" + - "{{ host3_fqdn }}" + allow_create_keytab_hostgroup: + - hostgroup1 + - hostgroup2 + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Host host1... ensure allow_create_keytab users,groups,hosts and hostgroups absent again + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + allow_create_keytab_user: + - user1 + - user2 + allow_create_keytab_group: + - group1 + - group2 + allow_create_keytab_host: + - "{{ host2_fqdn }}" + - "{{ host3_fqdn }}" + allow_create_keytab_hostgroup: + - hostgroup1 + - hostgroup2 + action: member + state: absent + register: result + failed_when: result.changed + + - name: Host host1..., host2... and host3... absent + ipahost: + ipaadmin_password: MyPassword123 + name: + - "{{ host1_fqdn }}" + - "{{ host2_fqdn }}" + - "{{ host3_fqdn }}" + state: absent + register: result + failed_when: not result.changed + + - name: Ensure host-groups hostgroup1 and hostgroup2 absent + ipahostgroup: + ipaadmin_password: MyPassword123 + name: hostgroup1,hostgroup2 + state: absent + register: result + failed_when: not result.changed + + - name: Ensure users user1 and user2 absent + ipauser: + ipaadmin_password: MyPassword123 + users: + - name: user1 + - name: user2 + state: absent + register: result + failed_when: not result.changed + + - name: Ensure group1 and group2 absent + ipagroup: + ipaadmin_password: MyPassword123 + name: group1,group2 + state: absent + register: result + failed_when: not result.changed diff --git a/tests/host/test_host_allow_retrieve_keytab.yml b/tests/host/test_host_allow_retrieve_keytab.yml new file mode 100644 index 0000000..65d86aa --- /dev/null +++ b/tests/host/test_host_allow_retrieve_keytab.yml @@ -0,0 +1,278 @@ +--- +- name: Test host allow_retrieve_keytab + hosts: ipaserver + become: true + + tasks: + - name: Get Domain from server name + set_fact: + ipaserver_domain: "{{ groups.ipaserver[0].split('.')[1:] | join ('.') }}" + when: ipaserver_domain is not defined + + - name: Get Realm from server name + set_fact: + ipaserver_realm: "{{ groups.ipaserver[0].split('.')[1:] | join ('.') | upper }}" + when: ipaserver_realm is not defined + + - name: Set host1_fqdn .. host3_fqdn + set_fact: + host1_fqdn: "{{ 'host1.' + ipaserver_domain }}" + host2_fqdn: "{{ 'host2.' + ipaserver_domain }}" + host3_fqdn: "{{ 'host3.' + ipaserver_domain }}" + + - name: Host host1..., host2... and host3... absent + ipahost: + ipaadmin_password: MyPassword123 + name: + - "{{ host1_fqdn }}" + - "{{ host2_fqdn }}" + - "{{ host3_fqdn }}" + state: absent + + - name: Ensure host-groups hostgroup1 and hostgroup2 absent + ipahostgroup: + ipaadmin_password: MyPassword123 + name: hostgroup1,hostgroup2 + state: absent + + - name: Ensure users user1 and user2 absent + ipauser: + ipaadmin_password: MyPassword123 + users: + - name: user1 + - name: user2 + state: absent + + - name: Ensure group1 and group2 absent + ipagroup: + ipaadmin_password: MyPassword123 + name: group1,group2 + state: absent + + - name: Host host2... and host3... present + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host2_fqdn }}" + force: yes + - name: "{{ host3_fqdn }}" + force: yes + register: result + failed_when: not result.changed + + - name: Ensure host-group hostgroup1 present + ipahostgroup: + ipaadmin_password: MyPassword123 + name: hostgroup1 + state: present + register: result + failed_when: not result.changed + + - name: Ensure host-group hostgroup2 present + ipahostgroup: + ipaadmin_password: MyPassword123 + name: hostgroup2 + state: present + register: result + failed_when: not result.changed + + - name: Ensure users user1 and user2 present + ipauser: + ipaadmin_password: MyPassword123 + users: + - name: user1 + first: First1 + last: Last1 + - name: user2 + first: First2 + last: Last2 + register: result + failed_when: not result.changed + + - name: Ensure group1 present + ipagroup: + ipaadmin_password: MyPassword123 + name: group1 + register: result + failed_when: not result.changed + + - name: Ensure group2 present + ipagroup: + ipaadmin_password: MyPassword123 + name: group2 + register: result + failed_when: not result.changed + + - name: Host host1... present with allow_retrieve_keytab users,groups,hosts and hostgroups + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + allow_retrieve_keytab_user: + - user1 + - user2 + allow_retrieve_keytab_group: + - group1 + - group2 + allow_retrieve_keytab_host: + - "{{ host2_fqdn }}" + - "{{ host3_fqdn }}" + allow_retrieve_keytab_hostgroup: + - hostgroup1 + - hostgroup2 + force: yes + register: result + failed_when: not result.changed + + - name: Host host1... present with allow_retrieve_keytab users,groups,hosts and hostgroups again + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + allow_retrieve_keytab_user: + - user1 + - user2 + allow_retrieve_keytab_group: + - group1 + - group2 + allow_retrieve_keytab_host: + - "{{ host2_fqdn }}" + - "{{ host3_fqdn }}" + allow_retrieve_keytab_hostgroup: + - hostgroup1 + - hostgroup2 + force: yes + register: result + failed_when: result.changed + + - name: Host host1... absent + ipahost: + ipaadmin_password: MyPassword123 + name: + - "{{ host1_fqdn }}" + state: absent + + - name: Host host1... present + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + force: yes + register: result + failed_when: not result.changed + + - name: Host host1... ensure allow_retrieve_keytab users,groups,hosts and hostgroups present + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + allow_retrieve_keytab_user: + - user1 + - user2 + allow_retrieve_keytab_group: + - group1 + - group2 + allow_retrieve_keytab_host: + - "{{ host2_fqdn }}" + - "{{ host3_fqdn }}" + allow_retrieve_keytab_hostgroup: + - hostgroup1 + - hostgroup2 + action: member + register: result + failed_when: not result.changed + + - name: Host host1... ensure allow_retrieve_keytab users,groups,hosts and hostgroups present again + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + allow_retrieve_keytab_user: + - user1 + - user2 + allow_retrieve_keytab_group: + - group1 + - group2 + allow_retrieve_keytab_host: + - "{{ host2_fqdn }}" + - "{{ host3_fqdn }}" + allow_retrieve_keytab_hostgroup: + - hostgroup1 + - hostgroup2 + action: member + register: result + failed_when: result.changed + + - name: Host host1... ensure allow_retrieve_keytab users,groups,hosts and hostgroups absent + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + allow_retrieve_keytab_user: + - user1 + - user2 + allow_retrieve_keytab_group: + - group1 + - group2 + allow_retrieve_keytab_host: + - "{{ host2_fqdn }}" + - "{{ host3_fqdn }}" + allow_retrieve_keytab_hostgroup: + - hostgroup1 + - hostgroup2 + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Host host1... ensure allow_retrieve_keytab users,groups,hosts and hostgroups absent again + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + allow_retrieve_keytab_user: + - user1 + - user2 + allow_retrieve_keytab_group: + - group1 + - group2 + allow_retrieve_keytab_host: + - "{{ host2_fqdn }}" + - "{{ host3_fqdn }}" + allow_retrieve_keytab_hostgroup: + - hostgroup1 + - hostgroup2 + action: member + state: absent + register: result + failed_when: result.changed + + - name: Host host1..., host2... and host3... absent + ipahost: + ipaadmin_password: MyPassword123 + name: + - "{{ host1_fqdn }}" + - "{{ host2_fqdn }}" + - "{{ host3_fqdn }}" + state: absent + register: result + failed_when: not result.changed + + - name: Ensure host-groups hostgroup1 and hostgroup2 absent + ipahostgroup: + ipaadmin_password: MyPassword123 + name: hostgroup1,hostgroup2 + state: absent + register: result + failed_when: not result.changed + + - name: Ensure users user1 and user2 absent + ipauser: + ipaadmin_password: MyPassword123 + users: + - name: user1 + - name: user2 + state: absent + register: result + failed_when: not result.changed + + - name: Ensure group1 and group2 absent + ipagroup: + ipaadmin_password: MyPassword123 + name: group1,group2 + state: absent + register: result + failed_when: not result.changed diff --git a/tests/host/test_host_managedby_host.yml b/tests/host/test_host_managedby_host.yml new file mode 100644 index 0000000..78c5a43 --- /dev/null +++ b/tests/host/test_host_managedby_host.yml @@ -0,0 +1,125 @@ +--- +- name: Test host managedby_host + hosts: ipaserver + become: true + + tasks: + - name: Get Domain from server name + set_fact: + ipaserver_domain: "{{ groups.ipaserver[0].split('.')[1:] | join ('.') }}" + when: ipaserver_domain is not defined + + - name: Set host1_fqdn .. host2_fqdn + set_fact: + host1_fqdn: "{{ 'host1.' + ipaserver_domain }}" + host2_fqdn: "{{ 'host2.' + ipaserver_domain }}" + + - name: Host absent + ipahost: + ipaadmin_password: MyPassword123 + name: + - "{{ host1_fqdn }}" + - "{{ host2_fqdn }}" + update_dns: yes + state: absent + + - name: Host "{{ host1_fqdn }}" present + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + force: yes + register: result + failed_when: not result.changed + + - name: Host "{{ host2_fqdn }}" present + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host2_fqdn }}" + force: yes + register: result + failed_when: not result.changed + + - name: Host "{{ host1_fqdn }}" managed by "{{ 'host2.' + ipaserver_domain }}" + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + managedby_host: "{{ host2_fqdn }}" + register: result + failed_when: not result.changed + + - name: Host "{{ host1_fqdn }}" managed by "{{ 'host2.' + ipaserver_domain }}" again + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + managedby_host: "{{ host2_fqdn }}" + register: result + failed_when: result.changed + + - name: Host "{{ host1_fqdn }}" managed by "{{ groups.ipaserver[0] }}" + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + managedby_host: "{{ groups.ipaserver[0] }}" + action: member + register: result + failed_when: not result.changed + + - name: Host "{{ host1_fqdn }}" managed by "{{ groups.ipaserver[0] }}" again + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + managedby_host: "{{ groups.ipaserver[0] }}" + action: member + register: result + failed_when: result.changed + + - name: Host "{{ host1_fqdn }}" not managed by "{{ groups.ipaserver[0] }}" + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + managedby_host: "{{ groups.ipaserver[0] }}" + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Host "{{ host1_fqdn }}" not managed by "{{ groups.ipaserver[0] }}" again + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + managedby_host: "{{ groups.ipaserver[0] }}" + action: member + state: absent + register: result + failed_when: result.changed + + - name: Host "{{ host1_fqdn }}" not managed by "{{ 'host2.' + ipaserver_domain }}" + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + managedby_host: "{{ host2_fqdn }}" + state: absent + action: member + register: result + failed_when: not result.changed + + - name: Host "{{ host1_fqdn }}" not managed by "{{ 'host2.' + ipaserver_domain }}" again + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + managedby_host: "{{ host2_fqdn }}" + action: member + state: absent + register: result + failed_when: result.changed + + - name: Host absent + ipahost: + ipaadmin_password: MyPassword123 + name: + - "{{ host1_fqdn }}" + - "{{ host2_fqdn }}" + update_dns: yes + state: absent + register: result + failed_when: not result.changed diff --git a/tests/host/test_host_principal.yml b/tests/host/test_host_principal.yml new file mode 100644 index 0000000..0dce400 --- /dev/null +++ b/tests/host/test_host_principal.yml @@ -0,0 +1,130 @@ +--- +- name: Test host principal + hosts: ipaserver + become: true + + tasks: + - name: Get Domain from server name + set_fact: + ipaserver_domain: "{{ groups.ipaserver[0].split('.')[1:] | join ('.') }}" + when: ipaserver_domain is not defined + + - name: Get Realm from server name + set_fact: + ipaserver_realm: "{{ groups.ipaserver[0].split('.')[1:] | join ('.') | upper }}" + when: ipaserver_realm is not defined + + - name: Set host1_fqdn + set_fact: + host1_fqdn: "{{ 'host1.' + ipaserver_domain }}" + + - name: Host host1 absent + ipahost: + ipaadmin_password: MyPassword123 + name: + - "{{ host1_fqdn }}" + update_dns: yes + state: absent + + - name: Host host1... present with principal host/testhost1... + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + principal: + - "{{ 'host/testhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + force: yes + register: result + failed_when: not result.changed + + - name: Host host1... principal host/host1... present (existing already) + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + principal: + - "{{ 'host/host1.' + ipaserver_domain + '@' + ipaserver_realm }}" + action: member + register: result + failed_when: result.changed + + - name: Host host1... principal host/testhost1... present again + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + principal: "{{ 'host/testhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + action: member + register: result + failed_when: result.changed + + - name: Host host1... principal host/testhost1... absent + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + principal: "{{ 'host/testhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Host host1... principal host/testhost1... absent again + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + principal: "{{ 'host/testhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + action: member + state: absent + register: result + failed_when: result.changed + + - name: Host host1... principal host/testhost1... and host/myhost1... present + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + principal: + - "{{ 'host/testhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + - "{{ 'host/myhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + action: member + register: result + failed_when: not result.changed + + - name: Host host1... principal host/testhost1... and host/myhost1... present again + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + principal: + - "{{ 'host/testhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + - "{{ 'host/myhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + action: member + register: result + failed_when: result.changed + + - name: Host host1... principal host/testhost1... and host/myhost1... absent + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + principal: + - "{{ 'host/testhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + - "{{ 'host/myhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Host host1... principal host/testhost1... and host/myhost1... absent again + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + principal: + - "{{ 'host/testhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + - "{{ 'host/myhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + action: member + state: absent + register: result + failed_when: result.changed + + - name: Host host1... absent + ipahost: + ipaadmin_password: MyPassword123 + name: + - "{{ host1_fqdn }}" + update_dns: yes + state: absent diff --git a/tests/host/test_host_random.yml b/tests/host/test_host_random.yml new file mode 100644 index 0000000..84893f9 --- /dev/null +++ b/tests/host/test_host_random.yml @@ -0,0 +1,86 @@ +--- +- name: Test ipahost random password generation + hosts: ipaserver + become: true + + tasks: + - name: Get Domain from server name + set_fact: + ipaserver_domain: "{{ groups.ipaserver[0].split('.')[1:] | join ('.') }}" + when: ipaserver_domain is not defined + + - name: Set host1_fqdn and host2_fqdn + set_fact: + host1_fqdn: "{{ 'host1.' + ipaserver_domain }}" + host2_fqdn: "{{ 'host2.' + ipaserver_domain }}" + + - name: Test hosts absent + ipahost: + ipaadmin_password: MyPassword123 + name: + - "{{ host1_fqdn }}" + - "{{ host2_fqdn }}" + update_dns: yes + state: absent + + - name: Host "{{ host1_fqdn }}" present with random password + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host1_fqdn }}" + random: yes + force: yes + update_password: on_create + register: ipahost + failed_when: not ipahost.changed + + - assert: + that: + - ipahost.host.randompassword is defined + + - name: Print generated random password + debug: + var: ipahost.host.randompassword + + - name: Host "{{ host1_fqdn }}" absent + ipahost: + ipaadmin_password: MyPassword123 + name: + - "{{ host1_fqdn }}" + state: absent + + - name: Hosts "{{ host1_fqdn }}" and "{{ host2_fqdn }}" present with random password + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host1_fqdn }}" + random: yes + force: yes + - name: "{{ host2_fqdn }}" + random: yes + force: yes + update_password: on_create + register: ipahost + failed_when: not ipahost.changed + + - assert: + that: + - ipahost.host["{{host1_fqdn }}"].randompassword is + defined + - ipahost.host["{{host2_fqdn }}"].randompassword is + defined + + - name: Print generated random password for "{{host1_fqdn }}" + debug: + var: ipahost.host["{{host1_fqdn }}"].randompassword + + - name: Print generated random password for "{{host2_fqdn }}" + debug: + var: ipahost.host["{{host2_fqdn }}"].randompassword + + - name: Hosts "{{ host1_fqdn }}" and "{{ host2_fqdn }}" absent + ipahost: + ipaadmin_password: MyPassword123 + name: + - "{{ host1_fqdn }}" + - "{{ host2_fqdn }}" + state: absent diff --git a/tests/host/test_hosts.yml b/tests/host/test_hosts.yml new file mode 100644 index 0000000..8e92bf3 --- /dev/null +++ b/tests/host/test_hosts.yml @@ -0,0 +1,98 @@ +--- +- name: Test hosts + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Get Domain from server name + set_fact: + ipaserver_domain: "{{ groups.ipaserver[0].split('.')[1:] | join ('.') }}" + when: ipaserver_domain is not defined + + - name: Set host1_fqdn .. host6_fqdn + set_fact: + host1_fqdn: "{{ 'host1.' + ipaserver_domain }}" + host2_fqdn: "{{ 'host2.' + ipaserver_domain }}" + host3_fqdn: "{{ 'host3.' + ipaserver_domain }}" + host4_fqdn: "{{ 'host4.' + ipaserver_domain }}" + host5_fqdn: "{{ 'host5.' + ipaserver_domain }}" + host6_fqdn: "{{ 'host6.' + ipaserver_domain }}" + + - name: Host host1..host6 absent + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host1_fqdn }}" + - name: "{{ host2_fqdn }}" + - name: "{{ host3_fqdn }}" + - name: "{{ host4_fqdn }}" + - name: "{{ host5_fqdn }}" + - name: "{{ host6_fqdn }}" + state: absent + + - name: Hosts host1..host6 present + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host1_fqdn }}" + force: yes + - name: "{{ host2_fqdn }}" + force: yes + - name: "{{ host3_fqdn }}" + force: yes + - name: "{{ host4_fqdn }}" + force: yes + - name: "{{ host5_fqdn }}" + force: yes + - name: "{{ host6_fqdn }}" + force: yes + register: result + failed_when: not result.changed + + - name: Hosts host1..host6 present again + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host1_fqdn }}" + force: yes + - name: "{{ host2_fqdn }}" + force: yes + - name: "{{ host3_fqdn }}" + force: yes + - name: "{{ host4_fqdn }}" + force: yes + - name: "{{ host5_fqdn }}" + force: yes + - name: "{{ host6_fqdn }}" + force: yes + register: result + failed_when: result.changed + + - name: Hosts host1..host6 absent + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host1_fqdn }}" + - name: "{{ host2_fqdn }}" + - name: "{{ host3_fqdn }}" + - name: "{{ host4_fqdn }}" + - name: "{{ host5_fqdn }}" + - name: "{{ host6_fqdn }}" + state: absent + register: result + failed_when: not result.changed + + - name: Hosts host1..host6 absent again + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host1_fqdn }}" + - name: "{{ host2_fqdn }}" + - name: "{{ host3_fqdn }}" + - name: "{{ host4_fqdn }}" + - name: "{{ host5_fqdn }}" + - name: "{{ host6_fqdn }}" + state: absent + register: result + failed_when: result.changed diff --git a/tests/host/test_hosts_managedby_host.yml b/tests/host/test_hosts_managedby_host.yml new file mode 100644 index 0000000..a692745 --- /dev/null +++ b/tests/host/test_hosts_managedby_host.yml @@ -0,0 +1,151 @@ +--- +- name: Test hosts managedby_host + hosts: ipaserver + become: true + + tasks: + - name: Get Domain from server name + set_fact: + ipaserver_domain: "{{ groups.ipaserver[0].split('.')[1:] | join ('.') }}" + when: ipaserver_domain is not defined + + - name: Set host1_fqdn .. host5_fqdn + set_fact: + host1_fqdn: "{{ 'host1.' + ipaserver_domain }}" + host2_fqdn: "{{ 'host2.' + ipaserver_domain }}" + host3_fqdn: "{{ 'host3.' + ipaserver_domain }}" + host4_fqdn: "{{ 'host4.' + ipaserver_domain }}" + host5_fqdn: "{{ 'host5.' + ipaserver_domain }}" + + - name: Host absent + ipahost: + ipaadmin_password: MyPassword123 + name: + - "{{ host1_fqdn }}" + - "{{ host2_fqdn }}" + - "{{ host3_fqdn }}" + - "{{ host4_fqdn }}" + - "{{ host5_fqdn }}" + update_dns: yes + state: absent + + - name: Host "{{ host5_fqdn }}" present + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ host5_fqdn }}" + force: yes + register: result + failed_when: not result.changed + + - name: Hosts "{{ host1_fqdn }}" .. "{{ 'host5.' + ipaserver_domain }}" present and managed by "{{ 'host5.' + ipaserver_domain }}" + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host1_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + force: yes + - name: "{{ host2_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + force: yes + - name: "{{ host3_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + force: yes + - name: "{{ host4_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + force: yes + - name: "{{ host5_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + force: yes + register: result + failed_when: not result.changed + + - name: Hosts "{{ host1_fqdn }}" .. "{{ 'host5.' + ipaserver_domain }}" present and managed by "{{ 'host5.' + ipaserver_domain }}" again + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host1_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + force: yes + - name: "{{ host2_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + force: yes + - name: "{{ host3_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + force: yes + - name: "{{ host4_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + force: yes + - name: "{{ host5_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + force: yes + register: result + failed_when: result.changed + + - name: Hosts "{{ host1_fqdn }}" .. "{{ 'host5.' + ipaserver_domain }}" managed by "{{ 'host5.' + ipaserver_domain }}" + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host1_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + - name: "{{ host2_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + - name: "{{ host3_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + - name: "{{ host4_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + - name: "{{ host5_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + action: member + register: result + failed_when: result.changed + + - name: Hosts "{{ host1_fqdn }}" .. "{{ 'host5.' + ipaserver_domain }}" not managed by "{{ 'host5.' + ipaserver_domain }}" + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host1_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + - name: "{{ host2_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + - name: "{{ host3_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + - name: "{{ host4_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + - name: "{{ host5_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Hosts "{{ host1_fqdn }}" .. "{{ 'host5.' + ipaserver_domain }}" not managed by "{{ 'host5.' + ipaserver_domain }}" again + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host1_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + - name: "{{ host2_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + - name: "{{ host3_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + - name: "{{ host4_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + - name: "{{ host5_fqdn }}" + managedby_host: "{{ host5_fqdn }}" + action: member + state: absent + register: result + failed_when: result.changed + + - name: Hosts "{{ host1_fqdn }}" .. "{{ 'host5.' + ipaserver_domain }}" absent + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host1_fqdn }}" + - name: "{{ host2_fqdn }}" + - name: "{{ host3_fqdn }}" + - name: "{{ host4_fqdn }}" + - name: "{{ host5_fqdn }}" + state: absent + register: result + failed_when: not result.changed diff --git a/tests/host/test_hosts_principal.yml b/tests/host/test_hosts_principal.yml new file mode 100644 index 0000000..5918d35 --- /dev/null +++ b/tests/host/test_hosts_principal.yml @@ -0,0 +1,179 @@ +--- +- name: Test hosts principal + hosts: ipaserver + become: true + + tasks: + - name: Get Domain from server name + set_fact: + ipaserver_domain: "{{ groups.ipaserver[0].split('.')[1:] | join ('.') }}" + when: ipaserver_domain is not defined + + - name: Get Realm from server name + set_fact: + ipaserver_realm: "{{ groups.ipaserver[0].split('.')[1:] | join ('.') | upper }}" + when: ipaserver_realm is not defined + + - name: Set host1_fqdn .. host2_fqdn + set_fact: + host1_fqdn: "{{ 'host1.' + ipaserver_domain }}" + host2_fqdn: "{{ 'host2.' + ipaserver_domain }}" + + - name: Host host1... and host2... absent + ipahost: + ipaadmin_password: MyPassword123 + name: + - "{{ host1_fqdn }}" + - "{{ host2_fqdn }}" + update_dns: yes + state: absent + + - name: Host hostX... present with principal host/testhostX... X=[1,2] + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host1_fqdn }}" + principal: + - "{{ 'host/testhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + force: yes + - name: "{{ host2_fqdn }}" + principal: + - "{{ 'host/testhost2.' + ipaserver_domain + '@' + ipaserver_realm }}" + force: yes + register: result + failed_when: not result.changed + + - name: Host hostX... principal 'host/hostX... present (existing already) X=[1,2] + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host1_fqdn }}" + principal: + - "{{ 'host/host1.' + ipaserver_domain + '@' + ipaserver_realm }}" + - name: "{{ host2_fqdn }}" + principal: + - "{{ 'host/host2.' + ipaserver_domain + '@' + ipaserver_realm }}" + action: member + register: result + failed_when: result.changed + + - name: Host hostX... principal host/testhostX... present again X=[1,2] + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host1_fqdn }}" + principal: + - "{{ 'host/testhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + - name: "{{ host2_fqdn }}" + principal: + - "{{ 'host/testhost2.' + ipaserver_domain + '@' + ipaserver_realm }}" + action: member + register: result + failed_when: result.changed + + - name: Host hostX.. principal host/testhostX... absent X=[1,2] + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host1_fqdn }}" + principal: + - "{{ 'host/testhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + - name: "{{ host2_fqdn }}" + principal: + - "{{ 'host/testhost2.' + ipaserver_domain + '@' + ipaserver_realm }}" + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Host hostX... principal host/testhostX... absent again X=[1,2] + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host1_fqdn }}" + principal: + - "{{ 'host/testhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + - name: "{{ host2_fqdn }}" + principal: + - "{{ 'host/testhost2.' + ipaserver_domain + '@' + ipaserver_realm }}" + action: member + state: absent + register: result + failed_when: result.changed + + - name: Host hostX... principal host/testhostX... and host/myhostX... present X=[1,2] + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host1_fqdn }}" + principal: + - "{{ 'host/testhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + - "{{ 'host/myhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + - name: "{{ host2_fqdn }}" + principal: + - "{{ 'host/testhost2.' + ipaserver_domain + '@' + ipaserver_realm }}" + - "{{ 'host/myhost2.' + ipaserver_domain + '@' + ipaserver_realm }}" + action: member + register: result + failed_when: not result.changed + + - name: Host hostX... principal host/testhostX... and host/myhostX... present again X=[1,2] + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host1_fqdn }}" + principal: + - "{{ 'host/testhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + - "{{ 'host/myhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + - name: "{{ host2_fqdn }}" + principal: + - "{{ 'host/testhost2.' + ipaserver_domain + '@' + ipaserver_realm }}" + - "{{ 'host/myhost2.' + ipaserver_domain + '@' + ipaserver_realm }}" + action: member + register: result + failed_when: result.changed + + - name: Host hostX... principal host/testhostX... and host/myhostX... absent X=[1,2] + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host1_fqdn }}" + principal: + - "{{ 'host/testhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + - "{{ 'host/myhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + hosts: + - name: "{{ host2_fqdn }}" + principal: + - "{{ 'host/testhost2.' + ipaserver_domain + '@' + ipaserver_realm }}" + - "{{ 'host/myhost2.' + ipaserver_domain + '@' + ipaserver_realm }}" + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Host hostX... principal host/testhostX... and host/myhostX... absent again X=[1,2] + ipahost: + ipaadmin_password: MyPassword123 + hosts: + - name: "{{ host1_fqdn }}" + principal: + - "{{ 'host/testhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + - "{{ 'host/myhost1.' + ipaserver_domain + '@' + ipaserver_realm }}" + hosts: + - name: "{{ host2_fqdn }}" + principal: + - "{{ 'host/testhost2.' + ipaserver_domain + '@' + ipaserver_realm }}" + - "{{ 'host/myhost2.' + ipaserver_domain + '@' + ipaserver_realm }}" + action: member + state: absent + register: result + failed_when: result.changed + + - name: Hosts host1... and host2... absent + ipahost: + ipaadmin_password: MyPassword123 + name: + - "{{ host1_fqdn }}" + - "{{ host2_fqdn }}" + update_dns: yes + state: absent diff --git a/tests/hostgroup/test_hostgroup.yml b/tests/hostgroup/test_hostgroup.yml new file mode 100644 index 0000000..c3c4496 --- /dev/null +++ b/tests/hostgroup/test_hostgroup.yml @@ -0,0 +1,185 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Get Domain from server name + set_fact: + ipaserver_domain: "{{ groups.ipaserver[0].split('.')[1:] | join ('.') }}" + when: ipaserver_domain is not defined + + - name: Ensure host-group databases, mysql-server and oracle-server are absent + ipahostgroup: + ipaadmin_password: MyPassword123 + name: + - databases + - mysql-server + - oracle-server + state: absent + + - name: Test hosts db1 and db2 absent + ipahost: + ipaadmin_password: MyPassword123 + name: + - "{{ 'db1.' + ipaserver_domain }}" + - "{{ 'db2.' + ipaserver_domain }}" + state: absent + + - name: Host "{{ 'db1.' + ipaserver_domain }}" present + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ 'db1.' + ipaserver_domain }}" + force: yes + register: result + failed_when: not result.changed + + - name: Host "{{ 'db2.' + ipaserver_domain }}" present + ipahost: + ipaadmin_password: MyPassword123 + name: "{{ 'db2.' + ipaserver_domain }}" + force: yes + register: result + failed_when: not result.changed + + - name: Ensure host-group mysql-server is present + ipahostgroup: + ipaadmin_password: MyPassword123 + name: mysql-server + state: present + register: result + failed_when: not result.changed + + - name: Ensure host-group mysql-server is present again + ipahostgroup: + ipaadmin_password: MyPassword123 + name: mysql-server + state: present + register: result + failed_when: result.changed + + - name: Ensure host-group oracle-server is present + ipahostgroup: + ipaadmin_password: MyPassword123 + name: oracle-server + state: present + register: result + failed_when: not result.changed + + - name: Ensure host-group oracle-server is present again + ipahostgroup: + ipaadmin_password: MyPassword123 + name: oracle-server + state: present + register: result + failed_when: result.changed + + - name: Ensure host-group databases is present + ipahostgroup: + ipaadmin_password: MyPassword123 + name: databases + state: present + host: + - "{{ 'db1.' + ipaserver_domain }}" + hostgroup: + - oracle-server + register: result + failed_when: not result.changed + + - name: Ensure host-group databases is present again + ipahostgroup: + ipaadmin_password: MyPassword123 + name: databases + state: present + host: + - "{{ 'db1.' + ipaserver_domain }}" + hostgroup: + - oracle-server + register: result + failed_when: result.changed + + - name: Ensure host db2 is member of host-group databases + ipahostgroup: + ipaadmin_password: MyPassword123 + name: databases + state: present + host: + - "{{ 'db2.' + ipaserver_domain }}" + action: member + register: result + failed_when: not result.changed + + - name: Ensure host db2 is member of host-group databases again + ipahostgroup: + ipaadmin_password: MyPassword123 + name: databases + state: present + host: + - "{{ 'db2.' + ipaserver_domain }}" + action: member + register: result + failed_when: result.changed + + - name: Ensure host-group mysql-server is member of host-group databases + ipahostgroup: + ipaadmin_password: MyPassword123 + name: databases + state: present + hostgroup: + - mysql-server + action: member + register: result + failed_when: not result.changed + + - name: Ensure host-group mysql-server is member of host-group databases again + ipahostgroup: + ipaadmin_password: MyPassword123 + name: databases + state: present + hostgroup: + - mysql-server + action: member + register: result + failed_when: result.changed + + - name: Ensure host-group oracle-server is member of host-group databases (again) + ipahostgroup: + ipaadmin_password: MyPassword123 + name: databases + state: present + hostgroup: + - oracle-server + action: member + register: result + failed_when: result.changed + + - name: Ensure host-group databases, mysql-server and oracle-server are absent + ipahostgroup: + ipaadmin_password: MyPassword123 + name: + - databases + - mysql-server + - oracle-server + state: absent + register: result + failed_when: not result.changed + + - name: Ensure host-group databases, mysql-server and oracle-server are absent again + ipahostgroup: + ipaadmin_password: MyPassword123 + name: + - databases + - mysql-server + - oracle-server + state: absent + register: result + failed_when: result.changed + + - name: Test hosts db1 and db2 absent + ipahost: + ipaadmin_password: MyPassword123 + name: + - "{{ 'db1.' + ipaserver_domain }}" + - "{{ 'db2.' + ipaserver_domain }}" + state: absent diff --git a/tests/pwpolicy/test_pwpolicy.yml b/tests/pwpolicy/test_pwpolicy.yml new file mode 100644 index 0000000..5c69345 --- /dev/null +++ b/tests/pwpolicy/test_pwpolicy.yml @@ -0,0 +1,59 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure presence of group ops + ipagroup: + ipaadmin_password: SomeADMINpassword + name: ops + + - name: Ensure presence of pwpolicies for group ops + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + name: ops + minlife: 7 + maxlife: 49 + history: 5 + priority: 1 + lockouttime: 300 + minlength: 8 + minclasses: 5 + maxfail: 3 + failinterval: 5 + register: result + failed_when: not result.changed + + - name: Ensure presence of pwpolicies for group ops again + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + name: ops + minlife: 7 + maxlife: 49 + history: 5 + priority: 1 + lockouttime: 300 + minlength: 8 + minclasses: 5 + maxfail: 3 + failinterval: 5 + register: result + failed_when: result.changed + + - name: Ensure absence of pwpolicies for group ops + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + name: ops + state: absent + register: result + failed_when: not result.changed + + - name: Ensure absence of pwpolicies for group ops + ipapwpolicy: + ipaadmin_password: SomeADMINpassword + name: ops + state: absent + register: result + failed_when: result.changed diff --git a/tests/sudocmd/test_sudocmd.yml b/tests/sudocmd/test_sudocmd.yml new file mode 100644 index 0000000..9742786 --- /dev/null +++ b/tests/sudocmd/test_sudocmd.yml @@ -0,0 +1,120 @@ +--- + +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure sudocmds are absent + ipasudocmd: + ipaadmin_password: MyPassword123 + name: + - /usr/bin/su + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + state: absent + + - name: Ensure sudocmd is present + ipasudocmd: + ipaadmin_password: MyPassword123 + name: /usr/bin/su + state: present + register: result + failed_when: not result.changed + + - name: Ensure sudocmd is present again + ipasudocmd: + ipaadmin_password: MyPassword123 + name: /usr/bin/su + state: present + register: result + failed_when: result.changed + + - name: Ensure sudocmd is absent + ipasudocmd: + ipaadmin_password: MyPassword123 + name: /usr/bin/su + state: absent + register: result + failed_when: not result.changed + + - name: Ensure sudocmd is absent again + ipasudocmd: + ipaadmin_password: MyPassword123 + name: /usr/bin/su + state: absent + register: result + failed_when: result.changed + + - name: Ensure multiple sudocmd are present + ipasudocmd: + ipaadmin_password: MyPassword123 + name: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + state: present + register: result + failed_when: not result.changed + + - name: Ensure multiple sudocmd are present again + ipasudocmd: + ipaadmin_password: MyPassword123 + name: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + state: present + register: result + failed_when: result.changed + + - name: Ensure multiple sudocmd are absent + ipasudocmd: + ipaadmin_password: MyPassword123 + name: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + state: absent + register: result + failed_when: not result.changed + + - name: Ensure multiple sudocmd are absent again + ipasudocmd: + ipaadmin_password: MyPassword123 + name: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + state: absent + register: result + failed_when: result.changed + - name: Ensure sudocmds are absent + ipasudocmd: + ipaadmin_password: MyPassword123 + name: + - /usr/bin/su + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + state: absent + + - name: Ensure sudocmds are absent + ipasudocmd: + ipaadmin_password: MyPassword123 + name: + - /usr/sbin/ifconfig + state: absent + + - name: Ensure sudocmds are present + ipasudocmd: + ipaadmin_password: MyPassword123 + name: + - /usr/sbin/iwlist + state: present + + - name: Ensure multiple sudocmd are absent when only one was present + ipasudocmd: + ipaadmin_password: MyPassword123 + name: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + state: absent + register: result + failed_when: not result.changed diff --git a/tests/sudocmdgroup/test_sudocmdgroup.yml b/tests/sudocmdgroup/test_sudocmdgroup.yml new file mode 100644 index 0000000..226e986 --- /dev/null +++ b/tests/sudocmdgroup/test_sudocmdgroup.yml @@ -0,0 +1,164 @@ +--- + +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Ensure sudocmds are present + ipasudocmd: + ipaadmin_password: MyPassword123 + name: + - /usr/bin/su + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + state: present + + - name: Ensure sudocmdgroup is absent + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + state: absent + + - name: Ensure sudocmdgroup is present + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + state: present + register: result + failed_when: not result.changed + + - name: Ensure sudocmdgroup is present again + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + state: present + register: result + failed_when: result.changed + + - name: Ensure sudocmdgroup is absent + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + state: absent + register: result + failed_when: not result.changed + + - name: Ensure sudocmdgroup is absent again + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + state: absent + register: result + failed_when: result.changed + + - name: Ensure testing sudocmdgroup is present + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + state: present + register: result + failed_when: not result.changed + + - name: Ensure sudo commands are present in existing sudocmdgroup + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + action: member + register: result + failed_when: not result.changed + + - name: Ensure sudo commands are present in existing sudocmdgroup, again + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + action: member + register: result + failed_when: result.changed + + - name: Ensure sudo commands are absent in existing sudocmdgroup + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Ensure sudo commands are absent in existing sudocmdgroup, again + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + action: member + state: absent + register: result + failed_when: result.changed + + - name: Ensure sudo commands are present in sudocmdgroup + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/ifconfig + - /usr/sbin/iwlist + action: member + state: present + register: result + failed_when: not result.changed + + - name: Ensure one sudo command is not present in sudocmdgroup + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/ifconfig + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Ensure one sudo command is present in sudocmdgroup + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/ifconfig + action: member + state: present + register: result + failed_when: not result.changed + + - name: Ensure the other sudo command is not present in sudocmdgroup + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/iwlist + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Ensure the other sudo commandsis not present in sudocmdgroup, again + ipasudocmdgroup: + ipaadmin_password: MyPassword123 + name: network + sudocmd: + - /usr/sbin/iwlist + action: member + state: absent + register: result + failed_when: result.changed diff --git a/tests/sudorule/test_sudorule.yml b/tests/sudorule/test_sudorule.yml new file mode 100644 index 0000000..88ed90a --- /dev/null +++ b/tests/sudorule/test_sudorule.yml @@ -0,0 +1,279 @@ +--- + +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + + - name: Ensure hostgroup is present, with a host. + ipahostgroup: + ipaadmin_password: MyPassword123 + name: cluster + host: + - "{{ groups.ipaserver[0] }}" + + - name: Ensure some sudocmds are available + ipasudocmd: + ipaadmin_password: pass1234 + name: + - /sbin/ifconfig + - /usr/bin/vim + state: present + + - name: Ensure sudorules are absent + ipasudorule: + ipaadmin_password: pass1234 + name: + - testrule1 + - allusers + - allhosts + - allcommands + state: absent + + - name: Ensure sudorule is present + ipasudorule: + ipaadmin_password: pass1234 + name: testrule1 + register: result + failed_when: not result.changed + + - name: Ensure sudorule is present again + ipasudorule: + ipaadmin_password: pass1234 + name: testrule1 + register: result + failed_when: result.changed + + - name: Ensure sudorule is present, runAsUserCategory. + ipasudorule: + ipaadmin_password: pass1234 + name: testrule1 + runAsUserCategory: all + register: result + failed_when: result.changed + + - name: Ensure sudorule is present, with usercategory 'all' + ipasudorule: + ipaadmin_password: pass1234 + name: allusers + usercategory: all + register: result + failed_when: not result.changed + + - name: Ensure sudorule is present, with usercategory 'all', again + ipasudorule: + ipaadmin_password: pass1234 + name: allusers + usercategory: all + register: result + failed_when: result.changed + + - name: Ensure sudorule is present, with hostategory 'all' + ipasudorule: + ipaadmin_password: pass1234 + name: allhosts + hostcategory: all + register: result + failed_when: not result.changed + + - name: Ensure sudorule is present, with hostategory 'all', again + ipasudorule: + ipaadmin_password: pass1234 + name: allhosts + hostcategory: all + register: result + failed_when: result.changed + + - name: Ensure sudorule is disabled + ipasudorule: + ipaadmin_password: pass1234 + name: testrule1 + state: disabled + + - name: Ensure sudorule is disabled, again + ipasudorule: + ipaadmin_password: pass1234 + name: testrule1 + state: disabled + register: result + failed_when: result.changed + + - name: Ensure sudorule is enabled + ipasudorule: + ipaadmin_password: pass1234 + name: testrule1 + state: enabled + register: result + failed_when: not result.changed + + - name: Ensure sudorule is enabled, again + ipasudorule: + ipaadmin_password: pass1234 + name: testrule1 + state: enabled + register: result + failed_when: result.changed + + - name: Ensure sudorule is present and some sudocmd are a member of it. + ipasudorule: + ipaadmin_password: pass1234 + name: testrule1 + cmd: + - /sbin/ifconfig + - /usr/bin/vim + action: member + register: result + failed_when: not result.changed + + - name: Ensure sudorule is present and some sudocmd are a member of it, again. + ipasudorule: + ipaadmin_password: pass1234 + name: testrule1 + cmd: + - /sbin/ifconfig + - /usr/bin/vim + action: member + register: result + failed_when: result.changed + + - name: Ensure sudorule is present with cmdcategory 'all'. + ipasudorule: + ipaadmin_password: pass1234 + name: allcommands + cmdcategory: all + register: result + failed_when: not result.changed + + - name: Ensure sudorule is present with cmdcategory 'all', again. + ipasudorule: + ipaadmin_password: pass1234 + name: allcommands + cmdcategory: all + register: result + failed_when: result.changed + + - name: Ensure host "{{ groups.ipaserver[0] }}" is present in sudorule. + ipasudorule: + ipaadmin_password: pass1234 + name: testrule1 + host: "{{ groups.ipaserver[0] }}" + action: member + register: result + failed_when: not result.changed + + - name: Ensure host "{{ groups.ipaserver[0] }}" is present in sudorule, again. + ipasudorule: + ipaadmin_password: pass1234 + name: testrule1 + host: "{{ groups.ipaserver[0] }}" + action: member + register: result + failed_when: result.changed + + - name: Ensure hostgroup is present in sudorule. + ipasudorule: + ipaadmin_password: MyPassword123 + name: testrule1 + hostgroup: cluster + action: member + register: result + failed_when: not result.changed + + - name: Ensure hostgroup is present in sudorule, again. + ipasudorule: + ipaadmin_password: MyPassword123 + name: testrule1 + hostgroup: cluster + action: member + register: result + failed_when: result.changed + + - name: Ensure sudorule sudocmds are absent + ipasudorule: + ipaadmin_password: pass1234 + name: testrule1 + cmd: + - /sbin/ifconfig + - /usr/bin/vim + action: member + state: absent + register: result + failed_when: not result.changed + + - name: Ensure sudorule sudocmds are absent, again + ipasudorule: + ipaadmin_password: pass1234 + name: testrule1 + cmd: + - /sbin/ifconfig + - /usr/bin/vim + action: member + state: absent + register: result + failed_when: result.changed + + - name: Ensure sudorule is absent + ipasudorule: + ipaadmin_password: pass1234 + name: testrule1 + state: absent + register: result + failed_when: not result.changed + + - name: Ensure sudorule is absent, again. + ipasudorule: + ipaadmin_password: pass1234 + name: testrule1 + state: absent + register: result + failed_when: result.changed + + - name: Ensure sudorule allhosts is absent + ipasudorule: + ipaadmin_password: pass1234 + name: allhosts + state: absent + register: result + failed_when: not result.changed + + - name: Ensure sudorule allhosts is absent, again + ipasudorule: + ipaadmin_password: pass1234 + name: allhosts + state: absent + register: result + failed_when: result.changed + + - name: Ensure sudorule allusers is absent + ipasudorule: + ipaadmin_password: pass1234 + name: allusers + state: absent + register: result + failed_when: not result.changed + + - name: Ensure sudorule allusers is absent, again + ipasudorule: + ipaadmin_password: pass1234 + name: allusers + state: absent + register: result + failed_when: result.changed + + - name: Ensure sudorule allcommands is absent + ipasudorule: + ipaadmin_password: pass1234 + name: allcommands + state: absent + register: result + failed_when: not result.changed + + - name: Ensure sudorule allcommands is absent, again + ipasudorule: + ipaadmin_password: pass1234 + name: allcommands + state: absent + register: result + failed_when: result.changed diff --git a/tests/user/certificate/cert1.der b/tests/user/certificate/cert1.der new file mode 100644 index 0000000..334511f Binary files /dev/null and b/tests/user/certificate/cert1.der differ diff --git a/tests/user/certificate/cert1.pem b/tests/user/certificate/cert1.pem new file mode 100644 index 0000000..6fb9d2e --- /dev/null +++ b/tests/user/certificate/cert1.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIC/zCCAeegAwIBAgIUZGHLaSYg1myp6EI4VGWSC27vOrswDQYJKoZIhvcNAQEL +BQAwDzENMAsGA1UEAwwEdGVzdDAeFw0xOTEwMTQxNjI4MzVaFw0yMDEwMTMxNjI4 +MzVaMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQDER/lB8wUAmPTSwSc/NOXNlzdpPOQDSwrhKH6XsqZF4KpQoSY/nmCjAhJm +OVpOUo4K2fGRZ0yAH9fkGv6yJP6c7IAFjLeec7GPHVwN4bZrP1DXfTAmfmXhcRQb +CYkV+wmq8Puzw/+xA9EJrrodnJPPsE6E8HnSVLF6Ys9+cJMJ7HuwOI+wYt3gkmsp +sir1tccmf4x1PP+yHJWdcXyetlFRcmZ8gspjqOR2jb89xSQsh8gcyDW6rPNlSTzY +Z2FmNtjES6ZhCsYL31fQbF2QglidlLGpAlvHUUS+xCigW73cvhFPMWXcfO51Mr15 +RcgYTckY+7QZ2nYqplRBoDlQl6DnAgMBAAGjUzBRMB0GA1UdDgQWBBTPG99XVRdx +pOXMZo3Nhy+ldnf13TAfBgNVHSMEGDAWgBTPG99XVRdxpOXMZo3Nhy+ldnf13TAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAjWTcnIl2mpNbfHAN8 +DB4Kk+RNRmhsH0y+r/47MXVTMMMToCfofeNY3Jeohu+2lIXMPQfTvXUbDTkNAGsG +Lv6LtQEUfSREqgk1eY7bT9BFfpH1uV2ZFhCO9jBA+E4bf55Kx7bgUNG31ykBshOs +OblOJM1lS/0q4TWHAxrsU2PNwPi8X0ten+eGeB8aRshxS17Ij2cH0fdAMmSA+jMA +vTIZl853Bxe0HuozauKwOFWL4qHm61c4O/j1mQCLqJKYfJ9mBDWFQLszd/tF+ePK +iNhZCQly60F8Lumn2CDZj5UIkl8wk9Wls5n1BIQs+M8AN65NAdv7+js8jKUKCuyj +i8r3 +-----END CERTIFICATE----- diff --git a/tests/user/certificate/cert2.der b/tests/user/certificate/cert2.der new file mode 100644 index 0000000..a3ba5a5 Binary files /dev/null and b/tests/user/certificate/cert2.der differ diff --git a/tests/user/certificate/cert2.pem b/tests/user/certificate/cert2.pem new file mode 100644 index 0000000..4a243b2 --- /dev/null +++ b/tests/user/certificate/cert2.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIC/zCCAeegAwIBAgIUAWE1vaA+mZd3nwZqwWH64EbHvR0wDQYJKoZIhvcNAQEL +BQAwDzENMAsGA1UEAwwEdGVzdDAeFw0xOTEwMTQxNjI4NDVaFw0yMDEwMTMxNjI4 +NDVaMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCWzJibKtN8Zf7LgandINhFonx99AKi44iaZkrlMKEObE6Faf8NTUbUgK3V +fJNYmCbA1baLVJ0YZJijJ7S/4o7h7eeqcJVXJkEhWNTimWXNW/YCzTHe3SSapnSY +OKmdHHRClplysL8OyyEG7pbX/aB9iAfFb/+vUFCX5sMwFFrYxOimKJ9Pc/NRFtdv +1wNw1rqWKF1ZzagWRlG4QgzRGwQ4quc7yO98TKikj2OPiIt7Zd46hbqQxmgGBtCk +VOZIhxu77OmNrFsXmM4rZZpmqh0UdqcpwkRojVnGXmNqeMCd6dNTnLhr9wukUYw0 +KgE57zCDVr9Ix+p/dA5R1mG4RJ2XAgMBAAGjUzBRMB0GA1UdDgQWBBSbuiH2lNVr +ID3yt1SsFwtOFKOnpTAfBgNVHSMEGDAWgBSbuiH2lNVrID3yt1SsFwtOFKOnpTAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBCVWd293wWyohFqMFM +HRBBg97T2Uc1yeT0dMH4BpuOaCqQp4q5ep+uLcXEI6+3mEwm8pa/ULQCD8yLLdot +IWlG3+h/4boFpdiPFcBDgT8kGe+0KOzB8Nt7E13QYOu12MNi10qwGrjKhdhu1xBe +4fpY5VCetVU1OLyuTsUyucQsFrtZI0SR83h+blbyoMZ7IhMngCfGUe1bnYeWnLbp +FbigKfPuVDWsMH2kgj05EAd5EgHkWbX8QA8hmcmDKfNT3YZM8kiGQwmFrnQdq8bN +0uHR8Nz+24cbmdbHcD65wlDW6GmYxi8mW+V6bAqn9pir/J14r4YFnqMGgjmdt81t +scJV +-----END CERTIFICATE----- diff --git a/tests/user/certificate/cert3.der b/tests/user/certificate/cert3.der new file mode 100644 index 0000000..783830e Binary files /dev/null and b/tests/user/certificate/cert3.der differ diff --git a/tests/user/certificate/cert3.pem b/tests/user/certificate/cert3.pem new file mode 100644 index 0000000..b2a6a99 --- /dev/null +++ b/tests/user/certificate/cert3.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIC/zCCAeegAwIBAgIUTC33WUoYGFoIVGMwgjbc5J6xCyowDQYJKoZIhvcNAQEL +BQAwDzENMAsGA1UEAwwEdGVzdDAeFw0xOTEwMTQxNjI4NTJaFw0yMDEwMTMxNjI4 +NTJaMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQDCA+6P2eieXHaVJivtWif7SntjjkJm0juRKRRGsT3wt+zCZqoDe8zylTBN +0mse/POWXdC+zXRMC2X/c4V10kgrvWbnNdFdUFfBUphiXSoqnUYHZ6Ta+b4UTzC2 +tECSUEnSCz9n1ofHnyqDyT9FELzVkRkQqexD+BFgZTF39R4q8BA4bWKQy94Kgvb+ +IP77+ou4fhkBLI1MX5nkWa3Oyu4TMzT/tqgPE70hk8wQzUU2aiwJ7IsmnWE6Ysk7 +c4DYMJQF/51bi2ByZWERNjyBY6L+ZV90aL4UFR9O+Pw9HatfHVBRdmzSkKJOr9iu +4summWgH0QYDmbkdhGwYvup0EmEfAgMBAAGjUzBRMB0GA1UdDgQWBBSJCQ8ho0Pp +e0khVhgiMqsvlgxIjzAfBgNVHSMEGDAWgBSJCQ8ho0Ppe0khVhgiMqsvlgxIjzAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAILLPnau32r/YoOVCV +WQotGtySy36aFlHa3T8IkSpatNCPIf3U0FWS6TVYBwY0PBfdqWBkvCuJTupLh0OE +P4TCsDa5pJGOK7blyfiAfcHajqyouACSVNlG63EPvB63h4H4F4HJnhDd4z7pVC/W +PB8w5GTBJNjELmeWfH7nj7lu8UkOdLhzTKL40RPs0k4l09yYBmZqqExxGsSfvRBQ +crwlAsvQ0E/cTNGbyzOKs3SbOM2WEHye6xNEsey01icYcjfjqvEd6mw3+WOUeJAu +DH9/EOloFM2iz5Xp31Ig3WT0RVy+lMriG9GesPpFBs2xp9wQCXLNIkpbHKyYs3vo +MyBH +-----END CERTIFICATE----- diff --git a/tests/user/certificate/private1.key b/tests/user/certificate/private1.key new file mode 100644 index 0000000..cb76298 --- /dev/null +++ b/tests/user/certificate/private1.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDER/lB8wUAmPTS +wSc/NOXNlzdpPOQDSwrhKH6XsqZF4KpQoSY/nmCjAhJmOVpOUo4K2fGRZ0yAH9fk +Gv6yJP6c7IAFjLeec7GPHVwN4bZrP1DXfTAmfmXhcRQbCYkV+wmq8Puzw/+xA9EJ +rrodnJPPsE6E8HnSVLF6Ys9+cJMJ7HuwOI+wYt3gkmspsir1tccmf4x1PP+yHJWd +cXyetlFRcmZ8gspjqOR2jb89xSQsh8gcyDW6rPNlSTzYZ2FmNtjES6ZhCsYL31fQ +bF2QglidlLGpAlvHUUS+xCigW73cvhFPMWXcfO51Mr15RcgYTckY+7QZ2nYqplRB +oDlQl6DnAgMBAAECggEBALtKTj6urHxId3xPEKsQR6Noglgp4Qx/Y687W6hWsLAG +051CV+PmtSF2DaZ7XX9U6PLTydzL68RqHjArzhKgmE+WoAYrot5QWQJNqpQYZ19o +uDQW4YYpn/+BTgUKkUNnGm+BqTt8b5QyJxoNHsy4ppZMDnBtomCfrgYxGPr2YmfZ +Ee7oEEf1xIU2maE5Nxv97lNR2Xvm2R4F8lzRcHmvejLYNiqZ2Ag4ijnKVTpoEMUy +afl5LNSzGJETXsv+LtaJGEr6x8/IesVSCdyX2LZeAyQPLKwb3YQDkQree54vwS7p +cVmQdx6fLTYV1tOSXUEC2ibInO188kGA198HSqSgHJkCgYEA7zhL+6tYZdXoMzTH +hXHLYGHmQsQXxleH5uciz4q+en7do6BFB2DqIgLTpcD/H8XMDlg9WO7756H1zqvb +6IOkqwsrro/fsgb6FrmjXl8zlkwT3pTNJfmBydRf7Qk2woCRPUoLZBRAumNL8RSx +Xm1/DbPbTR3jjVNH9dPb3Efd0qUCgYEA0gyesMgwDzjsXpPUsuWTMBMziy0KRFNT +lCMCI5DVpy/XnptyLdkY93jvmq+VWbily4KlOYbfYJ/16xeNZ7aNOMnC6z4z9p9+ +w3E9q5xKJcAJP5kN/WnjBwErveDK9r1YSj8RJpvapJFqjxA5WVTwADtyBhgNS4Og +mXPPBleMC5sCgYEA0Yw/AvXVOV9nR3O0UvCbdpJLYbDkIpoKMfnGRIcE08jN3cdG +sG/0qFZRj6C/2tUpKmehVYYCo6T77U4eFE88r5fZa9Ab45a4+68hrEk4py99ODyg +d+NYDbQ7Uyf/D+IPV+DEmaYkDSFuJIA73ruL0DT8pVDJQ8LwBibPMObDKQECgYBa +aUYxD6noE3diaj1GV5zYN5ubD2L47+jsvXjhOClOkkA8K+qko2qksrBno6YkfV8X +zv8xWMVzgMbIT1X1S1VUGTxGJ3sUb6iPlYGXCWm9AAC7GDU2W8p1rGJYk5apR+zl +4GmQdctRxKnaNICK3A2F/BBjYRzv4RNSmc+Fik9kewKBgHsCF3uEP7ONvmEjYLQ4 +7+6fZ+m4BXKeU/kKQoEXSjSFn0dBIHo+2yuafSUz04VJCVXUic3c47kHwVtgX5lu +jEUL1jgK4aBbl9cvywupHBf3spAP89aocgFiC9uUJzp3u39U0LpgXY7Z+1lUsCL+ +VG2oGh0KVgazjUzmbTf9ZcLp +-----END PRIVATE KEY----- diff --git a/tests/user/certificate/private2.key b/tests/user/certificate/private2.key new file mode 100644 index 0000000..59d83b9 --- /dev/null +++ b/tests/user/certificate/private2.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWzJibKtN8Zf7L +gandINhFonx99AKi44iaZkrlMKEObE6Faf8NTUbUgK3VfJNYmCbA1baLVJ0YZJij +J7S/4o7h7eeqcJVXJkEhWNTimWXNW/YCzTHe3SSapnSYOKmdHHRClplysL8OyyEG +7pbX/aB9iAfFb/+vUFCX5sMwFFrYxOimKJ9Pc/NRFtdv1wNw1rqWKF1ZzagWRlG4 +QgzRGwQ4quc7yO98TKikj2OPiIt7Zd46hbqQxmgGBtCkVOZIhxu77OmNrFsXmM4r +ZZpmqh0UdqcpwkRojVnGXmNqeMCd6dNTnLhr9wukUYw0KgE57zCDVr9Ix+p/dA5R +1mG4RJ2XAgMBAAECggEAS5nXCDO4Qy1/R9eBqXLF+mMztpGWoMMhwQZ3ld+DXw+9 +bfVuAOU1FWRNwjHqTQg6pYJ/Oer5tzj3rRRC8dBLgckb078Nn9t125oFYHU3LHVm +KJFm5yxHJaE94vLFVhbl0lxeIbmqj2gW7rq+tRpaU5TXEIzNyr6hKQZv5LLPuMx6 +MiBrSpkCwfPf9psv6k2GIGqE1JuY99dNqdEUi8UQryNMzV4pthUmVybO8NPxUY8M +s/VAbG1Hy9tgInR3wRgTjEc2ejUJrTziiqiZarZtCp+JSZufYakDU9yZbu9v4Oz9 +ityPdApkW8CuZnJcUDAtdgtKMhWyBPnWcrUgkbV0AQKBgQDGY1saiI9M7VlleyDc +QNVXpPCmOpDLso5X3hZrrHDgDIGkvXa026Q5ufkdxkybRYJeOCdYzIM/iXSJlgNe +R2a+aoAsePfEVFAe96ZgzrLrBq7lGvcPXGpT6GTVl0d0CwN/vG1Tzk89Hq3xIBbh +NTlM+j2ot66xgekIsE0v5Pi41wKBgQDCl14mgaui4DqYFYlI/ckI00r/X0/0HIhf +kf/Ck/pkF89IeOAK+O4GOfVoMk3vi1gDYgiz6G7h+sUsFTOYKuP9io/vX0pIFNOA +NPgaVtRKitiepNo4vwc+/PRmxvf2XXFXFRSiYf0jDzruvE3yDzWwX9P1nQFBQoPj +r8g/6+7pQQKBgDXHnVzWBDLQbNmLxV6v3KXDutD1M2dk4h2DwQQzXO3/te1YxyNE +H4LenV+q7/1vnGW6R0BVQIcq1gKuPf+Cz6Fy8Ygcyt3YFVgvvlSj8/CugR7ubmcl +oFVavGsCdYZJrgsko2aCmQxykqi5EDrA2OW7OJfSI3NPSkLmuCXxplNFAoGBALHD +D5pDqOTAzCY0vlY0qNrsEr4ZdvO8wQP1XtyEzB919MDy01CSuPZtKfeGxNWIyN1G +SEb5lZnQuSCdOaXPwLjURMralQQmKlQbj26YVZTHJD5AwK1ILTloYWgmaUzhbfGs +a04wD8xgVGjVEquHI3e9AueEBypztgJgiaGDSZxBAoGADpxUn3L6lJrPyOd3IJrj +ypU/EfvY7Qd5pRTrJd9tObbi8zF1sWi/FcQNgoZP7oz/aklFfq8WWwJbe0fL1Wk/ +MeVHj8JEc/dh1ISgbHYdBgegvS6L30RcNRUJWANYcifEQPlSHTzYXviQ8tEOCq+S +/TPqxnd2CkT6w3bSCJbxKVM= +-----END PRIVATE KEY----- diff --git a/tests/user/certificate/private3.key b/tests/user/certificate/private3.key new file mode 100644 index 0000000..f426162 --- /dev/null +++ b/tests/user/certificate/private3.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDCA+6P2eieXHaV +JivtWif7SntjjkJm0juRKRRGsT3wt+zCZqoDe8zylTBN0mse/POWXdC+zXRMC2X/ +c4V10kgrvWbnNdFdUFfBUphiXSoqnUYHZ6Ta+b4UTzC2tECSUEnSCz9n1ofHnyqD +yT9FELzVkRkQqexD+BFgZTF39R4q8BA4bWKQy94Kgvb+IP77+ou4fhkBLI1MX5nk +Wa3Oyu4TMzT/tqgPE70hk8wQzUU2aiwJ7IsmnWE6Ysk7c4DYMJQF/51bi2ByZWER +NjyBY6L+ZV90aL4UFR9O+Pw9HatfHVBRdmzSkKJOr9iu4summWgH0QYDmbkdhGwY +vup0EmEfAgMBAAECggEAdEgFAGSbHebPD79sDnq9gcf3QgjuVU/lcbAMPf5W4GJr +3WvItAPMJwwxgkL9/vmeSN37kY/0BuvB+yPStnYM2WJQPX0s+V+A6RZGzJWIAzh1 +01RUIwYR3XxE9wv7s3W5eNFS9DpI8OS9h3TjndJVSy8Gtc0SFP6l839S8dGQfiyN +eMqV16M+TM0LwyEogvGa79l4HjMIcorypCXg8NVcDaxNJYnAcegoRwdhGsb4jKG6 +kB+Z4dfAKLu7OFT6/20Q0QUdA/PrdBRAFt8KPhrrweKaAApVtrU0OHNs9ULFHXnu +kSVKZ+UTCUGWxMd4lJw5XZQ4FqUdb8Sxt8TUuvRioQKBgQDy6zY1EiqsDZE+sJbd +/jnUWn+I4/xR5y4KmbEx76dWL39TooyiHYKABJQ9BgvBIXi95AXaRodwn12DhaW7 +VW0m4RgJH/FNZoxc9xOE2+EPr2pQGn6bvJK9IjsITDoAmDbguzMZ+TCDGZqIYiIE +GTcgeW7NOBYM2Qy8Ufqe9zfV0QKBgQDMdo426TPxdU6Gb8AVOFFuXlL6Py0Sxk0q +pEAhyEzCKV9HM1eX8aDrJ5++lFiMwlhkYRrWVBENPyhPgWo9sMATJM0AIsBKTSyg +rVuqlaU8e+Pqyl8ZMOZ6uMq3zLts/Vp1sX5yU8vw5FqMddMas6SMpIoPEIAiJlse +CujyJ29T7wKBgQCiQnry+C+IvYdHWK1tm2MFdW27Ao6IJuOaMQ8rS+l6qD9kni9S +GmQRHv3lxSQU3UbJkIZYRsQxdkIAmEUb3PQMBE8JyUxlZxpa/q8LD9RFpeZdm1T2 +sf9SVosX/9K+ku4VLvXzY4AEEhYnA2W1VyJ7jqF0cwJHkrPvFtNRW9DwAQKBgCRi +6NYu1DahSLM2Cfn8xskccinkulHABpWTG3KnoblgAXu7UFhTAO84Yv5YihWqtG5Q +taT02v//gF39yvlljhkaEH14sb3HVCzYDRsjfH9yENKE5z2lbS7j2fexsJ0pzUJq +rvULopyhFtguU75Jv/vjgEpEBnmNV+PVzzTg/bfzAoGAGz11E33qpZjVw6becf9w +U8qnPfncIqSCg0fWNnsYwD56vI9L2ExCZG//SOUZ54b8GW+RaTFDHOlIE8dRAhrF +M5QnEjm2S+wVPJz7gKQ3cVART8EPi/Q6BT7YIgNIhemq+AwW5xMhZcBiA3vRI/Eu +vi807exD569efFLa9uspI8o= +-----END PRIVATE KEY----- diff --git a/tests/user/certificate/test_user_certificate.yml b/tests/user/certificate/test_user_certificate.yml new file mode 100644 index 0000000..527c0f3 --- /dev/null +++ b/tests/user/certificate/test_user_certificate.yml @@ -0,0 +1,92 @@ +# +# Generate self-signed certificates using openssl: +# +# openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout private1.key -out cert1.pem -subj '/CN=test' +# openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout private2.key -out cert2.pem -subj '/CN=test' +# openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout private3.key -out cert3.pem -subj '/CN=test' +# +# Convert the certificate do DER for easier handling through CLI +# +# openssl x509 -outform der -in cert1.pem -out cert1.der +# openssl x509 -outform der -in cert2.pem -out cert2.der +# openssl x509 -outform der -in cert3.pem -out cert3.der +# +# Use base64: +# +# base64 cert1.der -w5000 +# base64 cert2.der -w5000 +# base64 cert3.der -w5000 +# +--- +- name: Test user certificates + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: User test present + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + first: test + last: test + + - name: User test cert members present + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certificate: + - 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 + action: member + register: result + failed_when: not result.changed + + - name: User test cert members present again + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + first: test + last: test + certificate: + - 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 + action: member + register: result + failed_when: result.changed + + - name: User test cert members absent + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certificate: + - 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 + state: absent + action: member + register: result + failed_when: not result.changed + + - name: User test cert members absent again + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certificate: + - MIIC/zCCAeegAwIBAgIUZGHLaSYg1myp6EI4VGWSC27vOrswDQYJKoZIhvcNAQELBQAwDzENMAsGA1UEAwwEdGVzdDAeFw0xOTEwMTQxNjI4MzVaFw0yMDEwMTMxNjI4MzVaMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDER/lB8wUAmPTSwSc/NOXNlzdpPOQDSwrhKH6XsqZF4KpQoSY/nmCjAhJmOVpOUo4K2fGRZ0yAH9fkGv6yJP6c7IAFjLeec7GPHVwN4bZrP1DXfTAmfmXhcRQbCYkV+wmq8Puzw/+xA9EJrrodnJPPsE6E8HnSVLF6Ys9+cJMJ7HuwOI+wYt3gkmspsir1tccmf4x1PP+yHJWdcXyetlFRcmZ8gspjqOR2jb89xSQsh8gcyDW6rPNlSTzYZ2FmNtjES6ZhCsYL31fQbF2QglidlLGpAlvHUUS+xCigW73cvhFPMWXcfO51Mr15RcgYTckY+7QZ2nYqplRBoDlQl6DnAgMBAAGjUzBRMB0GA1UdDgQWBBTPG99XVRdxpOXMZo3Nhy+ldnf13TAfBgNVHSMEGDAWgBTPG99XVRdxpOXMZo3Nhy+ldnf13TAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAjWTcnIl2mpNbfHAN8DB4Kk+RNRmhsH0y+r/47MXVTMMMToCfofeNY3Jeohu+2lIXMPQfTvXUbDTkNAGsGLv6LtQEUfSREqgk1eY7bT9BFfpH1uV2ZFhCO9jBA+E4bf55Kx7bgUNG31ykBshOsOblOJM1lS/0q4TWHAxrsU2PNwPi8X0ten+eGeB8aRshxS17Ij2cH0fdAMmSA+jMAvTIZl853Bxe0HuozauKwOFWL4qHm61c4O/j1mQCLqJKYfJ9mBDWFQLszd/tF+ePKiNhZCQly60F8Lumn2CDZj5UIkl8wk9Wls5n1BIQs+M8AN65NAdv7+js8jKUKCuyji8r3 + - MIIC/zCCAeegAwIBAgIUAWE1vaA+mZd3nwZqwWH64EbHvR0wDQYJKoZIhvcNAQELBQAwDzENMAsGA1UEAwwEdGVzdDAeFw0xOTEwMTQxNjI4NDVaFw0yMDEwMTMxNjI4NDVaMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCWzJibKtN8Zf7LgandINhFonx99AKi44iaZkrlMKEObE6Faf8NTUbUgK3VfJNYmCbA1baLVJ0YZJijJ7S/4o7h7eeqcJVXJkEhWNTimWXNW/YCzTHe3SSapnSYOKmdHHRClplysL8OyyEG7pbX/aB9iAfFb/+vUFCX5sMwFFrYxOimKJ9Pc/NRFtdv1wNw1rqWKF1ZzagWRlG4QgzRGwQ4quc7yO98TKikj2OPiIt7Zd46hbqQxmgGBtCkVOZIhxu77OmNrFsXmM4rZZpmqh0UdqcpwkRojVnGXmNqeMCd6dNTnLhr9wukUYw0KgE57zCDVr9Ix+p/dA5R1mG4RJ2XAgMBAAGjUzBRMB0GA1UdDgQWBBSbuiH2lNVrID3yt1SsFwtOFKOnpTAfBgNVHSMEGDAWgBSbuiH2lNVrID3yt1SsFwtOFKOnpTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBCVWd293wWyohFqMFMHRBBg97T2Uc1yeT0dMH4BpuOaCqQp4q5ep+uLcXEI6+3mEwm8pa/ULQCD8yLLdotIWlG3+h/4boFpdiPFcBDgT8kGe+0KOzB8Nt7E13QYOu12MNi10qwGrjKhdhu1xBe4fpY5VCetVU1OLyuTsUyucQsFrtZI0SR83h+blbyoMZ7IhMngCfGUe1bnYeWnLbpFbigKfPuVDWsMH2kgj05EAd5EgHkWbX8QA8hmcmDKfNT3YZM8kiGQwmFrnQdq8bN0uHR8Nz+24cbmdbHcD65wlDW6GmYxi8mW+V6bAqn9pir/J14r4YFnqMGgjmdt81tscJV + - MIIC/zCCAeegAwIBAgIUTC33WUoYGFoIVGMwgjbc5J6xCyowDQYJKoZIhvcNAQELBQAwDzENMAsGA1UEAwwEdGVzdDAeFw0xOTEwMTQxNjI4NTJaFw0yMDEwMTMxNjI4NTJaMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCA+6P2eieXHaVJivtWif7SntjjkJm0juRKRRGsT3wt+zCZqoDe8zylTBN0mse/POWXdC+zXRMC2X/c4V10kgrvWbnNdFdUFfBUphiXSoqnUYHZ6Ta+b4UTzC2tECSUEnSCz9n1ofHnyqDyT9FELzVkRkQqexD+BFgZTF39R4q8BA4bWKQy94Kgvb+IP77+ou4fhkBLI1MX5nkWa3Oyu4TMzT/tqgPE70hk8wQzUU2aiwJ7IsmnWE6Ysk7c4DYMJQF/51bi2ByZWERNjyBY6L+ZV90aL4UFR9O+Pw9HatfHVBRdmzSkKJOr9iu4summWgH0QYDmbkdhGwYvup0EmEfAgMBAAGjUzBRMB0GA1UdDgQWBBSJCQ8ho0Ppe0khVhgiMqsvlgxIjzAfBgNVHSMEGDAWgBSJCQ8ho0Ppe0khVhgiMqsvlgxIjzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAILLPnau32r/YoOVCVWQotGtySy36aFlHa3T8IkSpatNCPIf3U0FWS6TVYBwY0PBfdqWBkvCuJTupLh0OEP4TCsDa5pJGOK7blyfiAfcHajqyouACSVNlG63EPvB63h4H4F4HJnhDd4z7pVC/WPB8w5GTBJNjELmeWfH7nj7lu8UkOdLhzTKL40RPs0k4l09yYBmZqqExxGsSfvRBQcrwlAsvQ0E/cTNGbyzOKs3SbOM2WEHye6xNEsey01icYcjfjqvEd6mw3+WOUeJAuDH9/EOloFM2iz5Xp31Ig3WT0RVy+lMriG9GesPpFBs2xp9wQCXLNIkpbHKyYs3voMyBH + state: absent + action: member + register: result + failed_when: result.changed + + - name: User test absent + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + state: absent + register: result + failed_when: not result.changed diff --git a/tests/user/certificate/test_users_certificate.yml b/tests/user/certificate/test_users_certificate.yml new file mode 100644 index 0000000..b5abbde --- /dev/null +++ b/tests/user/certificate/test_users_certificate.yml @@ -0,0 +1,103 @@ +# +# Generate self-signed certificates using openssl: +# +# openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout private1.key -out cert1.pem -subj '/CN=test' +# openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout private2.key -out cert2.pem -subj '/CN=test' +# openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout private3.key -out cert3.pem -subj '/CN=test' +# +# Convert the certificate do DER for easier handling through CLI +# +# openssl x509 -outform der -in cert1.pem -out cert1.der +# openssl x509 -outform der -in cert2.pem -out cert2.der +# openssl x509 -outform der -in cert3.pem -out cert3.der +# +# Use base64: +# +# base64 cert1.der -w5000 +# base64 cert2.der -w5000 +# base64 cert3.der -w5000 +# +--- +- name: Test user certificates + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: User test absent + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + state: absent + + - name: User test present + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + first: test + last: test + + - name: User test cert members present + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + certificate: + - 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 + action: member + register: result + failed_when: not result.changed + + - name: User test cert members present again + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + certificate: + - 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 + action: member + register: result + failed_when: result.changed + + - name: User test cert members absent + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + certificate: + - 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 + state: absent + action: member + #register: result + #failed_when: not result.changed + + - name: User test cert members absent again + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + certificate: + - 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 + state: absent + action: member + register: result + failed_when: result.changed + + - name: User test absent + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + state: absent + register: result + failed_when: not result.changed diff --git a/tests/user/certmapdata/test_user_certmapdata.yml b/tests/user/certmapdata/test_user_certmapdata.yml new file mode 100644 index 0000000..cf5576e --- /dev/null +++ b/tests/user/certmapdata/test_user_certmapdata.yml @@ -0,0 +1,160 @@ +# +# Generate self-signed certificates using openssl: +# +# openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout private1.key -out cert1.pem -subj '/CN=test1' +# openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout private2.key -out cert2.pem -subj '/CN=test2' +# openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout private3.key -out cert3.pem -subj '/CN=test2' +# +# Convert the certificate do DER for easier handling through CLI +# +# openssl x509 -outform der -in cert1.pem -out cert1.der +# openssl x509 -outform der -in cert2.pem -out cert2.der +# openssl x509 -outform der -in cert3.pem -out cert3.der +# +# Use base64: +# +# base64 cert1.der -w5000 +# base64 cert2.der -w5000 +# base64 cert3.der -w5000 +# +--- +- name: Test user certmapdata + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: User test absent + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + state: absent + + - name: User test present + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + first: test + last: test + register: result + failed_when: not result.changed + + - name: User test certmapdata members present + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certmapdata: + - certificate: MIIDATCCAemgAwIBAgIUFDZuUg9kBvN+ubTBaS6d62KafvQwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDEwHhcNMTkxMDE0MTk0ODM4WhcNMjAxMDEzMTk0ODM4WjAQMQ4wDAYDVQQDDAV0ZXN0MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALrwv6KuOIZMxjp9aueRgJWKns7P7Oo8lo4ojBVhyB3K11+1OifLqfo2SL9HPx9XynvouMj+XnqbY+NRLOX9wPYAvA7rXUgN1zQYK5stCCTjW0V4QloHnMUPM0shIuErcTP1UMatzTaFxd+UkvZCMNbE9jXHeCd3uDYoqA8Y4yRuqQ4HcnbB7DyMNZHaSCHxQCvZzllJA9m1b+c2sO3l7008PibG5RSpPnrjMUeH4yzjaB8R7A/WApAA1g5bn+IXD2dDsbdHAVFdzqwkxxLlacD8pbM7jWtCI+qJwE6dXpPb5WRkUz3tZLkEW7EQJAi3TMy//hF/SobEul3WVjiO/HUCAwEAAaNTMFEwHQYDVR0OBBYEFCkUPLMp0M5rgxHNAYuunGM0if/vMB8GA1UdIwQYMBaAFCkUPLMp0M5rgxHNAYuunGM0if/vMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAI7gj1VllrF0eRqPP9xtSqWla+qGMpHUWur1yIoy3R8I7LV3muQpbgxsfOyH2PHj69v+8yvI5einvOXJVgJxcn/ERxm/prIdo+QlK4O/SDdswJWfjCaDlk03PzjDzHXeEWRwDFV9zpRx/hjLUn8knwBOSbUE8ImUGt37ZBMBsz++y+oCZJVxzwZgcXZ3L4yuhJ0l48Cz+2EEoYN08gJqNk1EzmpBcAYJJ0Ai1psFqu12b32fNIhWSSf5THldqNefdpBlMo5ZtC9wE//NTml+nebA1FJDSppTMfHP/rTb/wfNx5vc5KLYYR9wZUUUDeJhdNPlBZZuuVbn29X6a4teoBk= + - certificate: MIIDATCCAemgAwIBAgIUK2Sa8/xr/4H9BOB0K0SswbVmmMcwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDIwHhcNMTkxMDE0MTk0ODM1WhcNMjAxMDEzMTk0ODM1WjAQMQ4wDAYDVQQDDAV0ZXN0MjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANQdGqRUTNCptUsZxVe03YTYyhg6+ovidYP9bPPsCS1FzOTCKV9maFv4GHGEI455lkGowW3okT2DPC5pgtH43vZkDtjXNy1JDyg1y4OmxfiAg4Uc0W9DnakvEokVyt44WRzXjlv2CBO+A7Zon8z3aJwKKCV4EfpuvRw/npqhrnGF/w/n7NXyeRXI6lom9hqIQzJoRjXrMIEbtzM8m2GWlFq8af1KJ+Cmm25c87aeyu7I0+BRCq21pwcyQ6Cx0Lo3szQVlD9ZN6wUfz3IDacLjoMOZkVrclIKO0DU595AVo86TD0C1TC/vCjmDGOfgoQsfS5OFfP+FmN6we9IVIAUcL8CAwEAAaNTMFEwHQYDVR0OBBYEFHTGQuoyDwtbHVLU0SseGvarrJKOMB8GA1UdIwQYMBaAFHTGQuoyDwtbHVLU0SseGvarrJKOMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAEGcA6GzCv67GYWoFK9zAjN01M79BDSuac2QV+9PYjQ7tLXQPGdoXJjJzrkJLgzTwqH2iIUqGoAZp30dW89yrJuA8s0gk3SbPOowYmst6pN6SeENxXL/1kn/IBm3+oHf5IWf7vaW4j8tYt7Q3x2K3V9GWRrozGEPIXk8yMeJq72wpfWynDxxYOepGG2+pSkm8soi9Fpt29pb+DtKB2U9GpMBS8vHU+1H4trIvEOMsd4v+X1+Vxlnt8tgy8/PrlKk1wLB9r1XA+W/vXPBe0tRcuntsXiniSKGC5oiR9AFS134HFEWuhxXWihNFzsLmNimvvxQBlXMRPZC1waCSoTKTAI= + - certificate: MIIDATCCAemgAwIBAgIUIa8TtXJ4Nq8VYrlgbSKcVt0FdckwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDMwHhcNMTkxMDE0MTk0ODQ1WhcNMjAxMDEzMTk0ODQ1WjAQMQ4wDAYDVQQDDAV0ZXN0MzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMyttsxCiNH8qx83FTkQaUKxQXer6aMa5DpyfnGMh1K3icfEHyiKLkrjiuFbnfYC1s/iYkz9UyBQrneglYROGgr8TSFC5LXiVppI3/LONKuBh0GfmBM8dhYkOg6WmEB15EL7Hj3V6Xi/Sx3WFnvY/wAzds06linDp/I46jRXqMrWFuhXbf4A09OXpQs6KOMWkkitw3lKuKLgiNzXEiAHaS2YqW7UwSy9RLCrIDMwEmVzZ/gh3vGwM2jyfhOZ75U/xOt9U6jMQsp7hFQAoSeVBTuGpjI40g3IbNBRNi2SnERFzQFkJr0tzAX250XGDaRwRuLhilYoQoJl59B2cTzYsLECAwEAAaNTMFEwHQYDVR0OBBYEFD8iZgdSshdfyAMNfQNzS74Dl9UVMB8GA1UdIwQYMBaAFD8iZgdSshdfyAMNfQNzS74Dl9UVMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFrip5Hl7dHz3oqXLcoza9rQCiMsXV7Q2gVHJ6W2YZZlQpVOBmNPf0n42OVZrnIkDnuGD+pGuw65Aq/TnfMI2KrW8o//aOXLitR60moMEbj61IlBx3aUGvdhyevrz4tM8SHX05p4K86ZJ9jZuS/sNyDwBKBaBAqjW1Rjuqb2o+C9zvDgPWFX++8OgXljDPHR6XYAKpRBmbBZHxYXazjhj1gGhb9/txqDn4EniPXE4rZ/X5MwXzAPs7ROgvr7fkIQypO+O++FGn8rWQL+5zZY0GavOK6FMBJj8M6RDHwEEsXQqSHicsrL8iMx4jun82wgu+gO5lBPTMd0hjsOYR3eT0A= + action: member + register: result + failed_when: not result.changed + + - name: User test certmapdata members present again + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certmapdata: + - certificate: MIIDATCCAemgAwIBAgIUFDZuUg9kBvN+ubTBaS6d62KafvQwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDEwHhcNMTkxMDE0MTk0ODM4WhcNMjAxMDEzMTk0ODM4WjAQMQ4wDAYDVQQDDAV0ZXN0MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALrwv6KuOIZMxjp9aueRgJWKns7P7Oo8lo4ojBVhyB3K11+1OifLqfo2SL9HPx9XynvouMj+XnqbY+NRLOX9wPYAvA7rXUgN1zQYK5stCCTjW0V4QloHnMUPM0shIuErcTP1UMatzTaFxd+UkvZCMNbE9jXHeCd3uDYoqA8Y4yRuqQ4HcnbB7DyMNZHaSCHxQCvZzllJA9m1b+c2sO3l7008PibG5RSpPnrjMUeH4yzjaB8R7A/WApAA1g5bn+IXD2dDsbdHAVFdzqwkxxLlacD8pbM7jWtCI+qJwE6dXpPb5WRkUz3tZLkEW7EQJAi3TMy//hF/SobEul3WVjiO/HUCAwEAAaNTMFEwHQYDVR0OBBYEFCkUPLMp0M5rgxHNAYuunGM0if/vMB8GA1UdIwQYMBaAFCkUPLMp0M5rgxHNAYuunGM0if/vMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAI7gj1VllrF0eRqPP9xtSqWla+qGMpHUWur1yIoy3R8I7LV3muQpbgxsfOyH2PHj69v+8yvI5einvOXJVgJxcn/ERxm/prIdo+QlK4O/SDdswJWfjCaDlk03PzjDzHXeEWRwDFV9zpRx/hjLUn8knwBOSbUE8ImUGt37ZBMBsz++y+oCZJVxzwZgcXZ3L4yuhJ0l48Cz+2EEoYN08gJqNk1EzmpBcAYJJ0Ai1psFqu12b32fNIhWSSf5THldqNefdpBlMo5ZtC9wE//NTml+nebA1FJDSppTMfHP/rTb/wfNx5vc5KLYYR9wZUUUDeJhdNPlBZZuuVbn29X6a4teoBk= + - certificate: MIIDATCCAemgAwIBAgIUK2Sa8/xr/4H9BOB0K0SswbVmmMcwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDIwHhcNMTkxMDE0MTk0ODM1WhcNMjAxMDEzMTk0ODM1WjAQMQ4wDAYDVQQDDAV0ZXN0MjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANQdGqRUTNCptUsZxVe03YTYyhg6+ovidYP9bPPsCS1FzOTCKV9maFv4GHGEI455lkGowW3okT2DPC5pgtH43vZkDtjXNy1JDyg1y4OmxfiAg4Uc0W9DnakvEokVyt44WRzXjlv2CBO+A7Zon8z3aJwKKCV4EfpuvRw/npqhrnGF/w/n7NXyeRXI6lom9hqIQzJoRjXrMIEbtzM8m2GWlFq8af1KJ+Cmm25c87aeyu7I0+BRCq21pwcyQ6Cx0Lo3szQVlD9ZN6wUfz3IDacLjoMOZkVrclIKO0DU595AVo86TD0C1TC/vCjmDGOfgoQsfS5OFfP+FmN6we9IVIAUcL8CAwEAAaNTMFEwHQYDVR0OBBYEFHTGQuoyDwtbHVLU0SseGvarrJKOMB8GA1UdIwQYMBaAFHTGQuoyDwtbHVLU0SseGvarrJKOMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAEGcA6GzCv67GYWoFK9zAjN01M79BDSuac2QV+9PYjQ7tLXQPGdoXJjJzrkJLgzTwqH2iIUqGoAZp30dW89yrJuA8s0gk3SbPOowYmst6pN6SeENxXL/1kn/IBm3+oHf5IWf7vaW4j8tYt7Q3x2K3V9GWRrozGEPIXk8yMeJq72wpfWynDxxYOepGG2+pSkm8soi9Fpt29pb+DtKB2U9GpMBS8vHU+1H4trIvEOMsd4v+X1+Vxlnt8tgy8/PrlKk1wLB9r1XA+W/vXPBe0tRcuntsXiniSKGC5oiR9AFS134HFEWuhxXWihNFzsLmNimvvxQBlXMRPZC1waCSoTKTAI= + - certificate: MIIDATCCAemgAwIBAgIUIa8TtXJ4Nq8VYrlgbSKcVt0FdckwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDMwHhcNMTkxMDE0MTk0ODQ1WhcNMjAxMDEzMTk0ODQ1WjAQMQ4wDAYDVQQDDAV0ZXN0MzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMyttsxCiNH8qx83FTkQaUKxQXer6aMa5DpyfnGMh1K3icfEHyiKLkrjiuFbnfYC1s/iYkz9UyBQrneglYROGgr8TSFC5LXiVppI3/LONKuBh0GfmBM8dhYkOg6WmEB15EL7Hj3V6Xi/Sx3WFnvY/wAzds06linDp/I46jRXqMrWFuhXbf4A09OXpQs6KOMWkkitw3lKuKLgiNzXEiAHaS2YqW7UwSy9RLCrIDMwEmVzZ/gh3vGwM2jyfhOZ75U/xOt9U6jMQsp7hFQAoSeVBTuGpjI40g3IbNBRNi2SnERFzQFkJr0tzAX250XGDaRwRuLhilYoQoJl59B2cTzYsLECAwEAAaNTMFEwHQYDVR0OBBYEFD8iZgdSshdfyAMNfQNzS74Dl9UVMB8GA1UdIwQYMBaAFD8iZgdSshdfyAMNfQNzS74Dl9UVMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFrip5Hl7dHz3oqXLcoza9rQCiMsXV7Q2gVHJ6W2YZZlQpVOBmNPf0n42OVZrnIkDnuGD+pGuw65Aq/TnfMI2KrW8o//aOXLitR60moMEbj61IlBx3aUGvdhyevrz4tM8SHX05p4K86ZJ9jZuS/sNyDwBKBaBAqjW1Rjuqb2o+C9zvDgPWFX++8OgXljDPHR6XYAKpRBmbBZHxYXazjhj1gGhb9/txqDn4EniPXE4rZ/X5MwXzAPs7ROgvr7fkIQypO+O++FGn8rWQL+5zZY0GavOK6FMBJj8M6RDHwEEsXQqSHicsrL8iMx4jun82wgu+gO5lBPTMd0hjsOYR3eT0A= + action: member + register: result + failed_when: result.changed + + - name: User test certmapdata members absent + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certmapdata: + - certificate: MIIDATCCAemgAwIBAgIUFDZuUg9kBvN+ubTBaS6d62KafvQwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDEwHhcNMTkxMDE0MTk0ODM4WhcNMjAxMDEzMTk0ODM4WjAQMQ4wDAYDVQQDDAV0ZXN0MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALrwv6KuOIZMxjp9aueRgJWKns7P7Oo8lo4ojBVhyB3K11+1OifLqfo2SL9HPx9XynvouMj+XnqbY+NRLOX9wPYAvA7rXUgN1zQYK5stCCTjW0V4QloHnMUPM0shIuErcTP1UMatzTaFxd+UkvZCMNbE9jXHeCd3uDYoqA8Y4yRuqQ4HcnbB7DyMNZHaSCHxQCvZzllJA9m1b+c2sO3l7008PibG5RSpPnrjMUeH4yzjaB8R7A/WApAA1g5bn+IXD2dDsbdHAVFdzqwkxxLlacD8pbM7jWtCI+qJwE6dXpPb5WRkUz3tZLkEW7EQJAi3TMy//hF/SobEul3WVjiO/HUCAwEAAaNTMFEwHQYDVR0OBBYEFCkUPLMp0M5rgxHNAYuunGM0if/vMB8GA1UdIwQYMBaAFCkUPLMp0M5rgxHNAYuunGM0if/vMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAI7gj1VllrF0eRqPP9xtSqWla+qGMpHUWur1yIoy3R8I7LV3muQpbgxsfOyH2PHj69v+8yvI5einvOXJVgJxcn/ERxm/prIdo+QlK4O/SDdswJWfjCaDlk03PzjDzHXeEWRwDFV9zpRx/hjLUn8knwBOSbUE8ImUGt37ZBMBsz++y+oCZJVxzwZgcXZ3L4yuhJ0l48Cz+2EEoYN08gJqNk1EzmpBcAYJJ0Ai1psFqu12b32fNIhWSSf5THldqNefdpBlMo5ZtC9wE//NTml+nebA1FJDSppTMfHP/rTb/wfNx5vc5KLYYR9wZUUUDeJhdNPlBZZuuVbn29X6a4teoBk= + - certificate: MIIDATCCAemgAwIBAgIUK2Sa8/xr/4H9BOB0K0SswbVmmMcwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDIwHhcNMTkxMDE0MTk0ODM1WhcNMjAxMDEzMTk0ODM1WjAQMQ4wDAYDVQQDDAV0ZXN0MjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANQdGqRUTNCptUsZxVe03YTYyhg6+ovidYP9bPPsCS1FzOTCKV9maFv4GHGEI455lkGowW3okT2DPC5pgtH43vZkDtjXNy1JDyg1y4OmxfiAg4Uc0W9DnakvEokVyt44WRzXjlv2CBO+A7Zon8z3aJwKKCV4EfpuvRw/npqhrnGF/w/n7NXyeRXI6lom9hqIQzJoRjXrMIEbtzM8m2GWlFq8af1KJ+Cmm25c87aeyu7I0+BRCq21pwcyQ6Cx0Lo3szQVlD9ZN6wUfz3IDacLjoMOZkVrclIKO0DU595AVo86TD0C1TC/vCjmDGOfgoQsfS5OFfP+FmN6we9IVIAUcL8CAwEAAaNTMFEwHQYDVR0OBBYEFHTGQuoyDwtbHVLU0SseGvarrJKOMB8GA1UdIwQYMBaAFHTGQuoyDwtbHVLU0SseGvarrJKOMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAEGcA6GzCv67GYWoFK9zAjN01M79BDSuac2QV+9PYjQ7tLXQPGdoXJjJzrkJLgzTwqH2iIUqGoAZp30dW89yrJuA8s0gk3SbPOowYmst6pN6SeENxXL/1kn/IBm3+oHf5IWf7vaW4j8tYt7Q3x2K3V9GWRrozGEPIXk8yMeJq72wpfWynDxxYOepGG2+pSkm8soi9Fpt29pb+DtKB2U9GpMBS8vHU+1H4trIvEOMsd4v+X1+Vxlnt8tgy8/PrlKk1wLB9r1XA+W/vXPBe0tRcuntsXiniSKGC5oiR9AFS134HFEWuhxXWihNFzsLmNimvvxQBlXMRPZC1waCSoTKTAI= + - certificate: MIIDATCCAemgAwIBAgIUIa8TtXJ4Nq8VYrlgbSKcVt0FdckwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDMwHhcNMTkxMDE0MTk0ODQ1WhcNMjAxMDEzMTk0ODQ1WjAQMQ4wDAYDVQQDDAV0ZXN0MzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMyttsxCiNH8qx83FTkQaUKxQXer6aMa5DpyfnGMh1K3icfEHyiKLkrjiuFbnfYC1s/iYkz9UyBQrneglYROGgr8TSFC5LXiVppI3/LONKuBh0GfmBM8dhYkOg6WmEB15EL7Hj3V6Xi/Sx3WFnvY/wAzds06linDp/I46jRXqMrWFuhXbf4A09OXpQs6KOMWkkitw3lKuKLgiNzXEiAHaS2YqW7UwSy9RLCrIDMwEmVzZ/gh3vGwM2jyfhOZ75U/xOt9U6jMQsp7hFQAoSeVBTuGpjI40g3IbNBRNi2SnERFzQFkJr0tzAX250XGDaRwRuLhilYoQoJl59B2cTzYsLECAwEAAaNTMFEwHQYDVR0OBBYEFD8iZgdSshdfyAMNfQNzS74Dl9UVMB8GA1UdIwQYMBaAFD8iZgdSshdfyAMNfQNzS74Dl9UVMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFrip5Hl7dHz3oqXLcoza9rQCiMsXV7Q2gVHJ6W2YZZlQpVOBmNPf0n42OVZrnIkDnuGD+pGuw65Aq/TnfMI2KrW8o//aOXLitR60moMEbj61IlBx3aUGvdhyevrz4tM8SHX05p4K86ZJ9jZuS/sNyDwBKBaBAqjW1Rjuqb2o+C9zvDgPWFX++8OgXljDPHR6XYAKpRBmbBZHxYXazjhj1gGhb9/txqDn4EniPXE4rZ/X5MwXzAPs7ROgvr7fkIQypO+O++FGn8rWQL+5zZY0GavOK6FMBJj8M6RDHwEEsXQqSHicsrL8iMx4jun82wgu+gO5lBPTMd0hjsOYR3eT0A= + action: member + state: absent + register: result + failed_when: not result.changed + + - name: User test certmapdata members absent again + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certmapdata: + - certificate: MIIDATCCAemgAwIBAgIUFDZuUg9kBvN+ubTBaS6d62KafvQwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDEwHhcNMTkxMDE0MTk0ODM4WhcNMjAxMDEzMTk0ODM4WjAQMQ4wDAYDVQQDDAV0ZXN0MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALrwv6KuOIZMxjp9aueRgJWKns7P7Oo8lo4ojBVhyB3K11+1OifLqfo2SL9HPx9XynvouMj+XnqbY+NRLOX9wPYAvA7rXUgN1zQYK5stCCTjW0V4QloHnMUPM0shIuErcTP1UMatzTaFxd+UkvZCMNbE9jXHeCd3uDYoqA8Y4yRuqQ4HcnbB7DyMNZHaSCHxQCvZzllJA9m1b+c2sO3l7008PibG5RSpPnrjMUeH4yzjaB8R7A/WApAA1g5bn+IXD2dDsbdHAVFdzqwkxxLlacD8pbM7jWtCI+qJwE6dXpPb5WRkUz3tZLkEW7EQJAi3TMy//hF/SobEul3WVjiO/HUCAwEAAaNTMFEwHQYDVR0OBBYEFCkUPLMp0M5rgxHNAYuunGM0if/vMB8GA1UdIwQYMBaAFCkUPLMp0M5rgxHNAYuunGM0if/vMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAI7gj1VllrF0eRqPP9xtSqWla+qGMpHUWur1yIoy3R8I7LV3muQpbgxsfOyH2PHj69v+8yvI5einvOXJVgJxcn/ERxm/prIdo+QlK4O/SDdswJWfjCaDlk03PzjDzHXeEWRwDFV9zpRx/hjLUn8knwBOSbUE8ImUGt37ZBMBsz++y+oCZJVxzwZgcXZ3L4yuhJ0l48Cz+2EEoYN08gJqNk1EzmpBcAYJJ0Ai1psFqu12b32fNIhWSSf5THldqNefdpBlMo5ZtC9wE//NTml+nebA1FJDSppTMfHP/rTb/wfNx5vc5KLYYR9wZUUUDeJhdNPlBZZuuVbn29X6a4teoBk= + - certificate: MIIDATCCAemgAwIBAgIUK2Sa8/xr/4H9BOB0K0SswbVmmMcwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDIwHhcNMTkxMDE0MTk0ODM1WhcNMjAxMDEzMTk0ODM1WjAQMQ4wDAYDVQQDDAV0ZXN0MjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANQdGqRUTNCptUsZxVe03YTYyhg6+ovidYP9bPPsCS1FzOTCKV9maFv4GHGEI455lkGowW3okT2DPC5pgtH43vZkDtjXNy1JDyg1y4OmxfiAg4Uc0W9DnakvEokVyt44WRzXjlv2CBO+A7Zon8z3aJwKKCV4EfpuvRw/npqhrnGF/w/n7NXyeRXI6lom9hqIQzJoRjXrMIEbtzM8m2GWlFq8af1KJ+Cmm25c87aeyu7I0+BRCq21pwcyQ6Cx0Lo3szQVlD9ZN6wUfz3IDacLjoMOZkVrclIKO0DU595AVo86TD0C1TC/vCjmDGOfgoQsfS5OFfP+FmN6we9IVIAUcL8CAwEAAaNTMFEwHQYDVR0OBBYEFHTGQuoyDwtbHVLU0SseGvarrJKOMB8GA1UdIwQYMBaAFHTGQuoyDwtbHVLU0SseGvarrJKOMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAEGcA6GzCv67GYWoFK9zAjN01M79BDSuac2QV+9PYjQ7tLXQPGdoXJjJzrkJLgzTwqH2iIUqGoAZp30dW89yrJuA8s0gk3SbPOowYmst6pN6SeENxXL/1kn/IBm3+oHf5IWf7vaW4j8tYt7Q3x2K3V9GWRrozGEPIXk8yMeJq72wpfWynDxxYOepGG2+pSkm8soi9Fpt29pb+DtKB2U9GpMBS8vHU+1H4trIvEOMsd4v+X1+Vxlnt8tgy8/PrlKk1wLB9r1XA+W/vXPBe0tRcuntsXiniSKGC5oiR9AFS134HFEWuhxXWihNFzsLmNimvvxQBlXMRPZC1waCSoTKTAI= + - certificate: MIIDATCCAemgAwIBAgIUIa8TtXJ4Nq8VYrlgbSKcVt0FdckwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDMwHhcNMTkxMDE0MTk0ODQ1WhcNMjAxMDEzMTk0ODQ1WjAQMQ4wDAYDVQQDDAV0ZXN0MzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMyttsxCiNH8qx83FTkQaUKxQXer6aMa5DpyfnGMh1K3icfEHyiKLkrjiuFbnfYC1s/iYkz9UyBQrneglYROGgr8TSFC5LXiVppI3/LONKuBh0GfmBM8dhYkOg6WmEB15EL7Hj3V6Xi/Sx3WFnvY/wAzds06linDp/I46jRXqMrWFuhXbf4A09OXpQs6KOMWkkitw3lKuKLgiNzXEiAHaS2YqW7UwSy9RLCrIDMwEmVzZ/gh3vGwM2jyfhOZ75U/xOt9U6jMQsp7hFQAoSeVBTuGpjI40g3IbNBRNi2SnERFzQFkJr0tzAX250XGDaRwRuLhilYoQoJl59B2cTzYsLECAwEAAaNTMFEwHQYDVR0OBBYEFD8iZgdSshdfyAMNfQNzS74Dl9UVMB8GA1UdIwQYMBaAFD8iZgdSshdfyAMNfQNzS74Dl9UVMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFrip5Hl7dHz3oqXLcoza9rQCiMsXV7Q2gVHJ6W2YZZlQpVOBmNPf0n42OVZrnIkDnuGD+pGuw65Aq/TnfMI2KrW8o//aOXLitR60moMEbj61IlBx3aUGvdhyevrz4tM8SHX05p4K86ZJ9jZuS/sNyDwBKBaBAqjW1Rjuqb2o+C9zvDgPWFX++8OgXljDPHR6XYAKpRBmbBZHxYXazjhj1gGhb9/txqDn4EniPXE4rZ/X5MwXzAPs7ROgvr7fkIQypO+O++FGn8rWQL+5zZY0GavOK6FMBJj8M6RDHwEEsXQqSHicsrL8iMx4jun82wgu+gO5lBPTMd0hjsOYR3eT0A= + action: member + state: absent + register: result + failed_when: result.changed + + - name: User test certmapdata members present + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certmapdata: + - issuer: CN=issuer1 + subject: CN=subject1 + - issuer: CN=issuer2 + subject: CN=subject2 + - issuer: CN=issuer3 + subject: CN=subject3 + action: member + register: result + failed_when: not result.changed + + - name: User test certmapdata members present again + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certmapdata: + - issuer: CN=issuer1 + subject: CN=subject1 + - issuer: CN=issuer2 + subject: CN=subject2 + - issuer: CN=issuer3 + subject: CN=subject3 + action: member + register: result + failed_when: result.changed + + - name: User test certmapdata members absent + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certmapdata: + - issuer: CN=issuer1 + subject: CN=subject1 + - issuer: CN=issuer2 + subject: CN=subject2 + - issuer: CN=issuer3 + subject: CN=subject3 + action: member + state: absent + register: result + failed_when: not result.changed + + - name: User test certmapdata members absent again + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certmapdata: + - issuer: CN=issuer1 + subject: CN=subject1 + - issuer: CN=issuer2 + subject: CN=subject2 + - issuer: CN=issuer3 + subject: CN=subject3 + action: member + state: absent + register: result + failed_when: result.changed + + - name: User test absent + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + state: absent + register: result + failed_when: not result.changed diff --git a/tests/user/certmapdata/test_user_certmapdata_issuer_subject.yml b/tests/user/certmapdata/test_user_certmapdata_issuer_subject.yml new file mode 100644 index 0000000..0309c6a --- /dev/null +++ b/tests/user/certmapdata/test_user_certmapdata_issuer_subject.yml @@ -0,0 +1,91 @@ +--- +- name: Test user certmapdata + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: User test absent + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + state: absent + + - name: User test present + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + first: test + last: test + register: result + failed_when: not result.changed + + - name: User test certmapdata members present + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certmapdata: + - issuer: CN=issuer1 + subject: CN=subject1 + - issuer: CN=issuer2 + subject: CN=subject2 + - issuer: CN=issuer3 + subject: CN=subject3 + action: member + register: result + failed_when: not result.changed + + - name: User test certmapdata members present again + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certmapdata: + - issuer: CN=issuer1 + subject: CN=subject1 + - issuer: CN=issuer2 + subject: CN=subject2 + - issuer: CN=issuer3 + subject: CN=subject3 + action: member + register: result + failed_when: result.changed + + - name: User test certmapdata members absent + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certmapdata: + - issuer: CN=issuer1 + subject: CN=subject1 + - issuer: CN=issuer2 + subject: CN=subject2 + - issuer: CN=issuer3 + subject: CN=subject3 + action: member + state: absent + register: result + failed_when: not result.changed + + - name: User test certmapdata members absent again + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + certmapdata: + - issuer: CN=issuer1 + subject: CN=subject1 + - issuer: CN=issuer2 + subject: CN=subject2 + - issuer: CN=issuer3 + subject: CN=subject3 + action: member + state: absent + register: result + failed_when: result.changed + + - name: User test absent + ipauser: + ipaadmin_password: SomeADMINpassword + name: test + state: absent + register: result + failed_when: not result.changed diff --git a/tests/user/certmapdata/test_users_certmapdata.yml b/tests/user/certmapdata/test_users_certmapdata.yml new file mode 100644 index 0000000..5509fa4 --- /dev/null +++ b/tests/user/certmapdata/test_users_certmapdata.yml @@ -0,0 +1,171 @@ +# +# Generate self-signed certificates using openssl: +# +# openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout private1.key -out cert1.pem -subj '/CN=test1' +# openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout private2.key -out cert2.pem -subj '/CN=test2' +# openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout private3.key -out cert3.pem -subj '/CN=test2' +# +# Convert the certificate do DER for easier handling through CLI +# +# openssl x509 -outform der -in cert1.pem -out cert1.der +# openssl x509 -outform der -in cert2.pem -out cert2.der +# openssl x509 -outform der -in cert3.pem -out cert3.der +# +# Use base64: +# +# base64 cert1.der -w5000 +# base64 cert2.der -w5000 +# base64 cert3.der -w5000 +# +--- +- name: Test user certmapdata + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: User test absent + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + state: absent + + - name: User test present + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + first: test + last: test + register: result + failed_when: not result.changed + + - name: User test certmapdata members present + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + certmapdata: + - certificate: MIIDATCCAemgAwIBAgIUFDZuUg9kBvN+ubTBaS6d62KafvQwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDEwHhcNMTkxMDE0MTk0ODM4WhcNMjAxMDEzMTk0ODM4WjAQMQ4wDAYDVQQDDAV0ZXN0MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALrwv6KuOIZMxjp9aueRgJWKns7P7Oo8lo4ojBVhyB3K11+1OifLqfo2SL9HPx9XynvouMj+XnqbY+NRLOX9wPYAvA7rXUgN1zQYK5stCCTjW0V4QloHnMUPM0shIuErcTP1UMatzTaFxd+UkvZCMNbE9jXHeCd3uDYoqA8Y4yRuqQ4HcnbB7DyMNZHaSCHxQCvZzllJA9m1b+c2sO3l7008PibG5RSpPnrjMUeH4yzjaB8R7A/WApAA1g5bn+IXD2dDsbdHAVFdzqwkxxLlacD8pbM7jWtCI+qJwE6dXpPb5WRkUz3tZLkEW7EQJAi3TMy//hF/SobEul3WVjiO/HUCAwEAAaNTMFEwHQYDVR0OBBYEFCkUPLMp0M5rgxHNAYuunGM0if/vMB8GA1UdIwQYMBaAFCkUPLMp0M5rgxHNAYuunGM0if/vMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAI7gj1VllrF0eRqPP9xtSqWla+qGMpHUWur1yIoy3R8I7LV3muQpbgxsfOyH2PHj69v+8yvI5einvOXJVgJxcn/ERxm/prIdo+QlK4O/SDdswJWfjCaDlk03PzjDzHXeEWRwDFV9zpRx/hjLUn8knwBOSbUE8ImUGt37ZBMBsz++y+oCZJVxzwZgcXZ3L4yuhJ0l48Cz+2EEoYN08gJqNk1EzmpBcAYJJ0Ai1psFqu12b32fNIhWSSf5THldqNefdpBlMo5ZtC9wE//NTml+nebA1FJDSppTMfHP/rTb/wfNx5vc5KLYYR9wZUUUDeJhdNPlBZZuuVbn29X6a4teoBk= + - certificate: MIIDATCCAemgAwIBAgIUK2Sa8/xr/4H9BOB0K0SswbVmmMcwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDIwHhcNMTkxMDE0MTk0ODM1WhcNMjAxMDEzMTk0ODM1WjAQMQ4wDAYDVQQDDAV0ZXN0MjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANQdGqRUTNCptUsZxVe03YTYyhg6+ovidYP9bPPsCS1FzOTCKV9maFv4GHGEI455lkGowW3okT2DPC5pgtH43vZkDtjXNy1JDyg1y4OmxfiAg4Uc0W9DnakvEokVyt44WRzXjlv2CBO+A7Zon8z3aJwKKCV4EfpuvRw/npqhrnGF/w/n7NXyeRXI6lom9hqIQzJoRjXrMIEbtzM8m2GWlFq8af1KJ+Cmm25c87aeyu7I0+BRCq21pwcyQ6Cx0Lo3szQVlD9ZN6wUfz3IDacLjoMOZkVrclIKO0DU595AVo86TD0C1TC/vCjmDGOfgoQsfS5OFfP+FmN6we9IVIAUcL8CAwEAAaNTMFEwHQYDVR0OBBYEFHTGQuoyDwtbHVLU0SseGvarrJKOMB8GA1UdIwQYMBaAFHTGQuoyDwtbHVLU0SseGvarrJKOMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAEGcA6GzCv67GYWoFK9zAjN01M79BDSuac2QV+9PYjQ7tLXQPGdoXJjJzrkJLgzTwqH2iIUqGoAZp30dW89yrJuA8s0gk3SbPOowYmst6pN6SeENxXL/1kn/IBm3+oHf5IWf7vaW4j8tYt7Q3x2K3V9GWRrozGEPIXk8yMeJq72wpfWynDxxYOepGG2+pSkm8soi9Fpt29pb+DtKB2U9GpMBS8vHU+1H4trIvEOMsd4v+X1+Vxlnt8tgy8/PrlKk1wLB9r1XA+W/vXPBe0tRcuntsXiniSKGC5oiR9AFS134HFEWuhxXWihNFzsLmNimvvxQBlXMRPZC1waCSoTKTAI= + - certificate: MIIDATCCAemgAwIBAgIUIa8TtXJ4Nq8VYrlgbSKcVt0FdckwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDMwHhcNMTkxMDE0MTk0ODQ1WhcNMjAxMDEzMTk0ODQ1WjAQMQ4wDAYDVQQDDAV0ZXN0MzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMyttsxCiNH8qx83FTkQaUKxQXer6aMa5DpyfnGMh1K3icfEHyiKLkrjiuFbnfYC1s/iYkz9UyBQrneglYROGgr8TSFC5LXiVppI3/LONKuBh0GfmBM8dhYkOg6WmEB15EL7Hj3V6Xi/Sx3WFnvY/wAzds06linDp/I46jRXqMrWFuhXbf4A09OXpQs6KOMWkkitw3lKuKLgiNzXEiAHaS2YqW7UwSy9RLCrIDMwEmVzZ/gh3vGwM2jyfhOZ75U/xOt9U6jMQsp7hFQAoSeVBTuGpjI40g3IbNBRNi2SnERFzQFkJr0tzAX250XGDaRwRuLhilYoQoJl59B2cTzYsLECAwEAAaNTMFEwHQYDVR0OBBYEFD8iZgdSshdfyAMNfQNzS74Dl9UVMB8GA1UdIwQYMBaAFD8iZgdSshdfyAMNfQNzS74Dl9UVMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFrip5Hl7dHz3oqXLcoza9rQCiMsXV7Q2gVHJ6W2YZZlQpVOBmNPf0n42OVZrnIkDnuGD+pGuw65Aq/TnfMI2KrW8o//aOXLitR60moMEbj61IlBx3aUGvdhyevrz4tM8SHX05p4K86ZJ9jZuS/sNyDwBKBaBAqjW1Rjuqb2o+C9zvDgPWFX++8OgXljDPHR6XYAKpRBmbBZHxYXazjhj1gGhb9/txqDn4EniPXE4rZ/X5MwXzAPs7ROgvr7fkIQypO+O++FGn8rWQL+5zZY0GavOK6FMBJj8M6RDHwEEsXQqSHicsrL8iMx4jun82wgu+gO5lBPTMd0hjsOYR3eT0A= + action: member + register: result + failed_when: not result.changed + + - name: User test certmapdata members present again + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + certmapdata: + - certificate: MIIDATCCAemgAwIBAgIUFDZuUg9kBvN+ubTBaS6d62KafvQwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDEwHhcNMTkxMDE0MTk0ODM4WhcNMjAxMDEzMTk0ODM4WjAQMQ4wDAYDVQQDDAV0ZXN0MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALrwv6KuOIZMxjp9aueRgJWKns7P7Oo8lo4ojBVhyB3K11+1OifLqfo2SL9HPx9XynvouMj+XnqbY+NRLOX9wPYAvA7rXUgN1zQYK5stCCTjW0V4QloHnMUPM0shIuErcTP1UMatzTaFxd+UkvZCMNbE9jXHeCd3uDYoqA8Y4yRuqQ4HcnbB7DyMNZHaSCHxQCvZzllJA9m1b+c2sO3l7008PibG5RSpPnrjMUeH4yzjaB8R7A/WApAA1g5bn+IXD2dDsbdHAVFdzqwkxxLlacD8pbM7jWtCI+qJwE6dXpPb5WRkUz3tZLkEW7EQJAi3TMy//hF/SobEul3WVjiO/HUCAwEAAaNTMFEwHQYDVR0OBBYEFCkUPLMp0M5rgxHNAYuunGM0if/vMB8GA1UdIwQYMBaAFCkUPLMp0M5rgxHNAYuunGM0if/vMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAI7gj1VllrF0eRqPP9xtSqWla+qGMpHUWur1yIoy3R8I7LV3muQpbgxsfOyH2PHj69v+8yvI5einvOXJVgJxcn/ERxm/prIdo+QlK4O/SDdswJWfjCaDlk03PzjDzHXeEWRwDFV9zpRx/hjLUn8knwBOSbUE8ImUGt37ZBMBsz++y+oCZJVxzwZgcXZ3L4yuhJ0l48Cz+2EEoYN08gJqNk1EzmpBcAYJJ0Ai1psFqu12b32fNIhWSSf5THldqNefdpBlMo5ZtC9wE//NTml+nebA1FJDSppTMfHP/rTb/wfNx5vc5KLYYR9wZUUUDeJhdNPlBZZuuVbn29X6a4teoBk= + - certificate: MIIDATCCAemgAwIBAgIUK2Sa8/xr/4H9BOB0K0SswbVmmMcwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDIwHhcNMTkxMDE0MTk0ODM1WhcNMjAxMDEzMTk0ODM1WjAQMQ4wDAYDVQQDDAV0ZXN0MjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANQdGqRUTNCptUsZxVe03YTYyhg6+ovidYP9bPPsCS1FzOTCKV9maFv4GHGEI455lkGowW3okT2DPC5pgtH43vZkDtjXNy1JDyg1y4OmxfiAg4Uc0W9DnakvEokVyt44WRzXjlv2CBO+A7Zon8z3aJwKKCV4EfpuvRw/npqhrnGF/w/n7NXyeRXI6lom9hqIQzJoRjXrMIEbtzM8m2GWlFq8af1KJ+Cmm25c87aeyu7I0+BRCq21pwcyQ6Cx0Lo3szQVlD9ZN6wUfz3IDacLjoMOZkVrclIKO0DU595AVo86TD0C1TC/vCjmDGOfgoQsfS5OFfP+FmN6we9IVIAUcL8CAwEAAaNTMFEwHQYDVR0OBBYEFHTGQuoyDwtbHVLU0SseGvarrJKOMB8GA1UdIwQYMBaAFHTGQuoyDwtbHVLU0SseGvarrJKOMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAEGcA6GzCv67GYWoFK9zAjN01M79BDSuac2QV+9PYjQ7tLXQPGdoXJjJzrkJLgzTwqH2iIUqGoAZp30dW89yrJuA8s0gk3SbPOowYmst6pN6SeENxXL/1kn/IBm3+oHf5IWf7vaW4j8tYt7Q3x2K3V9GWRrozGEPIXk8yMeJq72wpfWynDxxYOepGG2+pSkm8soi9Fpt29pb+DtKB2U9GpMBS8vHU+1H4trIvEOMsd4v+X1+Vxlnt8tgy8/PrlKk1wLB9r1XA+W/vXPBe0tRcuntsXiniSKGC5oiR9AFS134HFEWuhxXWihNFzsLmNimvvxQBlXMRPZC1waCSoTKTAI= + - certificate: MIIDATCCAemgAwIBAgIUIa8TtXJ4Nq8VYrlgbSKcVt0FdckwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDMwHhcNMTkxMDE0MTk0ODQ1WhcNMjAxMDEzMTk0ODQ1WjAQMQ4wDAYDVQQDDAV0ZXN0MzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMyttsxCiNH8qx83FTkQaUKxQXer6aMa5DpyfnGMh1K3icfEHyiKLkrjiuFbnfYC1s/iYkz9UyBQrneglYROGgr8TSFC5LXiVppI3/LONKuBh0GfmBM8dhYkOg6WmEB15EL7Hj3V6Xi/Sx3WFnvY/wAzds06linDp/I46jRXqMrWFuhXbf4A09OXpQs6KOMWkkitw3lKuKLgiNzXEiAHaS2YqW7UwSy9RLCrIDMwEmVzZ/gh3vGwM2jyfhOZ75U/xOt9U6jMQsp7hFQAoSeVBTuGpjI40g3IbNBRNi2SnERFzQFkJr0tzAX250XGDaRwRuLhilYoQoJl59B2cTzYsLECAwEAAaNTMFEwHQYDVR0OBBYEFD8iZgdSshdfyAMNfQNzS74Dl9UVMB8GA1UdIwQYMBaAFD8iZgdSshdfyAMNfQNzS74Dl9UVMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFrip5Hl7dHz3oqXLcoza9rQCiMsXV7Q2gVHJ6W2YZZlQpVOBmNPf0n42OVZrnIkDnuGD+pGuw65Aq/TnfMI2KrW8o//aOXLitR60moMEbj61IlBx3aUGvdhyevrz4tM8SHX05p4K86ZJ9jZuS/sNyDwBKBaBAqjW1Rjuqb2o+C9zvDgPWFX++8OgXljDPHR6XYAKpRBmbBZHxYXazjhj1gGhb9/txqDn4EniPXE4rZ/X5MwXzAPs7ROgvr7fkIQypO+O++FGn8rWQL+5zZY0GavOK6FMBJj8M6RDHwEEsXQqSHicsrL8iMx4jun82wgu+gO5lBPTMd0hjsOYR3eT0A= + action: member + register: result + failed_when: result.changed + + - name: User test certmapdata members absent + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + certmapdata: + - certificate: MIIDATCCAemgAwIBAgIUFDZuUg9kBvN+ubTBaS6d62KafvQwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDEwHhcNMTkxMDE0MTk0ODM4WhcNMjAxMDEzMTk0ODM4WjAQMQ4wDAYDVQQDDAV0ZXN0MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALrwv6KuOIZMxjp9aueRgJWKns7P7Oo8lo4ojBVhyB3K11+1OifLqfo2SL9HPx9XynvouMj+XnqbY+NRLOX9wPYAvA7rXUgN1zQYK5stCCTjW0V4QloHnMUPM0shIuErcTP1UMatzTaFxd+UkvZCMNbE9jXHeCd3uDYoqA8Y4yRuqQ4HcnbB7DyMNZHaSCHxQCvZzllJA9m1b+c2sO3l7008PibG5RSpPnrjMUeH4yzjaB8R7A/WApAA1g5bn+IXD2dDsbdHAVFdzqwkxxLlacD8pbM7jWtCI+qJwE6dXpPb5WRkUz3tZLkEW7EQJAi3TMy//hF/SobEul3WVjiO/HUCAwEAAaNTMFEwHQYDVR0OBBYEFCkUPLMp0M5rgxHNAYuunGM0if/vMB8GA1UdIwQYMBaAFCkUPLMp0M5rgxHNAYuunGM0if/vMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAI7gj1VllrF0eRqPP9xtSqWla+qGMpHUWur1yIoy3R8I7LV3muQpbgxsfOyH2PHj69v+8yvI5einvOXJVgJxcn/ERxm/prIdo+QlK4O/SDdswJWfjCaDlk03PzjDzHXeEWRwDFV9zpRx/hjLUn8knwBOSbUE8ImUGt37ZBMBsz++y+oCZJVxzwZgcXZ3L4yuhJ0l48Cz+2EEoYN08gJqNk1EzmpBcAYJJ0Ai1psFqu12b32fNIhWSSf5THldqNefdpBlMo5ZtC9wE//NTml+nebA1FJDSppTMfHP/rTb/wfNx5vc5KLYYR9wZUUUDeJhdNPlBZZuuVbn29X6a4teoBk= + - certificate: MIIDATCCAemgAwIBAgIUK2Sa8/xr/4H9BOB0K0SswbVmmMcwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDIwHhcNMTkxMDE0MTk0ODM1WhcNMjAxMDEzMTk0ODM1WjAQMQ4wDAYDVQQDDAV0ZXN0MjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANQdGqRUTNCptUsZxVe03YTYyhg6+ovidYP9bPPsCS1FzOTCKV9maFv4GHGEI455lkGowW3okT2DPC5pgtH43vZkDtjXNy1JDyg1y4OmxfiAg4Uc0W9DnakvEokVyt44WRzXjlv2CBO+A7Zon8z3aJwKKCV4EfpuvRw/npqhrnGF/w/n7NXyeRXI6lom9hqIQzJoRjXrMIEbtzM8m2GWlFq8af1KJ+Cmm25c87aeyu7I0+BRCq21pwcyQ6Cx0Lo3szQVlD9ZN6wUfz3IDacLjoMOZkVrclIKO0DU595AVo86TD0C1TC/vCjmDGOfgoQsfS5OFfP+FmN6we9IVIAUcL8CAwEAAaNTMFEwHQYDVR0OBBYEFHTGQuoyDwtbHVLU0SseGvarrJKOMB8GA1UdIwQYMBaAFHTGQuoyDwtbHVLU0SseGvarrJKOMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAEGcA6GzCv67GYWoFK9zAjN01M79BDSuac2QV+9PYjQ7tLXQPGdoXJjJzrkJLgzTwqH2iIUqGoAZp30dW89yrJuA8s0gk3SbPOowYmst6pN6SeENxXL/1kn/IBm3+oHf5IWf7vaW4j8tYt7Q3x2K3V9GWRrozGEPIXk8yMeJq72wpfWynDxxYOepGG2+pSkm8soi9Fpt29pb+DtKB2U9GpMBS8vHU+1H4trIvEOMsd4v+X1+Vxlnt8tgy8/PrlKk1wLB9r1XA+W/vXPBe0tRcuntsXiniSKGC5oiR9AFS134HFEWuhxXWihNFzsLmNimvvxQBlXMRPZC1waCSoTKTAI= + - certificate: MIIDATCCAemgAwIBAgIUIa8TtXJ4Nq8VYrlgbSKcVt0FdckwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDMwHhcNMTkxMDE0MTk0ODQ1WhcNMjAxMDEzMTk0ODQ1WjAQMQ4wDAYDVQQDDAV0ZXN0MzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMyttsxCiNH8qx83FTkQaUKxQXer6aMa5DpyfnGMh1K3icfEHyiKLkrjiuFbnfYC1s/iYkz9UyBQrneglYROGgr8TSFC5LXiVppI3/LONKuBh0GfmBM8dhYkOg6WmEB15EL7Hj3V6Xi/Sx3WFnvY/wAzds06linDp/I46jRXqMrWFuhXbf4A09OXpQs6KOMWkkitw3lKuKLgiNzXEiAHaS2YqW7UwSy9RLCrIDMwEmVzZ/gh3vGwM2jyfhOZ75U/xOt9U6jMQsp7hFQAoSeVBTuGpjI40g3IbNBRNi2SnERFzQFkJr0tzAX250XGDaRwRuLhilYoQoJl59B2cTzYsLECAwEAAaNTMFEwHQYDVR0OBBYEFD8iZgdSshdfyAMNfQNzS74Dl9UVMB8GA1UdIwQYMBaAFD8iZgdSshdfyAMNfQNzS74Dl9UVMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFrip5Hl7dHz3oqXLcoza9rQCiMsXV7Q2gVHJ6W2YZZlQpVOBmNPf0n42OVZrnIkDnuGD+pGuw65Aq/TnfMI2KrW8o//aOXLitR60moMEbj61IlBx3aUGvdhyevrz4tM8SHX05p4K86ZJ9jZuS/sNyDwBKBaBAqjW1Rjuqb2o+C9zvDgPWFX++8OgXljDPHR6XYAKpRBmbBZHxYXazjhj1gGhb9/txqDn4EniPXE4rZ/X5MwXzAPs7ROgvr7fkIQypO+O++FGn8rWQL+5zZY0GavOK6FMBJj8M6RDHwEEsXQqSHicsrL8iMx4jun82wgu+gO5lBPTMd0hjsOYR3eT0A= + action: member + state: absent + register: result + failed_when: not result.changed + + - name: User test certmapdata members absent again + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + certmapdata: + - certificate: MIIDATCCAemgAwIBAgIUFDZuUg9kBvN+ubTBaS6d62KafvQwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDEwHhcNMTkxMDE0MTk0ODM4WhcNMjAxMDEzMTk0ODM4WjAQMQ4wDAYDVQQDDAV0ZXN0MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALrwv6KuOIZMxjp9aueRgJWKns7P7Oo8lo4ojBVhyB3K11+1OifLqfo2SL9HPx9XynvouMj+XnqbY+NRLOX9wPYAvA7rXUgN1zQYK5stCCTjW0V4QloHnMUPM0shIuErcTP1UMatzTaFxd+UkvZCMNbE9jXHeCd3uDYoqA8Y4yRuqQ4HcnbB7DyMNZHaSCHxQCvZzllJA9m1b+c2sO3l7008PibG5RSpPnrjMUeH4yzjaB8R7A/WApAA1g5bn+IXD2dDsbdHAVFdzqwkxxLlacD8pbM7jWtCI+qJwE6dXpPb5WRkUz3tZLkEW7EQJAi3TMy//hF/SobEul3WVjiO/HUCAwEAAaNTMFEwHQYDVR0OBBYEFCkUPLMp0M5rgxHNAYuunGM0if/vMB8GA1UdIwQYMBaAFCkUPLMp0M5rgxHNAYuunGM0if/vMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAI7gj1VllrF0eRqPP9xtSqWla+qGMpHUWur1yIoy3R8I7LV3muQpbgxsfOyH2PHj69v+8yvI5einvOXJVgJxcn/ERxm/prIdo+QlK4O/SDdswJWfjCaDlk03PzjDzHXeEWRwDFV9zpRx/hjLUn8knwBOSbUE8ImUGt37ZBMBsz++y+oCZJVxzwZgcXZ3L4yuhJ0l48Cz+2EEoYN08gJqNk1EzmpBcAYJJ0Ai1psFqu12b32fNIhWSSf5THldqNefdpBlMo5ZtC9wE//NTml+nebA1FJDSppTMfHP/rTb/wfNx5vc5KLYYR9wZUUUDeJhdNPlBZZuuVbn29X6a4teoBk= + - certificate: MIIDATCCAemgAwIBAgIUK2Sa8/xr/4H9BOB0K0SswbVmmMcwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDIwHhcNMTkxMDE0MTk0ODM1WhcNMjAxMDEzMTk0ODM1WjAQMQ4wDAYDVQQDDAV0ZXN0MjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANQdGqRUTNCptUsZxVe03YTYyhg6+ovidYP9bPPsCS1FzOTCKV9maFv4GHGEI455lkGowW3okT2DPC5pgtH43vZkDtjXNy1JDyg1y4OmxfiAg4Uc0W9DnakvEokVyt44WRzXjlv2CBO+A7Zon8z3aJwKKCV4EfpuvRw/npqhrnGF/w/n7NXyeRXI6lom9hqIQzJoRjXrMIEbtzM8m2GWlFq8af1KJ+Cmm25c87aeyu7I0+BRCq21pwcyQ6Cx0Lo3szQVlD9ZN6wUfz3IDacLjoMOZkVrclIKO0DU595AVo86TD0C1TC/vCjmDGOfgoQsfS5OFfP+FmN6we9IVIAUcL8CAwEAAaNTMFEwHQYDVR0OBBYEFHTGQuoyDwtbHVLU0SseGvarrJKOMB8GA1UdIwQYMBaAFHTGQuoyDwtbHVLU0SseGvarrJKOMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAEGcA6GzCv67GYWoFK9zAjN01M79BDSuac2QV+9PYjQ7tLXQPGdoXJjJzrkJLgzTwqH2iIUqGoAZp30dW89yrJuA8s0gk3SbPOowYmst6pN6SeENxXL/1kn/IBm3+oHf5IWf7vaW4j8tYt7Q3x2K3V9GWRrozGEPIXk8yMeJq72wpfWynDxxYOepGG2+pSkm8soi9Fpt29pb+DtKB2U9GpMBS8vHU+1H4trIvEOMsd4v+X1+Vxlnt8tgy8/PrlKk1wLB9r1XA+W/vXPBe0tRcuntsXiniSKGC5oiR9AFS134HFEWuhxXWihNFzsLmNimvvxQBlXMRPZC1waCSoTKTAI= + - certificate: MIIDATCCAemgAwIBAgIUIa8TtXJ4Nq8VYrlgbSKcVt0FdckwDQYJKoZIhvcNAQELBQAwEDEOMAwGA1UEAwwFdGVzdDMwHhcNMTkxMDE0MTk0ODQ1WhcNMjAxMDEzMTk0ODQ1WjAQMQ4wDAYDVQQDDAV0ZXN0MzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMyttsxCiNH8qx83FTkQaUKxQXer6aMa5DpyfnGMh1K3icfEHyiKLkrjiuFbnfYC1s/iYkz9UyBQrneglYROGgr8TSFC5LXiVppI3/LONKuBh0GfmBM8dhYkOg6WmEB15EL7Hj3V6Xi/Sx3WFnvY/wAzds06linDp/I46jRXqMrWFuhXbf4A09OXpQs6KOMWkkitw3lKuKLgiNzXEiAHaS2YqW7UwSy9RLCrIDMwEmVzZ/gh3vGwM2jyfhOZ75U/xOt9U6jMQsp7hFQAoSeVBTuGpjI40g3IbNBRNi2SnERFzQFkJr0tzAX250XGDaRwRuLhilYoQoJl59B2cTzYsLECAwEAAaNTMFEwHQYDVR0OBBYEFD8iZgdSshdfyAMNfQNzS74Dl9UVMB8GA1UdIwQYMBaAFD8iZgdSshdfyAMNfQNzS74Dl9UVMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFrip5Hl7dHz3oqXLcoza9rQCiMsXV7Q2gVHJ6W2YZZlQpVOBmNPf0n42OVZrnIkDnuGD+pGuw65Aq/TnfMI2KrW8o//aOXLitR60moMEbj61IlBx3aUGvdhyevrz4tM8SHX05p4K86ZJ9jZuS/sNyDwBKBaBAqjW1Rjuqb2o+C9zvDgPWFX++8OgXljDPHR6XYAKpRBmbBZHxYXazjhj1gGhb9/txqDn4EniPXE4rZ/X5MwXzAPs7ROgvr7fkIQypO+O++FGn8rWQL+5zZY0GavOK6FMBJj8M6RDHwEEsXQqSHicsrL8iMx4jun82wgu+gO5lBPTMd0hjsOYR3eT0A= + action: member + state: absent + register: result + failed_when: result.changed + + - name: User test certmapdata members present + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + certmapdata: + - issuer: CN=issuer1 + subject: CN=subject1 + - issuer: CN=issuer2 + subject: CN=subject2 + - issuer: CN=issuer3 + subject: CN=subject3 + action: member + register: result + failed_when: not result.changed + + - name: User test certmapdata members present again + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + certmapdata: + - issuer: CN=issuer1 + subject: CN=subject1 + - issuer: CN=issuer2 + subject: CN=subject2 + - issuer: CN=issuer3 + subject: CN=subject3 + action: member + register: result + failed_when: result.changed + + - name: User test certmapdata members absent + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + certmapdata: + - issuer: CN=issuer1 + subject: CN=subject1 + - issuer: CN=issuer2 + subject: CN=subject2 + - issuer: CN=issuer3 + subject: CN=subject3 + action: member + state: absent + register: result + failed_when: not result.changed + + - name: User test certmapdata members absent again + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + certmapdata: + - issuer: CN=issuer1 + subject: CN=subject1 + - issuer: CN=issuer2 + subject: CN=subject2 + - issuer: CN=issuer3 + subject: CN=subject3 + action: member + state: absent + register: result + failed_when: result.changed + + - name: User test absent + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + state: absent + register: result + failed_when: not result.changed diff --git a/tests/user/test_user.yml b/tests/user/test_user.yml new file mode 100644 index 0000000..c172049 --- /dev/null +++ b/tests/user/test_user.yml @@ -0,0 +1,258 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Remove test users + ipauser: + ipaadmin_password: SomeADMINpassword + name: manager1,manager2,manager3,pinky,pinky2 + state: absent + + - name: User manager1 present + ipauser: + ipaadmin_password: SomeADMINpassword + name: manager1 + first: Manager + last: One + register: result + failed_when: not result.changed + + - name: User manager2 present + ipauser: + ipaadmin_password: SomeADMINpassword + name: manager2 + first: Manager + last: One + register: result + failed_when: not result.changed + + - name: User manager3 present + ipauser: + ipaadmin_password: SomeADMINpassword + name: manager3 + first: Manager + last: One + register: result + failed_when: not result.changed + + - name: User pinky present + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + uid: 10001 + gid: 100 + phone: "+555123457" + email: pinky@acme.com + principalexpiration: "20220119235959" + #passwordexpiration: "2022-01-19 23:59:59" + first: pinky + last: Acme + initials: pa + #password: foo2 + principal: pa + random: yes + city: PinkyCity + userstate: PinkyState + postalcode: PinkyZip + mobile: "+555123458,+555123459" + pager: "+555123450,+555123451" + fax: "+555123452,+555123453" + orgunit: PinkyOrgUnit + manager: manager1,manager2 + update_password: on_create + carlicense: PinkyCarLicense1,PinkyCarLicense2 + # sshpubkey + userauthtype: password,radius,otp + userclass: PinkyUserClass + #radius: "http://some.link/" + #radiususer: PinkyRadiusUser + departmentnumber: "1234" + employeenumber: "0815" + employeetype: "PinkyExmployeeType" + preferredlanguage: "en" + # certificate + noprivate: yes + nomembers: false + #issuer: PinkyIssuer + #subject: PinkySubject + register: result + failed_when: not result.changed + + - name: User pinky present with changed settings + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + first: pinky + last: Acme + #manager: manager1,manager2,manager3 + #principal: pa,pa1,pa3 + sshpubkey: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCqmVDpEX5gnSjKuv97AyzOhaUMMKz8ahOA3GY77tVC4o68KNgMCmDSEG1/kOIaElngNLaCha3p/2iAcU9Bi1tLKUlm2bbO5NHNwHfRxY/3cJtq+/7D1vxJzqThYwI4F9vr1WxyY2+mMTv3pXbfAJoR8Mu06XaEY5PDetlDKjHLuNWF+/O7ZU8PsULTa1dJZFrtXeFpmUoLoGxQBvlrlcPI1zDciCSU24t27Zan5Py2l5QchyI7yhCyMM77KDtj5+AFVpmkb9+zq50rYJAyFVeyUvwjzErvQrKJzYpA0NyBp7vskWbt36M16/M/LxEK7HA6mkcakO3ESWx5MT1LAjvdlnxbWG3787MxweHXuB8CZU+9bZPFBaJ+VQtOfJ7I8eH0S16moPC4ak8FlcFvOH8ERDPWLFDqfy09yaZ7bVIF0//5ZI7Nf3YDe3S7GrBX5ieYuECyP6UNkTx9BRsAQeVvXEc6otzB7iCSnYBMGUGzCqeigoAWaVQUONsSR3Uatks= pinky@ipaserver.el81.local + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDc8MIjaSrxLYHvu+hduoF4m6NUFSlXZWzYbd3BK4L47/U4eiXoOS6dcfuZJDjmLfOipc7XVp7NADwAgA1yBOAjbeVpXr2tC8w8saZibl75WBOEjDfNroiOh/f/ojrwwHg05QTVSZHs27sU1HBPyCQM/FHVM6EnRfmyiBkEBA/3ca0PJ9UJhWb2XisCaz6y6QcTh4gQnvHzgmEmK31GwiKnmBSEQuj8P5NGCO8RlN3cq3zpRpMDEoBRCjQYicllf/5P43r5OGvS1LhTiAMfyqE37URezNQa7aozBpH1GhIwAmjAtm84jXQjxUgZPYC0aSLuADYErScOP4792r6koH9t/DM5/M+jG2c4PNWynDczUw6Eaxl5E3hU0Ee9UN0Oee7iBnVenS/QMeZNyo5lMA/HXT5lrYiJGTYM0shRjGXXYBbJZhWerguSWDAdUd1gvuGP1nb7/+/Cvb46+HX7zYouS5Ojo0yPzMZ07X142jnKAfx9LnKdMUCwBJzbtoJ91Zc= pinky@ipaserver.el81.local + register: result + failed_when: not result.changed + + - name: User pinky add manager manager1 + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + manager: manager1 + action: member + register: result + failed_when: not result.changed + + - name: User pinky add manager manager1 again + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + manager: manager1 + action: member + register: result + failed_when: result.changed + + - name: User pinky add manager manager2, manager3 + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + manager: manager2,manager3 + action: member + register: result + failed_when: not result.changed + + - name: User pinky add manager manager2, manager3 again + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + manager: manager2,manager3 + action: member + register: result + failed_when: result.changed + + - name: User pinky remove manager manager1 + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + manager: manager1 + action: member + state: absent + register: result + failed_when: not result.changed + + - name: User pinky remove manager manager1 again + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + manager: manager1 + action: member + state: absent + register: result + failed_when: result.changed + + - name: User pinky add principal pa + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + principal: pa + action: member + register: result + failed_when: not result.changed + + - name: User pinky add principal pa again + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + principal: pa + action: member + register: result + failed_when: result.changed + + - name: User pinky add principal pa1 + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + principal: pa1 + action: member + register: result + failed_when: not result.changed + + - name: User pinky remove principal pa1 + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + principal: pa1 + action: member + state: absent + register: result + failed_when: not result.changed + + - name: User pinky remove principal pa1 again + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + principal: pa1 + action: member + state: absent + register: result + failed_when: result.changed + + - name: User pinky remove principal pa + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + principal: pa + action: member + state: absent + register: result + failed_when: not result.changed + + - name: User pinky remove principal non-existing pa2 + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + principal: pa2 + action: member + state: absent + register: result + failed_when: result.changed + + - name: User pinky absent and preserved + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + preserve: yes + state: absent + register: result + failed_when: not result.changed + + - name: User pinky undeleted (preserved before) + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + state: undeleted + register: result + failed_when: not result.changed + + - name: Users pinky disabled + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + state: disabled + register: result + failed_when: not result.changed + + - name: User pinky enabled + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + state: enabled + register: result + failed_when: not result.changed + + - name: Remove test users + ipauser: + ipaadmin_password: SomeADMINpassword + name: manager1,manager2,manager3,pinky,pinky2 + state: absent diff --git a/tests/user/test_user_random.yml b/tests/user/test_user_random.yml new file mode 100644 index 0000000..47e4a35 --- /dev/null +++ b/tests/user/test_user_random.yml @@ -0,0 +1,70 @@ +--- +- name: Test ipauser random password generation + hosts: ipaserver + become: true + + tasks: + - name: Users user1 and user2 absent + ipauser: + ipaadmin_password: MyPassword123 + name: + - user1 + - user2 + state: absent + + - name: User user1 present with random password + ipauser: + ipaadmin_password: MyPassword123 + name: user1 + first: first1 + last: last1 + random: yes + update_password: on_create + register: ipauser + failed_when: not ipauser.changed or + ipauser.user.randompassword is not defined + + - name: Print generated random password + debug: + var: ipauser.user.randompassword + + - name: User user1 absent + ipauser: + ipaadmin_password: MyPassword123 + name: + - user1 + state: absent + + - name: Users user1 and user1 present with random password + ipauser: + ipaadmin_password: MyPassword123 + users: + - name: user1 + first: first1 + last: last1 + random: yes + - name: user2 + first: first2 + last: last2 + random: yes + update_password: on_create + register: ipauser + failed_when: not ipauser.changed or + ipauser.user.user1.randompassword is not defined or + ipauser.user.user2.randompassword is not defined + + - name: Print generated random password for user1 + debug: + var: ipauser.user.user1.randompassword + + - name: Print generated random password for user2 + debug: + var: ipauser.user.user2.randompassword + + - name: Users user1 and user2 absent + ipauser: + ipaadmin_password: MyPassword123 + name: + - user1 + - user2 + state: absent diff --git a/tests/user/test_users.yml b/tests/user/test_users.yml new file mode 100644 index 0000000..129594d --- /dev/null +++ b/tests/user/test_users.yml @@ -0,0 +1,376 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Remove test users + ipauser: + ipaadmin_password: SomeADMINpassword + name: user1,user2,user3,user4,user5,user6,user7,user8,user9,user10 + state: absent + + - name: Users user1..10 present + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: user1 + givenname: user1 + last: Last + - name: user2 + first: user2 + last: Last + - name: user3 + first: user3 + last: Last + - name: user4 + first: user4 + last: Last + - name: user5 + first: user5 + last: Last + - name: user6 + first: user6 + last: Last + - name: user7 + first: user7 + last: Last + - name: user8 + first: user8 + last: Last + - name: user9 + first: user9 + last: Last + - name: user10 + first: user10 + last: Last + register: result + failed_when: not result.changed + + - name: Users user1..10 present + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: user1 + givenname: user1 + sn: Last + - name: user2 + first: user2 + last: Last + - name: user3 + first: user3 + last: Last + - name: user4 + first: user4 + last: Last + - name: user5 + first: user5 + last: Last + - name: user6 + first: user6 + last: Last + - name: user7 + first: user7 + last: Last + - name: user8 + first: user8 + last: Last + - name: user9 + first: user9 + last: Last + - name: user10 + first: user10 + last: Last + register: result + failed_when: result.changed + + - name: Remove test users + ipauser: + ipaadmin_password: SomeADMINpassword + name: user1,user2,user3,user4,user5,user6,user7,user8,user9,user10 + state: absent + + - name: Remove test users + ipauser: + ipaadmin_password: SomeADMINpassword + name: manager1,manager2,manager3,pinky,pinky2 + state: absent + + - name: User manager1 present + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: manager1 + first: Manager1 + last: One1 + - name: manager2 + first: Manager2 + last: One2 + - name: manager3 + first: Manager3 + last: One3 + register: result + failed_when: not result.changed + + - name: User pinky present + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + uid: 10001 + gid: 100 + phone: "+555123457" + email: pinky@acme.com + principalexpiration: "20220119235959" + #passwordexpiration: "2022-01-19 23:59:59" + first: pinky + last: Acme + initials: pa + #password: foo2 + principal: pa + random: yes + city: PinkyCity + userstate: PinkyState + postalcode: PinkyZip + mobile: "+555123458,+555123459" + pager: "+555123450,+555123451" + fax: "+555123452,+555123453" + orgunit: PinkyOrgUnit + manager: manager1,manager2 + update_password: on_create + carlicense: PinkyCarLicense1,PinkyCarLicense2 + # sshpubkey + userauthtype: password,radius,otp + userclass: PinkyUserClass + #radius: "http://some.link/" + #radiususer: PinkyRadiusUser + departmentnumber: "1234" + employeenumber: "0815" + employeetype: "PinkyExmployeeType" + preferredlanguage: "en" + # certificate + noprivate: yes + nomembers: false + #issuer: PinkyIssuer + #subject: PinkySubject + register: result + failed_when: not result.changed + + - name: Same user pinky present again + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + uid: 10001 + gid: 100 + phone: "+555123457" + email: pinky@acme.com + principalexpiration: "20220119235959" + #passwordexpiration: "2022-01-19 23:59:59" + first: pinky + last: Acme + initials: pa + #password: foo2 + principal: pa + random: yes + city: PinkyCity + userstate: PinkyState + postalcode: PinkyZip + mobile: "+555123458,+555123459" + pager: "+555123450,+555123451" + fax: "+555123452,+555123453" + orgunit: PinkyOrgUnit + manager: manager1,manager2 + update_password: on_create + carlicense: PinkyCarLicense1,PinkyCarLicense2 + # sshpubkey + userauthtype: password,radius,otp + userclass: PinkyUserClass + #radius: "http://some.link/" + #radiususer: PinkyRadiusUser + departmentnumber: "1234" + employeenumber: "0815" + employeetype: "PinkyExmployeeType" + preferredlanguage: "en" + # certificate + noprivate: yes + nomembers: false + #issuer: PinkyIssuer + #subject: PinkySubject + register: result + failed_when: result.changed + + - name: User pinky present with changed settings + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + first: pinky + last: Acme + #manager: manager1,manager2,manager3 + #principal: pa,pa1,pa3 + sshpubkey: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCqmVDpEX5gnSjKuv97AyzOhaUMMKz8ahOA3GY77tVC4o68KNgMCmDSEG1/kOIaElngNLaCha3p/2iAcU9Bi1tLKUlm2bbO5NHNwHfRxY/3cJtq+/7D1vxJzqThYwI4F9vr1WxyY2+mMTv3pXbfAJoR8Mu06XaEY5PDetlDKjHLuNWF+/O7ZU8PsULTa1dJZFrtXeFpmUoLoGxQBvlrlcPI1zDciCSU24t27Zan5Py2l5QchyI7yhCyMM77KDtj5+AFVpmkb9+zq50rYJAyFVeyUvwjzErvQrKJzYpA0NyBp7vskWbt36M16/M/LxEK7HA6mkcakO3ESWx5MT1LAjvdlnxbWG3787MxweHXuB8CZU+9bZPFBaJ+VQtOfJ7I8eH0S16moPC4ak8FlcFvOH8ERDPWLFDqfy09yaZ7bVIF0//5ZI7Nf3YDe3S7GrBX5ieYuECyP6UNkTx9BRsAQeVvXEc6otzB7iCSnYBMGUGzCqeigoAWaVQUONsSR3Uatks= pinky@ipaserver.el81.local + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDc8MIjaSrxLYHvu+hduoF4m6NUFSlXZWzYbd3BK4L47/U4eiXoOS6dcfuZJDjmLfOipc7XVp7NADwAgA1yBOAjbeVpXr2tC8w8saZibl75WBOEjDfNroiOh/f/ojrwwHg05QTVSZHs27sU1HBPyCQM/FHVM6EnRfmyiBkEBA/3ca0PJ9UJhWb2XisCaz6y6QcTh4gQnvHzgmEmK31GwiKnmBSEQuj8P5NGCO8RlN3cq3zpRpMDEoBRCjQYicllf/5P43r5OGvS1LhTiAMfyqE37URezNQa7aozBpH1GhIwAmjAtm84jXQjxUgZPYC0aSLuADYErScOP4792r6koH9t/DM5/M+jG2c4PNWynDczUw6Eaxl5E3hU0Ee9UN0Oee7iBnVenS/QMeZNyo5lMA/HXT5lrYiJGTYM0shRjGXXYBbJZhWerguSWDAdUd1gvuGP1nb7/+/Cvb46+HX7zYouS5Ojo0yPzMZ07X142jnKAfx9LnKdMUCwBJzbtoJ91Zc= pinky@ipaserver.el81.local + register: result + failed_when: not result.changed + + - name: User pinky add manager manager1 + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + manager: manager1 + action: member + register: result + failed_when: not result.changed + + - name: User pinky add manager manager1 again + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + manager: manager1 + action: member + register: result + failed_when: result.changed + + - name: User pinky add manager manager2, manager3 + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + manager: manager2,manager3 + action: member + register: result + failed_when: not result.changed + + - name: User pinky add manager manager2, manager3 again + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + manager: manager2,manager3 + action: member + register: result + failed_when: result.changed + + - name: User pinky remove manager manager1 + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + manager: manager1 + action: member + state: absent + register: result + failed_when: not result.changed + + - name: User pinky remove manager manager1 again + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + manager: manager1 + action: member + state: absent + register: result + failed_when: result.changed + + - name: User pinky add principal pa + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + principal: pa + action: member + register: result + failed_when: not result.changed + + - name: User pinky add principal pa again + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + principal: pa + action: member + register: result + failed_when: result.changed + + - name: User pinky add principal pa1 + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + principal: pa1 + action: member + register: result + failed_when: not result.changed + + - name: User pinky remove principal pa1 + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + principal: pa1 + action: member + state: absent + register: result + failed_when: not result.changed + + - name: User pinky remove principal pa1 again + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + principal: pa1 + action: member + state: absent + register: result + failed_when: result.changed + + - name: User pinky remove principal pa + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + principal: pa + action: member + state: absent + register: result + failed_when: not result.changed + + - name: User pinky remove principal non-existing pa2 + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + principal: pa2 + action: member + state: absent + register: result + failed_when: result.changed + + - name: User pinky absent and preserved + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + preserve: yes + state: absent + register: result + failed_when: not result.changed + + - name: User pinky undeleted (preserved before) + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + state: undeleted + register: result + failed_when: not result.changed + + - name: Users pinky disabled + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + state: disabled + register: result + failed_when: not result.changed + + - name: User pinky enabled + ipauser: + ipaadmin_password: SomeADMINpassword + name: pinky + state: enabled + register: result + failed_when: not result.changed + + - name: Remove test users + ipauser: + ipaadmin_password: SomeADMINpassword + name: manager1,manager2,manager3,pinky,pinky2 + state: absent diff --git a/tests/user/test_users_absent.yml b/tests/user/test_users_absent.yml new file mode 100644 index 0000000..eaab27f --- /dev/null +++ b/tests/user/test_users_absent.yml @@ -0,0 +1,16 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Include users_absent.json + include_vars: + file: users_absent.json + + - name: Users absent + ipauser: + ipaadmin_password: SomeADMINpassword + users: "{{ users }}" + state: absent diff --git a/tests/user/test_users_invalid_cert.yml b/tests/user/test_users_invalid_cert.yml new file mode 100644 index 0000000..62a8d9e --- /dev/null +++ b/tests/user/test_users_invalid_cert.yml @@ -0,0 +1,64 @@ +# +# Generate self-signed certificates using openssl: +# +# openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout private1.key -out cert1.pem -subj '/CN=test' +# openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout private2.key -out cert2.pem -subj '/CN=test' +# openssl req -x509 -newkey rsa:2048 -days 365 -nodes -keyout private3.key -out cert3.pem -subj '/CN=test' +# +# Convert the certificate do DER for easier handling through CLI +# +# openssl x509 -outform der -in cert1.pem -out cert1.der +# openssl x509 -outform der -in cert2.pem -out cert2.der +# openssl x509 -outform der -in cert3.pem -out cert3.der +# +# Use base64: +# +# base64 cert1.der -w5000 +# base64 cert2.der -w5000 +# base64 cert3.der -w5000 +# +--- +- name: Test user certificates + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: User test absent + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + state: absent + + - name: User test present + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + first: test + last: test + + - name: User test cert members present + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + certificate: + - MIIC/zCCAeegAwIBAgIUAWE1vaA+mZd3nwZqwWH64EbHvR0wDQYJKoZIhvcNAQELBQAwDzENMAsGA1UEAwwEdGVzdDAeFw0xOTEwMTQxNjI4NDVaFw0yMDEwMTMxNjI4NDVaMA8xDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCWzJibKtN8Zf7LgandINhFonx99AKi44iaZkrlMKEObE6Faf8NTUbUgK3VfJNYmCbA1baLVJ0YZJijJ7S/4o7h7eeqcJVXJkEhWNTimWXNW/YCzTHe3SSapnSYOKmdHHRClplysL8OyyEG7pbX/aB9iAfFb/+vUFCX5sMwFFrYxOimKJ9Pc/NRFtdv1wNw1rqWKF1ZzagWRlG4QgzRGwQ4quc7yO98TKikj2OPiIt7Zd46hbqQxmgGBtCkVOZIhxu77OmNrFsXmM4rZZpmqh0UdqcpwkRojVnGXmNqeMCd6dNTnLhr9wukUYw0KgE57zCDVr9Ix+p/dA5R1mG4RJ2XAgMBAAGjUzBRMB0GA1UdDgQWBBSbuiH2lNVrID3yt1SsFwtOFKOnpTAfBgNVHSMEGDAWgBSbuiH2lNVrID3yt1SsFwtOFKOnpTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBCVWd293wWyohFqMFMHRBBg97T2Uc1yeT0dMH4BpuOaCqQp4q5ep+uLcXEI6+3mEwm8pa/ULQCD8yLLdotIWlG3+h/4boFpdiPFcBDgT8kGe+0KOzB8Nt7E13QYOu12MNi10qwGrjKhdhu1xBe4fpY5VCetVU1OLyuTsUyucQsFrtZI0SR83h+blbyoMZ7IhMngCfGUe1bnYeWnLbpFbigKfPuVDWsMH2kgj05EAd5EgHkWbX8QA8hmcmDKfNT3YZM8kiGQwmFrnQdq8bN0uHR8Nz+24cbmdbHcD65wlDW6GmYxi8mW+V6bAqn9pir/J14r4YFnqMGgjmdt81tscJV + action: member + register: result + failed_when: not result.changed + + - name: User test cert members absent + ipauser: + ipaadmin_password: SomeADMINpassword + users: + - name: test + certificate: + - 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 + state: absent + action: member + #register: result + #failed_when: not result.changed diff --git a/tests/user/test_users_present.yml b/tests/user/test_users_present.yml new file mode 100644 index 0000000..370b137 --- /dev/null +++ b/tests/user/test_users_present.yml @@ -0,0 +1,15 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + tasks: + - name: Include users_present.json + include_vars: + file: users_present.json + + - name: Users present + ipauser: + ipaadmin_password: SomeADMINpassword + users: "{{ users }}" diff --git a/tests/user/test_users_present_slice.yml b/tests/user/test_users_present_slice.yml new file mode 100644 index 0000000..8c6524d --- /dev/null +++ b/tests/user/test_users_present_slice.yml @@ -0,0 +1,19 @@ +--- +- name: Tests + hosts: ipaserver + become: true + gather_facts: false + + vars: + slice_size: 500 + tasks: + - name: Include users_present.json + include_vars: + file: users_present.json + - debug: + msg: "{{ users | length }}" + - name: Users present + ipauser: + ipaadmin_password: SomeADMINpassword + users: "{{ users[item:item+slice_size] }}" + loop: "{{ range(0,users | length, slice_size) | list }}" diff --git a/tests/user/users_absent.json b/tests/user/users_absent.json new file mode 100644 index 0000000..6d29807 --- /dev/null +++ b/tests/user/users_absent.json @@ -0,0 +1,3004 @@ +{ + "users": [ + { + "name": "user1", + }, + { + "name": "user2", + }, + { + "name": "user3", + }, + { + "name": "user4", + }, + { + "name": "user5", + }, + { + "name": "user6", + }, + { + "name": "user7", + }, + { + "name": "user8", + }, + { + "name": "user9", + }, + { + "name": "user10", + }, + { + "name": "user11", + }, + { + "name": "user12", + }, + { + "name": "user13", + }, + { + "name": "user14", + }, + { + "name": "user15", + }, + { + "name": "user16", + }, + { + "name": "user17", + }, + { + "name": "user18", + }, + { + "name": "user19", + }, + { + "name": "user20", + }, + { + "name": "user21", + }, + { + "name": "user22", + }, + { + "name": "user23", + }, + { + "name": "user24", + }, + { + "name": "user25", + }, + { + "name": "user26", + }, + { + "name": "user27", + }, + { + "name": "user28", + }, + { + "name": "user29", + }, + { + "name": "user30", + }, + { + "name": "user31", + }, + { + "name": "user32", + }, + { + "name": "user33", + }, + { + "name": "user34", + }, + { + "name": "user35", + }, + { + "name": "user36", + }, + { + "name": "user37", + }, + { + "name": "user38", + }, + { + "name": "user39", + }, + { + "name": "user40", + }, + { + "name": "user41", + }, + { + "name": "user42", + }, + { + "name": "user43", + }, + { + "name": "user44", + }, + { + "name": "user45", + }, + { + "name": "user46", + }, + { + "name": "user47", + }, + { + "name": "user48", + }, + { + "name": "user49", + }, + { + "name": "user50", + }, + { + "name": "user51", + }, + { + "name": "user52", + }, + { + "name": "user53", + }, + { + "name": "user54", + }, + { + "name": "user55", + }, + { + "name": "user56", + }, + { + "name": "user57", + }, + { + "name": "user58", + }, + { + "name": "user59", + }, + { + "name": "user60", + }, + { + "name": "user61", + }, + { + "name": "user62", + }, + { + "name": "user63", + }, + { + "name": "user64", + }, + { + "name": "user65", + }, + { + "name": "user66", + }, + { + "name": "user67", + }, + { + "name": "user68", + }, + { + "name": "user69", + }, + { + "name": "user70", + }, + { + "name": "user71", + }, + { + "name": "user72", + }, + { + "name": "user73", + }, + { + "name": "user74", + }, + { + "name": "user75", + }, + { + "name": "user76", + }, + { + "name": "user77", + }, + { + "name": "user78", + }, + { + "name": "user79", + }, + { + "name": "user80", + }, + { + "name": "user81", + }, + { + "name": "user82", + }, + { + "name": "user83", + }, + { + "name": "user84", + }, + { + "name": "user85", + }, + { + "name": "user86", + }, + { + "name": "user87", + }, + { + "name": "user88", + }, + { + "name": "user89", + }, + { + "name": "user90", + }, + { + "name": "user91", + }, + { + "name": "user92", + }, + { + "name": "user93", + }, + { + "name": "user94", + }, + { + "name": "user95", + }, + { + "name": "user96", + }, + { + "name": "user97", + }, + { + "name": "user98", + }, + { + "name": "user99", + }, + { + "name": "user100", + }, + { + "name": "user101", + }, + { + "name": "user102", + }, + { + "name": "user103", + }, + { + "name": "user104", + }, + { + "name": "user105", + }, + { + "name": "user106", + }, + { + "name": "user107", + }, + { + "name": "user108", + }, + { + "name": "user109", + }, + { + "name": "user110", + }, + { + "name": "user111", + }, + { + "name": "user112", + }, + { + "name": "user113", + }, + { + "name": "user114", + }, + { + "name": "user115", + }, + { + "name": "user116", + }, + { + "name": "user117", + }, + { + "name": "user118", + }, + { + "name": "user119", + }, + { + "name": "user120", + }, + { + "name": "user121", + }, + { + "name": "user122", + }, + { + "name": "user123", + }, + { + "name": "user124", + }, + { + "name": "user125", + }, + { + "name": "user126", + }, + { + "name": "user127", + }, + { + "name": "user128", + }, + { + "name": "user129", + }, + { + "name": "user130", + }, + { + "name": "user131", + }, + { + "name": "user132", + }, + { + "name": "user133", + }, + { + "name": "user134", + }, + { + "name": "user135", + }, + { + "name": "user136", + }, + { + "name": "user137", + }, + { + "name": "user138", + }, + { + "name": "user139", + }, + { + "name": "user140", + }, + { + "name": "user141", + }, + { + "name": "user142", + }, + { + "name": "user143", + }, + { + "name": "user144", + }, + { + "name": "user145", + }, + { + "name": "user146", + }, + { + "name": "user147", + }, + { + "name": "user148", + }, + { + "name": "user149", + }, + { + "name": "user150", + }, + { + "name": "user151", + }, + { + "name": "user152", + }, + { + "name": "user153", + }, + { + "name": "user154", + }, + { + "name": "user155", + }, + { + "name": "user156", + }, + { + "name": "user157", + }, + { + "name": "user158", + }, + { + "name": "user159", + }, + { + "name": "user160", + }, + { + "name": "user161", + }, + { + "name": "user162", + }, + { + "name": "user163", + }, + { + "name": "user164", + }, + { + "name": "user165", + }, + { + "name": "user166", + }, + { + "name": "user167", + }, + { + "name": "user168", + }, + { + "name": "user169", + }, + { + "name": "user170", + }, + { + "name": "user171", + }, + { + "name": "user172", + }, + { + "name": "user173", + }, + { + "name": "user174", + }, + { + "name": "user175", + }, + { + "name": "user176", + }, + { + "name": "user177", + }, + { + "name": "user178", + }, + { + "name": "user179", + }, + { + "name": "user180", + }, + { + "name": "user181", + }, + { + "name": "user182", + }, + { + "name": "user183", + }, + { + "name": "user184", + }, + { + "name": "user185", + }, + { + "name": "user186", + }, + { + "name": "user187", + }, + { + "name": "user188", + }, + { + "name": "user189", + }, + { + "name": "user190", + }, + { + "name": "user191", + }, + { + "name": "user192", + }, + { + "name": "user193", + }, + { + "name": "user194", + }, + { + "name": "user195", + }, + { + "name": "user196", + }, + { + "name": "user197", + }, + { + "name": "user198", + }, + { + "name": "user199", + }, + { + "name": "user200", + }, + { + "name": "user201", + }, + { + "name": "user202", + }, + { + "name": "user203", + }, + { + "name": "user204", + }, + { + "name": "user205", + }, + { + "name": "user206", + }, + { + "name": "user207", + }, + { + "name": "user208", + }, + { + "name": "user209", + }, + { + "name": "user210", + }, + { + "name": "user211", + }, + { + "name": "user212", + }, + { + "name": "user213", + }, + { + "name": "user214", + }, + { + "name": "user215", + }, + { + "name": "user216", + }, + { + "name": "user217", + }, + { + "name": "user218", + }, + { + "name": "user219", + }, + { + "name": "user220", + }, + { + "name": "user221", + }, + { + "name": "user222", + }, + { + "name": "user223", + }, + { + "name": "user224", + }, + { + "name": "user225", + }, + { + "name": "user226", + }, + { + "name": "user227", + }, + { + "name": "user228", + }, + { + "name": "user229", + }, + { + "name": "user230", + }, + { + "name": "user231", + }, + { + "name": "user232", + }, + { + "name": "user233", + }, + { + "name": "user234", + }, + { + "name": "user235", + }, + { + "name": "user236", + }, + { + "name": "user237", + }, + { + "name": "user238", + }, + { + "name": "user239", + }, + { + "name": "user240", + }, + { + "name": "user241", + }, + { + "name": "user242", + }, + { + "name": "user243", + }, + { + "name": "user244", + }, + { + "name": "user245", + }, + { + "name": "user246", + }, + { + "name": "user247", + }, + { + "name": "user248", + }, + { + "name": "user249", + }, + { + "name": "user250", + }, + { + "name": "user251", + }, + { + "name": "user252", + }, + { + "name": "user253", + }, + { + "name": "user254", + }, + { + "name": "user255", + }, + { + "name": "user256", + }, + { + "name": "user257", + }, + { + "name": "user258", + }, + { + "name": "user259", + }, + { + "name": "user260", + }, + { + "name": "user261", + }, + { + "name": "user262", + }, + { + "name": "user263", + }, + { + "name": "user264", + }, + { + "name": "user265", + }, + { + "name": "user266", + }, + { + "name": "user267", + }, + { + "name": "user268", + }, + { + "name": "user269", + }, + { + "name": "user270", + }, + { + "name": "user271", + }, + { + "name": "user272", + }, + { + "name": "user273", + }, + { + "name": "user274", + }, + { + "name": "user275", + }, + { + "name": "user276", + }, + { + "name": "user277", + }, + { + "name": "user278", + }, + { + "name": "user279", + }, + { + "name": "user280", + }, + { + "name": "user281", + }, + { + "name": "user282", + }, + { + "name": "user283", + }, + { + "name": "user284", + }, + { + "name": "user285", + }, + { + "name": "user286", + }, + { + "name": "user287", + }, + { + "name": "user288", + }, + { + "name": "user289", + }, + { + "name": "user290", + }, + { + "name": "user291", + }, + { + "name": "user292", + }, + { + "name": "user293", + }, + { + "name": "user294", + }, + { + "name": "user295", + }, + { + "name": "user296", + }, + { + "name": "user297", + }, + { + "name": "user298", + }, + { + "name": "user299", + }, + { + "name": "user300", + }, + { + "name": "user301", + }, + { + "name": "user302", + }, + { + "name": "user303", + }, + { + "name": "user304", + }, + { + "name": "user305", + }, + { + "name": "user306", + }, + { + "name": "user307", + }, + { + "name": "user308", + }, + { + "name": "user309", + }, + { + "name": "user310", + }, + { + "name": "user311", + }, + { + "name": "user312", + }, + { + "name": "user313", + }, + { + "name": "user314", + }, + { + "name": "user315", + }, + { + "name": "user316", + }, + { + "name": "user317", + }, + { + "name": "user318", + }, + { + "name": "user319", + }, + { + "name": "user320", + }, + { + "name": "user321", + }, + { + "name": "user322", + }, + { + "name": "user323", + }, + { + "name": "user324", + }, + { + "name": "user325", + }, + { + "name": "user326", + }, + { + "name": "user327", + }, + { + "name": "user328", + }, + { + "name": "user329", + }, + { + "name": "user330", + }, + { + "name": "user331", + }, + { + "name": "user332", + }, + { + "name": "user333", + }, + { + "name": "user334", + }, + { + "name": "user335", + }, + { + "name": "user336", + }, + { + "name": "user337", + }, + { + "name": "user338", + }, + { + "name": "user339", + }, + { + "name": "user340", + }, + { + "name": "user341", + }, + { + "name": "user342", + }, + { + "name": "user343", + }, + { + "name": "user344", + }, + { + "name": "user345", + }, + { + "name": "user346", + }, + { + "name": "user347", + }, + { + "name": "user348", + }, + { + "name": "user349", + }, + { + "name": "user350", + }, + { + "name": "user351", + }, + { + "name": "user352", + }, + { + "name": "user353", + }, + { + "name": "user354", + }, + { + "name": "user355", + }, + { + "name": "user356", + }, + { + "name": "user357", + }, + { + "name": "user358", + }, + { + "name": "user359", + }, + { + "name": "user360", + }, + { + "name": "user361", + }, + { + "name": "user362", + }, + { + "name": "user363", + }, + { + "name": "user364", + }, + { + "name": "user365", + }, + { + "name": "user366", + }, + { + "name": "user367", + }, + { + "name": "user368", + }, + { + "name": "user369", + }, + { + "name": "user370", + }, + { + "name": "user371", + }, + { + "name": "user372", + }, + { + "name": "user373", + }, + { + "name": "user374", + }, + { + "name": "user375", + }, + { + "name": "user376", + }, + { + "name": "user377", + }, + { + "name": "user378", + }, + { + "name": "user379", + }, + { + "name": "user380", + }, + { + "name": "user381", + }, + { + "name": "user382", + }, + { + "name": "user383", + }, + { + "name": "user384", + }, + { + "name": "user385", + }, + { + "name": "user386", + }, + { + "name": "user387", + }, + { + "name": "user388", + }, + { + "name": "user389", + }, + { + "name": "user390", + }, + { + "name": "user391", + }, + { + "name": "user392", + }, + { + "name": "user393", + }, + { + "name": "user394", + }, + { + "name": "user395", + }, + { + "name": "user396", + }, + { + "name": "user397", + }, + { + "name": "user398", + }, + { + "name": "user399", + }, + { + "name": "user400", + }, + { + "name": "user401", + }, + { + "name": "user402", + }, + { + "name": "user403", + }, + { + "name": "user404", + }, + { + "name": "user405", + }, + { + "name": "user406", + }, + { + "name": "user407", + }, + { + "name": "user408", + }, + { + "name": "user409", + }, + { + "name": "user410", + }, + { + "name": "user411", + }, + { + "name": "user412", + }, + { + "name": "user413", + }, + { + "name": "user414", + }, + { + "name": "user415", + }, + { + "name": "user416", + }, + { + "name": "user417", + }, + { + "name": "user418", + }, + { + "name": "user419", + }, + { + "name": "user420", + }, + { + "name": "user421", + }, + { + "name": "user422", + }, + { + "name": "user423", + }, + { + "name": "user424", + }, + { + "name": "user425", + }, + { + "name": "user426", + }, + { + "name": "user427", + }, + { + "name": "user428", + }, + { + "name": "user429", + }, + { + "name": "user430", + }, + { + "name": "user431", + }, + { + "name": "user432", + }, + { + "name": "user433", + }, + { + "name": "user434", + }, + { + "name": "user435", + }, + { + "name": "user436", + }, + { + "name": "user437", + }, + { + "name": "user438", + }, + { + "name": "user439", + }, + { + "name": "user440", + }, + { + "name": "user441", + }, + { + "name": "user442", + }, + { + "name": "user443", + }, + { + "name": "user444", + }, + { + "name": "user445", + }, + { + "name": "user446", + }, + { + "name": "user447", + }, + { + "name": "user448", + }, + { + "name": "user449", + }, + { + "name": "user450", + }, + { + "name": "user451", + }, + { + "name": "user452", + }, + { + "name": "user453", + }, + { + "name": "user454", + }, + { + "name": "user455", + }, + { + "name": "user456", + }, + { + "name": "user457", + }, + { + "name": "user458", + }, + { + "name": "user459", + }, + { + "name": "user460", + }, + { + "name": "user461", + }, + { + "name": "user462", + }, + { + "name": "user463", + }, + { + "name": "user464", + }, + { + "name": "user465", + }, + { + "name": "user466", + }, + { + "name": "user467", + }, + { + "name": "user468", + }, + { + "name": "user469", + }, + { + "name": "user470", + }, + { + "name": "user471", + }, + { + "name": "user472", + }, + { + "name": "user473", + }, + { + "name": "user474", + }, + { + "name": "user475", + }, + { + "name": "user476", + }, + { + "name": "user477", + }, + { + "name": "user478", + }, + { + "name": "user479", + }, + { + "name": "user480", + }, + { + "name": "user481", + }, + { + "name": "user482", + }, + { + "name": "user483", + }, + { + "name": "user484", + }, + { + "name": "user485", + }, + { + "name": "user486", + }, + { + "name": "user487", + }, + { + "name": "user488", + }, + { + "name": "user489", + }, + { + "name": "user490", + }, + { + "name": "user491", + }, + { + "name": "user492", + }, + { + "name": "user493", + }, + { + "name": "user494", + }, + { + "name": "user495", + }, + { + "name": "user496", + }, + { + "name": "user497", + }, + { + "name": "user498", + }, + { + "name": "user499", + }, + { + "name": "user500", + }, + { + "name": "user501", + }, + { + "name": "user502", + }, + { + "name": "user503", + }, + { + "name": "user504", + }, + { + "name": "user505", + }, + { + "name": "user506", + }, + { + "name": "user507", + }, + { + "name": "user508", + }, + { + "name": "user509", + }, + { + "name": "user510", + }, + { + "name": "user511", + }, + { + "name": "user512", + }, + { + "name": "user513", + }, + { + "name": "user514", + }, + { + "name": "user515", + }, + { + "name": "user516", + }, + { + "name": "user517", + }, + { + "name": "user518", + }, + { + "name": "user519", + }, + { + "name": "user520", + }, + { + "name": "user521", + }, + { + "name": "user522", + }, + { + "name": "user523", + }, + { + "name": "user524", + }, + { + "name": "user525", + }, + { + "name": "user526", + }, + { + "name": "user527", + }, + { + "name": "user528", + }, + { + "name": "user529", + }, + { + "name": "user530", + }, + { + "name": "user531", + }, + { + "name": "user532", + }, + { + "name": "user533", + }, + { + "name": "user534", + }, + { + "name": "user535", + }, + { + "name": "user536", + }, + { + "name": "user537", + }, + { + "name": "user538", + }, + { + "name": "user539", + }, + { + "name": "user540", + }, + { + "name": "user541", + }, + { + "name": "user542", + }, + { + "name": "user543", + }, + { + "name": "user544", + }, + { + "name": "user545", + }, + { + "name": "user546", + }, + { + "name": "user547", + }, + { + "name": "user548", + }, + { + "name": "user549", + }, + { + "name": "user550", + }, + { + "name": "user551", + }, + { + "name": "user552", + }, + { + "name": "user553", + }, + { + "name": "user554", + }, + { + "name": "user555", + }, + { + "name": "user556", + }, + { + "name": "user557", + }, + { + "name": "user558", + }, + { + "name": "user559", + }, + { + "name": "user560", + }, + { + "name": "user561", + }, + { + "name": "user562", + }, + { + "name": "user563", + }, + { + "name": "user564", + }, + { + "name": "user565", + }, + { + "name": "user566", + }, + { + "name": "user567", + }, + { + "name": "user568", + }, + { + "name": "user569", + }, + { + "name": "user570", + }, + { + "name": "user571", + }, + { + "name": "user572", + }, + { + "name": "user573", + }, + { + "name": "user574", + }, + { + "name": "user575", + }, + { + "name": "user576", + }, + { + "name": "user577", + }, + { + "name": "user578", + }, + { + "name": "user579", + }, + { + "name": "user580", + }, + { + "name": "user581", + }, + { + "name": "user582", + }, + { + "name": "user583", + }, + { + "name": "user584", + }, + { + "name": "user585", + }, + { + "name": "user586", + }, + { + "name": "user587", + }, + { + "name": "user588", + }, + { + "name": "user589", + }, + { + "name": "user590", + }, + { + "name": "user591", + }, + { + "name": "user592", + }, + { + "name": "user593", + }, + { + "name": "user594", + }, + { + "name": "user595", + }, + { + "name": "user596", + }, + { + "name": "user597", + }, + { + "name": "user598", + }, + { + "name": "user599", + }, + { + "name": "user600", + }, + { + "name": "user601", + }, + { + "name": "user602", + }, + { + "name": "user603", + }, + { + "name": "user604", + }, + { + "name": "user605", + }, + { + "name": "user606", + }, + { + "name": "user607", + }, + { + "name": "user608", + }, + { + "name": "user609", + }, + { + "name": "user610", + }, + { + "name": "user611", + }, + { + "name": "user612", + }, + { + "name": "user613", + }, + { + "name": "user614", + }, + { + "name": "user615", + }, + { + "name": "user616", + }, + { + "name": "user617", + }, + { + "name": "user618", + }, + { + "name": "user619", + }, + { + "name": "user620", + }, + { + "name": "user621", + }, + { + "name": "user622", + }, + { + "name": "user623", + }, + { + "name": "user624", + }, + { + "name": "user625", + }, + { + "name": "user626", + }, + { + "name": "user627", + }, + { + "name": "user628", + }, + { + "name": "user629", + }, + { + "name": "user630", + }, + { + "name": "user631", + }, + { + "name": "user632", + }, + { + "name": "user633", + }, + { + "name": "user634", + }, + { + "name": "user635", + }, + { + "name": "user636", + }, + { + "name": "user637", + }, + { + "name": "user638", + }, + { + "name": "user639", + }, + { + "name": "user640", + }, + { + "name": "user641", + }, + { + "name": "user642", + }, + { + "name": "user643", + }, + { + "name": "user644", + }, + { + "name": "user645", + }, + { + "name": "user646", + }, + { + "name": "user647", + }, + { + "name": "user648", + }, + { + "name": "user649", + }, + { + "name": "user650", + }, + { + "name": "user651", + }, + { + "name": "user652", + }, + { + "name": "user653", + }, + { + "name": "user654", + }, + { + "name": "user655", + }, + { + "name": "user656", + }, + { + "name": "user657", + }, + { + "name": "user658", + }, + { + "name": "user659", + }, + { + "name": "user660", + }, + { + "name": "user661", + }, + { + "name": "user662", + }, + { + "name": "user663", + }, + { + "name": "user664", + }, + { + "name": "user665", + }, + { + "name": "user666", + }, + { + "name": "user667", + }, + { + "name": "user668", + }, + { + "name": "user669", + }, + { + "name": "user670", + }, + { + "name": "user671", + }, + { + "name": "user672", + }, + { + "name": "user673", + }, + { + "name": "user674", + }, + { + "name": "user675", + }, + { + "name": "user676", + }, + { + "name": "user677", + }, + { + "name": "user678", + }, + { + "name": "user679", + }, + { + "name": "user680", + }, + { + "name": "user681", + }, + { + "name": "user682", + }, + { + "name": "user683", + }, + { + "name": "user684", + }, + { + "name": "user685", + }, + { + "name": "user686", + }, + { + "name": "user687", + }, + { + "name": "user688", + }, + { + "name": "user689", + }, + { + "name": "user690", + }, + { + "name": "user691", + }, + { + "name": "user692", + }, + { + "name": "user693", + }, + { + "name": "user694", + }, + { + "name": "user695", + }, + { + "name": "user696", + }, + { + "name": "user697", + }, + { + "name": "user698", + }, + { + "name": "user699", + }, + { + "name": "user700", + }, + { + "name": "user701", + }, + { + "name": "user702", + }, + { + "name": "user703", + }, + { + "name": "user704", + }, + { + "name": "user705", + }, + { + "name": "user706", + }, + { + "name": "user707", + }, + { + "name": "user708", + }, + { + "name": "user709", + }, + { + "name": "user710", + }, + { + "name": "user711", + }, + { + "name": "user712", + }, + { + "name": "user713", + }, + { + "name": "user714", + }, + { + "name": "user715", + }, + { + "name": "user716", + }, + { + "name": "user717", + }, + { + "name": "user718", + }, + { + "name": "user719", + }, + { + "name": "user720", + }, + { + "name": "user721", + }, + { + "name": "user722", + }, + { + "name": "user723", + }, + { + "name": "user724", + }, + { + "name": "user725", + }, + { + "name": "user726", + }, + { + "name": "user727", + }, + { + "name": "user728", + }, + { + "name": "user729", + }, + { + "name": "user730", + }, + { + "name": "user731", + }, + { + "name": "user732", + }, + { + "name": "user733", + }, + { + "name": "user734", + }, + { + "name": "user735", + }, + { + "name": "user736", + }, + { + "name": "user737", + }, + { + "name": "user738", + }, + { + "name": "user739", + }, + { + "name": "user740", + }, + { + "name": "user741", + }, + { + "name": "user742", + }, + { + "name": "user743", + }, + { + "name": "user744", + }, + { + "name": "user745", + }, + { + "name": "user746", + }, + { + "name": "user747", + }, + { + "name": "user748", + }, + { + "name": "user749", + }, + { + "name": "user750", + }, + { + "name": "user751", + }, + { + "name": "user752", + }, + { + "name": "user753", + }, + { + "name": "user754", + }, + { + "name": "user755", + }, + { + "name": "user756", + }, + { + "name": "user757", + }, + { + "name": "user758", + }, + { + "name": "user759", + }, + { + "name": "user760", + }, + { + "name": "user761", + }, + { + "name": "user762", + }, + { + "name": "user763", + }, + { + "name": "user764", + }, + { + "name": "user765", + }, + { + "name": "user766", + }, + { + "name": "user767", + }, + { + "name": "user768", + }, + { + "name": "user769", + }, + { + "name": "user770", + }, + { + "name": "user771", + }, + { + "name": "user772", + }, + { + "name": "user773", + }, + { + "name": "user774", + }, + { + "name": "user775", + }, + { + "name": "user776", + }, + { + "name": "user777", + }, + { + "name": "user778", + }, + { + "name": "user779", + }, + { + "name": "user780", + }, + { + "name": "user781", + }, + { + "name": "user782", + }, + { + "name": "user783", + }, + { + "name": "user784", + }, + { + "name": "user785", + }, + { + "name": "user786", + }, + { + "name": "user787", + }, + { + "name": "user788", + }, + { + "name": "user789", + }, + { + "name": "user790", + }, + { + "name": "user791", + }, + { + "name": "user792", + }, + { + "name": "user793", + }, + { + "name": "user794", + }, + { + "name": "user795", + }, + { + "name": "user796", + }, + { + "name": "user797", + }, + { + "name": "user798", + }, + { + "name": "user799", + }, + { + "name": "user800", + }, + { + "name": "user801", + }, + { + "name": "user802", + }, + { + "name": "user803", + }, + { + "name": "user804", + }, + { + "name": "user805", + }, + { + "name": "user806", + }, + { + "name": "user807", + }, + { + "name": "user808", + }, + { + "name": "user809", + }, + { + "name": "user810", + }, + { + "name": "user811", + }, + { + "name": "user812", + }, + { + "name": "user813", + }, + { + "name": "user814", + }, + { + "name": "user815", + }, + { + "name": "user816", + }, + { + "name": "user817", + }, + { + "name": "user818", + }, + { + "name": "user819", + }, + { + "name": "user820", + }, + { + "name": "user821", + }, + { + "name": "user822", + }, + { + "name": "user823", + }, + { + "name": "user824", + }, + { + "name": "user825", + }, + { + "name": "user826", + }, + { + "name": "user827", + }, + { + "name": "user828", + }, + { + "name": "user829", + }, + { + "name": "user830", + }, + { + "name": "user831", + }, + { + "name": "user832", + }, + { + "name": "user833", + }, + { + "name": "user834", + }, + { + "name": "user835", + }, + { + "name": "user836", + }, + { + "name": "user837", + }, + { + "name": "user838", + }, + { + "name": "user839", + }, + { + "name": "user840", + }, + { + "name": "user841", + }, + { + "name": "user842", + }, + { + "name": "user843", + }, + { + "name": "user844", + }, + { + "name": "user845", + }, + { + "name": "user846", + }, + { + "name": "user847", + }, + { + "name": "user848", + }, + { + "name": "user849", + }, + { + "name": "user850", + }, + { + "name": "user851", + }, + { + "name": "user852", + }, + { + "name": "user853", + }, + { + "name": "user854", + }, + { + "name": "user855", + }, + { + "name": "user856", + }, + { + "name": "user857", + }, + { + "name": "user858", + }, + { + "name": "user859", + }, + { + "name": "user860", + }, + { + "name": "user861", + }, + { + "name": "user862", + }, + { + "name": "user863", + }, + { + "name": "user864", + }, + { + "name": "user865", + }, + { + "name": "user866", + }, + { + "name": "user867", + }, + { + "name": "user868", + }, + { + "name": "user869", + }, + { + "name": "user870", + }, + { + "name": "user871", + }, + { + "name": "user872", + }, + { + "name": "user873", + }, + { + "name": "user874", + }, + { + "name": "user875", + }, + { + "name": "user876", + }, + { + "name": "user877", + }, + { + "name": "user878", + }, + { + "name": "user879", + }, + { + "name": "user880", + }, + { + "name": "user881", + }, + { + "name": "user882", + }, + { + "name": "user883", + }, + { + "name": "user884", + }, + { + "name": "user885", + }, + { + "name": "user886", + }, + { + "name": "user887", + }, + { + "name": "user888", + }, + { + "name": "user889", + }, + { + "name": "user890", + }, + { + "name": "user891", + }, + { + "name": "user892", + }, + { + "name": "user893", + }, + { + "name": "user894", + }, + { + "name": "user895", + }, + { + "name": "user896", + }, + { + "name": "user897", + }, + { + "name": "user898", + }, + { + "name": "user899", + }, + { + "name": "user900", + }, + { + "name": "user901", + }, + { + "name": "user902", + }, + { + "name": "user903", + }, + { + "name": "user904", + }, + { + "name": "user905", + }, + { + "name": "user906", + }, + { + "name": "user907", + }, + { + "name": "user908", + }, + { + "name": "user909", + }, + { + "name": "user910", + }, + { + "name": "user911", + }, + { + "name": "user912", + }, + { + "name": "user913", + }, + { + "name": "user914", + }, + { + "name": "user915", + }, + { + "name": "user916", + }, + { + "name": "user917", + }, + { + "name": "user918", + }, + { + "name": "user919", + }, + { + "name": "user920", + }, + { + "name": "user921", + }, + { + "name": "user922", + }, + { + "name": "user923", + }, + { + "name": "user924", + }, + { + "name": "user925", + }, + { + "name": "user926", + }, + { + "name": "user927", + }, + { + "name": "user928", + }, + { + "name": "user929", + }, + { + "name": "user930", + }, + { + "name": "user931", + }, + { + "name": "user932", + }, + { + "name": "user933", + }, + { + "name": "user934", + }, + { + "name": "user935", + }, + { + "name": "user936", + }, + { + "name": "user937", + }, + { + "name": "user938", + }, + { + "name": "user939", + }, + { + "name": "user940", + }, + { + "name": "user941", + }, + { + "name": "user942", + }, + { + "name": "user943", + }, + { + "name": "user944", + }, + { + "name": "user945", + }, + { + "name": "user946", + }, + { + "name": "user947", + }, + { + "name": "user948", + }, + { + "name": "user949", + }, + { + "name": "user950", + }, + { + "name": "user951", + }, + { + "name": "user952", + }, + { + "name": "user953", + }, + { + "name": "user954", + }, + { + "name": "user955", + }, + { + "name": "user956", + }, + { + "name": "user957", + }, + { + "name": "user958", + }, + { + "name": "user959", + }, + { + "name": "user960", + }, + { + "name": "user961", + }, + { + "name": "user962", + }, + { + "name": "user963", + }, + { + "name": "user964", + }, + { + "name": "user965", + }, + { + "name": "user966", + }, + { + "name": "user967", + }, + { + "name": "user968", + }, + { + "name": "user969", + }, + { + "name": "user970", + }, + { + "name": "user971", + }, + { + "name": "user972", + }, + { + "name": "user973", + }, + { + "name": "user974", + }, + { + "name": "user975", + }, + { + "name": "user976", + }, + { + "name": "user977", + }, + { + "name": "user978", + }, + { + "name": "user979", + }, + { + "name": "user980", + }, + { + "name": "user981", + }, + { + "name": "user982", + }, + { + "name": "user983", + }, + { + "name": "user984", + }, + { + "name": "user985", + }, + { + "name": "user986", + }, + { + "name": "user987", + }, + { + "name": "user988", + }, + { + "name": "user989", + }, + { + "name": "user990", + }, + { + "name": "user991", + }, + { + "name": "user992", + }, + { + "name": "user993", + }, + { + "name": "user994", + }, + { + "name": "user995", + }, + { + "name": "user996", + }, + { + "name": "user997", + }, + { + "name": "user998", + }, + { + "name": "user999", + }, + { + "name": "user1000", + } + ] +} diff --git a/tests/user/users_absent.sh b/tests/user/users_absent.sh new file mode 100644 index 0000000..1ee89ad --- /dev/null +++ b/tests/user/users_absent.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +NUM=1000 +FILE="users_absent.json" + +echo "{" > $FILE + +echo " \"users\": [" >> $FILE + +for i in $(seq 1 $NUM); do + echo " {" >> $FILE + echo " \"name\": \"user$i\"," >> $FILE + if [ $i -lt $NUM ]; then + echo " }," >> $FILE + else + echo " }" >> $FILE + fi +done + +echo " ]" >> $FILE + +echo "}" >> $FILE diff --git a/tests/user/users_present.json b/tests/user/users_present.json new file mode 100644 index 0000000..514edc8 --- /dev/null +++ b/tests/user/users_present.json @@ -0,0 +1,5004 @@ +{ + "users": [ + { + "name": "user1", + "first": "First 1", + "last": "Last 1" + }, + { + "name": "user2", + "first": "First 2", + "last": "Last 2" + }, + { + "name": "user3", + "first": "First 3", + "last": "Last 3" + }, + { + "name": "user4", + "first": "First 4", + "last": "Last 4" + }, + { + "name": "user5", + "first": "First 5", + "last": "Last 5" + }, + { + "name": "user6", + "first": "First 6", + "last": "Last 6" + }, + { + "name": "user7", + "first": "First 7", + "last": "Last 7" + }, + { + "name": "user8", + "first": "First 8", + "last": "Last 8" + }, + { + "name": "user9", + "first": "First 9", + "last": "Last 9" + }, + { + "name": "user10", + "first": "First 10", + "last": "Last 10" + }, + { + "name": "user11", + "first": "First 11", + "last": "Last 11" + }, + { + "name": "user12", + "first": "First 12", + "last": "Last 12" + }, + { + "name": "user13", + "first": "First 13", + "last": "Last 13" + }, + { + "name": "user14", + "first": "First 14", + "last": "Last 14" + }, + { + "name": "user15", + "first": "First 15", + "last": "Last 15" + }, + { + "name": "user16", + "first": "First 16", + "last": "Last 16" + }, + { + "name": "user17", + "first": "First 17", + "last": "Last 17" + }, + { + "name": "user18", + "first": "First 18", + "last": "Last 18" + }, + { + "name": "user19", + "first": "First 19", + "last": "Last 19" + }, + { + "name": "user20", + "first": "First 20", + "last": "Last 20" + }, + { + "name": "user21", + "first": "First 21", + "last": "Last 21" + }, + { + "name": "user22", + "first": "First 22", + "last": "Last 22" + }, + { + "name": "user23", + "first": "First 23", + "last": "Last 23" + }, + { + "name": "user24", + "first": "First 24", + "last": "Last 24" + }, + { + "name": "user25", + "first": "First 25", + "last": "Last 25" + }, + { + "name": "user26", + "first": "First 26", + "last": "Last 26" + }, + { + "name": "user27", + "first": "First 27", + "last": "Last 27" + }, + { + "name": "user28", + "first": "First 28", + "last": "Last 28" + }, + { + "name": "user29", + "first": "First 29", + "last": "Last 29" + }, + { + "name": "user30", + "first": "First 30", + "last": "Last 30" + }, + { + "name": "user31", + "first": "First 31", + "last": "Last 31" + }, + { + "name": "user32", + "first": "First 32", + "last": "Last 32" + }, + { + "name": "user33", + "first": "First 33", + "last": "Last 33" + }, + { + "name": "user34", + "first": "First 34", + "last": "Last 34" + }, + { + "name": "user35", + "first": "First 35", + "last": "Last 35" + }, + { + "name": "user36", + "first": "First 36", + "last": "Last 36" + }, + { + "name": "user37", + "first": "First 37", + "last": "Last 37" + }, + { + "name": "user38", + "first": "First 38", + "last": "Last 38" + }, + { + "name": "user39", + "first": "First 39", + "last": "Last 39" + }, + { + "name": "user40", + "first": "First 40", + "last": "Last 40" + }, + { + "name": "user41", + "first": "First 41", + "last": "Last 41" + }, + { + "name": "user42", + "first": "First 42", + "last": "Last 42" + }, + { + "name": "user43", + "first": "First 43", + "last": "Last 43" + }, + { + "name": "user44", + "first": "First 44", + "last": "Last 44" + }, + { + "name": "user45", + "first": "First 45", + "last": "Last 45" + }, + { + "name": "user46", + "first": "First 46", + "last": "Last 46" + }, + { + "name": "user47", + "first": "First 47", + "last": "Last 47" + }, + { + "name": "user48", + "first": "First 48", + "last": "Last 48" + }, + { + "name": "user49", + "first": "First 49", + "last": "Last 49" + }, + { + "name": "user50", + "first": "First 50", + "last": "Last 50" + }, + { + "name": "user51", + "first": "First 51", + "last": "Last 51" + }, + { + "name": "user52", + "first": "First 52", + "last": "Last 52" + }, + { + "name": "user53", + "first": "First 53", + "last": "Last 53" + }, + { + "name": "user54", + "first": "First 54", + "last": "Last 54" + }, + { + "name": "user55", + "first": "First 55", + "last": "Last 55" + }, + { + "name": "user56", + "first": "First 56", + "last": "Last 56" + }, + { + "name": "user57", + "first": "First 57", + "last": "Last 57" + }, + { + "name": "user58", + "first": "First 58", + "last": "Last 58" + }, + { + "name": "user59", + "first": "First 59", + "last": "Last 59" + }, + { + "name": "user60", + "first": "First 60", + "last": "Last 60" + }, + { + "name": "user61", + "first": "First 61", + "last": "Last 61" + }, + { + "name": "user62", + "first": "First 62", + "last": "Last 62" + }, + { + "name": "user63", + "first": "First 63", + "last": "Last 63" + }, + { + "name": "user64", + "first": "First 64", + "last": "Last 64" + }, + { + "name": "user65", + "first": "First 65", + "last": "Last 65" + }, + { + "name": "user66", + "first": "First 66", + "last": "Last 66" + }, + { + "name": "user67", + "first": "First 67", + "last": "Last 67" + }, + { + "name": "user68", + "first": "First 68", + "last": "Last 68" + }, + { + "name": "user69", + "first": "First 69", + "last": "Last 69" + }, + { + "name": "user70", + "first": "First 70", + "last": "Last 70" + }, + { + "name": "user71", + "first": "First 71", + "last": "Last 71" + }, + { + "name": "user72", + "first": "First 72", + "last": "Last 72" + }, + { + "name": "user73", + "first": "First 73", + "last": "Last 73" + }, + { + "name": "user74", + "first": "First 74", + "last": "Last 74" + }, + { + "name": "user75", + "first": "First 75", + "last": "Last 75" + }, + { + "name": "user76", + "first": "First 76", + "last": "Last 76" + }, + { + "name": "user77", + "first": "First 77", + "last": "Last 77" + }, + { + "name": "user78", + "first": "First 78", + "last": "Last 78" + }, + { + "name": "user79", + "first": "First 79", + "last": "Last 79" + }, + { + "name": "user80", + "first": "First 80", + "last": "Last 80" + }, + { + "name": "user81", + "first": "First 81", + "last": "Last 81" + }, + { + "name": "user82", + "first": "First 82", + "last": "Last 82" + }, + { + "name": "user83", + "first": "First 83", + "last": "Last 83" + }, + { + "name": "user84", + "first": "First 84", + "last": "Last 84" + }, + { + "name": "user85", + "first": "First 85", + "last": "Last 85" + }, + { + "name": "user86", + "first": "First 86", + "last": "Last 86" + }, + { + "name": "user87", + "first": "First 87", + "last": "Last 87" + }, + { + "name": "user88", + "first": "First 88", + "last": "Last 88" + }, + { + "name": "user89", + "first": "First 89", + "last": "Last 89" + }, + { + "name": "user90", + "first": "First 90", + "last": "Last 90" + }, + { + "name": "user91", + "first": "First 91", + "last": "Last 91" + }, + { + "name": "user92", + "first": "First 92", + "last": "Last 92" + }, + { + "name": "user93", + "first": "First 93", + "last": "Last 93" + }, + { + "name": "user94", + "first": "First 94", + "last": "Last 94" + }, + { + "name": "user95", + "first": "First 95", + "last": "Last 95" + }, + { + "name": "user96", + "first": "First 96", + "last": "Last 96" + }, + { + "name": "user97", + "first": "First 97", + "last": "Last 97" + }, + { + "name": "user98", + "first": "First 98", + "last": "Last 98" + }, + { + "name": "user99", + "first": "First 99", + "last": "Last 99" + }, + { + "name": "user100", + "first": "First 100", + "last": "Last 100" + }, + { + "name": "user101", + "first": "First 101", + "last": "Last 101" + }, + { + "name": "user102", + "first": "First 102", + "last": "Last 102" + }, + { + "name": "user103", + "first": "First 103", + "last": "Last 103" + }, + { + "name": "user104", + "first": "First 104", + "last": "Last 104" + }, + { + "name": "user105", + "first": "First 105", + "last": "Last 105" + }, + { + "name": "user106", + "first": "First 106", + "last": "Last 106" + }, + { + "name": "user107", + "first": "First 107", + "last": "Last 107" + }, + { + "name": "user108", + "first": "First 108", + "last": "Last 108" + }, + { + "name": "user109", + "first": "First 109", + "last": "Last 109" + }, + { + "name": "user110", + "first": "First 110", + "last": "Last 110" + }, + { + "name": "user111", + "first": "First 111", + "last": "Last 111" + }, + { + "name": "user112", + "first": "First 112", + "last": "Last 112" + }, + { + "name": "user113", + "first": "First 113", + "last": "Last 113" + }, + { + "name": "user114", + "first": "First 114", + "last": "Last 114" + }, + { + "name": "user115", + "first": "First 115", + "last": "Last 115" + }, + { + "name": "user116", + "first": "First 116", + "last": "Last 116" + }, + { + "name": "user117", + "first": "First 117", + "last": "Last 117" + }, + { + "name": "user118", + "first": "First 118", + "last": "Last 118" + }, + { + "name": "user119", + "first": "First 119", + "last": "Last 119" + }, + { + "name": "user120", + "first": "First 120", + "last": "Last 120" + }, + { + "name": "user121", + "first": "First 121", + "last": "Last 121" + }, + { + "name": "user122", + "first": "First 122", + "last": "Last 122" + }, + { + "name": "user123", + "first": "First 123", + "last": "Last 123" + }, + { + "name": "user124", + "first": "First 124", + "last": "Last 124" + }, + { + "name": "user125", + "first": "First 125", + "last": "Last 125" + }, + { + "name": "user126", + "first": "First 126", + "last": "Last 126" + }, + { + "name": "user127", + "first": "First 127", + "last": "Last 127" + }, + { + "name": "user128", + "first": "First 128", + "last": "Last 128" + }, + { + "name": "user129", + "first": "First 129", + "last": "Last 129" + }, + { + "name": "user130", + "first": "First 130", + "last": "Last 130" + }, + { + "name": "user131", + "first": "First 131", + "last": "Last 131" + }, + { + "name": "user132", + "first": "First 132", + "last": "Last 132" + }, + { + "name": "user133", + "first": "First 133", + "last": "Last 133" + }, + { + "name": "user134", + "first": "First 134", + "last": "Last 134" + }, + { + "name": "user135", + "first": "First 135", + "last": "Last 135" + }, + { + "name": "user136", + "first": "First 136", + "last": "Last 136" + }, + { + "name": "user137", + "first": "First 137", + "last": "Last 137" + }, + { + "name": "user138", + "first": "First 138", + "last": "Last 138" + }, + { + "name": "user139", + "first": "First 139", + "last": "Last 139" + }, + { + "name": "user140", + "first": "First 140", + "last": "Last 140" + }, + { + "name": "user141", + "first": "First 141", + "last": "Last 141" + }, + { + "name": "user142", + "first": "First 142", + "last": "Last 142" + }, + { + "name": "user143", + "first": "First 143", + "last": "Last 143" + }, + { + "name": "user144", + "first": "First 144", + "last": "Last 144" + }, + { + "name": "user145", + "first": "First 145", + "last": "Last 145" + }, + { + "name": "user146", + "first": "First 146", + "last": "Last 146" + }, + { + "name": "user147", + "first": "First 147", + "last": "Last 147" + }, + { + "name": "user148", + "first": "First 148", + "last": "Last 148" + }, + { + "name": "user149", + "first": "First 149", + "last": "Last 149" + }, + { + "name": "user150", + "first": "First 150", + "last": "Last 150" + }, + { + "name": "user151", + "first": "First 151", + "last": "Last 151" + }, + { + "name": "user152", + "first": "First 152", + "last": "Last 152" + }, + { + "name": "user153", + "first": "First 153", + "last": "Last 153" + }, + { + "name": "user154", + "first": "First 154", + "last": "Last 154" + }, + { + "name": "user155", + "first": "First 155", + "last": "Last 155" + }, + { + "name": "user156", + "first": "First 156", + "last": "Last 156" + }, + { + "name": "user157", + "first": "First 157", + "last": "Last 157" + }, + { + "name": "user158", + "first": "First 158", + "last": "Last 158" + }, + { + "name": "user159", + "first": "First 159", + "last": "Last 159" + }, + { + "name": "user160", + "first": "First 160", + "last": "Last 160" + }, + { + "name": "user161", + "first": "First 161", + "last": "Last 161" + }, + { + "name": "user162", + "first": "First 162", + "last": "Last 162" + }, + { + "name": "user163", + "first": "First 163", + "last": "Last 163" + }, + { + "name": "user164", + "first": "First 164", + "last": "Last 164" + }, + { + "name": "user165", + "first": "First 165", + "last": "Last 165" + }, + { + "name": "user166", + "first": "First 166", + "last": "Last 166" + }, + { + "name": "user167", + "first": "First 167", + "last": "Last 167" + }, + { + "name": "user168", + "first": "First 168", + "last": "Last 168" + }, + { + "name": "user169", + "first": "First 169", + "last": "Last 169" + }, + { + "name": "user170", + "first": "First 170", + "last": "Last 170" + }, + { + "name": "user171", + "first": "First 171", + "last": "Last 171" + }, + { + "name": "user172", + "first": "First 172", + "last": "Last 172" + }, + { + "name": "user173", + "first": "First 173", + "last": "Last 173" + }, + { + "name": "user174", + "first": "First 174", + "last": "Last 174" + }, + { + "name": "user175", + "first": "First 175", + "last": "Last 175" + }, + { + "name": "user176", + "first": "First 176", + "last": "Last 176" + }, + { + "name": "user177", + "first": "First 177", + "last": "Last 177" + }, + { + "name": "user178", + "first": "First 178", + "last": "Last 178" + }, + { + "name": "user179", + "first": "First 179", + "last": "Last 179" + }, + { + "name": "user180", + "first": "First 180", + "last": "Last 180" + }, + { + "name": "user181", + "first": "First 181", + "last": "Last 181" + }, + { + "name": "user182", + "first": "First 182", + "last": "Last 182" + }, + { + "name": "user183", + "first": "First 183", + "last": "Last 183" + }, + { + "name": "user184", + "first": "First 184", + "last": "Last 184" + }, + { + "name": "user185", + "first": "First 185", + "last": "Last 185" + }, + { + "name": "user186", + "first": "First 186", + "last": "Last 186" + }, + { + "name": "user187", + "first": "First 187", + "last": "Last 187" + }, + { + "name": "user188", + "first": "First 188", + "last": "Last 188" + }, + { + "name": "user189", + "first": "First 189", + "last": "Last 189" + }, + { + "name": "user190", + "first": "First 190", + "last": "Last 190" + }, + { + "name": "user191", + "first": "First 191", + "last": "Last 191" + }, + { + "name": "user192", + "first": "First 192", + "last": "Last 192" + }, + { + "name": "user193", + "first": "First 193", + "last": "Last 193" + }, + { + "name": "user194", + "first": "First 194", + "last": "Last 194" + }, + { + "name": "user195", + "first": "First 195", + "last": "Last 195" + }, + { + "name": "user196", + "first": "First 196", + "last": "Last 196" + }, + { + "name": "user197", + "first": "First 197", + "last": "Last 197" + }, + { + "name": "user198", + "first": "First 198", + "last": "Last 198" + }, + { + "name": "user199", + "first": "First 199", + "last": "Last 199" + }, + { + "name": "user200", + "first": "First 200", + "last": "Last 200" + }, + { + "name": "user201", + "first": "First 201", + "last": "Last 201" + }, + { + "name": "user202", + "first": "First 202", + "last": "Last 202" + }, + { + "name": "user203", + "first": "First 203", + "last": "Last 203" + }, + { + "name": "user204", + "first": "First 204", + "last": "Last 204" + }, + { + "name": "user205", + "first": "First 205", + "last": "Last 205" + }, + { + "name": "user206", + "first": "First 206", + "last": "Last 206" + }, + { + "name": "user207", + "first": "First 207", + "last": "Last 207" + }, + { + "name": "user208", + "first": "First 208", + "last": "Last 208" + }, + { + "name": "user209", + "first": "First 209", + "last": "Last 209" + }, + { + "name": "user210", + "first": "First 210", + "last": "Last 210" + }, + { + "name": "user211", + "first": "First 211", + "last": "Last 211" + }, + { + "name": "user212", + "first": "First 212", + "last": "Last 212" + }, + { + "name": "user213", + "first": "First 213", + "last": "Last 213" + }, + { + "name": "user214", + "first": "First 214", + "last": "Last 214" + }, + { + "name": "user215", + "first": "First 215", + "last": "Last 215" + }, + { + "name": "user216", + "first": "First 216", + "last": "Last 216" + }, + { + "name": "user217", + "first": "First 217", + "last": "Last 217" + }, + { + "name": "user218", + "first": "First 218", + "last": "Last 218" + }, + { + "name": "user219", + "first": "First 219", + "last": "Last 219" + }, + { + "name": "user220", + "first": "First 220", + "last": "Last 220" + }, + { + "name": "user221", + "first": "First 221", + "last": "Last 221" + }, + { + "name": "user222", + "first": "First 222", + "last": "Last 222" + }, + { + "name": "user223", + "first": "First 223", + "last": "Last 223" + }, + { + "name": "user224", + "first": "First 224", + "last": "Last 224" + }, + { + "name": "user225", + "first": "First 225", + "last": "Last 225" + }, + { + "name": "user226", + "first": "First 226", + "last": "Last 226" + }, + { + "name": "user227", + "first": "First 227", + "last": "Last 227" + }, + { + "name": "user228", + "first": "First 228", + "last": "Last 228" + }, + { + "name": "user229", + "first": "First 229", + "last": "Last 229" + }, + { + "name": "user230", + "first": "First 230", + "last": "Last 230" + }, + { + "name": "user231", + "first": "First 231", + "last": "Last 231" + }, + { + "name": "user232", + "first": "First 232", + "last": "Last 232" + }, + { + "name": "user233", + "first": "First 233", + "last": "Last 233" + }, + { + "name": "user234", + "first": "First 234", + "last": "Last 234" + }, + { + "name": "user235", + "first": "First 235", + "last": "Last 235" + }, + { + "name": "user236", + "first": "First 236", + "last": "Last 236" + }, + { + "name": "user237", + "first": "First 237", + "last": "Last 237" + }, + { + "name": "user238", + "first": "First 238", + "last": "Last 238" + }, + { + "name": "user239", + "first": "First 239", + "last": "Last 239" + }, + { + "name": "user240", + "first": "First 240", + "last": "Last 240" + }, + { + "name": "user241", + "first": "First 241", + "last": "Last 241" + }, + { + "name": "user242", + "first": "First 242", + "last": "Last 242" + }, + { + "name": "user243", + "first": "First 243", + "last": "Last 243" + }, + { + "name": "user244", + "first": "First 244", + "last": "Last 244" + }, + { + "name": "user245", + "first": "First 245", + "last": "Last 245" + }, + { + "name": "user246", + "first": "First 246", + "last": "Last 246" + }, + { + "name": "user247", + "first": "First 247", + "last": "Last 247" + }, + { + "name": "user248", + "first": "First 248", + "last": "Last 248" + }, + { + "name": "user249", + "first": "First 249", + "last": "Last 249" + }, + { + "name": "user250", + "first": "First 250", + "last": "Last 250" + }, + { + "name": "user251", + "first": "First 251", + "last": "Last 251" + }, + { + "name": "user252", + "first": "First 252", + "last": "Last 252" + }, + { + "name": "user253", + "first": "First 253", + "last": "Last 253" + }, + { + "name": "user254", + "first": "First 254", + "last": "Last 254" + }, + { + "name": "user255", + "first": "First 255", + "last": "Last 255" + }, + { + "name": "user256", + "first": "First 256", + "last": "Last 256" + }, + { + "name": "user257", + "first": "First 257", + "last": "Last 257" + }, + { + "name": "user258", + "first": "First 258", + "last": "Last 258" + }, + { + "name": "user259", + "first": "First 259", + "last": "Last 259" + }, + { + "name": "user260", + "first": "First 260", + "last": "Last 260" + }, + { + "name": "user261", + "first": "First 261", + "last": "Last 261" + }, + { + "name": "user262", + "first": "First 262", + "last": "Last 262" + }, + { + "name": "user263", + "first": "First 263", + "last": "Last 263" + }, + { + "name": "user264", + "first": "First 264", + "last": "Last 264" + }, + { + "name": "user265", + "first": "First 265", + "last": "Last 265" + }, + { + "name": "user266", + "first": "First 266", + "last": "Last 266" + }, + { + "name": "user267", + "first": "First 267", + "last": "Last 267" + }, + { + "name": "user268", + "first": "First 268", + "last": "Last 268" + }, + { + "name": "user269", + "first": "First 269", + "last": "Last 269" + }, + { + "name": "user270", + "first": "First 270", + "last": "Last 270" + }, + { + "name": "user271", + "first": "First 271", + "last": "Last 271" + }, + { + "name": "user272", + "first": "First 272", + "last": "Last 272" + }, + { + "name": "user273", + "first": "First 273", + "last": "Last 273" + }, + { + "name": "user274", + "first": "First 274", + "last": "Last 274" + }, + { + "name": "user275", + "first": "First 275", + "last": "Last 275" + }, + { + "name": "user276", + "first": "First 276", + "last": "Last 276" + }, + { + "name": "user277", + "first": "First 277", + "last": "Last 277" + }, + { + "name": "user278", + "first": "First 278", + "last": "Last 278" + }, + { + "name": "user279", + "first": "First 279", + "last": "Last 279" + }, + { + "name": "user280", + "first": "First 280", + "last": "Last 280" + }, + { + "name": "user281", + "first": "First 281", + "last": "Last 281" + }, + { + "name": "user282", + "first": "First 282", + "last": "Last 282" + }, + { + "name": "user283", + "first": "First 283", + "last": "Last 283" + }, + { + "name": "user284", + "first": "First 284", + "last": "Last 284" + }, + { + "name": "user285", + "first": "First 285", + "last": "Last 285" + }, + { + "name": "user286", + "first": "First 286", + "last": "Last 286" + }, + { + "name": "user287", + "first": "First 287", + "last": "Last 287" + }, + { + "name": "user288", + "first": "First 288", + "last": "Last 288" + }, + { + "name": "user289", + "first": "First 289", + "last": "Last 289" + }, + { + "name": "user290", + "first": "First 290", + "last": "Last 290" + }, + { + "name": "user291", + "first": "First 291", + "last": "Last 291" + }, + { + "name": "user292", + "first": "First 292", + "last": "Last 292" + }, + { + "name": "user293", + "first": "First 293", + "last": "Last 293" + }, + { + "name": "user294", + "first": "First 294", + "last": "Last 294" + }, + { + "name": "user295", + "first": "First 295", + "last": "Last 295" + }, + { + "name": "user296", + "first": "First 296", + "last": "Last 296" + }, + { + "name": "user297", + "first": "First 297", + "last": "Last 297" + }, + { + "name": "user298", + "first": "First 298", + "last": "Last 298" + }, + { + "name": "user299", + "first": "First 299", + "last": "Last 299" + }, + { + "name": "user300", + "first": "First 300", + "last": "Last 300" + }, + { + "name": "user301", + "first": "First 301", + "last": "Last 301" + }, + { + "name": "user302", + "first": "First 302", + "last": "Last 302" + }, + { + "name": "user303", + "first": "First 303", + "last": "Last 303" + }, + { + "name": "user304", + "first": "First 304", + "last": "Last 304" + }, + { + "name": "user305", + "first": "First 305", + "last": "Last 305" + }, + { + "name": "user306", + "first": "First 306", + "last": "Last 306" + }, + { + "name": "user307", + "first": "First 307", + "last": "Last 307" + }, + { + "name": "user308", + "first": "First 308", + "last": "Last 308" + }, + { + "name": "user309", + "first": "First 309", + "last": "Last 309" + }, + { + "name": "user310", + "first": "First 310", + "last": "Last 310" + }, + { + "name": "user311", + "first": "First 311", + "last": "Last 311" + }, + { + "name": "user312", + "first": "First 312", + "last": "Last 312" + }, + { + "name": "user313", + "first": "First 313", + "last": "Last 313" + }, + { + "name": "user314", + "first": "First 314", + "last": "Last 314" + }, + { + "name": "user315", + "first": "First 315", + "last": "Last 315" + }, + { + "name": "user316", + "first": "First 316", + "last": "Last 316" + }, + { + "name": "user317", + "first": "First 317", + "last": "Last 317" + }, + { + "name": "user318", + "first": "First 318", + "last": "Last 318" + }, + { + "name": "user319", + "first": "First 319", + "last": "Last 319" + }, + { + "name": "user320", + "first": "First 320", + "last": "Last 320" + }, + { + "name": "user321", + "first": "First 321", + "last": "Last 321" + }, + { + "name": "user322", + "first": "First 322", + "last": "Last 322" + }, + { + "name": "user323", + "first": "First 323", + "last": "Last 323" + }, + { + "name": "user324", + "first": "First 324", + "last": "Last 324" + }, + { + "name": "user325", + "first": "First 325", + "last": "Last 325" + }, + { + "name": "user326", + "first": "First 326", + "last": "Last 326" + }, + { + "name": "user327", + "first": "First 327", + "last": "Last 327" + }, + { + "name": "user328", + "first": "First 328", + "last": "Last 328" + }, + { + "name": "user329", + "first": "First 329", + "last": "Last 329" + }, + { + "name": "user330", + "first": "First 330", + "last": "Last 330" + }, + { + "name": "user331", + "first": "First 331", + "last": "Last 331" + }, + { + "name": "user332", + "first": "First 332", + "last": "Last 332" + }, + { + "name": "user333", + "first": "First 333", + "last": "Last 333" + }, + { + "name": "user334", + "first": "First 334", + "last": "Last 334" + }, + { + "name": "user335", + "first": "First 335", + "last": "Last 335" + }, + { + "name": "user336", + "first": "First 336", + "last": "Last 336" + }, + { + "name": "user337", + "first": "First 337", + "last": "Last 337" + }, + { + "name": "user338", + "first": "First 338", + "last": "Last 338" + }, + { + "name": "user339", + "first": "First 339", + "last": "Last 339" + }, + { + "name": "user340", + "first": "First 340", + "last": "Last 340" + }, + { + "name": "user341", + "first": "First 341", + "last": "Last 341" + }, + { + "name": "user342", + "first": "First 342", + "last": "Last 342" + }, + { + "name": "user343", + "first": "First 343", + "last": "Last 343" + }, + { + "name": "user344", + "first": "First 344", + "last": "Last 344" + }, + { + "name": "user345", + "first": "First 345", + "last": "Last 345" + }, + { + "name": "user346", + "first": "First 346", + "last": "Last 346" + }, + { + "name": "user347", + "first": "First 347", + "last": "Last 347" + }, + { + "name": "user348", + "first": "First 348", + "last": "Last 348" + }, + { + "name": "user349", + "first": "First 349", + "last": "Last 349" + }, + { + "name": "user350", + "first": "First 350", + "last": "Last 350" + }, + { + "name": "user351", + "first": "First 351", + "last": "Last 351" + }, + { + "name": "user352", + "first": "First 352", + "last": "Last 352" + }, + { + "name": "user353", + "first": "First 353", + "last": "Last 353" + }, + { + "name": "user354", + "first": "First 354", + "last": "Last 354" + }, + { + "name": "user355", + "first": "First 355", + "last": "Last 355" + }, + { + "name": "user356", + "first": "First 356", + "last": "Last 356" + }, + { + "name": "user357", + "first": "First 357", + "last": "Last 357" + }, + { + "name": "user358", + "first": "First 358", + "last": "Last 358" + }, + { + "name": "user359", + "first": "First 359", + "last": "Last 359" + }, + { + "name": "user360", + "first": "First 360", + "last": "Last 360" + }, + { + "name": "user361", + "first": "First 361", + "last": "Last 361" + }, + { + "name": "user362", + "first": "First 362", + "last": "Last 362" + }, + { + "name": "user363", + "first": "First 363", + "last": "Last 363" + }, + { + "name": "user364", + "first": "First 364", + "last": "Last 364" + }, + { + "name": "user365", + "first": "First 365", + "last": "Last 365" + }, + { + "name": "user366", + "first": "First 366", + "last": "Last 366" + }, + { + "name": "user367", + "first": "First 367", + "last": "Last 367" + }, + { + "name": "user368", + "first": "First 368", + "last": "Last 368" + }, + { + "name": "user369", + "first": "First 369", + "last": "Last 369" + }, + { + "name": "user370", + "first": "First 370", + "last": "Last 370" + }, + { + "name": "user371", + "first": "First 371", + "last": "Last 371" + }, + { + "name": "user372", + "first": "First 372", + "last": "Last 372" + }, + { + "name": "user373", + "first": "First 373", + "last": "Last 373" + }, + { + "name": "user374", + "first": "First 374", + "last": "Last 374" + }, + { + "name": "user375", + "first": "First 375", + "last": "Last 375" + }, + { + "name": "user376", + "first": "First 376", + "last": "Last 376" + }, + { + "name": "user377", + "first": "First 377", + "last": "Last 377" + }, + { + "name": "user378", + "first": "First 378", + "last": "Last 378" + }, + { + "name": "user379", + "first": "First 379", + "last": "Last 379" + }, + { + "name": "user380", + "first": "First 380", + "last": "Last 380" + }, + { + "name": "user381", + "first": "First 381", + "last": "Last 381" + }, + { + "name": "user382", + "first": "First 382", + "last": "Last 382" + }, + { + "name": "user383", + "first": "First 383", + "last": "Last 383" + }, + { + "name": "user384", + "first": "First 384", + "last": "Last 384" + }, + { + "name": "user385", + "first": "First 385", + "last": "Last 385" + }, + { + "name": "user386", + "first": "First 386", + "last": "Last 386" + }, + { + "name": "user387", + "first": "First 387", + "last": "Last 387" + }, + { + "name": "user388", + "first": "First 388", + "last": "Last 388" + }, + { + "name": "user389", + "first": "First 389", + "last": "Last 389" + }, + { + "name": "user390", + "first": "First 390", + "last": "Last 390" + }, + { + "name": "user391", + "first": "First 391", + "last": "Last 391" + }, + { + "name": "user392", + "first": "First 392", + "last": "Last 392" + }, + { + "name": "user393", + "first": "First 393", + "last": "Last 393" + }, + { + "name": "user394", + "first": "First 394", + "last": "Last 394" + }, + { + "name": "user395", + "first": "First 395", + "last": "Last 395" + }, + { + "name": "user396", + "first": "First 396", + "last": "Last 396" + }, + { + "name": "user397", + "first": "First 397", + "last": "Last 397" + }, + { + "name": "user398", + "first": "First 398", + "last": "Last 398" + }, + { + "name": "user399", + "first": "First 399", + "last": "Last 399" + }, + { + "name": "user400", + "first": "First 400", + "last": "Last 400" + }, + { + "name": "user401", + "first": "First 401", + "last": "Last 401" + }, + { + "name": "user402", + "first": "First 402", + "last": "Last 402" + }, + { + "name": "user403", + "first": "First 403", + "last": "Last 403" + }, + { + "name": "user404", + "first": "First 404", + "last": "Last 404" + }, + { + "name": "user405", + "first": "First 405", + "last": "Last 405" + }, + { + "name": "user406", + "first": "First 406", + "last": "Last 406" + }, + { + "name": "user407", + "first": "First 407", + "last": "Last 407" + }, + { + "name": "user408", + "first": "First 408", + "last": "Last 408" + }, + { + "name": "user409", + "first": "First 409", + "last": "Last 409" + }, + { + "name": "user410", + "first": "First 410", + "last": "Last 410" + }, + { + "name": "user411", + "first": "First 411", + "last": "Last 411" + }, + { + "name": "user412", + "first": "First 412", + "last": "Last 412" + }, + { + "name": "user413", + "first": "First 413", + "last": "Last 413" + }, + { + "name": "user414", + "first": "First 414", + "last": "Last 414" + }, + { + "name": "user415", + "first": "First 415", + "last": "Last 415" + }, + { + "name": "user416", + "first": "First 416", + "last": "Last 416" + }, + { + "name": "user417", + "first": "First 417", + "last": "Last 417" + }, + { + "name": "user418", + "first": "First 418", + "last": "Last 418" + }, + { + "name": "user419", + "first": "First 419", + "last": "Last 419" + }, + { + "name": "user420", + "first": "First 420", + "last": "Last 420" + }, + { + "name": "user421", + "first": "First 421", + "last": "Last 421" + }, + { + "name": "user422", + "first": "First 422", + "last": "Last 422" + }, + { + "name": "user423", + "first": "First 423", + "last": "Last 423" + }, + { + "name": "user424", + "first": "First 424", + "last": "Last 424" + }, + { + "name": "user425", + "first": "First 425", + "last": "Last 425" + }, + { + "name": "user426", + "first": "First 426", + "last": "Last 426" + }, + { + "name": "user427", + "first": "First 427", + "last": "Last 427" + }, + { + "name": "user428", + "first": "First 428", + "last": "Last 428" + }, + { + "name": "user429", + "first": "First 429", + "last": "Last 429" + }, + { + "name": "user430", + "first": "First 430", + "last": "Last 430" + }, + { + "name": "user431", + "first": "First 431", + "last": "Last 431" + }, + { + "name": "user432", + "first": "First 432", + "last": "Last 432" + }, + { + "name": "user433", + "first": "First 433", + "last": "Last 433" + }, + { + "name": "user434", + "first": "First 434", + "last": "Last 434" + }, + { + "name": "user435", + "first": "First 435", + "last": "Last 435" + }, + { + "name": "user436", + "first": "First 436", + "last": "Last 436" + }, + { + "name": "user437", + "first": "First 437", + "last": "Last 437" + }, + { + "name": "user438", + "first": "First 438", + "last": "Last 438" + }, + { + "name": "user439", + "first": "First 439", + "last": "Last 439" + }, + { + "name": "user440", + "first": "First 440", + "last": "Last 440" + }, + { + "name": "user441", + "first": "First 441", + "last": "Last 441" + }, + { + "name": "user442", + "first": "First 442", + "last": "Last 442" + }, + { + "name": "user443", + "first": "First 443", + "last": "Last 443" + }, + { + "name": "user444", + "first": "First 444", + "last": "Last 444" + }, + { + "name": "user445", + "first": "First 445", + "last": "Last 445" + }, + { + "name": "user446", + "first": "First 446", + "last": "Last 446" + }, + { + "name": "user447", + "first": "First 447", + "last": "Last 447" + }, + { + "name": "user448", + "first": "First 448", + "last": "Last 448" + }, + { + "name": "user449", + "first": "First 449", + "last": "Last 449" + }, + { + "name": "user450", + "first": "First 450", + "last": "Last 450" + }, + { + "name": "user451", + "first": "First 451", + "last": "Last 451" + }, + { + "name": "user452", + "first": "First 452", + "last": "Last 452" + }, + { + "name": "user453", + "first": "First 453", + "last": "Last 453" + }, + { + "name": "user454", + "first": "First 454", + "last": "Last 454" + }, + { + "name": "user455", + "first": "First 455", + "last": "Last 455" + }, + { + "name": "user456", + "first": "First 456", + "last": "Last 456" + }, + { + "name": "user457", + "first": "First 457", + "last": "Last 457" + }, + { + "name": "user458", + "first": "First 458", + "last": "Last 458" + }, + { + "name": "user459", + "first": "First 459", + "last": "Last 459" + }, + { + "name": "user460", + "first": "First 460", + "last": "Last 460" + }, + { + "name": "user461", + "first": "First 461", + "last": "Last 461" + }, + { + "name": "user462", + "first": "First 462", + "last": "Last 462" + }, + { + "name": "user463", + "first": "First 463", + "last": "Last 463" + }, + { + "name": "user464", + "first": "First 464", + "last": "Last 464" + }, + { + "name": "user465", + "first": "First 465", + "last": "Last 465" + }, + { + "name": "user466", + "first": "First 466", + "last": "Last 466" + }, + { + "name": "user467", + "first": "First 467", + "last": "Last 467" + }, + { + "name": "user468", + "first": "First 468", + "last": "Last 468" + }, + { + "name": "user469", + "first": "First 469", + "last": "Last 469" + }, + { + "name": "user470", + "first": "First 470", + "last": "Last 470" + }, + { + "name": "user471", + "first": "First 471", + "last": "Last 471" + }, + { + "name": "user472", + "first": "First 472", + "last": "Last 472" + }, + { + "name": "user473", + "first": "First 473", + "last": "Last 473" + }, + { + "name": "user474", + "first": "First 474", + "last": "Last 474" + }, + { + "name": "user475", + "first": "First 475", + "last": "Last 475" + }, + { + "name": "user476", + "first": "First 476", + "last": "Last 476" + }, + { + "name": "user477", + "first": "First 477", + "last": "Last 477" + }, + { + "name": "user478", + "first": "First 478", + "last": "Last 478" + }, + { + "name": "user479", + "first": "First 479", + "last": "Last 479" + }, + { + "name": "user480", + "first": "First 480", + "last": "Last 480" + }, + { + "name": "user481", + "first": "First 481", + "last": "Last 481" + }, + { + "name": "user482", + "first": "First 482", + "last": "Last 482" + }, + { + "name": "user483", + "first": "First 483", + "last": "Last 483" + }, + { + "name": "user484", + "first": "First 484", + "last": "Last 484" + }, + { + "name": "user485", + "first": "First 485", + "last": "Last 485" + }, + { + "name": "user486", + "first": "First 486", + "last": "Last 486" + }, + { + "name": "user487", + "first": "First 487", + "last": "Last 487" + }, + { + "name": "user488", + "first": "First 488", + "last": "Last 488" + }, + { + "name": "user489", + "first": "First 489", + "last": "Last 489" + }, + { + "name": "user490", + "first": "First 490", + "last": "Last 490" + }, + { + "name": "user491", + "first": "First 491", + "last": "Last 491" + }, + { + "name": "user492", + "first": "First 492", + "last": "Last 492" + }, + { + "name": "user493", + "first": "First 493", + "last": "Last 493" + }, + { + "name": "user494", + "first": "First 494", + "last": "Last 494" + }, + { + "name": "user495", + "first": "First 495", + "last": "Last 495" + }, + { + "name": "user496", + "first": "First 496", + "last": "Last 496" + }, + { + "name": "user497", + "first": "First 497", + "last": "Last 497" + }, + { + "name": "user498", + "first": "First 498", + "last": "Last 498" + }, + { + "name": "user499", + "first": "First 499", + "last": "Last 499" + }, + { + "name": "user500", + "first": "First 500", + "last": "Last 500" + }, + { + "name": "user501", + "first": "First 501", + "last": "Last 501" + }, + { + "name": "user502", + "first": "First 502", + "last": "Last 502" + }, + { + "name": "user503", + "first": "First 503", + "last": "Last 503" + }, + { + "name": "user504", + "first": "First 504", + "last": "Last 504" + }, + { + "name": "user505", + "first": "First 505", + "last": "Last 505" + }, + { + "name": "user506", + "first": "First 506", + "last": "Last 506" + }, + { + "name": "user507", + "first": "First 507", + "last": "Last 507" + }, + { + "name": "user508", + "first": "First 508", + "last": "Last 508" + }, + { + "name": "user509", + "first": "First 509", + "last": "Last 509" + }, + { + "name": "user510", + "first": "First 510", + "last": "Last 510" + }, + { + "name": "user511", + "first": "First 511", + "last": "Last 511" + }, + { + "name": "user512", + "first": "First 512", + "last": "Last 512" + }, + { + "name": "user513", + "first": "First 513", + "last": "Last 513" + }, + { + "name": "user514", + "first": "First 514", + "last": "Last 514" + }, + { + "name": "user515", + "first": "First 515", + "last": "Last 515" + }, + { + "name": "user516", + "first": "First 516", + "last": "Last 516" + }, + { + "name": "user517", + "first": "First 517", + "last": "Last 517" + }, + { + "name": "user518", + "first": "First 518", + "last": "Last 518" + }, + { + "name": "user519", + "first": "First 519", + "last": "Last 519" + }, + { + "name": "user520", + "first": "First 520", + "last": "Last 520" + }, + { + "name": "user521", + "first": "First 521", + "last": "Last 521" + }, + { + "name": "user522", + "first": "First 522", + "last": "Last 522" + }, + { + "name": "user523", + "first": "First 523", + "last": "Last 523" + }, + { + "name": "user524", + "first": "First 524", + "last": "Last 524" + }, + { + "name": "user525", + "first": "First 525", + "last": "Last 525" + }, + { + "name": "user526", + "first": "First 526", + "last": "Last 526" + }, + { + "name": "user527", + "first": "First 527", + "last": "Last 527" + }, + { + "name": "user528", + "first": "First 528", + "last": "Last 528" + }, + { + "name": "user529", + "first": "First 529", + "last": "Last 529" + }, + { + "name": "user530", + "first": "First 530", + "last": "Last 530" + }, + { + "name": "user531", + "first": "First 531", + "last": "Last 531" + }, + { + "name": "user532", + "first": "First 532", + "last": "Last 532" + }, + { + "name": "user533", + "first": "First 533", + "last": "Last 533" + }, + { + "name": "user534", + "first": "First 534", + "last": "Last 534" + }, + { + "name": "user535", + "first": "First 535", + "last": "Last 535" + }, + { + "name": "user536", + "first": "First 536", + "last": "Last 536" + }, + { + "name": "user537", + "first": "First 537", + "last": "Last 537" + }, + { + "name": "user538", + "first": "First 538", + "last": "Last 538" + }, + { + "name": "user539", + "first": "First 539", + "last": "Last 539" + }, + { + "name": "user540", + "first": "First 540", + "last": "Last 540" + }, + { + "name": "user541", + "first": "First 541", + "last": "Last 541" + }, + { + "name": "user542", + "first": "First 542", + "last": "Last 542" + }, + { + "name": "user543", + "first": "First 543", + "last": "Last 543" + }, + { + "name": "user544", + "first": "First 544", + "last": "Last 544" + }, + { + "name": "user545", + "first": "First 545", + "last": "Last 545" + }, + { + "name": "user546", + "first": "First 546", + "last": "Last 546" + }, + { + "name": "user547", + "first": "First 547", + "last": "Last 547" + }, + { + "name": "user548", + "first": "First 548", + "last": "Last 548" + }, + { + "name": "user549", + "first": "First 549", + "last": "Last 549" + }, + { + "name": "user550", + "first": "First 550", + "last": "Last 550" + }, + { + "name": "user551", + "first": "First 551", + "last": "Last 551" + }, + { + "name": "user552", + "first": "First 552", + "last": "Last 552" + }, + { + "name": "user553", + "first": "First 553", + "last": "Last 553" + }, + { + "name": "user554", + "first": "First 554", + "last": "Last 554" + }, + { + "name": "user555", + "first": "First 555", + "last": "Last 555" + }, + { + "name": "user556", + "first": "First 556", + "last": "Last 556" + }, + { + "name": "user557", + "first": "First 557", + "last": "Last 557" + }, + { + "name": "user558", + "first": "First 558", + "last": "Last 558" + }, + { + "name": "user559", + "first": "First 559", + "last": "Last 559" + }, + { + "name": "user560", + "first": "First 560", + "last": "Last 560" + }, + { + "name": "user561", + "first": "First 561", + "last": "Last 561" + }, + { + "name": "user562", + "first": "First 562", + "last": "Last 562" + }, + { + "name": "user563", + "first": "First 563", + "last": "Last 563" + }, + { + "name": "user564", + "first": "First 564", + "last": "Last 564" + }, + { + "name": "user565", + "first": "First 565", + "last": "Last 565" + }, + { + "name": "user566", + "first": "First 566", + "last": "Last 566" + }, + { + "name": "user567", + "first": "First 567", + "last": "Last 567" + }, + { + "name": "user568", + "first": "First 568", + "last": "Last 568" + }, + { + "name": "user569", + "first": "First 569", + "last": "Last 569" + }, + { + "name": "user570", + "first": "First 570", + "last": "Last 570" + }, + { + "name": "user571", + "first": "First 571", + "last": "Last 571" + }, + { + "name": "user572", + "first": "First 572", + "last": "Last 572" + }, + { + "name": "user573", + "first": "First 573", + "last": "Last 573" + }, + { + "name": "user574", + "first": "First 574", + "last": "Last 574" + }, + { + "name": "user575", + "first": "First 575", + "last": "Last 575" + }, + { + "name": "user576", + "first": "First 576", + "last": "Last 576" + }, + { + "name": "user577", + "first": "First 577", + "last": "Last 577" + }, + { + "name": "user578", + "first": "First 578", + "last": "Last 578" + }, + { + "name": "user579", + "first": "First 579", + "last": "Last 579" + }, + { + "name": "user580", + "first": "First 580", + "last": "Last 580" + }, + { + "name": "user581", + "first": "First 581", + "last": "Last 581" + }, + { + "name": "user582", + "first": "First 582", + "last": "Last 582" + }, + { + "name": "user583", + "first": "First 583", + "last": "Last 583" + }, + { + "name": "user584", + "first": "First 584", + "last": "Last 584" + }, + { + "name": "user585", + "first": "First 585", + "last": "Last 585" + }, + { + "name": "user586", + "first": "First 586", + "last": "Last 586" + }, + { + "name": "user587", + "first": "First 587", + "last": "Last 587" + }, + { + "name": "user588", + "first": "First 588", + "last": "Last 588" + }, + { + "name": "user589", + "first": "First 589", + "last": "Last 589" + }, + { + "name": "user590", + "first": "First 590", + "last": "Last 590" + }, + { + "name": "user591", + "first": "First 591", + "last": "Last 591" + }, + { + "name": "user592", + "first": "First 592", + "last": "Last 592" + }, + { + "name": "user593", + "first": "First 593", + "last": "Last 593" + }, + { + "name": "user594", + "first": "First 594", + "last": "Last 594" + }, + { + "name": "user595", + "first": "First 595", + "last": "Last 595" + }, + { + "name": "user596", + "first": "First 596", + "last": "Last 596" + }, + { + "name": "user597", + "first": "First 597", + "last": "Last 597" + }, + { + "name": "user598", + "first": "First 598", + "last": "Last 598" + }, + { + "name": "user599", + "first": "First 599", + "last": "Last 599" + }, + { + "name": "user600", + "first": "First 600", + "last": "Last 600" + }, + { + "name": "user601", + "first": "First 601", + "last": "Last 601" + }, + { + "name": "user602", + "first": "First 602", + "last": "Last 602" + }, + { + "name": "user603", + "first": "First 603", + "last": "Last 603" + }, + { + "name": "user604", + "first": "First 604", + "last": "Last 604" + }, + { + "name": "user605", + "first": "First 605", + "last": "Last 605" + }, + { + "name": "user606", + "first": "First 606", + "last": "Last 606" + }, + { + "name": "user607", + "first": "First 607", + "last": "Last 607" + }, + { + "name": "user608", + "first": "First 608", + "last": "Last 608" + }, + { + "name": "user609", + "first": "First 609", + "last": "Last 609" + }, + { + "name": "user610", + "first": "First 610", + "last": "Last 610" + }, + { + "name": "user611", + "first": "First 611", + "last": "Last 611" + }, + { + "name": "user612", + "first": "First 612", + "last": "Last 612" + }, + { + "name": "user613", + "first": "First 613", + "last": "Last 613" + }, + { + "name": "user614", + "first": "First 614", + "last": "Last 614" + }, + { + "name": "user615", + "first": "First 615", + "last": "Last 615" + }, + { + "name": "user616", + "first": "First 616", + "last": "Last 616" + }, + { + "name": "user617", + "first": "First 617", + "last": "Last 617" + }, + { + "name": "user618", + "first": "First 618", + "last": "Last 618" + }, + { + "name": "user619", + "first": "First 619", + "last": "Last 619" + }, + { + "name": "user620", + "first": "First 620", + "last": "Last 620" + }, + { + "name": "user621", + "first": "First 621", + "last": "Last 621" + }, + { + "name": "user622", + "first": "First 622", + "last": "Last 622" + }, + { + "name": "user623", + "first": "First 623", + "last": "Last 623" + }, + { + "name": "user624", + "first": "First 624", + "last": "Last 624" + }, + { + "name": "user625", + "first": "First 625", + "last": "Last 625" + }, + { + "name": "user626", + "first": "First 626", + "last": "Last 626" + }, + { + "name": "user627", + "first": "First 627", + "last": "Last 627" + }, + { + "name": "user628", + "first": "First 628", + "last": "Last 628" + }, + { + "name": "user629", + "first": "First 629", + "last": "Last 629" + }, + { + "name": "user630", + "first": "First 630", + "last": "Last 630" + }, + { + "name": "user631", + "first": "First 631", + "last": "Last 631" + }, + { + "name": "user632", + "first": "First 632", + "last": "Last 632" + }, + { + "name": "user633", + "first": "First 633", + "last": "Last 633" + }, + { + "name": "user634", + "first": "First 634", + "last": "Last 634" + }, + { + "name": "user635", + "first": "First 635", + "last": "Last 635" + }, + { + "name": "user636", + "first": "First 636", + "last": "Last 636" + }, + { + "name": "user637", + "first": "First 637", + "last": "Last 637" + }, + { + "name": "user638", + "first": "First 638", + "last": "Last 638" + }, + { + "name": "user639", + "first": "First 639", + "last": "Last 639" + }, + { + "name": "user640", + "first": "First 640", + "last": "Last 640" + }, + { + "name": "user641", + "first": "First 641", + "last": "Last 641" + }, + { + "name": "user642", + "first": "First 642", + "last": "Last 642" + }, + { + "name": "user643", + "first": "First 643", + "last": "Last 643" + }, + { + "name": "user644", + "first": "First 644", + "last": "Last 644" + }, + { + "name": "user645", + "first": "First 645", + "last": "Last 645" + }, + { + "name": "user646", + "first": "First 646", + "last": "Last 646" + }, + { + "name": "user647", + "first": "First 647", + "last": "Last 647" + }, + { + "name": "user648", + "first": "First 648", + "last": "Last 648" + }, + { + "name": "user649", + "first": "First 649", + "last": "Last 649" + }, + { + "name": "user650", + "first": "First 650", + "last": "Last 650" + }, + { + "name": "user651", + "first": "First 651", + "last": "Last 651" + }, + { + "name": "user652", + "first": "First 652", + "last": "Last 652" + }, + { + "name": "user653", + "first": "First 653", + "last": "Last 653" + }, + { + "name": "user654", + "first": "First 654", + "last": "Last 654" + }, + { + "name": "user655", + "first": "First 655", + "last": "Last 655" + }, + { + "name": "user656", + "first": "First 656", + "last": "Last 656" + }, + { + "name": "user657", + "first": "First 657", + "last": "Last 657" + }, + { + "name": "user658", + "first": "First 658", + "last": "Last 658" + }, + { + "name": "user659", + "first": "First 659", + "last": "Last 659" + }, + { + "name": "user660", + "first": "First 660", + "last": "Last 660" + }, + { + "name": "user661", + "first": "First 661", + "last": "Last 661" + }, + { + "name": "user662", + "first": "First 662", + "last": "Last 662" + }, + { + "name": "user663", + "first": "First 663", + "last": "Last 663" + }, + { + "name": "user664", + "first": "First 664", + "last": "Last 664" + }, + { + "name": "user665", + "first": "First 665", + "last": "Last 665" + }, + { + "name": "user666", + "first": "First 666", + "last": "Last 666" + }, + { + "name": "user667", + "first": "First 667", + "last": "Last 667" + }, + { + "name": "user668", + "first": "First 668", + "last": "Last 668" + }, + { + "name": "user669", + "first": "First 669", + "last": "Last 669" + }, + { + "name": "user670", + "first": "First 670", + "last": "Last 670" + }, + { + "name": "user671", + "first": "First 671", + "last": "Last 671" + }, + { + "name": "user672", + "first": "First 672", + "last": "Last 672" + }, + { + "name": "user673", + "first": "First 673", + "last": "Last 673" + }, + { + "name": "user674", + "first": "First 674", + "last": "Last 674" + }, + { + "name": "user675", + "first": "First 675", + "last": "Last 675" + }, + { + "name": "user676", + "first": "First 676", + "last": "Last 676" + }, + { + "name": "user677", + "first": "First 677", + "last": "Last 677" + }, + { + "name": "user678", + "first": "First 678", + "last": "Last 678" + }, + { + "name": "user679", + "first": "First 679", + "last": "Last 679" + }, + { + "name": "user680", + "first": "First 680", + "last": "Last 680" + }, + { + "name": "user681", + "first": "First 681", + "last": "Last 681" + }, + { + "name": "user682", + "first": "First 682", + "last": "Last 682" + }, + { + "name": "user683", + "first": "First 683", + "last": "Last 683" + }, + { + "name": "user684", + "first": "First 684", + "last": "Last 684" + }, + { + "name": "user685", + "first": "First 685", + "last": "Last 685" + }, + { + "name": "user686", + "first": "First 686", + "last": "Last 686" + }, + { + "name": "user687", + "first": "First 687", + "last": "Last 687" + }, + { + "name": "user688", + "first": "First 688", + "last": "Last 688" + }, + { + "name": "user689", + "first": "First 689", + "last": "Last 689" + }, + { + "name": "user690", + "first": "First 690", + "last": "Last 690" + }, + { + "name": "user691", + "first": "First 691", + "last": "Last 691" + }, + { + "name": "user692", + "first": "First 692", + "last": "Last 692" + }, + { + "name": "user693", + "first": "First 693", + "last": "Last 693" + }, + { + "name": "user694", + "first": "First 694", + "last": "Last 694" + }, + { + "name": "user695", + "first": "First 695", + "last": "Last 695" + }, + { + "name": "user696", + "first": "First 696", + "last": "Last 696" + }, + { + "name": "user697", + "first": "First 697", + "last": "Last 697" + }, + { + "name": "user698", + "first": "First 698", + "last": "Last 698" + }, + { + "name": "user699", + "first": "First 699", + "last": "Last 699" + }, + { + "name": "user700", + "first": "First 700", + "last": "Last 700" + }, + { + "name": "user701", + "first": "First 701", + "last": "Last 701" + }, + { + "name": "user702", + "first": "First 702", + "last": "Last 702" + }, + { + "name": "user703", + "first": "First 703", + "last": "Last 703" + }, + { + "name": "user704", + "first": "First 704", + "last": "Last 704" + }, + { + "name": "user705", + "first": "First 705", + "last": "Last 705" + }, + { + "name": "user706", + "first": "First 706", + "last": "Last 706" + }, + { + "name": "user707", + "first": "First 707", + "last": "Last 707" + }, + { + "name": "user708", + "first": "First 708", + "last": "Last 708" + }, + { + "name": "user709", + "first": "First 709", + "last": "Last 709" + }, + { + "name": "user710", + "first": "First 710", + "last": "Last 710" + }, + { + "name": "user711", + "first": "First 711", + "last": "Last 711" + }, + { + "name": "user712", + "first": "First 712", + "last": "Last 712" + }, + { + "name": "user713", + "first": "First 713", + "last": "Last 713" + }, + { + "name": "user714", + "first": "First 714", + "last": "Last 714" + }, + { + "name": "user715", + "first": "First 715", + "last": "Last 715" + }, + { + "name": "user716", + "first": "First 716", + "last": "Last 716" + }, + { + "name": "user717", + "first": "First 717", + "last": "Last 717" + }, + { + "name": "user718", + "first": "First 718", + "last": "Last 718" + }, + { + "name": "user719", + "first": "First 719", + "last": "Last 719" + }, + { + "name": "user720", + "first": "First 720", + "last": "Last 720" + }, + { + "name": "user721", + "first": "First 721", + "last": "Last 721" + }, + { + "name": "user722", + "first": "First 722", + "last": "Last 722" + }, + { + "name": "user723", + "first": "First 723", + "last": "Last 723" + }, + { + "name": "user724", + "first": "First 724", + "last": "Last 724" + }, + { + "name": "user725", + "first": "First 725", + "last": "Last 725" + }, + { + "name": "user726", + "first": "First 726", + "last": "Last 726" + }, + { + "name": "user727", + "first": "First 727", + "last": "Last 727" + }, + { + "name": "user728", + "first": "First 728", + "last": "Last 728" + }, + { + "name": "user729", + "first": "First 729", + "last": "Last 729" + }, + { + "name": "user730", + "first": "First 730", + "last": "Last 730" + }, + { + "name": "user731", + "first": "First 731", + "last": "Last 731" + }, + { + "name": "user732", + "first": "First 732", + "last": "Last 732" + }, + { + "name": "user733", + "first": "First 733", + "last": "Last 733" + }, + { + "name": "user734", + "first": "First 734", + "last": "Last 734" + }, + { + "name": "user735", + "first": "First 735", + "last": "Last 735" + }, + { + "name": "user736", + "first": "First 736", + "last": "Last 736" + }, + { + "name": "user737", + "first": "First 737", + "last": "Last 737" + }, + { + "name": "user738", + "first": "First 738", + "last": "Last 738" + }, + { + "name": "user739", + "first": "First 739", + "last": "Last 739" + }, + { + "name": "user740", + "first": "First 740", + "last": "Last 740" + }, + { + "name": "user741", + "first": "First 741", + "last": "Last 741" + }, + { + "name": "user742", + "first": "First 742", + "last": "Last 742" + }, + { + "name": "user743", + "first": "First 743", + "last": "Last 743" + }, + { + "name": "user744", + "first": "First 744", + "last": "Last 744" + }, + { + "name": "user745", + "first": "First 745", + "last": "Last 745" + }, + { + "name": "user746", + "first": "First 746", + "last": "Last 746" + }, + { + "name": "user747", + "first": "First 747", + "last": "Last 747" + }, + { + "name": "user748", + "first": "First 748", + "last": "Last 748" + }, + { + "name": "user749", + "first": "First 749", + "last": "Last 749" + }, + { + "name": "user750", + "first": "First 750", + "last": "Last 750" + }, + { + "name": "user751", + "first": "First 751", + "last": "Last 751" + }, + { + "name": "user752", + "first": "First 752", + "last": "Last 752" + }, + { + "name": "user753", + "first": "First 753", + "last": "Last 753" + }, + { + "name": "user754", + "first": "First 754", + "last": "Last 754" + }, + { + "name": "user755", + "first": "First 755", + "last": "Last 755" + }, + { + "name": "user756", + "first": "First 756", + "last": "Last 756" + }, + { + "name": "user757", + "first": "First 757", + "last": "Last 757" + }, + { + "name": "user758", + "first": "First 758", + "last": "Last 758" + }, + { + "name": "user759", + "first": "First 759", + "last": "Last 759" + }, + { + "name": "user760", + "first": "First 760", + "last": "Last 760" + }, + { + "name": "user761", + "first": "First 761", + "last": "Last 761" + }, + { + "name": "user762", + "first": "First 762", + "last": "Last 762" + }, + { + "name": "user763", + "first": "First 763", + "last": "Last 763" + }, + { + "name": "user764", + "first": "First 764", + "last": "Last 764" + }, + { + "name": "user765", + "first": "First 765", + "last": "Last 765" + }, + { + "name": "user766", + "first": "First 766", + "last": "Last 766" + }, + { + "name": "user767", + "first": "First 767", + "last": "Last 767" + }, + { + "name": "user768", + "first": "First 768", + "last": "Last 768" + }, + { + "name": "user769", + "first": "First 769", + "last": "Last 769" + }, + { + "name": "user770", + "first": "First 770", + "last": "Last 770" + }, + { + "name": "user771", + "first": "First 771", + "last": "Last 771" + }, + { + "name": "user772", + "first": "First 772", + "last": "Last 772" + }, + { + "name": "user773", + "first": "First 773", + "last": "Last 773" + }, + { + "name": "user774", + "first": "First 774", + "last": "Last 774" + }, + { + "name": "user775", + "first": "First 775", + "last": "Last 775" + }, + { + "name": "user776", + "first": "First 776", + "last": "Last 776" + }, + { + "name": "user777", + "first": "First 777", + "last": "Last 777" + }, + { + "name": "user778", + "first": "First 778", + "last": "Last 778" + }, + { + "name": "user779", + "first": "First 779", + "last": "Last 779" + }, + { + "name": "user780", + "first": "First 780", + "last": "Last 780" + }, + { + "name": "user781", + "first": "First 781", + "last": "Last 781" + }, + { + "name": "user782", + "first": "First 782", + "last": "Last 782" + }, + { + "name": "user783", + "first": "First 783", + "last": "Last 783" + }, + { + "name": "user784", + "first": "First 784", + "last": "Last 784" + }, + { + "name": "user785", + "first": "First 785", + "last": "Last 785" + }, + { + "name": "user786", + "first": "First 786", + "last": "Last 786" + }, + { + "name": "user787", + "first": "First 787", + "last": "Last 787" + }, + { + "name": "user788", + "first": "First 788", + "last": "Last 788" + }, + { + "name": "user789", + "first": "First 789", + "last": "Last 789" + }, + { + "name": "user790", + "first": "First 790", + "last": "Last 790" + }, + { + "name": "user791", + "first": "First 791", + "last": "Last 791" + }, + { + "name": "user792", + "first": "First 792", + "last": "Last 792" + }, + { + "name": "user793", + "first": "First 793", + "last": "Last 793" + }, + { + "name": "user794", + "first": "First 794", + "last": "Last 794" + }, + { + "name": "user795", + "first": "First 795", + "last": "Last 795" + }, + { + "name": "user796", + "first": "First 796", + "last": "Last 796" + }, + { + "name": "user797", + "first": "First 797", + "last": "Last 797" + }, + { + "name": "user798", + "first": "First 798", + "last": "Last 798" + }, + { + "name": "user799", + "first": "First 799", + "last": "Last 799" + }, + { + "name": "user800", + "first": "First 800", + "last": "Last 800" + }, + { + "name": "user801", + "first": "First 801", + "last": "Last 801" + }, + { + "name": "user802", + "first": "First 802", + "last": "Last 802" + }, + { + "name": "user803", + "first": "First 803", + "last": "Last 803" + }, + { + "name": "user804", + "first": "First 804", + "last": "Last 804" + }, + { + "name": "user805", + "first": "First 805", + "last": "Last 805" + }, + { + "name": "user806", + "first": "First 806", + "last": "Last 806" + }, + { + "name": "user807", + "first": "First 807", + "last": "Last 807" + }, + { + "name": "user808", + "first": "First 808", + "last": "Last 808" + }, + { + "name": "user809", + "first": "First 809", + "last": "Last 809" + }, + { + "name": "user810", + "first": "First 810", + "last": "Last 810" + }, + { + "name": "user811", + "first": "First 811", + "last": "Last 811" + }, + { + "name": "user812", + "first": "First 812", + "last": "Last 812" + }, + { + "name": "user813", + "first": "First 813", + "last": "Last 813" + }, + { + "name": "user814", + "first": "First 814", + "last": "Last 814" + }, + { + "name": "user815", + "first": "First 815", + "last": "Last 815" + }, + { + "name": "user816", + "first": "First 816", + "last": "Last 816" + }, + { + "name": "user817", + "first": "First 817", + "last": "Last 817" + }, + { + "name": "user818", + "first": "First 818", + "last": "Last 818" + }, + { + "name": "user819", + "first": "First 819", + "last": "Last 819" + }, + { + "name": "user820", + "first": "First 820", + "last": "Last 820" + }, + { + "name": "user821", + "first": "First 821", + "last": "Last 821" + }, + { + "name": "user822", + "first": "First 822", + "last": "Last 822" + }, + { + "name": "user823", + "first": "First 823", + "last": "Last 823" + }, + { + "name": "user824", + "first": "First 824", + "last": "Last 824" + }, + { + "name": "user825", + "first": "First 825", + "last": "Last 825" + }, + { + "name": "user826", + "first": "First 826", + "last": "Last 826" + }, + { + "name": "user827", + "first": "First 827", + "last": "Last 827" + }, + { + "name": "user828", + "first": "First 828", + "last": "Last 828" + }, + { + "name": "user829", + "first": "First 829", + "last": "Last 829" + }, + { + "name": "user830", + "first": "First 830", + "last": "Last 830" + }, + { + "name": "user831", + "first": "First 831", + "last": "Last 831" + }, + { + "name": "user832", + "first": "First 832", + "last": "Last 832" + }, + { + "name": "user833", + "first": "First 833", + "last": "Last 833" + }, + { + "name": "user834", + "first": "First 834", + "last": "Last 834" + }, + { + "name": "user835", + "first": "First 835", + "last": "Last 835" + }, + { + "name": "user836", + "first": "First 836", + "last": "Last 836" + }, + { + "name": "user837", + "first": "First 837", + "last": "Last 837" + }, + { + "name": "user838", + "first": "First 838", + "last": "Last 838" + }, + { + "name": "user839", + "first": "First 839", + "last": "Last 839" + }, + { + "name": "user840", + "first": "First 840", + "last": "Last 840" + }, + { + "name": "user841", + "first": "First 841", + "last": "Last 841" + }, + { + "name": "user842", + "first": "First 842", + "last": "Last 842" + }, + { + "name": "user843", + "first": "First 843", + "last": "Last 843" + }, + { + "name": "user844", + "first": "First 844", + "last": "Last 844" + }, + { + "name": "user845", + "first": "First 845", + "last": "Last 845" + }, + { + "name": "user846", + "first": "First 846", + "last": "Last 846" + }, + { + "name": "user847", + "first": "First 847", + "last": "Last 847" + }, + { + "name": "user848", + "first": "First 848", + "last": "Last 848" + }, + { + "name": "user849", + "first": "First 849", + "last": "Last 849" + }, + { + "name": "user850", + "first": "First 850", + "last": "Last 850" + }, + { + "name": "user851", + "first": "First 851", + "last": "Last 851" + }, + { + "name": "user852", + "first": "First 852", + "last": "Last 852" + }, + { + "name": "user853", + "first": "First 853", + "last": "Last 853" + }, + { + "name": "user854", + "first": "First 854", + "last": "Last 854" + }, + { + "name": "user855", + "first": "First 855", + "last": "Last 855" + }, + { + "name": "user856", + "first": "First 856", + "last": "Last 856" + }, + { + "name": "user857", + "first": "First 857", + "last": "Last 857" + }, + { + "name": "user858", + "first": "First 858", + "last": "Last 858" + }, + { + "name": "user859", + "first": "First 859", + "last": "Last 859" + }, + { + "name": "user860", + "first": "First 860", + "last": "Last 860" + }, + { + "name": "user861", + "first": "First 861", + "last": "Last 861" + }, + { + "name": "user862", + "first": "First 862", + "last": "Last 862" + }, + { + "name": "user863", + "first": "First 863", + "last": "Last 863" + }, + { + "name": "user864", + "first": "First 864", + "last": "Last 864" + }, + { + "name": "user865", + "first": "First 865", + "last": "Last 865" + }, + { + "name": "user866", + "first": "First 866", + "last": "Last 866" + }, + { + "name": "user867", + "first": "First 867", + "last": "Last 867" + }, + { + "name": "user868", + "first": "First 868", + "last": "Last 868" + }, + { + "name": "user869", + "first": "First 869", + "last": "Last 869" + }, + { + "name": "user870", + "first": "First 870", + "last": "Last 870" + }, + { + "name": "user871", + "first": "First 871", + "last": "Last 871" + }, + { + "name": "user872", + "first": "First 872", + "last": "Last 872" + }, + { + "name": "user873", + "first": "First 873", + "last": "Last 873" + }, + { + "name": "user874", + "first": "First 874", + "last": "Last 874" + }, + { + "name": "user875", + "first": "First 875", + "last": "Last 875" + }, + { + "name": "user876", + "first": "First 876", + "last": "Last 876" + }, + { + "name": "user877", + "first": "First 877", + "last": "Last 877" + }, + { + "name": "user878", + "first": "First 878", + "last": "Last 878" + }, + { + "name": "user879", + "first": "First 879", + "last": "Last 879" + }, + { + "name": "user880", + "first": "First 880", + "last": "Last 880" + }, + { + "name": "user881", + "first": "First 881", + "last": "Last 881" + }, + { + "name": "user882", + "first": "First 882", + "last": "Last 882" + }, + { + "name": "user883", + "first": "First 883", + "last": "Last 883" + }, + { + "name": "user884", + "first": "First 884", + "last": "Last 884" + }, + { + "name": "user885", + "first": "First 885", + "last": "Last 885" + }, + { + "name": "user886", + "first": "First 886", + "last": "Last 886" + }, + { + "name": "user887", + "first": "First 887", + "last": "Last 887" + }, + { + "name": "user888", + "first": "First 888", + "last": "Last 888" + }, + { + "name": "user889", + "first": "First 889", + "last": "Last 889" + }, + { + "name": "user890", + "first": "First 890", + "last": "Last 890" + }, + { + "name": "user891", + "first": "First 891", + "last": "Last 891" + }, + { + "name": "user892", + "first": "First 892", + "last": "Last 892" + }, + { + "name": "user893", + "first": "First 893", + "last": "Last 893" + }, + { + "name": "user894", + "first": "First 894", + "last": "Last 894" + }, + { + "name": "user895", + "first": "First 895", + "last": "Last 895" + }, + { + "name": "user896", + "first": "First 896", + "last": "Last 896" + }, + { + "name": "user897", + "first": "First 897", + "last": "Last 897" + }, + { + "name": "user898", + "first": "First 898", + "last": "Last 898" + }, + { + "name": "user899", + "first": "First 899", + "last": "Last 899" + }, + { + "name": "user900", + "first": "First 900", + "last": "Last 900" + }, + { + "name": "user901", + "first": "First 901", + "last": "Last 901" + }, + { + "name": "user902", + "first": "First 902", + "last": "Last 902" + }, + { + "name": "user903", + "first": "First 903", + "last": "Last 903" + }, + { + "name": "user904", + "first": "First 904", + "last": "Last 904" + }, + { + "name": "user905", + "first": "First 905", + "last": "Last 905" + }, + { + "name": "user906", + "first": "First 906", + "last": "Last 906" + }, + { + "name": "user907", + "first": "First 907", + "last": "Last 907" + }, + { + "name": "user908", + "first": "First 908", + "last": "Last 908" + }, + { + "name": "user909", + "first": "First 909", + "last": "Last 909" + }, + { + "name": "user910", + "first": "First 910", + "last": "Last 910" + }, + { + "name": "user911", + "first": "First 911", + "last": "Last 911" + }, + { + "name": "user912", + "first": "First 912", + "last": "Last 912" + }, + { + "name": "user913", + "first": "First 913", + "last": "Last 913" + }, + { + "name": "user914", + "first": "First 914", + "last": "Last 914" + }, + { + "name": "user915", + "first": "First 915", + "last": "Last 915" + }, + { + "name": "user916", + "first": "First 916", + "last": "Last 916" + }, + { + "name": "user917", + "first": "First 917", + "last": "Last 917" + }, + { + "name": "user918", + "first": "First 918", + "last": "Last 918" + }, + { + "name": "user919", + "first": "First 919", + "last": "Last 919" + }, + { + "name": "user920", + "first": "First 920", + "last": "Last 920" + }, + { + "name": "user921", + "first": "First 921", + "last": "Last 921" + }, + { + "name": "user922", + "first": "First 922", + "last": "Last 922" + }, + { + "name": "user923", + "first": "First 923", + "last": "Last 923" + }, + { + "name": "user924", + "first": "First 924", + "last": "Last 924" + }, + { + "name": "user925", + "first": "First 925", + "last": "Last 925" + }, + { + "name": "user926", + "first": "First 926", + "last": "Last 926" + }, + { + "name": "user927", + "first": "First 927", + "last": "Last 927" + }, + { + "name": "user928", + "first": "First 928", + "last": "Last 928" + }, + { + "name": "user929", + "first": "First 929", + "last": "Last 929" + }, + { + "name": "user930", + "first": "First 930", + "last": "Last 930" + }, + { + "name": "user931", + "first": "First 931", + "last": "Last 931" + }, + { + "name": "user932", + "first": "First 932", + "last": "Last 932" + }, + { + "name": "user933", + "first": "First 933", + "last": "Last 933" + }, + { + "name": "user934", + "first": "First 934", + "last": "Last 934" + }, + { + "name": "user935", + "first": "First 935", + "last": "Last 935" + }, + { + "name": "user936", + "first": "First 936", + "last": "Last 936" + }, + { + "name": "user937", + "first": "First 937", + "last": "Last 937" + }, + { + "name": "user938", + "first": "First 938", + "last": "Last 938" + }, + { + "name": "user939", + "first": "First 939", + "last": "Last 939" + }, + { + "name": "user940", + "first": "First 940", + "last": "Last 940" + }, + { + "name": "user941", + "first": "First 941", + "last": "Last 941" + }, + { + "name": "user942", + "first": "First 942", + "last": "Last 942" + }, + { + "name": "user943", + "first": "First 943", + "last": "Last 943" + }, + { + "name": "user944", + "first": "First 944", + "last": "Last 944" + }, + { + "name": "user945", + "first": "First 945", + "last": "Last 945" + }, + { + "name": "user946", + "first": "First 946", + "last": "Last 946" + }, + { + "name": "user947", + "first": "First 947", + "last": "Last 947" + }, + { + "name": "user948", + "first": "First 948", + "last": "Last 948" + }, + { + "name": "user949", + "first": "First 949", + "last": "Last 949" + }, + { + "name": "user950", + "first": "First 950", + "last": "Last 950" + }, + { + "name": "user951", + "first": "First 951", + "last": "Last 951" + }, + { + "name": "user952", + "first": "First 952", + "last": "Last 952" + }, + { + "name": "user953", + "first": "First 953", + "last": "Last 953" + }, + { + "name": "user954", + "first": "First 954", + "last": "Last 954" + }, + { + "name": "user955", + "first": "First 955", + "last": "Last 955" + }, + { + "name": "user956", + "first": "First 956", + "last": "Last 956" + }, + { + "name": "user957", + "first": "First 957", + "last": "Last 957" + }, + { + "name": "user958", + "first": "First 958", + "last": "Last 958" + }, + { + "name": "user959", + "first": "First 959", + "last": "Last 959" + }, + { + "name": "user960", + "first": "First 960", + "last": "Last 960" + }, + { + "name": "user961", + "first": "First 961", + "last": "Last 961" + }, + { + "name": "user962", + "first": "First 962", + "last": "Last 962" + }, + { + "name": "user963", + "first": "First 963", + "last": "Last 963" + }, + { + "name": "user964", + "first": "First 964", + "last": "Last 964" + }, + { + "name": "user965", + "first": "First 965", + "last": "Last 965" + }, + { + "name": "user966", + "first": "First 966", + "last": "Last 966" + }, + { + "name": "user967", + "first": "First 967", + "last": "Last 967" + }, + { + "name": "user968", + "first": "First 968", + "last": "Last 968" + }, + { + "name": "user969", + "first": "First 969", + "last": "Last 969" + }, + { + "name": "user970", + "first": "First 970", + "last": "Last 970" + }, + { + "name": "user971", + "first": "First 971", + "last": "Last 971" + }, + { + "name": "user972", + "first": "First 972", + "last": "Last 972" + }, + { + "name": "user973", + "first": "First 973", + "last": "Last 973" + }, + { + "name": "user974", + "first": "First 974", + "last": "Last 974" + }, + { + "name": "user975", + "first": "First 975", + "last": "Last 975" + }, + { + "name": "user976", + "first": "First 976", + "last": "Last 976" + }, + { + "name": "user977", + "first": "First 977", + "last": "Last 977" + }, + { + "name": "user978", + "first": "First 978", + "last": "Last 978" + }, + { + "name": "user979", + "first": "First 979", + "last": "Last 979" + }, + { + "name": "user980", + "first": "First 980", + "last": "Last 980" + }, + { + "name": "user981", + "first": "First 981", + "last": "Last 981" + }, + { + "name": "user982", + "first": "First 982", + "last": "Last 982" + }, + { + "name": "user983", + "first": "First 983", + "last": "Last 983" + }, + { + "name": "user984", + "first": "First 984", + "last": "Last 984" + }, + { + "name": "user985", + "first": "First 985", + "last": "Last 985" + }, + { + "name": "user986", + "first": "First 986", + "last": "Last 986" + }, + { + "name": "user987", + "first": "First 987", + "last": "Last 987" + }, + { + "name": "user988", + "first": "First 988", + "last": "Last 988" + }, + { + "name": "user989", + "first": "First 989", + "last": "Last 989" + }, + { + "name": "user990", + "first": "First 990", + "last": "Last 990" + }, + { + "name": "user991", + "first": "First 991", + "last": "Last 991" + }, + { + "name": "user992", + "first": "First 992", + "last": "Last 992" + }, + { + "name": "user993", + "first": "First 993", + "last": "Last 993" + }, + { + "name": "user994", + "first": "First 994", + "last": "Last 994" + }, + { + "name": "user995", + "first": "First 995", + "last": "Last 995" + }, + { + "name": "user996", + "first": "First 996", + "last": "Last 996" + }, + { + "name": "user997", + "first": "First 997", + "last": "Last 997" + }, + { + "name": "user998", + "first": "First 998", + "last": "Last 998" + }, + { + "name": "user999", + "first": "First 999", + "last": "Last 999" + }, + { + "name": "user1000", + "first": "First 1000", + "last": "Last 1000" + } + ] +} diff --git a/tests/user/users_present.sh b/tests/user/users_present.sh new file mode 100644 index 0000000..c23f8d1 --- /dev/null +++ b/tests/user/users_present.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +NUM=1000 +FILE="users_present.json" + +echo "{" > $FILE + +echo " \"users\": [" >> $FILE + +for i in $(seq 1 $NUM); do + echo " {" >> $FILE + echo " \"name\": \"user$i\"," >> $FILE + echo " \"first\": \"First $i\"," >> $FILE + echo " \"last\": \"Last $i\"" >> $FILE + if [ $i -lt $NUM ]; then + echo " }," >> $FILE + else + echo " }" >> $FILE + fi +done + +echo " ]" >> $FILE + +echo "}" >> $FILE diff --git a/utils/ansible-ipa-client-install b/utils/ansible-ipa-client-install new file mode 100755 index 0000000..9f9ae0e --- /dev/null +++ b/utils/ansible-ipa-client-install @@ -0,0 +1,412 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Copyright (C) 2019 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +import os +import sys +import shutil +import tempfile +import argparse +import traceback +import subprocess + + +def parse_options(): + usage = "Usage: ansible-ipa-client-install [options] " + + parser = argparse.ArgumentParser(usage=usage) + parser.add_argument("--version", dest="version", + action="store_true", + help="show program's version number and exit") + parser.add_argument("-U", "--unattended", dest="unattended", + action="store_true", + help="unattended (un)installation never prompts the " + "user") + parser.add_argument("--uninstall", dest="uninstall", + action="store_true", + help="uninstall an existing installation. The " + "uninstall can be run with --unattended option") + # basic + parser.add_argument("-p", "--principal", dest="principal", + default=None, + help="principal to use to join the IPA realm") + parser.add_argument("--ca-cert-file", dest="ca_cert_file", + default=None, + help="load the CA certificate from this file") + parser.add_argument("--ip-address", dest="ip_addresses", + metavar="IP_ADDRESS", + action='append', default=None, + help="Specify IP address that should be added to DNS. " + "This option can be used multiple times") + parser.add_argument("--all-ip-addresses", dest="all_ip_addresses", + action='store_true', + help="All routable IP addresses configured on any " + "interface will be added to DNS") + parser.add_argument("--domain", dest="domain", + default=None, + help="primary DNS domain of the IPA deployment (not " + "necessarily related to the current hostname)") + parser.add_argument("--server", dest="servers", + metavar="SERVER", + action='append', default=None, + help="FQDN of IPA server") + parser.add_argument("--realm", dest="realm", + default=None, + help="Kerberos realm name of the IPA deployment " + "(typically an upper-cased name of the primary DNS " + "domain)") + parser.add_argument("--hostname", dest="hostname", + default=None, + help="The hostname of this machine (FQDN). If " + "specified, the hostname will be set and the system " + "configuration will be updated to persist over " + "reboot. By default the result of getfqdn() call " + "from Python's socket module is used.") + # client + parser.add_argument("-w", "--password", dest="password", + default=None, + help="password to join the IPA realm (assumes bulk " + "password unless principal is also set)") + parser.add_argument("-W", dest="password_prompt", + action="store_true", + help="Prompt for a password to join the IPA realm") + parser.add_argument("-f", "--force", dest="force", + action="store_true", + help="force setting of LDAP/Kerberos conf") + parser.add_argument("--configure-firefox", dest="configure_firefox", + action="store_true", + help="configure Firefox to use IPA domain credentials") + parser.add_argument("--firefox-dir", dest="firefox_dir", + default=None, + help="specify directory where Firefox is installed " + "(for example: '/usr/lib/firefox')") + parser.add_argument("-k", "--keytab", dest="keytab", + default=None, + help="path to backed up keytab from previous " + "enrollment") + parser.add_argument("--mkhomedir", dest="mkhomedir", + action="store_true", + help="create home directories for users on their " + "first login") + parser.add_argument("--force-join", dest="force_join", + action="store_true", + help="Force client enrollment even if already " + "enrolled") + parser.add_argument("--ntp-server", dest="ntp_servers", + metavar="NTP_SERVER", + action='append', default=None, + help="ntp server to use. This option can be used " + "multiple times") + parser.add_argument("--ntp-pool", dest="ntp_pool", + default=None, + help="ntp server pool to use") + parser.add_argument("-N", "--no-ntp", dest="no_ntp", + action="store_true", + help="do not configure ntp") + parser.add_argument("--nisdomain", dest="nisdomain", + default=None, + help="NIS domain name") + parser.add_argument("--no-nisdomain", dest="no_nisdomain", + action="store_true", + help="do not configure NIS domain name") + parser.add_argument("--ssh-trust-dns", dest="ssh_trust_dns", + action="store_true", + help="configure OpenSSH client to trust DNS SSHFP " + "records") + parser.add_argument("--no-ssh", dest="no_ssh", + action="store_true", + help="do not configure OpenSSH client") + parser.add_argument("--no-sshd", dest="no_sshd", + action="store_true", + help="do not configure OpenSSH server") + parser.add_argument("--no-sudo", dest="no_sudo", + action="store_true", + help="do not configure SSSD as data source for sudo") + parser.add_argument("--no-dns-sshfp", dest="no_dns_sshfp", + action="store_true", + help="do not automatically create DNS SSHFP records") + parser.add_argument("--kinit-attempts", dest="kinit_attempts", + type=int, default=None, + help="number of attempts to obtain host TGT (defaults " + "to 5)") + # sssd + parser.add_argument("--fixed-primary", dest="fixed_primary", + action="store_true", + help="Configure sssd to use fixed server as primary " + "IPA server") + parser.add_argument("--permit", dest="permit", + action="store_true", + help="disable access rules by default, permit all " + "access") + parser.add_argument("--enable-dns-updates", dest="enable_dns_updates", + action="store_true", + help="Configures the machine to attempt dns updates " + "when the ip address changes") + parser.add_argument("--no-krb5-offline-passwords", + dest="no_krb5_offline_passwords", + action="store_true", + help="Configure SSSD not to store user password when " + "the server is offline") + parser.add_argument("--preserve-sssd", dest="preserve_sssd", + action="store_true", + help="Preserve old SSSD configuration if possible") + + # automount + parser.add_argument("--automount-location", dest="automount_location", + default=None, + help="Automount location") + # logging and output + parser.add_argument("-v", "--verbose", dest="verbose", + action="store_true", + help="print debugging information") + parser.add_argument("-d", "--debug", dest="verbose", + action="store_true", + help="alias for --verbose (deprecated)") + parser.add_argument("-q", "--quiet", dest="quiet", + action="store_true", + help="output only errors") + parser.add_argument("--log-file", dest="log_file", + help="log to the given file") + # ansible + parser.add_argument("--ipaclient-use-otp", dest="ipaclient_use_otp", + choices=("yes", "no"), default=None, + help="The bool value defines if a one-time password " + "will be generated to join a new or existing host. " + "Default: no") + parser.add_argument("--ipaclient-allow-repair", + dest="ipaclient_allow_repair", + choices=("yes", "no"), default=None, + help="The bool value defines if an already joined or " + "partly set-up client can be repaired. Default: no") + parser.add_argument("--ipaclient-install-packages", + dest="ipaclient_install_packages", + choices=("yes", "no"), default=None, + help="The bool value defines if the needed packages " + "are installed on the node. Default: yes") + # playbook + parser.add_argument("--playbook-dir", + dest="playbook_dir", + default=None, + help="If defined will be used as to create inventory " + "file and playbook in. The files will not be removed " + "after the playbook processing ended.") + parser.add_argument("--become-method", + dest="become_method", + default="sudo", + help="privilege escalation method to use " + "(default=sudo), use `ansible-doc -t become -l` to " + "list valid choices.") + parser.add_argument("--ansible-verbose", + dest="ansible_verbose", + type=int, default=None, + help="privilege escalation method to use " + "(default=sudo), use `ansible-doc -t become -l` to " + "list valid choices.") + + options, args = parser.parse_known_args() + + if options.playbook_dir and not os.path.isdir(options.playbook_dir): + parser.error("playbook dir does not exist") + + if options.password_prompt: + parser.error("password prompt is not possible with ansible") + if options.log_file: + parser.error("log_file is not supported") + + if len(args) < 1: + parser.error("ansible host not set") + elif len(args) > 1: + parser.error("too many arguments: %s" % ",".join(args)) + + return options, args + + +def run_cmd(args): + """ + Execute an external command. + """ + p_out = subprocess.PIPE + p_err = subprocess.STDOUT + try: + p = subprocess.Popen(args, stdout=p_out, stderr=p_err, + close_fds=True, bufsize=1, + universal_newlines=True) + while True: + line = p.stdout.readline() + if p.poll() is not None and line == "": + break + sys.stdout.write(line) + except KeyboardInterrupt: + p.wait() + raise + else: + p.wait() + return p.returncode + + +def main(options, args): + if options.playbook_dir: + playbook_dir = options.playbook_dir + else: + temp_dir = tempfile.mkdtemp(prefix='ansible-ipa-client') + playbook_dir = temp_dir + + inventory = os.path.join(playbook_dir, "ipaclient-inventory") + playbook = os.path.join(playbook_dir, "ipaclient-playbook.yml") + + with open(inventory, 'w') as f: + if options.servers: + f.write("[ipaservers]\n") + for server in options.servers: + f.write("%s\n" % server) + f.write("\n") + f.write("[ipaclients]\n") + f.write("%s\n" % args[0]) + f.write("\n") + f.write("[ipaclients:vars]\n") + # basic + if options.principal: + f.write("ipaadmin_principal=%s\n" % options.principal) + if options.ca_cert_file: + f.write("ipaclient_ca_cert_file=%s\n" % options.ca_cert_file) + if options.ip_addresses: + f.write("ipaclient_ip_addresses=%s\n" % + ",".join(options.ip_addresses)) + if options.all_ip_addresses: + f.write("ipaclient_all_ip_addresses=yes\n") + if options.domain: + f.write("ipaclient_domain=%s\n" % options.domain) + # --servers are handled above + if options.realm: + f.write("ipaclient_realm=%s\n" % options.realm) + if options.hostname: + f.write("ipaclient_hostname=%s\n" % options.hostname) + # client + if options.password: + f.write("ipaadmin_password=%s\n" % options.password) + if options.force: + f.write("ipaclient_force=yes\n") + if options.configure_firefox: + f.write("ipaclient_configure_firefox=yes\n") + if options.firefox_dir: + f.write("ipaclient_firefox_dir=%s\n" % options.firefox_dir) + if options.keytab: + f.write("ipaclient_keytab=%s\n" % options.keytab) + if options.mkhomedir: + f.write("ipaclient_mkhomedir=yes\n") + if options.force_join: + f.write("ipaclient_force_join=%s\n" % options.force_join) + if options.ntp_servers: + f.write("ipaclient_ntp_servers=%s\n" % + ",".join(options.ntp_servers)) + if options.ntp_pool: + f.write("ipaclient_ntp_pool=%s\n" % options.ntp_pool) + if options.no_ntp: + f.write("ipaclient_no_ntp=yes\n") + if options.nisdomain: + f.write("ipaclient_nisdomain=%s\n" % options.nisdomain) + if options.no_nisdomain: + f.write("ipaclient_no_nisdomain=yes\n") + if options.ssh_trust_dns: + f.write("ipaclient_ssh_trust_dns=yes\n") + if options.no_ssh: + f.write("ipaclient_no_ssh=yes\n") + if options.no_sshd: + f.write("ipaclient_no_sshd=yes\n") + if options.no_sudo: + f.write("ipaclient_no_sudo=yes\n") + if options.no_dns_sshfp: + f.write("ipaclient_no_dns_sshfp=yes\n") + if options.kinit_attempts: + f.write("ipaclient_kinit_attempts=%d\n" % options.kinit_attempts) + # sssd + if options.fixed_primary: + f.write("ipasssd_fixed_primary=yes\n") + if options.permit: + f.write("ipasssd_permit=yes\n") + if options.enable_dns_updates: + f.write("ipasssd_enable_dns_updates=yes\n") + if options.no_krb5_offline_passwords: + f.write("ipasssd_no_krb5_offline_passwords=yes\n") + if options.preserve_sssd: + f.write("ipasssd_preserve_sssd=yes\n") + # automount + if options.automount_location: + f.write("ipaclient_automount_location=%s\n" % + options.automount_location) + # ansible + if options.ipaclient_use_otp: + f.write("ipaclient_use_otp=%s\n" % options.ipaclient_use_otp) + if options.ipaclient_allow_repair: + f.write("ipaclient_allow_repair=%s\n" % + options.ipaclient_allow_repair) + if options.ipaclient_install_packages: + f.write("ipaclient_install_packages=%s\n" % + options.ipaclient_install_packages) + + if options.uninstall: + state = "absent" + else: + state = "present" + + with open(playbook, 'w') as f: + f.write("---\n") + f.write("- name: Playbook to configure IPA clients\n") + f.write(" hosts: ipaclients\n") + f.write(" become: true\n") + if options.become_method: + f.write(" become_method: %s\n" % options.become_method) + f.write("\n") + f.write(" roles:\n") + f.write(" - role: ipaclient\n") + f.write(" state: %s\n" % state) + + cmd = [ 'ansible-playbook' ] + if options.ansible_verbose: + cmd.append("-"+"v"*options.ansible_verbose) + cmd.extend(['-i', inventory, playbook]) + try: + returncode = run_cmd(cmd) + if returncode != 0: + raise RuntimeError() + finally: + if not options.playbook_dir: + shutil.rmtree(temp_dir, ignore_errors=True) + + +options, args = parse_options() +try: + main(options, args) +except KeyboardInterrupt: + sys.exit(1) +except SystemExit as e: + sys.exit(e) +except RuntimeError as e: + sys.exit(e) +except Exception as e: + if options.verbose: + traceback.print_exc(file=sys.stdout) + else: + print("Re-run %s with --verbose option to get more information" % + sys.argv[0]) + + print("Unexpected error: %s" % str(e)) + sys.exit(1) diff --git a/utils/ansible-ipa-replica-install b/utils/ansible-ipa-replica-install new file mode 100755 index 0000000..0d9d0e2 --- /dev/null +++ b/utils/ansible-ipa-replica-install @@ -0,0 +1,528 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Copyright (C) 2019 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +import os +import sys +import shutil +import tempfile +import argparse +import traceback +import subprocess + + +def parse_options(): + usage = "Usage: anisble-ipa-replica-install [options] " + + parser = argparse.ArgumentParser(usage=usage) + parser.add_argument("--version", dest="version", + action="store_true", + help="show program's version number and exit") + parser.add_argument("-U", "--unattended", dest="unattended", + action="store_true", + help="unattended (un)installation never prompts the " + "user") + # basic + parser.add_argument("-w", "--admin-password", dest="admin_password", + default=None, + help="Kerberos password for the specified admin " + "principal") + parser.add_argument("--ip-address", dest="ip_addresses", + metavar="IP_ADDRESS", + action='append', default=None, + help="Replica server IP Address. This option can be " + "used multiple times") + parser.add_argument("-n", "--domain", dest="domain", + metavar="DOMAIN_NAME", default=None, + help="primary DNS domain of the IPA deployment (not " + "necessarily related to the current hostname)") + parser.add_argument("--server", dest="servers", + metavar="SERVER", + action='append', default=None, + help="fully qualified name of IPA server to enroll to") + parser.add_argument("-r", "--realm", dest="realm", + metavar="REALM_NAME", default=None, + help="Kerberos realm name of the IPA deployment " + "(typically un upper-cased name of the primary DNS " + "domain)") + parser.add_argument("--hostname", dest="hostname", + metavar="HOST_NAME", default=None, + help="fully qualified name of this host") + parser.add_argument("-P", "--principal", dest="principal", + default=None, + help="User Principal allowed to promote replicas and " + "join IPA realm") + parser.add_argument("--pki-config-override", dest="pki_config_override", + default=None, + help="Path to ini file with config overrides.") + parser.add_argument("--no-host-dns", dest="no_host_dns", + action="store_true", + help="Do not use DNS for hostname lookup during " + "installation") + parser.add_argument("--skip-conncheck", dest="skip_conncheck", + action="store_true", + help="skip connection check to remote master") + # server + parser.add_argument("-p", "--password", dest="dm_password", + default=None, + help="Password to join the IPA realm. Assumes bulk " + "password unless principal is also set. (domain " + "level 1+) Directory Manager (existing master) " + "password. (domain level 0)") + parser.add_argument("--hidden-replica", dest="hidden_replica", + action="store_true", + help="Install a hidden replica") + parser.add_argument("--setup-adtrust", dest="setup_adtrust", + action="store_true", + help="configure AD trust capability") + parser.add_argument("--setup-ca", dest="setup_ca", + action="store_true", + help="configure a dogtag CA") + parser.add_argument("--setup-kra", dest="setup_kra", + action="store_true", + help="configure a dogtag KRA") + parser.add_argument("--setup-dns", dest="setup_dns", + action="store_true", + help="configure bind with our zone") + parser.add_argument("--no-pkinit", dest="no_pkinit", + action="store_true", + help="disables pkinit setup steps") + parser.add_argument("--no-ui-redirect", dest="no_ui_redirect", + action="store_true", + help="Do not automatically redirect to the Web UI") + parser.add_argument("--dirsrv-config-file", dest="dirsrv_config_file", + metavar="FILE", default=None, + help="The path to LDIF file that will be used to " + "modify configuration of dse.ldif during " + "installation of the directory server instance") + # ssl certificate + parser.add_argument("--dirsrv-cert-file", dest="dirsrv_cert_files", + metavar="FILE", default=None, action="append", + help="File containing the Directory Server SSL " + "certificate and private key") + parser.add_argument("--http-cert-file", dest="http_cert_files", + metavar="FILE", default=None, action="append", + help="File containing the Apache Server SSL " + "certificate and private key") + parser.add_argument("--pkinit-cert-file", dest="pkinit_cert_files", + metavar="FILE", default=None, action="append", + help="File containing the Kerberos KDC SSL " + "certificate and Private key") + parser.add_argument("--dirsrv-pin", dest="dirsrv_pin", + metavar="PIN", default=None, + help="The password to unlock the Directory Server " + "private key") + parser.add_argument("--http-pin", dest="http_pin", + metavar="PIN", default=None, + help="The password to unlock the Apache Server " + "private key") + parser.add_argument("--pkinit-pin", dest="pkinit_pin", + metavar="PIN", default=None, + help="The password to unlock the Kerberos KDC " + "private key") + parser.add_argument("--dirsrv-cert-name", dest="dirsrv_cert_name", + metavar="NAME", default=None, + help="Name of the Directory Server SSL certificate " + "to install") + parser.add_argument("--http-cert-name", dest="http_cert_name", + metavar="NAME", default=None, + help="Name of the Apache Server SSL certificate to " + "install") + parser.add_argument("--pkinit-cert-name", dest="pkinit_cert_name", + metavar="NAME", default=None, + help="Name of the Kerberos KDC SSL certificate to " + "install") + # client + parser.add_argument("-k", "--keytab", dest="keytab", + default=None, + help="path to backed up keytab from previous " + "enrollment") + parser.add_argument("--mkhomedir", dest="mkhomedir", + action="store_true", + help="create home directories for users on their " + "first login") + parser.add_argument("--force-join", dest="force_join", + action="store_true", + help="Force client enrollment even if already " + "enrolled") + parser.add_argument("--ntp-server", dest="ntp_servers", + metavar="NTP_SERVER", + action='append', default=None, + help="ntp server to use. This option can be used " + "multiple times") + parser.add_argument("--ntp-pool", dest="ntp_pool", + default=None, + help="ntp server pool to use") + parser.add_argument("-N", "--no-ntp", dest="no_ntp", + action="store_true", + help="do not configure ntp") + parser.add_argument("--ssh-trust-dns", dest="ssh_trust_dns", + action="store_true", + help="configure OpenSSH client to trust DNS SSHFP " + "records") + parser.add_argument("--no-ssh", dest="no_ssh", + action="store_true", + help="do not configure OpenSSH client") + parser.add_argument("--no-sshd", dest="no_sshd", + action="store_true", + help="do not configure OpenSSH server") + parser.add_argument("--no-dns-sshfp", dest="no_dns_sshfp", + action="store_true", + help="do not automatically create DNS SSHFP records") + # certificate system + parser.add_argument("--skip-schema-check", dest="skip_schema_check", + action="store_true", + help="skip check for updated CA DS schema on the " + "remote master") + # dns + parser.add_argument("--allow-zone-overlap", dest="allow_zone_overlap", + action="store_true", + help="Create DNS zone even if it already exists") + parser.add_argument("--reverse-zone", dest="reverse_zones", + metavar="REVERSE_ZONE", action="append", default=None, + help="The reverse DNS zone to use. This option can " + "be used multiple times") + parser.add_argument("--no-reverse", dest="no_reverse", + action="store_true", + help="Do not create new reverse DNS zone") + parser.add_argument("--auto-reverse", dest="auto_reverse", + action="store_true", + help="Create necessary reverse zones") + parser.add_argument("--forwarder", dest="forwarders", + action="append", default=None, + help="Add a DNS forwarder. This option can be used " + "multiple times") + parser.add_argument("--no-forwarders", dest="no_forwarders", + action="store_true", + help="Do not add any DNS forwarders, use root " + "servers instead") + parser.add_argument("--auto-forwarders", dest="auto_forwarders", + action="store_true", + help="Use DNS forwarders configured in " + "/etc/resolv.conf") + parser.add_argument("-forward-policy-", dest="forward_policy", + choices=("only", "first"), default=None, + help="DNS forwarding policy for global forwarders") + parser.add_argument("--no-dnssec-validation", dest="no_dnssec_validation", + action="store_true", + help="Disable DNSSEC validation") + # ad trust + parser.add_argument("--add-sids", dest="add_sids", + action="store_true", + help="Add SIDs for existing users and groups as the " + "final step") + parser.add_argument("--add-agents", dest="add_agents", + action="store_true", + help="Add IPA masters to a list of hosts allowed to " + "serve information about users from trusted forests") + parser.add_argument("--enable-compat", dest="enable_compat", + action="store_true", + help="Enable support for trusted domains for old " + "clients") + parser.add_argument("--netbios-name", dest="netbios_name", + default=None, + help="NetBIOS name of the IPA domain") + parser.add_argument("--rid-base", dest="rid_base", + default=None, type=int, + help="Start value for mapping UIDs and GIDs to RIDs") + parser.add_argument("--secondary-rid-base", dest="secondary_rid_base", + default=None, type=int, + help="Start value of the secondary range for mapping " + "UIDs and GIDs to RIDs") + # logging and output + parser.add_argument("-v", "--verbose", dest="verbose", + action="store_true", + help="print debugging information") + parser.add_argument("-d", "--debug", dest="verbose", + action="store_true", + help="alias for --verbose (deprecated)") + parser.add_argument("-q", "--quiet", dest="quiet", + action="store_true", + help="output only errors") + parser.add_argument("--log-file", dest="log_file", + help="log to the given file") + # ansible + parser.add_argument("--ipareplica-install-packages", + dest="ipareplica_install_packages", + choices=("yes", "no"), default=None, + help="The bool value defines if the needed packages " + "are installed on the node. Default: yes") + parser.add_argument("--ipareplica-setup-firewalld", + dest="ipareplica_setup_firewalld", + choices=("yes", "no"), default=None, + help="The value defines if the needed services will " + "automatically be openen in the firewall managed by " + "firewalld. Default: yes") + # playbook + parser.add_argument("--playbook-dir", + dest="playbook_dir", + default=None, + help="If defined will be used as to create inventory " + "file and playbook in. The files will not be removed " + "after the playbook processing ended.") + parser.add_argument("--become-method", + dest="become_method", + default="sudo", + help="privilege escalation method to use " + "(default=sudo), use `ansible-doc -t become -l` to " + "list valid choices.") + parser.add_argument("--ansible-verbose", + dest="ansible_verbose", + type=int, default=None, + help="privilege escalation method to use " + "(default=sudo), use `ansible-doc -t become -l` to " + "list valid choices.") + + options, args = parser.parse_known_args() + + if options.playbook_dir and not os.path.isdir(options.playbook_dir): + parser.error("playbook dir does not exist") + + if options.log_file: + parser.error("log_file is not supported") + + if len(args) < 1: + parser.error("ansible host not set") + elif len(args) > 1: + parser.error("too many arguments: %s" % ",".join(args)) + + return options, args + + +def run_cmd(args): + """ + Execute an external command. + """ + p_out = subprocess.PIPE + p_err = subprocess.STDOUT + try: + p = subprocess.Popen(args, stdout=p_out, stderr=p_err, + close_fds=True, bufsize=1, + universal_newlines=True) + while True: + line = p.stdout.readline() + if p.poll() is not None and line == "": + break + sys.stdout.write(line) + except KeyboardInterrupt: + p.wait() + raise + else: + p.wait() + return p.returncode + + +def main(options, args): + if options.playbook_dir: + playbook_dir = options.playbook_dir + else: + temp_dir = tempfile.mkdtemp(prefix='ansible-ipa-replica') + playbook_dir = temp_dir + + inventory = os.path.join(playbook_dir, "ipareplica-inventory") + playbook = os.path.join(playbook_dir, "ipareplica-playbook.yml") + + with open(inventory, 'w') as f: + if options.servers: + f.write("[ipaservers]\n") + for server in options.servers: + f.write("%s\n" % server) + f.write("\n") + f.write("[ipareplicas]\n") + f.write("%s\n" % args[0]) + f.write("\n") + f.write("[ipareplicas:vars]\n") + # basic + if options.admin_password: + f.write("ipaadmin_password=%s\n" % options.admin_password) + if options.ip_addresses: + f.write("ipareplica_ip_addresses=%s\n" % + ",".join(options.ip_addresses)) + if options.domain: + f.write("ipareplica_domain=%s\n" % options.domain) + # --servers are handled above + if options.realm: + f.write("ipareplica_realm=%s\n" % options.realm) + if options.hostname: + f.write("ipareplica_hostname=%s\n" % options.hostname) + if options.principal: + f.write("ipaadmin_principal=%s\n" % options.principal) + if options.pki_config_override: + f.write("ipareplica_pki_config_override=yes\n") + if options.no_host_dns: + f.write("ipareplica_no_host_dns=yes\n") + if options.skip_conncheck: + f.write("ipareplica_skip_conncheck=yes\n") + # server + if options.dm_password: + f.write("ipadm_password=%s\n" % options.dm_password) + if options.hidden_replica: + f.write("ipareplica_hidden_replica=yes\n") + if options.setup_adtrust: + f.write("ipareplica_setup_adtrust=yes\n") + if options.setup_ca: + f.write("ipareplica_setup_ca=yes\n") + if options.setup_kra: + f.write("ipareplica_setup_kra=yes\n") + if options.setup_dns: + f.write("ipareplica_setup_dns=yes\n") + if options.no_pkinit: + f.write("ipareplica_no_pkinit=yes\n") + if options.no_ui_redirect: + f.write("ipareplica_no_ui_redirect=yes\n") + # ssl certificate + if options.dirsrv_cert_files: + f.write("ipareplica_dirsrv_cert_files=%s\n" % + ",".join(options.dirsrv_cert_files)) + if options.http_cert_files: + f.write("ipareplica_http_cert_files=%s\n" % + ",".join(options.http_cert_files)) + if options.pkinit_cert_files: + f.write("ipareplica_pkinit_cert_files=%s\n" % + ",".join(options.pkinit_cert_files)) + if options.dirsrv_pin: + f.write("ipareplica_dirsrv_pin=%s\n" % options.dirsrv_pin) + if options.http_pin: + f.write("ipareplica_http_pin=%s\n" % options.http_pin) + if options.pkinit_pin: + f.write("ipareplica_pkinit_pin=%s\n" % options.pkinit_pin) + if options.dirsrv_cert_name: + f.write("ipareplica_dirsrv_cert_name=%s\n" % + options.dirsrv_cert_name) + if options.http_cert_name: + f.write("ipareplica_http_cert_name=%s\n" % options.http_cert_name) + if options.pkinit_cert_name: + f.write("ipareplica_pkinit_cert_name=%s\n" % + options.pkinit_cert_name) + # client + if options.keytab: + f.write("ipaclient_keytab=%s\n" % options.keytab) + if options.mkhomedir: + f.write("ipaclient_mkhomedir=yes\n") + if options.force_join: + f.write("ipaclient_force_join=yes\n") + if options.ntp_servers: + f.write("ipaclient_ntp_server=%s\n" % + ",".join(options.ntp_replicas)) + if options.ntp_pool: + f.write("ipaclient_ntp_pool=%s\n" % options.ntp_pool) + if options.no_ntp: + f.write("ipaclient_no_ntp=yes\n") + if options.ssh_trust_dns: + f.write("ipaclient_ssh_trust_dns=yes\n") + if options.no_ssh: + f.write("ipaclient_no_ssh=yes\n") + if options.no_sshd: + f.write("ipaclient_no_sshd=yes\n") + if options.no_dns_sshfp: + f.write("ipaclient_no_dns_sshfp=yes\n") + # certificate system + if options.skip_schema_check: + f.write("ipareplica_skip_schema_check=yes\n") + # dns + if options.allow_zone_overlap: + f.write("ipareplica_allow_zone_overlap=yes\n") + if options.reverse_zones: + f.write("ipareplica_reverse_zones=%s\n" % + ",".join(options.reverse_zones)) + if options.no_reverse: + f.write("ipareplica_no_reverse=yes\n") + if options.auto_reverse: + f.write("ipareplica_auto_reverse=yes\n") + if options.forwarders: + f.write("ipareplica_forwarders=%s\n" % + ",".join(options.forwarders)) + if options.no_forwarders: + f.write("ipareplica_no_forwarders=yes\n") + if options.auto_forwarders: + f.write("ipareplica_auto_forwarders=yes\n") + if options.forward_policy: + f.write("ipareplica_forward_policy=%s\n" % options.forward_policy) + if options.no_dnssec_validation: + f.write("ipareplica_no_dnssec_validation=yes\n") + # ad trust + if options.add_sids: + f.write("ipareplica_add_sids=yes\n") + if options.add_agents: + f.write("ipareplica_add_agents=yes\n") + if options.enable_compat: + f.write("ipareplica_enable_compat=yes\n") + if options.netbios_name: + f.write("ipareplica_netbios_name=%s\n" % options.netbios_name) + if options.rid_base: + f.write("ipareplica_rid_base=%s\n" % options.rid_base) + if options.secondary_rid_base: + f.write("ipareplica_secondary_rid_base=%s\n" % + options.secondary_rid_base) + # ansible + if options.ipareplica_install_packages: + f.write("ipareplica_install_packages=%s\n" % + options.ipareplica_install_packages) + if options.ipareplica_setup_firewalld: + f.write("ipareplica_setup_firewalld=%s\n" % + options.ipareplica_setup_firewalld) + + # uninstall done with ipaserver role + state = "present" + + with open(playbook, 'w') as f: + f.write("---\n") + f.write("- name: Playbook to configure IPA replicas\n") + f.write(" hosts: ipareplicas\n") + f.write(" become: true\n") + if options.become_method: + f.write(" become_method: %s\n" % options.become_method) + f.write("\n") + f.write(" roles:\n") + f.write(" - role: ipareplica\n") + f.write(" state: %s\n" % state) + + cmd = [ 'ansible-playbook' ] + if options.ansible_verbose: + cmd.append("-"+"v"*options.ansible_verbose) + cmd.extend(['-i', inventory, playbook]) + try: + returncode = run_cmd(cmd) + if returncode != 0: + raise RuntimeError() + finally: + if not options.playbook_dir: + shutil.rmtree(temp_dir, ignore_errors=True) + + +options, args = parse_options() +try: + main(options, args) +except KeyboardInterrupt: + sys.exit(1) +except SystemExit as e: + sys.exit(e) +except RuntimeError as e: + sys.exit(e) +except Exception as e: + if options.verbose: + traceback.print_exc(file=sys.stdout) + else: + print("Re-run %s with --verbose option to get more information" % + sys.argv[0]) + + print("Unexpected error: %s" % str(e)) + sys.exit(1) diff --git a/utils/ansible-ipa-server-install b/utils/ansible-ipa-server-install new file mode 100755 index 0000000..62e7595 --- /dev/null +++ b/utils/ansible-ipa-server-install @@ -0,0 +1,588 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner +# +# Copyright (C) 2019 Red Hat +# see file 'COPYING' for use and warranty information +# +# 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 . + +import os +import sys +import shutil +import tempfile +import argparse +import traceback +import subprocess + + +def parse_options(): + usage = "Usage: anisble-ipa-server-install [options] " + + parser = argparse.ArgumentParser(usage=usage) + parser.add_argument("--version", dest="version", + action="store_true", + help="show program's version number and exit") + parser.add_argument("-U", "--unattended", dest="unattended", + action="store_true", + help="unattended (un)installation never prompts the " + "user") + parser.add_argument("--uninstall", dest="uninstall", + action="store_true", + help="uninstall an existing installation. The " + "uninstall can be run with --unattended option") + # basic + parser.add_argument("-p", "--ds-password", dest="dm_password", + default=None, + help="Directory Manager password") + parser.add_argument("-a", "--admin-password", dest="admin_password", + default=None, + help="admin user kerberos password") + parser.add_argument("--ip-address", dest="ip_addresses", + metavar="IP_ADDRESS", + action='append', default=None, + help="Master Server IP Address. This option can be " + "used multiple times") + parser.add_argument("-n", "--domain", dest="domain", + metavar="DOMAIN_NAME", default=None, + help="primary DNS domain of the IPA deployment (not " + "necessarily related to the current hostname)") + parser.add_argument("-r", "--realm", dest="realm", + metavar="REALM_NAME", default=None, + help="Kerberos realm name of the IPA deployment " + "(typically un upper-cased name of the primary DNS " + "domain)") + parser.add_argument("--hostname", dest="hostname", + metavar="HOST_NAME", default=None, + help="fully qualified name of this host") + parser.add_argument("--ca-cert-file", dest="ca_cert_file", + metavar="FILE", default=None, + help="File containing CA certificates for the " + "service certificate files") + parser.add_argument("--pki-config-override", dest="pki_config_override", + default=None, + help="Path to ini file with config overrides.") + parser.add_argument("--no-host-dns", dest="no_host_dns", + action="store_true", + help="Do not use DNS for hostname lookup during " + "installation") + # server + parser.add_argument("--setup-adtrust", dest="setup_adtrust", + action="store_true", + help="configure AD trust capability") + parser.add_argument("--setup-kra", dest="setup_kra", + action="store_true", + help="configure a dogtag KRA") + parser.add_argument("--setup-dns", dest="setup_dns", + action="store_true", + help="configure bind with our zone") + parser.add_argument("--idstart", dest="idstart", + type=int, default=None, + help="The starting value for the IDs range (default " + "random)") + parser.add_argument("--idmax", dest="idmax", + default=None, type=int, + help="The max value for the IDs range (default: " + "idstart+199999)") + parser.add_argument("--no-hbac-allow", dest="no_hbac_allow", + action="store_true", + help="Don't install allow_all HBAC rule") + parser.add_argument("--no-pkinit", dest="no_pkinit", + action="store_true", + help="disables pkinit setup steps") + parser.add_argument("--no-ui-redirect", dest="no_ui_redirect", + action="store_true", + help="Do not automatically redirect to the Web UI") + parser.add_argument("--dirsrv-config-file", dest="dirsrv_config_file", + metavar="FILE", default=None, + help="The path to LDIF file that will be used to " + "modify configuration of dse.ldif during " + "installation of the directory server instance") + # ssl certificate + parser.add_argument("--dirsrv-cert-file", dest="dirsrv_cert_files", + metavar="FILE", default=None, action="append", + help="File containing the Directory Server SSL " + "certificate and private key") + parser.add_argument("--http-cert-file", dest="http_cert_files", + metavar="FILE", default=None, action="append", + help="File containing the Apache Server SSL " + "certificate and private key") + parser.add_argument("--pkinit-cert-file", dest="pkinit_cert_files", + metavar="FILE", default=None, action="append", + help="File containing the Kerberos KDC SSL " + "certificate and Private key") + parser.add_argument("--dirsrv-pin", dest="dirsrv_pin", + metavar="PIN", default=None, + help="The password to unlock the Directory Server " + "private key") + parser.add_argument("--http-pin", dest="http_pin", + metavar="PIN", default=None, + help="The password to unlock the Apache Server " + "private key") + parser.add_argument("--pkinit-pin", dest="pkinit_pin", + metavar="PIN", default=None, + help="The password to unlock the Kerberos KDC " + "private key") + parser.add_argument("--dirsrv-cert-name", dest="dirsrv_cert_name", + metavar="NAME", default=None, + help="Name of the Directory Server SSL certificate " + "to install") + parser.add_argument("--http-cert-name", dest="http_cert_name", + metavar="NAME", default=None, + help="Name of the Apache Server SSL certificate to " + "install") + parser.add_argument("--pkinit-cert-name", dest="pkinit_cert_name", + metavar="NAME", default=None, + help="Name of the Kerberos KDC SSL certificate to " + "install") + # client + parser.add_argument("--mkhomedir", dest="mkhomedir", + action="store_true", + help="create home directories for users on their " + "first login") + parser.add_argument("--ntp-server", dest="ntp_servers", + metavar="NTP_SERVER", + action='append', default=None, + help="ntp server to use. This option can be used " + "multiple times") + parser.add_argument("--ntp-pool", dest="ntp_pool", + default=None, + help="ntp server pool to use") + parser.add_argument("-N", "--no-ntp", dest="no_ntp", + action="store_true", + help="do not configure ntp") + parser.add_argument("--ssh-trust-dns", dest="ssh_trust_dns", + action="store_true", + help="configure OpenSSH client to trust DNS SSHFP " + "records") + parser.add_argument("--no-ssh", dest="no_ssh", + action="store_true", + help="do not configure OpenSSH client") + parser.add_argument("--no-sshd", dest="no_sshd", + action="store_true", + help="do not configure OpenSSH server") + parser.add_argument("--no-dns-sshfp", dest="no_dns_sshfp", + action="store_true", + help="do not automatically create DNS SSHFP records") + # certificate system + parser.add_argument("--external-ca", dest="external_ca", + action="store_true", + help="Generate a CSR for the IPA CA certificate to " + "be signed by an external CA") + parser.add_argument("--external-ca-type", dest="external_ca_type", + choices=("generic", "ms-cs"), default=None, + help="Type of the external CA") + parser.add_argument("--external-ca-profile", dest="external_ca_profile", + default=None, + help="Specify the certificate profile/template to " + "use at the external CA") + parser.add_argument("--external-cert-file", dest="external_cert_files", + metavar="FILE", default=None, action="append", + help="File containing the IPA CA certificate and the " + "external CA certificate chain") + parser.add_argument("--subject-base", dest="subject_base", + default=None, + help="The certificate subject base (default " + "O=). RDNs are in LDAP order (most " + "specific RDN first).") + parser.add_argument("--ca-subject", dest="ca_subject", + default=None, + help="The CA certificate subject DN (default " + "CN=Certificate Authority,O=). RDNs are " + "in LDAP order (most specific RDN first).") + parser.add_argument("--ca-signing-algorithm", dest="ca_signing_algorithm", + choices=("SHA1withRSA", "SHA256withRSA", + "SHA512withRSA"), + default=None, + help="Signing algorithm of the IPA CA certificate") + # dns + parser.add_argument("--allow-zone-overlap", dest="allow_zone_overlap", + action="store_true", + help="Create DNS zone even if it already exists") + parser.add_argument("--reverse-zone", dest="reverse_zones", + metavar="REVERSE_ZONE", action="append", default=None, + help="The reverse DNS zone to use. This option can " + "be used multiple times") + parser.add_argument("--no-reverse", dest="no_reverse", + action="store_true", + help="Do not create new reverse DNS zone") + parser.add_argument("--auto-reverse", dest="auto_reverse", + action="store_true", + help="Create necessary reverse zones") + parser.add_argument("--zonemgr", dest="zonemgr", + default=None, + help="DNS zone manager e-mail address. Defaults to " + "hostmaster@DOMAIN") + parser.add_argument("--forwarder", dest="forwarders", + action="append", default=None, + help="Add a DNS forwarder. This option can be used " + "multiple times") + parser.add_argument("--no-forwarders", dest="no_forwarders", + action="store_true", + help="Do not add any DNS forwarders, use root " + "servers instead") + parser.add_argument("--auto-forwarders", dest="auto_forwarders", + action="store_true", + help="Use DNS forwarders configured in " + "/etc/resolv.conf") + parser.add_argument("-forward-policy-", dest="forward_policy", + choices=("only", "first"), default=None, + help="DNS forwarding policy for global forwarders") + parser.add_argument("--no-dnssec-validation", dest="no_dnssec_validation", + action="store_true", + help="Disable DNSSEC validation") + # ad trust + parser.add_argument("--enable-compat", dest="enable_compat", + action="store_true", + help="Enable support for trusted domains for old " + "clients") + parser.add_argument("--netbios-name", dest="netbios_name", + default=None, + help="NetBIOS name of the IPA domain") + parser.add_argument("--rid-base", dest="rid_base", + default=None, type=int, + help="Start value for mapping UIDs and GIDs to RIDs") + parser.add_argument("--secondary-rid-base", dest="secondary_rid_base", + default=None, type=int, + help="Start value of the secondary range for mapping " + "UIDs and GIDs to RIDs") + # deprecated + parser.add_argument("--domain-level", type=int, + help="IPA domain level (deprecated)") + # uninstall + parser.add_argument("--ignore-topology-disconnect", + dest="ignore_topology_disconnect", + action="store_true", + help="do not check whether server uninstall " + "disconnects the topology (domain level 1+)") + parser.add_argument("--ignore-last-of-role", dest="ignore_last_of_role", + action="store_true", + help="do not check whether server uninstall removes " + "last CA/DNS server or DNSSec master (domain level " + "1+)") + # logging and output + parser.add_argument("-v", "--verbose", dest="verbose", + action="store_true", + help="print debugging information") + parser.add_argument("-d", "--debug", dest="verbose", + action="store_true", + help="alias for --verbose (deprecated)") + parser.add_argument("-q", "--quiet", dest="quiet", + action="store_true", + help="output only errors") + parser.add_argument("--log-file", dest="log_file", + help="log to the given file") + + # ansible + parser.add_argument("--ipaserver-install-packages", + dest="ipaserver_install_packages", + choices=("yes", "no"), default=None, + help="The bool value defines if the needed packages " + "are installed on the node. Default: yes") + parser.add_argument("--ipaserver-setup-firewalld", + dest="ipaserver_setup_firewalld", + choices=("yes", "no"), default=None, + help="The value defines if the needed services will " + "automatically be openen in the firewall managed by " + "firewalld. Default: yes") + parser.add_argument("--ipaserver-external-cert-files-from-controller", + dest="ipaserver_external_cert_files_from_controller", + default=None, action="append", + help="Files containing the IPA CA certificates and " + "the external CA certificate chains on the " + "controller that will be copied to the ipaserver " + "host to /root folder.") + parser.add_argument("--ipaserver-copy-csr-to-controller", + dest="ipaserver_copy_csr_to_controller", + choices=("yes", "no"), default=None, + help="Copy the generated CSR from the ipaserver to " + "the controller as -ipa.csr.") + # playbook + parser.add_argument("--playbook-dir", + dest="playbook_dir", + default=None, + help="If defined will be used as to create inventory " + "file and playbook in. The files will not be removed " + "after the playbook processing ended.") + parser.add_argument("--become-method", + dest="become_method", + default="sudo", + help="privilege escalation method to use " + "(default=sudo), use `ansible-doc -t become -l` to " + "list valid choices.") + parser.add_argument("--ansible-verbose", + dest="ansible_verbose", + type=int, default=None, + help="privilege escalation method to use " + "(default=sudo), use `ansible-doc -t become -l` to " + "list valid choices.") + + options, args = parser.parse_known_args() + + if options.playbook_dir and not os.path.isdir(options.playbook_dir): + parser.error("playbook dir does not exist") + + if options.log_file: + parser.error("log_file is not supported") + + if len(args) < 1: + parser.error("ansible host not set") + elif len(args) > 1: + parser.error("too many arguments: %s" % ",".join(args)) + + return options, args + + +def run_cmd(args): + """ + Execute an external command. + """ + p_out = subprocess.PIPE + p_err = subprocess.STDOUT + try: + p = subprocess.Popen(args, stdout=p_out, stderr=p_err, + close_fds=True, bufsize=1, + universal_newlines=True) + while True: + line = p.stdout.readline() + if p.poll() is not None and line == "": + break + sys.stdout.write(line) + except KeyboardInterrupt: + p.wait() + raise + else: + p.wait() + return p.returncode + + +def main(options, args): + if options.playbook_dir: + playbook_dir = options.playbook_dir + else: + temp_dir = tempfile.mkdtemp(prefix='ansible-ipa-server') + playbook_dir = temp_dir + + inventory = os.path.join(playbook_dir, "ipaserver-inventory") + playbook = os.path.join(playbook_dir, "ipaserver-playbook.yml") + + with open(inventory, 'w') as f: + f.write("[ipaserver]\n") + f.write("%s\n" % args[0]) + f.write("\n") + f.write("[ipaserver:vars]\n") + # basic + if options.dm_password: + f.write("ipadm_password=%s\n" % options.dm_password) + if options.admin_password: + f.write("ipaadmin_password=%s\n" % options.admin_password) + if options.ip_addresses: + f.write("ipaserver_ip_addresses=%s\n" % + ",".join(options.ip_addresses)) + if options.domain: + f.write("ipaserver_domain=%s\n" % options.domain) + if options.realm: + f.write("ipaserver_realm=%s\n" % options.realm) + if options.hostname: + f.write("ipaserver_hostname=%s\n" % options.hostname) + if options.ca_cert_file: + f.write("ipaserver_ca_cert_files=%s\n" % options.ca_cert_file) + if options.pki_config_override: + f.write("ipaserver_pki_config_override=yes\n") + if options.no_host_dns: + f.write("ipaserver_no_host_dns=yes\n") + # server + if options.setup_adtrust: + f.write("ipaserver_setup_adtrust=yes\n") + if options.setup_kra: + f.write("ipaserver_setup_kra=yes\n") + if options.setup_dns: + f.write("ipaserver_setup_dns=yes\n") + if options.idstart: + f.write("ipaserver_idstart=%s\n" % options.idstart) + if options.idmax: + f.write("ipaserver_idmax=%s\n" % options.idmax) + if options.no_hbac_allow: + f.write("ipaserver_no_hbac_allow=yes\n") + if options.no_pkinit: + f.write("ipaserver_no_pkinit=yes\n") + if options.no_ui_redirect: + f.write("ipaserver_no_ui_redirect=yes\n") + if options.dirsrv_config_file: + f.write("ipaserver_dirsrv_config_file=%s\n" % + options.dirsrv_config_file) + # ssl certificate + if options.dirsrv_cert_files: + f.write("ipaserver_dirsrv_cert_files=%s\n" % + ",".join(options.dirsrv_cert_files)) + if options.http_cert_files: + f.write("ipaserver_http_cert_files=%s\n" % + ",".join(options.http_cert_files)) + if options.pkinit_cert_files: + f.write("ipaserver_pkinit_cert_files=%s\n" % + ",".join(options.pkinit_cert_files)) + if options.dirsrv_pin: + f.write("ipaserver_dirsrv_pin=%s\n" % options.dirsrv_pin) + if options.http_pin: + f.write("ipaserver_http_pin=%s\n" % options.http_pin) + if options.pkinit_pin: + f.write("ipaserver_pkinit_pin=%s\n" % options.pkinit_pin) + if options.dirsrv_cert_name: + f.write("ipaserver_dirsrv_cert_name=%s\n" % + options.dirsrv_cert_name) + if options.http_cert_name: + f.write("ipaserver_http_cert_name=%s\n" % options.http_cert_name) + if options.pkinit_cert_name: + f.write("ipaserver_pkinit_cert_name=%s\n" % + options.pkinit_cert_name) + # client + if options.mkhomedir: + f.write("ipaclient_mkhomedir=yes\n") + if options.ntp_servers: + f.write("ipaclient_ntp_servers=%s\n" % + ",".join(options.ntp_servers)) + if options.ntp_pool: + f.write("ipaclient_ntp_pool=%s\n" % options.ntp_pool) + if options.no_ntp: + f.write("ipaclient_no_ntp=yes\n") + if options.ssh_trust_dns: + f.write("ipaclient_ssh_trust_dns=yes\n") + if options.no_ssh: + f.write("ipaclient_no_ssh=yes\n") + if options.no_sshd: + f.write("ipaclient_no_sshd=yes\n") + if options.no_dns_sshfp: + f.write("ipaclient_no_dns_sshfp=yes\n") + # certificate system + if options.external_ca: + f.write("ipaserver_external_ca=yes\n") + if options.external_ca_type: + f.write("ipaserver_external_ca_type=%s\n" % + options.external_ca_type) + if options.external_ca_profile: + f.write("ipaserver_external_ca_profile=%s\n" % + options.external_ca_profile) + if options.external_cert_files: + f.write("ipaserver_external_cert_files=%s\n" % + ",".join(options.external_cert_files)) + if options.subject_base: + f.write("ipaserver_subject_base=%s\n" % options.subject_base) + if options.ca_subject: + f.write("ipaserver_ca_subject=%s\n" % options.ca_subject) + if options.ca_signing_algorithm: + f.write("ipaserver_ca_signing_algorithm=%s\n" % + options.ca_signing_algorithm) + # dns + if options.allow_zone_overlap: + f.write("ipaserver_allow_zone_overlap=yes\n") + if options.reverse_zones: + f.write("ipaserver_reverse_zones=%s\n" % + ",".join(options.reverse_zones)) + if options.no_reverse: + f.write("ipaserver_no_reverse=yes\n") + if options.auto_reverse: + f.write("ipaserver_auto_reverse=yes\n") + if options.zonemgr: + f.write("ipaserver_zonemgr=%s\n" % options.zonemgr) + if options.forwarders: + f.write("ipaserver_forwarders=%s\n" % + ",".join(options.forwarders)) + if options.no_forwarders: + f.write("ipaserver_no_forwarders=yes\n") + if options.auto_forwarders: + f.write("ipaserver_auto_forwarders=yes\n") + if options.forward_policy: + f.write("ipaserver_forward_policy=%s\n" % options.forward_policy) + if options.no_dnssec_validation: + f.write("ipaserver_no_dnssec_validation=yes\n") + # ad trust + if options.enable_compat: + f.write("ipaserver_enable_compat=yes\n") + if options.netbios_name: + f.write("ipaserver_netbios_name=%s\n" % options.netbios_name) + if options.rid_base: + f.write("ipaserver_rid_base=%s\n" % options.rid_base) + if options.secondary_rid_base: + f.write("ipaserver_secondary_rid_base=%s\n" % + options.secondary_rid_base) + # uninstall + if options.ignore_topology_disconnect: + f.write("ipaserver_ignore_topology_disconnect=yes\n") + if options.ignore_last_of_role: + f.write("ipaserver_ignore_last_of_role=yes\n") + # ansible + if options.ipaserver_install_packages: + f.write("ipaserver_install_packages=%s\n" % + options.ipaserver_install_packages) + if options.ipaserver_setup_firewalld: + f.write("ipaserver_setup_firewalld=%s\n" % + options.ipaserver_setup_firewalld) + if options.ipaserver_external_cert_files_from_controller: + f.write("ipaserver_external_cert_files_from_controller=%s\n" % + ",".join( + options.ipaserver_external_cert_files_from_controller)) + if options.ipaserver_copy_csr_to_controller: + f.write("ipaserver_copy_csr_to_controller=%s\n" % + options.ipaserver_copy_csr_to_controller) + + if options.uninstall: + state = "absent" + else: + state = "present" + + with open(playbook, 'w') as f: + f.write("---\n") + f.write("- name: Playbook to configure IPA server\n") + f.write(" hosts: ipaserver\n") + f.write(" become: true\n") + if options.become_method: + f.write(" become_method: %s\n" % options.become_method) + f.write("\n") + f.write(" roles:\n") + f.write(" - role: ipaserver\n") + f.write(" state: %s\n" % state) + + cmd = [ 'ansible-playbook' ] + if options.ansible_verbose: + cmd.append("-"+"v"*options.ansible_verbose) + cmd.extend(['-i', inventory, playbook]) + try: + returncode = run_cmd(cmd) + if returncode != 0: + raise RuntimeError() + finally: + if not options.playbook_dir: + shutil.rmtree(temp_dir, ignore_errors=True) + + +options, args = parse_options() +try: + main(options, args) +except KeyboardInterrupt: + sys.exit(1) +except SystemExit as e: + sys.exit(e) +except RuntimeError as e: + sys.exit(e) +except Exception as e: + if options.verbose: + traceback.print_exc(file=sys.stdout) + else: + print("Re-run %s with --verbose option to get more information" % + sys.argv[0]) + + print("Unexpected error: %s" % str(e)) + sys.exit(1) diff --git a/utils/build-galaxy-release.sh b/utils/build-galaxy-release.sh new file mode 100644 index 0000000..fc6bcdd --- /dev/null +++ b/utils/build-galaxy-release.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +galaxy_version=$(git describe --tags | sed -e "s/^v//") +echo $galaxy_version | grep "-" -q || galaxy_version="${galaxy_version}-1" +sed -i -e "s/version: .*/version: \"$galaxy_version\"/" galaxy.yml + +find . -name "*~" -exec rm {} \; + +sed -i -e "s/ansible.module_utils.ansible_freeipa_module/ansible_collections.freeipa.ansible_freeipa.plugins.module_utils.ansible_freeipa_module/" plugins/modules/*.py + +cd plugins/module_utils && { + ln -s ../../roles/ipa*/module_utils/*.py . + cd ../.. +} + +cd plugins/modules && { + sed -i -e "s/ansible.module_utils.ansible_ipa_/ansible_collections.freeipa.ansible_freeipa.plugins.module_utils.ansible_ipa_/" ../../roles/ipa*/library/*.py + ln -s ../../roles/ipa*/library/*.py . + cd ../.. +} + +[ ! -x plugins/action_plugins ] && mkdir plugins/action_plugins +cd plugins/action_plugins && { + ln -s ../../roles/ipa*/action_plugins/*.py . + cd ../.. +} + +for x in roles/ipa*/tasks/*.yml; do + python utils/galaxyify-playbook.py "$x" +done + +for x in $(find playbooks -name "*.yml" -print); do + python utils/galaxyify-playbook.py "$x" +done + +#git diff + +ansible-galaxy collection build + +rm plugins/module_utils/ansible_ipa_* +rm plugins/modules/ipaserver_* +rm plugins/modules/ipareplica_* +rm plugins/modules/ipaclient_* +rm plugins/action_plugins/ipaclient_* +git reset --hard + diff --git a/utils/galaxyify-playbook.py b/utils/galaxyify-playbook.py new file mode 100644 index 0000000..c2ad620 --- /dev/null +++ b/utils/galaxyify-playbook.py @@ -0,0 +1,43 @@ +import sys +import re + + +def galaxify_playbook(playbook_in): + p1 = re.compile('(ipa.*:)$') + p2 = re.compile('(.*:) (ipa.*)$') + lines = [] + + with open(playbook_in) as in_f: + changed = False + changeable = False + include_role = False + for line in in_f: + stripped = line.strip() + if stripped.startswith("- name:") or \ + stripped.startswith("- block:"): + changeable = True + elif stripped in ["set_fact:", "vars:"]: + changeable = False + include_role = False + elif stripped.startswith("include_role:"): + include_role = True + elif include_role and stripped.startswith("name:"): + line = p2.sub(r'\1 freeipa.ansible_freeipa.\2', line) + changed = True + elif changeable and stripped.startswith("- role:"): + line = p2.sub(r'\1 freeipa.ansible_freeipa.\2', line) + changed = True + elif changeable and not stripped.startswith( + "freeipa.ansible_freeipa."): + line = p1.sub(r'freeipa.ansible_freeipa.\1', line) + changed = True + + lines.append(line) + + if changed: + with open(playbook_in, "w") as out_f: + for line in lines: + out_f.write(line) + + +galaxify_playbook(sys.argv[1]) diff --git a/utils/gen_module_docs.py b/utils/gen_module_docs.py new file mode 100644 index 0000000..a09a653 --- /dev/null +++ b/utils/gen_module_docs.py @@ -0,0 +1,307 @@ +import sys + +param_docs = { + "ccache": "The local ccache", + "installer_ccache": "The installer ccache setting", + "_top_dir": "The installer _top_dir setting", + "_ca_enabled": "The installer _ca_enabled setting", + "_add_to_ipaservers": "The installer _add_to_ipaservers setting", + "_ca_subject": "The installer _ca_subject setting", + "_subject_base": "The installer _subject_base setting", + "config_setup_ca": "The config setup_ca setting", + "config_master_host_name": "The config master_host_name setting", + "config_ca_host_name": "The config ca_host_name setting", + "config_ips": "The config ips setting", + "_ca_file": "The installer _ca_file setting", + "_kra_enabled": "The installer _kra_enabled setting", + "_dirsrv_pkcs12_info": "The installer _dirsrv_pkcs12_info setting", + "_pkinit_pkcs12_info": "The installer _pkinit_pkcs12_info setting", + "_http_pkcs12_info": "The installer _http_pkcs12_info setting", + "ds_ca_subject": "The ds.ca_subject setting", + "ca_subject": "The installer ca_subject setting", + "_hostname_overridden": "The installer _hostname_overridden setting", + "_kra_host_name": "The installer _kra_host_name setting", + "_http_ca_cert": "The installer _http_ca_cert setting", + "_update_hosts_file": "The installer _update_host_file setting", + "sssd": "The installer sssd setting", + "dnsok": "The installer dnsok setting", + + "dm_password": "Directory Manager password", + "password": "Admin user kerberos password", + "ip_addresses": "List of Master Server IP Addresses", + "domain": "Primary DNS domain of the IPA deployment", + "realm": "Kerberos realm name of the IPA deployment", + "hostname": "Fully qualified name of this host", + "ca_cert_file": [ + "A CA certificate to use. Do not acquire the IPA CA certificate via", + "automated means" + ], + "ca_cert_files": [ + "List of files containing CA certificates for the service certificate", + "files" + ], + "no_host_dns": "Do not use DNS for hostname lookup during installation", + "setup_adtrust": "Configure AD trust capability", + "setup_ca": "Configure a dogtag CA", + "setup_kra": "Configure a dogtag KRA", + "setup_dns": "Configure bind with our zone", + "force_join": "Force client enrollment even if already enrolled", + "subject_base": [ + "The certificate subject base (default O=).", + "RDNs are in LDAP order (most specific RDN first)." + ], + "server": "Fully qualified name of IPA server to enroll to", + "dirman_password": "Directory Manager (master) password", + "no_pkinit": "Disable pkinit setup steps", + "no_ui_redirect": "Do not automatically redirect to the Web UI", + "external_ca": "External ca setting", + "setup_adtrust": "Configure AD trust capability", + "external_cert_files": [ + "File containing the IPA CA certificate and the external CA certificate", + "chain" + ], + "reverse_zones": "The reverse DNS zones to use", + "no_reverse": "Do not create new reverse DNS zone", + "auto_reverse": "Create necessary reverse zones", + "forwarders": "Add DNS forwarders", + "no_forwarders": "Do not add any DNS forwarders, use root servers instead", + "auto_forwarders": "Use DNS forwarders configured in /etc/resolv.conf", + "forward_policy": "DNS forwarding policy for global forwarders", + "enable_compat": "Enable support for trusted domains for old clients", + "netbios_name": "NetBIOS name of the IPA domain", + "rid_base": "Start value for mapping UIDs and GIDs to RIDs", + "secondary_rid_base": [ + "Start value of the secondary range for mapping UIDs and GIDs to RIDs" + ], + "pki_config_override": "Path to ini file with config overrides", + "servers": "Fully qualified name of IPA servers to enroll to", + "hidden_replica": "Install a hidden replica", + "dirsrv_config_file": [ + "The path to LDIF file that will be used to modify configuration of", + "dse.ldif during installation of the directory server instance" + ], + "dirsrv_cert_files": [ + "Files containing the Directory Server SSL certificate and private key" + ], + "http_cert_files": [ + "File containing the Apache Server SSL certificate and private key" + ], + "pkinit_cert_files": [ + "File containing the Kerberos KDC SSL certificate and private key" + ], + "no_ntp": "Do not configure ntp", + "ntp_servers": "ntp servers to use", + "ntp_pool": "ntp server pool to use", + "no_dnssec_validation": "Disable DNSSEC validation", + "master": "Master host name", + "master_password": "kerberos master password (normally autogenerated)", + "principal": [ + "User Principal allowed to promote replicas and join IPA realm" + ], + "dirsrv_pin": "The password to unlock the Directory Server private key", + "http_pin": "The password to unlock the Apache Server private key", + "pkinit_pin": "The password to unlock the Kerberos KDC private key", + "dirsrv_cert_name": + "Name of the Directory Server SSL certificate to install", + "http_cert_name": "Name of the Apache Server SSL certificate to install", + "pkinit_cert_name": "Name of the Kerberos KDC SSL certificate to install", + "keytab": "Path to backed up keytab from previous enrollment", + "mkhomedir": "Create home directories for users on their first login", + "adtrust_netbios_name": "The adtrust netbios_name setting", + "adtrust_reset_netbios_name": "The adtrust reset_netbios_name setting", + "zonemgr": + "DNS zone manager e-mail address. Defaults to hostmaster@DOMAIN", + "ssh_trust_dns": "Configure OpenSSH client to trust DNS SSHFP records", + "dns_ip_addresses": "The dns ip_addresses setting", + "dns_reverse_zones": "The dns reverse_zones setting", + "no_ssh": "Do not configure OpenSSH client", + "no_sshd": "Do not configure OpenSSH server", + "no_dns_sshfp": "Do not automatically create DNS SSHFP records", + "allow_zone_overlap": "Create DNS zone even if it already exists", + "skip_conncheck": "Skip connection check to remote master", + "idstart": "The starting value for the IDs range (default random)", + "idmax": "The max value for the IDs range (default: idstart+199999)", + "no_hbac_allow": "Don't install allow_all HBAC rule", + "domainlevel": "The domain level", + "external_ca_type": "Type of the external CA", + "external_ca_profile": [ + "Specify the certificate profile/template to use at the external CA" + ], + "force": "Installer force parameter", + "ca_signing_algorithm": "Signing algorithm of the IPA CA certificate", + "debug": "Turn on extra debugging", + "basedn": "The basedn of the IPA server (of the form dc=example,dc=com)", + "allow_repair": [ + "Allow repair of already joined hosts. Contrary to ipaclient_force_join", + "the host entry will not be changed on the server" + ], + "backup": "File to backup", + "fqdn": [ + "The fully-qualified hostname of the host to add/modify/remove" + ], + "certificates": "A list of host certificates", + "sshpubkey": "The SSH public key for the host", + "ipaddress": "The IP address for the host", + "random": "Generate a random password to be used in bulk enrollment", + "state": "The desired host state", + "kdc": "The name or address of the host running the KDC", + "admin_keytab": "The path to a local admin keytab", + "kinit_attempts": "Repeat the request for host Kerberos ticket X times", + "automount_location": "The automount location", + "firefox_dir": [ + "Specify directory where Firefox is installed (for example", + "'/usr/lib/firefox')" + ], + "client_domain": "Primary DNS domain of the IPA deployment", + "nisdomain": "The NIS domain name", + "ca_enabled": "Whether the Certificate Authority is enabled or not", + "on_master": "Whether the configuration is done on the master or not", + "enable_dns_updates": [ + "Configures the machine to attempt dns updates when the ip address", + "changes" + ], + "all_ip_addresses": [ + "All routable IP addresses configured on any interface will be added", + "to DNS" + ], + "request_cert": "Request certificate for the machine", + "preserve_sssd": "Preserve old SSSD configuration if possible", + "no_sudo": "Do not configure SSSD as data source for sudo", + "fixed_primary": + "Configure sssd to use fixed server as primary IPA server", + "permit": "Disable access rules by default, permit all access", + "no_krb5_offline_passwords": [ + "Configure SSSD not to store user password when the server is offline" + ], + "force_ntpd": [ + "Stop and disable any time&date synchronization services besides ntpd", + "Deprecated since 4.7" + ], + "no_nisdomain": "Do not configure NIS domain name", + "configure_firefox": "Configure Firefox to use IPA domain credentials", +} + + +def gen_module_docs(module_in): + with open(module_in) as in_f: + in_lines = in_f.readlines() + + arg_spec = False + args = [] + i = 0 + while i < len(in_lines): + line = in_lines[i] + stripped = line.strip() + # print("stripped: %s" % repr(stripped)) + if stripped.startswith("# "): + pass + elif stripped.startswith("argument_spec=dict()"): + pass + elif stripped.startswith("argument_spec=dict("): + arg_spec = True + elif stripped.startswith("),") and arg_spec: + arg_spec = False + elif arg_spec: + # if not "dict=(" in stripped: + # print("%s: Bad argument dict line '%s'" % (module_in, + # stripped)) + while ")," not in stripped and i < len(in_lines) - 1: + next_stripped = in_lines[i+1].strip() + if not next_stripped.startswith("# "): + stripped += next_stripped + i += 1 + # print("stripped: '%s'" % stripped) + + try: + param, _dict = stripped.split("=", 1) + except Exception: + print("Failed to split line '%s'" % stripped) + sys.exit(1) + + # print("_dict: '%s'" % _dict) + if not _dict.startswith("dict(") or not _dict.endswith("),"): + print("%s: Bad argument dict line 2 '%s'" % (module_in, _dict)) + sys.exit(1) + else: + _dict = _dict[5:-2] + + if param not in param_docs: + print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + print("%s: param '%s' is not in param_docs" % (module_in, + param)) + print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + sys.exit(1) + + # print("param: '%s', dict: '%s'" % (param, _dict)) + + opts = _dict.split(',') + opts = [o.strip() for o in opts] + required = False + # no_log = False + if "required=True" in opts: + required = True + # if "no_log=True" in opts: + # no_log = True + + # args.append([param, required, no_log]) + args.append([param, required]) + i += 1 + + # print("%s: %s" % (module_in, repr(args))) + + def add_options(args): + for param, required in args: + out_lines.append(" %s:\n" % param) + if isinstance(param_docs[param], list): + out_lines.append(" description:\n") + for x in param_docs[param]: + out_lines.append(" %s\n" % x) + else: + out_lines.append(" description: %s\n" % param_docs[param]) + out_lines.append(" required: %s\n" % ("yes", "no")[required]) + + out_lines = [] + options = False + in_options = False + changed = False + docs = False + for line in in_lines: + stripped = line.strip() + if stripped.startswith("DOCUMENTATION = '''"): + docs = True + elif stripped.startswith("options:"): + out_lines.append(line) + add_options(args) + options = True + in_options = True + changed = True + continue + elif stripped.startswith("author:"): + if not options: + add_options(args) + options = True + changed = True + in_options = False + elif stripped.startswith("'''"): + if not options: + add_options(args) + options = True + changed = True + in_options = False + docs = False + elif docs and in_options: + continue + + out_lines.append(line) + + print(module_in) + # for line in out_lines: + # sys.stdout.write(line) + + if changed: + with open(module_in, "w") as out_f: + for line in out_lines: + out_f.write(line) + + +gen_module_docs(sys.argv[1]) diff --git a/utils/gen_modules_docs.sh b/utils/gen_modules_docs.sh new file mode 100644 index 0000000..de1d931 --- /dev/null +++ b/utils/gen_modules_docs.sh @@ -0,0 +1,3 @@ +for i in roles/ipa*/*/*.py; do + python utils/gen_module_docs.py $i +done