diff --git a/Puppetfile b/Puppetfile index 8bfe706ee..cb36fdc17 100644 --- a/Puppetfile +++ b/Puppetfile @@ -19,8 +19,8 @@ mod 'common', :git => 'https://github.com/purpleidea/puppet-common.git' mod 'concat', - :commit => '644fb1b6dc8b64accc4d1208d6127b79a08a38b7' - :git => 'https://github.com/puppetlabs/puppetlabs-concat.git', + :commit => '644fb1b6dc8b64accc4d1208d6127b79a08a38b7', + :git => 'https://github.com/puppetlabs/puppetlabs-concat.git' mod 'firewall', :commit => 'd5a10f5a52d84b9fcfb8fc65ef505685a07d5799', @@ -54,6 +54,10 @@ mod 'inifile', :commit => 'fe9b0d5229ea37179a08c4b49239da9bc950acd1', :git => 'https://github.com/puppetlabs/puppetlabs-inifile.git' +mod 'ipa', + :commit => '2cbd870b0dba2b8f588d74fc5ff8aa9cd0dc9ccf', + :git => 'https://github.com/xbezdick/puppet-ipa.git' + mod 'keystone', :commit => '605161f3d4b7bbcffc657c86b367159701dfdcbe', :git => 'https://github.com/stackforge/puppet-keystone.git' diff --git a/ipa/.gitignore b/ipa/.gitignore new file mode 100644 index 000000000..16c0d1ef6 --- /dev/null +++ b/ipa/.gitignore @@ -0,0 +1,6 @@ +tmp/ +old/ +pkg/ +hacking/ +.pmtignore +puppet-ipa-documentation.pdf diff --git a/ipa/.gitmodules b/ipa/.gitmodules new file mode 100644 index 000000000..24eadf7f2 --- /dev/null +++ b/ipa/.gitmodules @@ -0,0 +1,21 @@ +[submodule "vagrant/puppet/modules/puppet"] + path = vagrant/puppet/modules/puppet + url = https://github.com/purpleidea/puppet-puppet +[submodule "vagrant/puppet/modules/module-data"] + path = vagrant/puppet/modules/module-data + url = https://github.com/purpleidea/puppet-module-data +[submodule "vagrant/puppet/modules/stdlib"] + path = vagrant/puppet/modules/stdlib + url = https://github.com/purpleidea/puppetlabs-stdlib +[submodule "vagrant/puppet/modules/shorewall"] + path = vagrant/puppet/modules/shorewall + url = https://github.com/purpleidea/puppet-shorewall +[submodule "vagrant/puppet/modules/yum"] + path = vagrant/puppet/modules/yum + url = https://github.com/purpleidea/puppet-yum +[submodule "vagrant/puppet/modules/ssh"] + path = vagrant/puppet/modules/ssh + url = https://github.com/purpleidea/puppet-ssh +[submodule "vagrant/puppet/modules/keepalived"] + path = vagrant/puppet/modules/keepalived + url = https://github.com/purpleidea/puppet-keepalived diff --git a/ipa/.travis.yml b/ipa/.travis.yml new file mode 100644 index 000000000..354f4ef05 --- /dev/null +++ b/ipa/.travis.yml @@ -0,0 +1,16 @@ +language: ruby +rvm: 1.8.7 +notifications: + email: + - travis-ci@shubin.ca +# TODO: do a full rake test once warnings are all gone! +#script: 'bundle exec rake test' +script: 'bundle exec rake syntax' +env: +# - PUPPET_VERSION=2.7.26 + - PUPPET_VERSION=3.0.2 + - PUPPET_VERSION=3.1.1 + - PUPPET_VERSION=3.2.4 + - PUPPET_VERSION=3.3.2 + - PUPPET_VERSION=3.4.3 + diff --git a/ipa/COPYING b/ipa/COPYING new file mode 100644 index 000000000..dba13ed2d --- /dev/null +++ b/ipa/COPYING @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 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 Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +. diff --git a/ipa/COPYRIGHT b/ipa/COPYRIGHT new file mode 100644 index 000000000..480149fb7 --- /dev/null +++ b/ipa/COPYRIGHT @@ -0,0 +1,16 @@ +Copyright (C) 2012-2013+ James Shubin +Written by James Shubin + +This puppet module is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This puppet module 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + diff --git a/ipa/DOCUMENTATION.md b/ipa/DOCUMENTATION.md new file mode 100644 index 000000000..19d143755 --- /dev/null +++ b/ipa/DOCUMENTATION.md @@ -0,0 +1,851 @@ +#Puppet-IPA + + + +##A FreeIPA Puppet module by [James](https://ttboj.wordpress.com/) +####Available from: +####[https://github.com/purpleidea/puppet-ipa/](https://github.com/purpleidea/puppet-ipa/) + +####Also available from: +####[https://gitorious.org/purpleidea/puppet-ipa/](https://gitorious.org/purpleidea/puppet-ipa/) + +####This documentation is available in: [Markdown](https://github.com/purpleidea/puppet-ipa/blob/master/DOCUMENTATION.md) or [PDF](https://pdfdoc-purpleidea.rhcloud.com/pdf/https://github.com/purpleidea/puppet-ipa/blob/master/DOCUMENTATION.md) format. + +####Table of Contents + +1. [Overview](#overview) +2. [Module description - What the module does](#module-description) +3. [Setup - Getting started with Puppet-IPA](#setup) + * [What can Puppet-IPA manage?](#what-can-puppet-ipa-manage) + * [Basic setup](#basic-setup) + * [Advanced setup](#advanced-setup) + * [Multi-master setup](#multi-master-setup) + * [Type setup](#type-setup) + * [Client setup](#client-setup) +4. [Usage/FAQ - Notes on management and frequently asked questions](#usage-and-frequently-asked-questions) +5. [Reference - Class and type reference](#reference) + * [ipa::server](#ipaserver) + * [ipa::server::host](#ipaserverhost) + * [ipa::server::service](#ipaserverservice) + * [ipa::server::user](#ipaserveruser) + * [ipa::client::deploy](#ipaclientdeploy) +6. [Examples - Example configurations](#examples) +7. [Limitations - Puppet versions, OS compatibility, etc...](#limitations) +8. [Development - Background on module development and reporting bugs](#development) +9. [Author - Author and contact information](#author) + +##Overview + +The Puppet-IPA module installs, configures, and manages a FreeIPA server, +replicas, and clients. + +##Module Description + +This Puppet-IPA module handles installation, configuration, and management +of FreeIPA across all of the replicas and clients in your infrastructure. + +##Setup + +###What can Puppet-IPA manage? + +Puppet-IPA is designed to be able to manage as much or as little of your +FreeIPA infrastructure as you wish. All features are optional. If there is a +feature that doesn't appear to be optional, and you believe it should be, +please let me know. Having said that, it makes good sense to me to have +Puppet-IPA manage as much of your FreeIPA infrastructure as it can. At the +moment, it cannot rack new servers, but I am accepting funding to explore this +feature ;) At the moment it can manage: + +* FreeIPA packages (rpm) +* FreeIPA server installation (ipa-server-install) +* FreeIPA configuration files (/etc/ipa/) +* FreeIPA replica peering (ipa-replica-{prepare,install}) +* FreeIPA replica topology (ipa-replica-manage) +* FreeIPA firewalling (whitelisting) +* FreeIPA host creation/modification/deletion (ipa host-{add,mod,del}) +* FreeIPA service creation/modification/deletion (ipa service-{add,mod,del}) +* FreeIPA user creation/modification/deletion (ipa user-{add,mod,del}) +* FreeIPA client installation (ipa-client-install, ipa-getkeytab) +* And much more... + +###Basic setup + +For a single host FreeIPA server, setup is quite simple and straight-forward. + +```puppet +node ipa1 { + class { '::ipa::server': + dm_password => 'changeme', # pick a dm password + admin_password => 'changeme2', # pick an admin password + shorewall => true, + } +} +``` + +Please be careful to keep the the _dm_ and _admin_ passwords secret. For a +better way to do this, please have a look at the [advanced setup](#advanced-setup). + +###Advanced setup + +Storing secrets in puppet is generally a bad idea. Because FreeIPA is usually +the security cornerstone of your infrastructure, care was taken to ensure that +this puppet modules does all that it can to survice even the harshest scrutiny. + +If desired, the local FreeIPA server can securely generate _dm_ and _admin_ +passwords, and encrypt them with your public GPG key. This way, you can have an +automatic FreeIPA deployment without ever having the secrets touch puppet. For +more information on this technique and how it works with puppet please read: +[Securely managing secrets for FreeIPA with Puppet](https://ttboj.wordpress.com/2014/06/06/securely-managing-secrets-for-freeipa-with-puppet/). + +Here is an example of how you can use the GPG parameters: + +```puppet +node ipa1 { + class { '::ipa::server': + # NOTE: email must exist in the public key if we use gpg_sendemail + #email => 'root@example.com', + gpg_recipient => '24090D66', # use your own GPG key id here! + #gpg_publickey => '...', # alternatively, paste it here! + gpg_keyserver => 'hkp://keys.gnupg.net', # pick your own + gpg_sendemail => false, # optional choice you can make. + shorewall => true, + } +} + +``` + +For information on how Puppet-IPA can be used for hybrid management of FreeIPA +types, please read: +[Hybrid management of FreeIPA types with Puppet](https://ttboj.wordpress.com/2014/07/24/hybrid-management-of-freeipa-types-with-puppet/). + +For information on additional advanced options, please see the +[reference](#reference) section below for the specifics. + +###Multi-master setup + +Puppet-IPA can be used to setup a cluster of FreeIPA servers. Each server in +the cluster becomes a FreeIPA replica. There are some additional requirements +and (unfortunately) limitations at this time. You will require: + +* An additional IP address to be used as a VIP. (It can be a private address.) +* More than one server or virtual machine. + +The limitation is that [Securely managing secrets for FreeIPA with Puppet](https://ttboj.wordpress.com/2014/06/06/securely-managing-secrets-for-freeipa-with-puppet/) +is not currently compatible with the automatic multi-master deployment. This is +due to a limitation in FreeIPA, and it will hopefully be fixed in a future +release. Help is appreciated! + +```puppet +node /^ipa\d+$/ { # ipa{1,2,..N} + class { '::ipa::server': + vip => '192.168.123.253', # pick an available VIP + topology => 'ring', # choose your favourite + dm_password => 'changeme', # pick a dm password + admin_password => 'changeme2', # pick an admin password + # NOTE: Unfortunately the gpg_* options are not currently + # compatible with automatic multi-master replication. This is + # due to a limitation in FreeIPA, but should hopefully get + # resolved in a future FreeIPA release. Help is appreciated! + vrrp => true, # setup keepalived + shorewall => true, + } +} + +``` + +The server configuration that you choose must be identical on each FreeIPA +server. As you can see in the above example, this is achieved by including a +_node_ pattern that matches ipa{1,2,...N} so that this is automatic. + +Another point of interest is the _topology_ parameter. This automatically +figures out the correct relationships for all of the hosts in your cluster +algorithmically, so that you can worry about other issues. Other algorithms can +be written and included, and contributions are encouraged. You can also specify +the topology manually if you want to be extremely hands on with your layout. + +###Type setup + +Naturally you'll probably want to define FreeIPA types on your server. The most +common ones are _host_, _service_, and _user_. These should be defined on all +the server hosts. Some examples include: + +```puppet +# create a managed user +ipa::server::user { 'ntesla': + first => 'Nikola', + last => 'Tesla', + city => 'Shoreham', + state => 'New York', + postalcode => '11786', +} +``` + +```puppet +# create a host +ipa::server::host { 'nfs': # NOTE: adding .${domain} is a good idea.... + domain => 'example.com', + macaddress => "00:11:22:33:44:55", + random => true, # set a one time password randomly + locality => 'Montreal, Canada', + location => 'Room 641A', + platform => 'Supermicro', + osstring => 'CentOS 6.5 x86_64', + comment => 'Simple NFSv4 Server', + watch => true, # read and understand the docs well +} +``` + +```puppet +# create a service for the above host +ipa::server::service { 'nfs': + service => 'nfs', + host => 'nfs', # needs to match ipa::server::host $name + domain => 'example.com', # used to figure out realm +} +``` + +For more information on FreeIPA type management, and the hybrid management +feature that Puppet-IPA supports, please read: +[Hybrid management of FreeIPA types with Puppet](https://ttboj.wordpress.com/2014/07/24/hybrid-management-of-freeipa-types-with-puppet/). + +###Client setup + +Getting a client host to enroll and work magically with the FreeIPA server is +particularly easy. Simply include the IPA client _deploy_ class: + +```puppet +# there is now a single "deploy" which can be used for both hosts and services! +include ipa::client::deploy +``` + +This will automatically pull down any host registration and service definitions +that were defined on the Puppet-IPA server. If you want to be more specific +about which you include, you can include each set of types separately: + +```puppet +# if you use fqdn's for the ipa:server:host $name's, then you can deploy with: +include ipa::client::host::deploy +``` + +or to only include services: + +```puppet +# pull down any defined FreeIPA services. +include ipa::client::service::deploy +``` + +For an NFS host (which is a FreeIPA client), you might want to use: + +```puppet +# and on the nfs server (an ipa client): +class { '::ipa::client::deploy': + nametag => 'nfs', # needs to match the ipa:server:host $name +} +``` + +All of this happens automatically through the magic of puppet and exported +resources. See the [examples](#examples) for more ideas. + +##Usage and frequently asked questions + +All management should be done by manipulating the arguments on the appropriate +Puppet-IPA classes and types. Hybrid management is also supported for certain +aspects of FreeIPA management. This is a stellar feature of Puppet-IPA. Please +read: [Hybrid management of FreeIPA types with Puppet](https://ttboj.wordpress.com/2014/07/24/hybrid-management-of-freeipa-types-with-puppet/). + +###Do I need to use a virtual IP? + +Using a virtual IP (VIP) is strongly recommended as a distributed lock manager +(DLM) for certain operations. For an article explaning the mechanism (but for a +different puppet module), please see: +[How to avoid cluster race conditions or: How to implement a distributed lock manager in puppet](https://ttboj.wordpress.com/2012/08/23/how-to-avoid-cluster-race-conditions-or-how-to-implement-a-distributed-lock-manager-in-puppet/) + +Remember that even if you're using a hosted solution (such as AWS) that doesn't +provide an additional IP address, or you want to avoid using an additional IP, +you can use an unused private RFC1918 IP address as the DLM VIP. Remember that +a layer 3 IP can co-exist on the same layer 2 network with the layer 3 network +that is used by your cluster. + +###Is it possible to have Puppet-IPA complete in a single run? + +No. This is a limitation of Puppet, and is related to how FreeIPA operates. It +is possible for a single FreeIPA server, but for many multi-host scenarios, +including the multi-master FreeIPA case, you will require more than one run. + +For example, + +###Can you integrate this with vagrant? + +Yes, see the +[vagrant/](https://github.com/purpleidea/puppet-ipa/tree/master/vagrant) +directory. This has been tested on Fedora 20, with vagrant-libvirt, as I have +no desire to use VirtualBox for fun. I have written many articles about this on +my [technical blog](https://ttboj.wordpress.com/?s=vagrant). In particular, I +would recommend: [Vagrant on Fedora with libvirt (reprise)](https://ttboj.wordpress.com/2014/05/13/vagrant-on-fedora-with-libvirt-reprise/). + +###Puppet runs fail with "Connection refused - connect(2)" errors. + +You may see a "_Connection refused - connect(2)_" message when running puppet. +This typically happens if your puppet vm guest is overloaded. When running high +guest counts on your laptop, or running without hardware virtualization support +this is quite common. Another common causes of this is if your domain type is +set to _qemu_ instead of the accelerated _kvm_. Since the _qemu_ domain type is +much slower, puppet timeouts and failures are common when it doesn't respond. + +###Will this work on my favourite OS? (eg: GNU/Linux F00bar OS v12 ?) +If it's a GNU/Linux based OS, can run FreeIPA, and Puppet, then it will +probably work. Typically, you might need to add a yaml data file to the _data/_ +folder so that Puppet-IPA knows where certain operating system specific +things are found. The multi-distro support has been designed to make it +particularly easy to add support for additional platforms. If your platform +doesn't work, please submit a yaml data file with the platform specific values. + +###Awesome work, but it's missing support for a feature and/or platform! + +Since this is an Open Source / Free Software project that I also give away for +free (as in beer, free as in gratis, free as in libre), I'm unable to provide +unlimited support. Please consider donating funds, hardware, virtual machines, +and other resources. For specific needs, you could perhaps sponsor a feature! + +###You didn't answer my question, or I have a question! + +Contact me through my [technical blog](https://ttboj.wordpress.com/contact/) +and I'll do my best to help. If you have a good question, please remind me to +add my answer to this documentation! + +##Reference +Please note that there are a number of undocumented options. For more +information on these options, please view the source at: +[https://github.com/purpleidea/puppet-ipa/](https://github.com/purpleidea/puppet-ipa/). +If you feel that a well used option needs documenting here, please contact me. + +###Overview of classes and types +Please note that the most common, user facing classes and types are documented, +while there may be many other undocumented classes and types that are used +internally by other classes and types. These will be documented as time allows. + +* [ipa::server](#ipaserver): Base class for server hosts. +* [ipa::server::host](#ipaserverhost): Host type for each FreeIPA client. +* [ipa::server::service](#ipaserverservice): Service type for each FreeIPA service. +* [ipa::server::user](#ipaserveruser): User type for each FreeIPA user. +* [ipa::client::deploy](#ipaclientdeploy): Client class to deploy types. + +###ipa::server +This is the main class to be used for IPA server installation. It is used for +both simple standalone FreeIPA servers, and for complex multi-master scenarios. + +####`hostname` +The hostname of the IPA server. This defaults to _$::hostname_. + +####`domain` +The domain of the IPA server and of the cluster. This defaults to _$::domain_. + +####`realm` +The realm of the cluster. This defaults to _upcase($domain)_. + +####`vip` +The virtual IP address to be used for the cluster distributed lock manager. +This option can be used in conjunction with the _vrrp_ option, but it does not +require it. If you don't want to provide a virtual ip, but you do want to +enforce that certain operations only run on one host, then you can set this +option to be the ip address of an arbitrary host in your cluster. Keep in mind +that if that host is down, certain options won't ever occur. + +####`peers` +Specify the peering topology manually in dictionary form. Each dictionary value +should be an array of the peers to connect to from the originating key. + +####`topology` +The topology algorithm to use when setting up mult-master cluster +automatically. A few default algorithms are included with Puppet-IPA. They are: +_ring_, _flat_. If you'd like to include an algorithm that generates a +different topology, it is easy to drop one in! Please contact me with the +details! + +####`topology_arguments` +A list of arguments to pass to the topology algorithm. Not all topology +algorithms support this, however it can be very useful so that it's easy to +generalize certain algorithms in the same function in terms of these variables. + +####`dm_password` +The dm_password in plaintext. It is recommended that you use a better mechanism +for handling secrets. Please see: +[Securely managing secrets for FreeIPA with Puppet](https://ttboj.wordpress.com/2014/06/06/securely-managing-secrets-for-freeipa-with-puppet/). + +####`admin_password` +The admin_password in plaintext. It is recommended that you use a better method +for handling secrets. Please see: +[Securely managing secrets for FreeIPA with Puppet](https://ttboj.wordpress.com/2014/06/06/securely-managing-secrets-for-freeipa-with-puppet/). + +####`gpg_recipient` +The GPG recipient ID of your public key goes here. This is a valid _-r_ value +for the _gpg_ command line tool. + +####`gpg_publickey` +This is the value of your GPG public key, or a _puppet:///_ uri pointing to the +file. This can't be used in conjunction with the `gpg_keyserver` option. + +####`gpg_keyserver` +This is the value of the GPG keyserver of your choice. You can use a public one +such as _hkp://keys.gnupg.net_ or you can use your own private keyserver. + +####`gpg_sendemail` +Do you want to mail out the _dm_ and _admin_ passwords encrypted with your GPG +key or not? Defaults to _false_. + +####`idstart` +The FreeIPA _idstart_ value to use. This is the starting id for users created. +This comes with a mostly sensible default, but recommendations are welcome. + +####`idmax` +The FreeIPA _idmax_ value to use. + +####`email_domain` +The email domain to use. This defaults to _$domain_. + +####`shell` +The default user shell to assign. This default to _/bin/sh_. + +####`homes` +The default home prefix. This defaults to _/home_. + +####`ntp` +Should we install an NTP server along with FreeIPA? This defaults to _false_. + +####`dns` +Should we install a DNS server along with FreeIPA? This defaults to _false_. +This must be set at install time to be used. + +####`dogtag` +Should we install a cert server along with FreeIPA? This defaults to _false_. +This is not currently managed by Puppet-IPA. + +####`email` +The email address to associate with the FreeIPA server. This defaults to +_root@$domain_. This is important for FreeIPA, and it is used to mail out +encrypted passwords depending on your `gpg_sendemail` settings. + +####`vrrp` +Whether to automatically deploy and manage _Keepalived_ for use as a _DLM_ and +for use in volume mounting, etc... Using this option requires the _vip_ option. + +####`shorewall` +Boolean to specify whether puppet-shorewall integration should be used or not. + +####`again` +Do you want to use _Exec['again']_ ? This helps build your cluster quickly! + +####`host_excludes` +This list matches and excludes certain _hosts_ from being removed by puppet. +The `host_excludes` are matched with bash regexp matching in: `[[ =~ ]]`. If +the string regexp passed contains quotes, string matching is done: + +`$string='"hostname.example.com"' vs $regexp='hostname.example.com'` + +Obviously, each pattern in the array is tried, and any match will do. Invalid +expressions might cause breakage! Use this at your own risk!! Remember that you +might be matching against strings which have dots. A value of true will +automatically add the `*` character to match all. For more information on this +option, please read: +[Hybrid management of FreeIPA types with Puppet](https://ttboj.wordpress.com/2014/07/24/hybrid-management-of-freeipa-types-with-puppet/). + +####`service_excludes` +This list matches and excludes certain _services_ from being removed by puppet. +The `service_excludes` are matched with bash regexp matching in: `[[ =~ ]]`. If +the string regexp passed contains quotes, string matching is done: + +`$string='"hostname.example.com"' vs $regexp='hostname.example.com'` + +Obviously, each pattern in the array is tried, and any match will do. Invalid +expressions might cause breakage! Use this at your own risk!! Remember that you +might be matching against strings which have dots. A value of true will +automatically add the `*` character to match all. For more information on this +option, please read: +[Hybrid management of FreeIPA types with Puppet](https://ttboj.wordpress.com/2014/07/24/hybrid-management-of-freeipa-types-with-puppet/). + +####`user_excludes` +This list matches and excludes certain _users_ from being removed by puppet. +The `user_excludes` are matched with bash regexp matching in: `[[ =~ ]]`. If +the string regexp passed contains quotes, string matching is done: + +`$string='"hostname.example.com"' vs $regexp='hostname.example.com'` + +Obviously, each pattern in the array is tried, and any match will do. Invalid +expressions might cause breakage! Use this at your own risk!! Remember that you +might be matching against strings which have dots. A value of true will +automatically add the `*` character to match all. For more information on this +option, please read: +[Hybrid management of FreeIPA types with Puppet](https://ttboj.wordpress.com/2014/07/24/hybrid-management-of-freeipa-types-with-puppet/). + +####`peer_excludes` +This list matches and excludes certain _peers_ from being removed by puppet. +The `peer_excludes` are matched with bash regexp matching in: `[[ =~ ]]`. If +the string regexp passed contains quotes, string matching is done: + +`$string='"hostname.example.com"' vs $regexp='hostname.example.com'` + +Obviously, each pattern in the array is tried, and any match will do. Invalid +expressions might cause breakage! Use this at your own risk!! Remember that you +might be matching against strings which have dots. A value of true will +automatically add the `*` character to match all. This particular option is +difference than the other excludes because it only prevents un-peering of the +listed hosts. + +###ipa::server::host +This is the main FreeIPA type that maps to host entries in FreeIPA. This gets +set on the FreeIPA server (or servers) and with the associated _ipa::client_ +types and classes, will automatically setup the FreeIPA client associated with +this host type. Here are a few examples: + +```puppet +# create a host +ipa::server::host { 'nfs': # NOTE: adding .${domain} is a good idea.... + domain => 'example.com', + macaddress => "00:11:22:33:44:55", + random => true, # set a one time password randomly + locality => 'Montreal, Canada', + location => 'Room 641A', + platform => 'Supermicro', + osstring => 'CentOS 6.5 x86_64', + comment => 'Simple NFSv4 Server', + watch => true, # read and understand the docs well +} +``` + +```puppet +ipa::server::host { 'test1': + domain => 'example.com', + password => 'password', + watch => true, # read and understand the docs well +} +``` + +####`domain` +The domain of the host. This defaults to the ipa server _$domain_ variable. + +####`server` +Where the client will find the IPA server. This has a sensible default. + +####`macaddress` +The [MAC address](https://en.wikipedia.org/wiki/MAC_address) of the host. + +####`sshpubkeys` +Leave this at the default for automatic public ssh keys to get transferred. + +####`password` +The one time password used for host provisioning. Not to be used with +`$random`. + +####`random` +Generate the one time password used for host provisioning. Conflicts with +`$password`. + +####`locality` +Host description parameter for _locality_. Example: "_Montreal, Canada_". + +####`location` +Host description parameter for _location_. Example: "_Lab 42_". + +####`platform` +Host description parameter for hardware _platform_. Example: "_Lenovo X201_". + +####`osstring` +Host description parameter for _os string_. Example: "_CentOS 6.5_". + +####`comments` +Host description parameter for _comments_. Example: "_NFS Server_". + +####`admin` +Should this client get the admin tools installed ? + +####`watch` +Manage all changes to this resource, reverting others if this is true. For more +information on this option please read: +[Hybrid management of FreeIPA types with Puppet](https://ttboj.wordpress.com/2014/07/24/hybrid-management-of-freeipa-types-with-puppet/). + +####`modify` +Modify this resource on puppet changes or not ? Do so if true. For more +information on this option please read: +[Hybrid management of FreeIPA types with Puppet](https://ttboj.wordpress.com/2014/07/24/hybrid-management-of-freeipa-types-with-puppet/). + +###ipa::server::service +This is the main FreeIPA type that maps to service entries in FreeIPA. This is +set on the FreeIPA server (or servers) and with the associated _ipa::client_ +types and classes, will automatically setup the FreeIPA service associated with +the correct host. Here is an example: + +```puppet +ipa::server::service { 'nfs': # this $name should match nfs::server => $ipa + service => 'nfs', + host => 'nfs', # needs to match ipa::server::host $name + domain => 'example.com', # used to figure out realm +} +``` + +####`service` +The service string. Common examples include: _nfs_, _HTTP_, _ldap_. + +####`host` +The hostname where the service will reside. + +####`domain` +The domain of the host where the service will reside. + +####`realm` +The realm of the host where the service will reside. + +####`principal` +Specify the desired principal of this service, overriding the principal that +can be ascertained by using the above values. + +####`server` +Where the client will find the IPA server. This has a sensible default. + +####`pactype` +The service _pac type_. Bad values are silently discarded, `[]` is _NONE_. + +####`watch` +Manage all changes to this resource, reverting others if this is true. For more +information on this option please read: +[Hybrid management of FreeIPA types with Puppet](https://ttboj.wordpress.com/2014/07/24/hybrid-management-of-freeipa-types-with-puppet/). + +####`modify` +Modify this resource on puppet changes or not ? Do so if true. For more +information on this option please read: +[Hybrid management of FreeIPA types with Puppet](https://ttboj.wordpress.com/2014/07/24/hybrid-management-of-freeipa-types-with-puppet/). + +####`comment` +A comment field that you can use however you want. + +###ipa::server::user +This is the main FreeIPA type that maps to user entries in FreeIPA. This is set +on the FreeIPA server (or servers) and creates user entries which can be seen +in the FreeIPA LDAP database, and which are then available on IPA clients. Here +are a few examples: + +```puppet +# create a managed user +ipa::server::user { 'ntesla': + first => 'Nikola', + last => 'Tesla', + city => 'Shoreham', + state => 'New York', + postalcode => '11786', +} +``` + +```puppet +# create a user by principal but without the instance set +ipa::server::user { 'arthur@EXAMPLE.COM': + first => 'Arthur', + last => 'Guyton', + jobtitle => 'Physiologist', + orgunit => 'Research', +} +``` + +```puppet +# create a user using a full principal as the primary key +# NOTE: the principal itself can't be edited without a remove/add +ipa::server::user { 'aturing/admin@EXAMPLE.COM': + first => 'Alan', + last => 'Turning', + random => true, # set a password randomly + password_file => true, # store the password in plain text ! (bad) +} +``` + +####`login` +The login of this user. In the principal, the pattern is: +`login/instance@REALM`. + +####`instance` +The instance of this user. In the principal, the pattern is: +`login/instance@REALM`. + +####`domain` +The domain associated with the user. Uppercase version can be used as the realm +default. + +####`realm` +The realm of this user. In the principal, the pattern is: +`login/instance@REALM`. + +####`principal` +Specify the desired principal of this user, overriding the principal that +can be ascertained by using the above values. + +####`first` +First name of user. Required. + +####`last` +Last name of user. Required. + +####`cn` +Full name of user. Defaults to: "_$first $last_". + +####`displayname` +Display name of user. Defaults to: "_$first $last_". + +####`initials` +Initials of user. Defaults to: "_$first[0] $last[0]_". + +####`email` +Email address of user. + +####`gecos` +Legacy field. Can be set manually if needed. + +####`uid` +UID value for user. By default this is automatically assigned. + +####`gid` +GID value for user. By default this is automatically assigned. + +####`shell` +Shell for user. By default this is automatically assigned. + +####`home` +Home dir for user. By default this is automatically assigned. + +####`sshpubkeys` +Public SSH keys for the user. + +####`random` +Set to _true_ to have the user password auto generated. + +####`password_file` +Save user password to a file. The file is in: _${vardir}/ipa/users/passwords/_. + +####`password_mail` +Mail out a GPG encrypted password to the admin. +*TODO*: this option is not yet implemented. + +####`street` +The users street address. + +####`city` +The users city. + +####`state` +The users state or province. + +####`postalcode` +The users zip or postal code. + +####`phone` +The users phone number. Can be an array of numbers. + +####`mobile` +The users mobile number. Can be an array of numbers. + +####`pager` +The users pager number. Can be an array of numbers. Users with pager numbers +are particularly cool. + +####`fax` +The users fax number. Can be an array of numbers. + +####`jobtitle` +The users job title. Silly titles are allowed. + +####`orgunit` +The users organization unit, otherwise known as a department. + +####`manager` +The users manager. Should match an existing FreeIPA user name. + +####`carlicense` +The users car license string. FreeIPA created these fields, I just wrap them. + +####`watch` +Manage all changes to this resource, reverting others if this is true. For more +information on this option please read: +[Hybrid management of FreeIPA types with Puppet](https://ttboj.wordpress.com/2014/07/24/hybrid-management-of-freeipa-types-with-puppet/). + +####`modify` +Modify this resource on puppet changes or not ? Do so if true. For more +information on this option please read: +[Hybrid management of FreeIPA types with Puppet](https://ttboj.wordpress.com/2014/07/24/hybrid-management-of-freeipa-types-with-puppet/). + +####`comment` +A comment field that you can use however you want. + +###ipa::client::deploy +Include this class to deploy the client host itself and any services. The +necessary information will automatically get exported from the FreeIPA server. +This class takes care of fetching this information through exported resources, +and safely running _ipa-getkeytab_ so that the admin sees an automatic process. + +##Examples +For example configurations, please consult the [examples/](https://github.com/purpleidea/puppet-ipa/tree/master/examples) +directory in the git source repository. It is available from: + +[https://github.com/purpleidea/puppet-ipa/tree/master/examples](https://github.com/purpleidea/puppet-ipa/tree/master/examples) + +It is also available from: + +[https://gitorious.org/purpleidea/puppet-ipa/source/examples](https://gitorious.org/purpleidea/puppet-ipa/source/examples) + +##Limitations + +This module has been tested against open source Puppet 3.2.4 and higher. + +The module is routinely tested on: + +* CentOS 6.5 + +It will probably work without incident or without major modification on: + +* CentOS 5.x/6.x +* RHEL 5.x/6.x + +It has patches to support: + +* Fedora 20+ + +It will most likely work with other Puppet versions and on other platforms, but +testing on those platforms has been minimal due to lack of time and resources. + +Testing is community supported! Please report any issues as there are a lot of +features, and in particular, support for additional distros isn't well tested. +The multi-distro architecture has been chosen to easily support new additions. +Most platforms and versions will only require a change to the yaml based data/ +folder. + +##Development + +This is my personal project that I work on in my free time. +Donations of funding, hardware, virtual machines, and other resources are +appreciated. Please contact me if you'd like to sponsor a feature, invite me to +talk/teach or for consulting. + +You can follow along [on my technical blog](https://ttboj.wordpress.com/). + +To report any bugs, please [contact me](https://ttboj.wordpress.com/contact/). + +##Author + +Copyright (C) 2012-2013+ James Shubin + +* [github](https://github.com/purpleidea/) +* [@purpleidea](https://twitter.com/#!/purpleidea) +* [https://ttboj.wordpress.com/](https://ttboj.wordpress.com/) + diff --git a/ipa/Gemfile b/ipa/Gemfile new file mode 100644 index 000000000..07dd7f4c2 --- /dev/null +++ b/ipa/Gemfile @@ -0,0 +1,11 @@ +source 'https://rubygems.org' + +puppet_version = ENV.key?('PUPPET_VERSION') ? "= #{ENV['PUPPET_VERSION']}" : ['>= 3.0'] + +gem 'rake' +gem 'puppet', puppet_version +gem 'puppet-lint' # style things, eg: tabs vs. spaces +gem 'rspec-puppet', :git => 'https://github.com/rodjek/rspec-puppet.git' +gem 'puppet-syntax' # syntax checking +gem 'puppetlabs_spec_helper' + diff --git a/ipa/INSTALL b/ipa/INSTALL new file mode 100644 index 000000000..f497841f7 --- /dev/null +++ b/ipa/INSTALL @@ -0,0 +1,18 @@ +To install this puppet module, copy this folder to your puppet modulepath. + +You can usually find out where this is by running: + +$ puppet config print modulepath + +on your puppetmaster. In my case, this contains the directory: + +/etc/puppet/modules/ + +I keep all of my puppet modules in git managed directories named: + +puppet- + +You must remove the 'puppet-' prefix from the directory name for it to work! + +Happy hacking! + diff --git a/ipa/Makefile b/ipa/Makefile new file mode 100644 index 000000000..5ab177d33 --- /dev/null +++ b/ipa/Makefile @@ -0,0 +1,155 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +.PHONY: all push docs test rpm srpm spec tar upload upload-sources upload-srpms upload-rpms +.SILENT: + +# version of the program +VERSION := $(shell gjs -c "print(`cat metadata.json`['version'])") +RELEASE = 1 +SPEC = rpmbuild/SPECS/puppet-ipa.spec +SOURCE = rpmbuild/SOURCES/puppet-ipa-$(VERSION).tar.bz2 +SRPM = rpmbuild/SRPMS/puppet-ipa-$(VERSION)-$(RELEASE).src.rpm +RPM = rpmbuild/RPMS/puppet-ipa-$(VERSION)-$(RELEASE).rpm +SERVER = 'download.gluster.org' +REMOTE_PATH = 'purpleidea/puppet-ipa' + +all: docs rpm + +push: + # use blacksmith to push module to forge + git checkout master && rake module:push + +docs: puppet-ipa-documentation.pdf + +puppet-ipa-documentation.pdf: DOCUMENTATION.md + pandoc DOCUMENTATION.md -o 'puppet-ipa-documentation.pdf' + +test: + # TODO: avoid exiting with non-zero when there are only warnings? + #rake syntax + rake test + +# +# aliases +# +# TODO: does making an rpm depend on making a .srpm first ? +rpm: $(SRPM) $(RPM) + # do nothing + +srpm: $(SRPM) + # do nothing + +spec: $(SPEC) + # do nothing + +tar: $(SOURCE) + # do nothing + +upload: upload-sources upload-srpms upload-rpms + # do nothing + +# +# rpmbuild +# +$(RPM): $(SPEC) $(SOURCE) + @echo Running rpmbuild -bb... + rpmbuild --define '_topdir $(shell pwd)/rpmbuild' -bb $(SPEC) && \ + mv rpmbuild/RPMS/noarch/puppet-ipa-$(VERSION)-$(RELEASE).*.rpm $(RPM) + +$(SRPM): $(SPEC) $(SOURCE) + @echo Running rpmbuild -bs... + rpmbuild --define '_topdir $(shell pwd)/rpmbuild' -bs $(SPEC) + # renaming is not needed because we aren't using the dist variable + #mv rpmbuild/SRPMS/puppet-ipa-$(VERSION)-$(RELEASE).*.src.rpm $(SRPM) + +# +# spec +# +$(SPEC): rpmbuild/ puppet-ipa.spec.in + @echo Running templater... + #cat puppet-ipa.spec.in > $(SPEC) + sed -e s/__VERSION__/$(VERSION)/ -e s/__RELEASE__/$(RELEASE)/ < puppet-ipa.spec.in > $(SPEC) + # append a changelog to the .spec file + git log --format="* %cd %aN <%aE>%n- (%h) %s%d%n" --date=local | sed -r 's/[0-9]+:[0-9]+:[0-9]+ //' >> $(SPEC) + +# +# archive +# +$(SOURCE): rpmbuild/ + @echo Running git archive... + # use HEAD if tag doesn't exist yet, so that development is easier... + git archive --prefix=puppet-ipa-$(VERSION)/ -o $(SOURCE) $(VERSION) 2> /dev/null || (echo 'Warning: $(VERSION) does not exist.' && git archive --prefix=puppet-ipa-$(VERSION)/ -o $(SOURCE) HEAD) + +# TODO: ensure that each sub directory exists +rpmbuild/: + mkdir -p rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} + +# +# sha256sum +# +rpmbuild/SOURCES/SHA256SUMS: rpmbuild/SOURCES/ $(SOURCE) + @echo Running SOURCES sha256sum... + cd rpmbuild/SOURCES/ && sha256sum *.tar.bz2 > SHA256SUMS; cd - + +rpmbuild/SRPMS/SHA256SUMS: rpmbuild/SRPMS/ $(SRPM) + @echo Running SRPMS sha256sum... + cd rpmbuild/SRPMS/ && sha256sum *src.rpm > SHA256SUMS; cd - + +rpmbuild/RPMS/SHA256SUMS: rpmbuild/RPMS/ $(RPM) + @echo Running RPMS sha256sum... + cd rpmbuild/RPMS/ && sha256sum *.rpm > SHA256SUMS; cd - + +# +# gpg +# +rpmbuild/SOURCES/SHA256SUMS.asc: rpmbuild/SOURCES/SHA256SUMS + @echo Running SOURCES gpg... + # the --yes forces an overwrite of the SHA256SUMS.asc if necessary + gpg2 --yes --clearsign rpmbuild/SOURCES/SHA256SUMS + +rpmbuild/SRPMS/SHA256SUMS.asc: rpmbuild/SRPMS/SHA256SUMS + @echo Running SRPMS gpg... + gpg2 --yes --clearsign rpmbuild/SRPMS/SHA256SUMS + +rpmbuild/RPMS/SHA256SUMS.asc: rpmbuild/RPMS/SHA256SUMS + @echo Running RPMS gpg... + gpg2 --yes --clearsign rpmbuild/RPMS/SHA256SUMS + +# +# upload +# +# upload to public server +upload-sources: rpmbuild/SOURCES/ rpmbuild/SOURCES/SHA256SUMS rpmbuild/SOURCES/SHA256SUMS.asc + if [ "`cat rpmbuild/SOURCES/SHA256SUMS`" != "`ssh $(SERVER) 'cd $(REMOTE_PATH)/SOURCES/ && cat SHA256SUMS'`" ]; then \ + echo Running SOURCES upload...; \ + rsync -avz rpmbuild/SOURCES/ $(SERVER):$(REMOTE_PATH)/SOURCES/; \ + fi + +upload-srpms: rpmbuild/SRPMS/ rpmbuild/SRPMS/SHA256SUMS rpmbuild/SRPMS/SHA256SUMS.asc + if [ "`cat rpmbuild/SRPMS/SHA256SUMS`" != "`ssh $(SERVER) 'cd $(REMOTE_PATH)/SRPMS/ && cat SHA256SUMS'`" ]; then \ + echo Running SRPMS upload...; \ + rsync -avz rpmbuild/SRPMS/ $(SERVER):$(REMOTE_PATH)/SRPMS/; \ + fi + +upload-rpms: rpmbuild/RPMS/ rpmbuild/RPMS/SHA256SUMS rpmbuild/RPMS/SHA256SUMS.asc + if [ "`cat rpmbuild/RPMS/SHA256SUMS`" != "`ssh $(SERVER) 'cd $(REMOTE_PATH)/RPMS/ && cat SHA256SUMS'`" ]; then \ + echo Running RPMS upload...; \ + rsync -avz --prune-empty-dirs rpmbuild/RPMS/ $(SERVER):$(REMOTE_PATH)/RPMS/; \ + fi + +# vim: ts=8 diff --git a/ipa/README b/ipa/README new file mode 100644 index 000000000..91bb599b8 --- /dev/null +++ b/ipa/README @@ -0,0 +1,2 @@ +Please see README.md + diff --git a/ipa/README.md b/ipa/README.md new file mode 100644 index 000000000..dc9e4dcfb --- /dev/null +++ b/ipa/README.md @@ -0,0 +1,33 @@ +# *puppet-ipa*: a puppet module for FreeIPA + +[![Build Status](https://secure.travis-ci.org/purpleidea/puppet-ipa.png)](http://travis-ci.org/purpleidea/puppet-ipa) + +## Documentation: +Please see: [DOCUMENTATION.md](DOCUMENTATION.md) or [puppet-ipa-documentation.pdf](puppet-ipa-documentation.pdf). + +## Installation: +Please read the [INSTALL](INSTALL) file for instructions on getting this installed. + +## Examples: +Please look in the [examples/](examples/) folder for usage. If none exist, please contribute one! + +## Module specific notes: +* This is a rather elaborate module. Be sure to review the code before using. +* There are a number of (useful) lengthy comments and explanations in the code. +* This module plays nicely with my puppet-nfs module. Use them together :) + +## Dependencies: +* [puppetlabs-stdlib](https://github.com/puppetlabs/puppetlabs-stdlib) (required) +* my [puppet-shorewall](https://github.com/purpleidea/puppet-shorewall) module (optional) +* my [puppet-puppet](https://github.com/purpleidea/puppet-puppet) module (optional) +* my [puppet-nfs](https://github.com/purpleidea/puppet-nfs) module (optional) + +## Patches: +This code may be a work in progress. The interfaces may change without notice. +Patches are welcome, but please be patient. They are best received by email. +Please ping me if you have big changes in mind, before you write a giant patch. + +## + +Happy hacking! + diff --git a/ipa/Rakefile b/ipa/Rakefile new file mode 100644 index 000000000..72ce0318e --- /dev/null +++ b/ipa/Rakefile @@ -0,0 +1,48 @@ +require 'puppetlabs_spec_helper/rake_tasks' +require 'puppet-lint/tasks/puppet-lint' +require 'puppet-syntax/tasks/puppet-syntax' + +# These two gems aren't always present, for instance +# on Travis with --without development +begin + require 'puppet_blacksmith/rake_tasks' +rescue LoadError +end + +PuppetLint.configuration.relative = true +PuppetLint.configuration.send('disable_2sp_soft_tabs') +PuppetLint.configuration.send('disable_hard_tabs') +PuppetLint.configuration.send('disable_arrow_alignment') +PuppetLint.configuration.send('disable_80chars') +PuppetLint.configuration.log_format = "%{path}:%{linenumber}:%{check}:%{KIND}:%{message}" +PuppetLint.configuration.fail_on_warnings = true + +# Forsake support for Puppet 2.6.2 for the benefit of cleaner code. +# http://puppet-lint.com/checks/class_parameter_defaults/ +PuppetLint.configuration.send('disable_class_parameter_defaults') +# http://puppet-lint.com/checks/class_inherits_from_params_class/ +PuppetLint.configuration.send('disable_class_inherits_from_params_class') + +exclude_paths = [ + "pkg/**/*", + "vendor/**/*", + "spec/**/*", + "tmp/**/*", + "rpmbuild/**/*", + "vagrant/**/*", # TODO: remove this, once we update vagrant/ to puppet4 +] +PuppetLint.configuration.ignore_paths = exclude_paths +PuppetSyntax.exclude_paths = exclude_paths + +desc 'Run acceptance tests' +RSpec::Core::RakeTask.new(:acceptance) do |t| + t.pattern = 'spec/acceptance' +end + +desc 'Run syntax, lint, and spec tests.' +task :test => [ + :syntax, + :lint, + :spec, +] + diff --git a/ipa/THANKS b/ipa/THANKS new file mode 100644 index 000000000..4578fc969 --- /dev/null +++ b/ipa/THANKS @@ -0,0 +1,3 @@ +Special thanks to the friendly folks in #kerberos and #freeipa for their time. +Special thanks to the freeipa developers for knowing how to set return codes! + diff --git a/ipa/docs/Red_Hat_Enterprise_Linux-6-Identity_Management_Guide-en-US.pdf b/ipa/docs/Red_Hat_Enterprise_Linux-6-Identity_Management_Guide-en-US.pdf new file mode 100644 index 000000000..df36658b9 Binary files /dev/null and b/ipa/docs/Red_Hat_Enterprise_Linux-6-Identity_Management_Guide-en-US.pdf differ diff --git a/ipa/docs/adminkerberos.pdf b/ipa/docs/adminkerberos.pdf new file mode 100644 index 000000000..ca0b4206d Binary files /dev/null and b/ipa/docs/adminkerberos.pdf differ diff --git a/ipa/examples/host-excludes.pp b/ipa/examples/host-excludes.pp new file mode 100644 index 000000000..61eba49d8 --- /dev/null +++ b/ipa/examples/host-excludes.pp @@ -0,0 +1,36 @@ +# here is an example of how to use host excludes: +class { '::ipa::server': + shorewall => true, + host_excludes => [ + "'foo-42.example.com'", # exact string match + '"foo-bar.example.com"', # exact string match + "^[a-z0-9-]*\\-foo\\.example\\.com$", # *-foo.example.com or: + "^[[:alpha:]]{1}[[:alnum:]-]*\\-foo\\.example\\.com$", + "^foo\\-[0-9]{1,}\\.example\\.com" # foo-<\d>.example.com + ], +} + +# you'll see that you need double \\ to escape out the one we want in the match + +# if you just want to match most sane domain strings and avoid auto deletion: +class { '::ipa::server': + shorewall => true, + host_excludes => true, # automatically chooses a match all pattern +} + +# please remember that *any* match in the list will exclude a host deletion +# if you prefer to specify only one match, a single string will work too... +# if you want to be more dynamic, you can use something like: + +$match_domain = regsubst("${domain}", '\.', '\\.', 'G') +class { '::ipa::server': + domain => "${domain}", + shorewall => true, + host_excludes => [ + "^test[0-9]{1,}\\.${match_domain}\$", # test\d.domain + ], +} + +# i found some notes on available bracket expressions here: +# http://www.regular-expressions.info/posixbrackets.html + diff --git a/ipa/examples/simple-usage.pp b/ipa/examples/simple-usage.pp new file mode 100644 index 000000000..449105151 --- /dev/null +++ b/ipa/examples/simple-usage.pp @@ -0,0 +1,36 @@ +# here is some basic usage of the ipa module + +# on the ipa server: +$domain = $::domain +class { '::ipa::server': + domain => "${domain}", + shorewall => true, # uses my puppet-shorewall module +} + +ipa::server::host { 'nfs': # NOTE: adding .${domain} is a good idea.... + domain => "${domain}", + macaddress => "00:11:22:33:44:55", + random => true, # set a one time password randomly + locality => 'Montreal, Canada', + location => 'Room 641A', + platform => 'Supermicro', + osstring => 'CentOS 6.4 x86_64', + comment => 'Simple NFSv4 Server', + watch => true, # read and understand the docs well +} + +ipa::server::host { 'test1': + domain => "${domain}", + password => 'password', + watch => true, # read and understand the docs well +} + + +# and on the nfs server (an ipa client): +class { '::ipa::client::host::deploy': + nametag => 'nfs', # needs to match the ipa:server:host $name +} + +# if you use fqdn's for the ipa:server:host $name's, then you can deploy with: +include ipa::client::host::deploy + diff --git a/ipa/examples/simple-usage2.pp b/ipa/examples/simple-usage2.pp new file mode 100644 index 000000000..e1288b02d --- /dev/null +++ b/ipa/examples/simple-usage2.pp @@ -0,0 +1,45 @@ +# here is some basic usage of the ipa module, now with services! + +# on the ipa server: +$domain = $::domain +class { '::ipa::server': + domain => "${domain}", + shorewall => true, # uses my puppet-shorewall module +} + +ipa::server::host { 'nfs': # NOTE: adding .${domain} is a good idea.... + domain => "${domain}", + macaddress => "00:11:22:33:44:55", + random => true, # set a one time password randomly + locality => 'Montreal, Canada', + location => 'Room 641A', + platform => 'Supermicro', + osstring => 'CentOS 6.4 x86_64', + comment => 'Simple NFSv4 Server', + watch => true, # read and understand the docs well +} + +ipa::server::service { 'nfs': # this $name should match nfs::server => $ipa + service => 'nfs', + host => 'nfs', # needs to match ipa::server::host $name + domain => "${domain}", # used to figure out realm +} + +ipa::server::host { 'test1': + domain => "${domain}", + password => 'password', + watch => true, # read and understand the docs well +} + + +# and on the nfs server (an ipa client): +class { '::ipa::client::deploy': + nametag => 'nfs', # needs to match the ipa:server:host $name +} + +# if you use fqdn's for the ipa:server:host $name's, then you can deploy with: +#include ipa::client::host::deploy + +# there is now a single "deploy" which can be used for both hosts and services! +include ipa::client::deploy + diff --git a/ipa/examples/simple-usage3.pp b/ipa/examples/simple-usage3.pp new file mode 100644 index 000000000..30c6c8838 --- /dev/null +++ b/ipa/examples/simple-usage3.pp @@ -0,0 +1,45 @@ +# here is an example of how to use user excludes and types: + +# on the ipa server: +# NOTE: the 'admin' user is automatically excluded from being auto purged... +class { '::ipa::server': + shorewall => true, + user_excludes => [ + "^test[0-9]{1,}\$", # test\d + ], +} + +# create an unmanaged user +ipa::server::user { 'james': + first => 'James', + last => 'Shubin', + modify => false, + watch => false, +} + +# create a managed user +ipa::server::user { 'ntesla': + first => 'Nikola', + last => 'Tesla', + city => 'Shoreham', + state => 'New York', + postalcode => '11786', +} + +# create a user using a full principal as the primary key +# NOTE: the principal itself can't be edited without a remove/add +ipa::server::user { 'aturing/admin@EXAMPLE.COM': + first => 'Alan', + last => 'Turning', + random => true, # set a password randomly + password_file => true, # store the password in plain text ! (bad) +} + +# create a user by principal but without the instance set +ipa::server::user { 'arthur@EXAMPLE.COM': + first => 'Arthur', + last => 'Guyton', + jobtitle => 'Physiologist', + orgunit => 'Research', +} + diff --git a/ipa/files/diff.py b/ipa/files/diff.py new file mode 100644 index 000000000..6bdcfb7e5 --- /dev/null +++ b/ipa/files/diff.py @@ -0,0 +1,587 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: this script should always be called from puppet, it's useless by itself +# NOTE: for manual command line viewing of data you can use: +# $ ipa host-show 'foo.example.com' --all --raw | cut -b 3- +# +# EXAMPLE: +# $ ./diff.py host ${valid_hostname} ${args} && echo TRUE || echo FALSE +# $ ./diff.py service ${valid_service} ${args} && echo TRUE || echo FALSE +# $ ./diff.py user ${valid_login} ${args} && echo TRUE || echo FALSE +# +# where ${} are puppet variables... +# +# NOTE: this is a particularly finicky piece of code. edit at your own risk... +# the reason it is so tricky, is because it has to cater to ipa's intricacies! + +import sys +import argparse +import ipalib +from ipapython.ssh import SSHPublicKey + +# +# helper functions +# +def p(x, f=lambda x: x): + """Pass None values through, otherwise apply function.""" + if x is None: return None + return f(x) + +def get(value, default): + if value is None: return default + else: return value + +# NOTE: a lot of places you'll see [0] because everything is wrapped in tuples! +# IPA does this for some reason, feel free to use the @left(untuple) decorator! +def untuple(x): + """Untuple a single value that was wrapped in a tuple.""" + assert type(x) == type(()), 'Expected tuple.' # safety check + assert len(x) == 1, 'Expected tuple singleton.' # safety check + return x[0] + +def listclean(x): + """Clean empty value lists to match puppets 'manage', but empty.""" + # NOTE: 0 length list values in ipa are None, ideally they should be (,) + # TODO: uncomment this first condition as well if it is ever needed... + #if x == []: return None # empties show up as 'None' in freeipa + if x == ['']: return None # this is the empty: --argument-value= + return x + +def lowerize(x): + """Transform input value into upper, recursively.""" + # TODO: dict ? + if type(x) == type([]): + return [lowerize(i) for i in x] # recurse + elif type(x) == type(()): + return tuple([lowerize(i) for i in x]) # recurse + elif isinstance(x, basestring): + return x.lower() # don't recurse + else: + return x # int's, etc... + +def upperize(x): + """Transform input value into upper, recursively.""" + # TODO: dict ? + if type(x) == type([]): + return [upperize(i) for i in x] # recurse + elif type(x) == type(()): + return tuple([upperize(i) for i in x]) # recurse + elif isinstance(x, basestring): + return x.upper() # don't recurse + else: + return x # int's, etc... + +def unicodeize(x): + """Transform input value into unicode, recursively.""" + # TODO: dict ? + if type(x) == type([]): + return [unicodeize(i) for i in x] # recurse + elif type(x) == type(()): + return tuple([unicodeize(i) for i in x]) # recurse + elif isinstance(x, basestring): + return unicode(x) # don't recurse + else: + return x # int's, etc... + +def sshfp(x): + """Transform a public ssh key into the ipa style fingerprint.""" + if type(x) == type([]): + return [sshfp(i) for i in x] # recurse + + # this code is the algorithm used in: ipalib/util.py + pubkey = SSHPublicKey(x) + fp = pubkey.fingerprint_hex_md5() + comment = pubkey.comment() + if comment: fp = u'%s %s' % (fp, comment) + fp = u'%s (%s)' % (fp, pubkey.keytype()) + return fp + +def sshdsp(x): + """Transform a public ssh key into the ipa style display.""" + if type(x) == type([]): + return [sshdsp(i) for i in x] # recurse + + # this code is the algorithm used in: ipalib/util.py + return SSHPublicKey(x).openssh() # normalize_sshpubkey + +# +# function decorators to wrap cmp functions +# +def debug(f): + """Function decorator to help debug cmp values.""" + def r(x, y): + if args.debug: + # NOTE: f.func_name works if it is closest to function! + print 'f:', f.func_name, 'x:', x, 'y:', y + return f(x, y) + + return r # we're a function decorator + +def lower(f): + """Function decorator to lower case x and y inputs.""" + # NOTE: this shows the longer versions of the decorator... + #def r(x, y): + # #_x = None if x is None else lowerize(x) + # #_y = None if y is None else lowerize(y) + # #return f(_x, _y) + # return f(p(x, lowerize), p(y, lowerize)) + #return r + return lambda x, y: f(p(x, lowerize), p(y, lowerize)) + +def upper(f): + """Function decorator to upper case x and y inputs.""" + return lambda x, y: f(p(x, upperize), p(y, upperize)) + +# TODO: is this unused because of @left(list) ? +def lists(f): + """Function decorator to ensure both inputs are lists.""" + return lambda x, y: f(p(x, list), p(y, list)) + +def sort(f): + """Function decorator to sort x and y inputs.""" + return lambda x, y: f(p(x, sorted), p(y, sorted)) + +def unique(f): + """Function decorator to remove duplicates in x and y inputs.""" + d = lambda z: list(set(z)) # remove duplicates + return lambda x, y: f(p(x, d), p(y, d)) + +def unicoded(f): + """Function decorator to unicode x and y inputs including lists, and + tuples. Recurses into compound types like lists.""" + return lambda x, y: f(p(x, unicodeize), p(y, unicodeize)) + +def left(l=lambda z: z): + """Return a function decorator using a lambda l for the left only.""" + def inner_left(f): + """Function decorator to ensure l is applied on the left.""" + return lambda x, y: f(p(x, l), y) + return inner_left + +def right(l=lambda z: z): + """Return a function decorator using a lambda l for the right only.""" + def inner_right(f): + """Function decorator to ensure l is applied on the right.""" + return lambda x, y: f(x, p(y, l)) + return inner_right + +# NOTE: we could rewrite lower,upper,lists,sort,unique and etc in terms of this +def both(l=lambda z: z): + """Return a function decorator using a lambda l for both x and y.""" + def inner_both(f): + """Function decorator to ensure l is applied to both x and y.""" + return lambda x, y: f(p(x, l), p(y, l)) + return inner_both + +# +# composed decorators +# +# http://docs.python.org/2/reference/compound_stmts.html#grammar-token-decorated +def ipalist(f): + # equivalent to decorating with: + # @left(list) + # @right(listclean) + # @unicoded + return left(list)(right(listclean)(unicoded(f))) + +def ipastr(f): + # @left(untuple) + # @unicoded + return left(untuple)(unicoded(f)) + +# +# cmp functions +# +@unicoded +def cmp_default(x, y): + return x == y + +# +# host cmp functions +# +@left(untuple) +@lower # TODO: should we drop the @lower ? +@unicoded +def cmp_host_primarykey(x, y): + return x == y + +@left(list) +@right(listclean) +@sort +@upper # ipa expects uppercase mac addresses +@unicoded +def cmp_host_macaddress(x, y): + return x == y + +#@left(list) +#@right(listclean) +#@right(sshfp) +#@unicoded +#@debug # should usually be closest to the cmp function +#def cmp_host_sshpubkeyfp(x, y): +# # in comes lists of ssh keys. we need to transform each one into the +# # format as returned by freeipa. freeipa returns a tuple of strings! +# # eg x is usually something like: +# # (u'AB:98:62:82:C0:74:47:5E:FC:36:F7:5A:D7:8F:8E:FF (ssh-dss)', +# # u'62:6D:8B:7B:3F:E3:EA:4C:50:4D:86:AA:BF:17:9D:8B (ssh-rsa)') +# return x == y + +@left(list) +@right(listclean) +@right(sshdsp) +@unicoded +@debug # should usually be closest to the cmp function +def cmp_host_ipasshpubkey(x, y): + # this is only seen when using all=True + return x == y + +@ipastr +def cmp_host_l(x, y): + return x == y + +@ipastr +def cmp_host_nshostlocation(x, y): + return x == y + +@ipastr +def cmp_host_nshardwareplatform(x, y): + return x == y + +@ipastr +def cmp_host_nsosversion(x, y): + return x == y + +@ipastr +def cmp_host_description(x, y): + return x == y + +# +# service cmp functions +# +@left(untuple) +@unicoded +def cmp_service_primarykey(x, y): + return x == y + +@left(list) +@unicoded +def cmp_service_ipakrbauthzdata(x, y): + # TODO: is it possible that instead of (u'NONE',) some return None ? + return x == y + +# +# user cmp functions +# +@left(untuple) +@unicoded +def cmp_user_primarykey(x, y): + return x == y + +@ipastr +def cmp_user_givenname(x, y): + return x == y + +@ipastr +def cmp_user_sn(x, y): + return x == y + +@ipastr +def cmp_user_cn(x, y): + return x == y + +@ipastr +def cmp_user_displayname(x, y): + return x == y + +@ipastr +def cmp_user_initials(x, y): + return x == y + +@ipastr +def cmp_user_krbprincipalname(x, y): + return x == y + +@ipalist +def cmp_user_mail(x, y): + return x == y + +@ipastr +def cmp_user_gecos(x, y): + return x == y + +@ipastr +def cmp_user_uidnumber(x, y): + return x == y + +@ipastr +def cmp_user_gidnumber(x, y): + return x == y + +@ipastr +def cmp_user_loginshell(x, y): + return x == y + +@ipastr +def cmp_user_homedirectory(x, y): + return x == y + +@left(list) +@right(listclean) +@right(sshdsp) +@unicoded +def cmp_user_ipasshpubkey(x, y): + return x == y + +@ipastr +def cmp_user_street(x, y): + return x == y + +@ipastr +def cmp_user_l(x, y): + return x == y + +@ipastr +def cmp_user_st(x, y): + return x == y + +@ipastr +def cmp_user_postalcode(x, y): + return x == y + +@ipalist +def cmp_user_telephonenumber(x, y): + return x == y + +@ipalist +def cmp_user_mobile(x, y): + return x == y + +@ipalist +def cmp_user_pager(x, y): + return x == y + +@ipalist +def cmp_user_facsimiletelephonenumber(x, y): + return x == y + +@ipastr +def cmp_user_title(x, y): + return x == y + +@ipastr +def cmp_user_ou(x, y): + return x == y + +@ipastr +def cmp_user_manager(x, y): + return x == y + +@ipastr +def cmp_user_carlicense(x, y): + return x == y + +# +# initialize ipa +# +ipalib.api.bootstrap() +ipalib.api.load_plugins() +ipalib.api.finalize() +ipalib.api.Backend.xmlclient.connect() + +# +# parser to match ipa arguments +# +parser = argparse.ArgumentParser(description='ipa difference engine') + +parser.add_argument('--debug', dest='debug', action='store_true', default=False) +parser.add_argument('--not', dest='n', action='store_true', default=False) + +subparsers = parser.add_subparsers(dest='subparser_name') + +# parent parser (contains common subparser arguments) +parent_parser = argparse.ArgumentParser(add_help=False) +parent_parser.add_argument('primarykey', action='store') # positional arg + +# NOTE: this is a mapping with dest being the --raw key to look for the data in +# NOTE: this --raw key to dest values can be seen by looking in the ipa API.txt + +# +# 'host' parser +# +parser_host = subparsers.add_parser('host', parents=[parent_parser]) +parser_host.add_argument('--macaddress', dest='macaddress', action='append') # list +# this is actually part of DNS, ignore it... +#parser_host.add_argument('--ip-address', dest='ip?', action='store') +#parser_host.add_argument('--sshpubkey', dest='sshpubkeyfp', action='append') +parser_host.add_argument('--sshpubkey', dest='ipasshpubkey', action='append') +parser_host.add_argument('--locality', dest='l', action='store') +parser_host.add_argument('--location', dest='nshostlocation', action='store') +parser_host.add_argument('--platform', dest='nshardwareplatform', action='store') +parser_host.add_argument('--os', dest='nsosversion', action='store') +parser_host.add_argument('--desc', dest='description', action='store') + +# +# 'service' parser +# +parser_service = subparsers.add_parser('service', parents=[parent_parser]) +parser_service.add_argument('--pac-type', dest='ipakrbauthzdata', action='append') + +# +# 'user' parser +# +parser_user = subparsers.add_parser('user', parents=[parent_parser]) +parser_user.add_argument('--first', dest='givenname', action='store') +parser_user.add_argument('--last', dest='sn', action='store') +parser_user.add_argument('--cn', dest='cn', action='store') +parser_user.add_argument('--displayname', dest='displayname', action='store') +parser_user.add_argument('--initials', dest='initials', action='store') +parser_user.add_argument('--principal', dest='krbprincipalname', action='store') +parser_user.add_argument('--email', dest='mail', action='append') +parser_user.add_argument('--gecos', dest='gecos', action='store') +parser_user.add_argument('--uid', dest='uidnumber', action='store') +parser_user.add_argument('--gidnumber', dest='gidnumber', action='store') +parser_user.add_argument('--shell', dest='loginshell', action='store') +parser_user.add_argument('--homedir', dest='homedirectory', action='store') +parser_user.add_argument('--sshpubkey', dest='ipasshpubkey', action='append') +parser_user.add_argument('--street', dest='street', action='store') +parser_user.add_argument('--city', dest='l', action='store') +parser_user.add_argument('--state', dest='st', action='store') +parser_user.add_argument('--postalcode', dest='postalcode', action='store') +parser_user.add_argument('--phone', dest='telephonenumber', action='append') +parser_user.add_argument('--mobile', dest='mobile', action='append') +parser_user.add_argument('--pager', dest='pager', action='append') +parser_user.add_argument('--fax', dest='facsimiletelephonenumber', action='append') +parser_user.add_argument('--title', dest='title', action='store') +parser_user.add_argument('--orgunit', dest='ou', action='store') +parser_user.add_argument('--manager', dest='manager', action='store') +parser_user.add_argument('--carlicense', dest='carlicense', action='store') + +args = parser.parse_args() + +# TODO: the process dictionaries could probably be generated by argparse data +if args.subparser_name == 'host': + process = { + 'macaddress': cmp_host_macaddress, + #'sshpubkeyfp': cmp_host_sshpubkeyfp, + 'ipasshpubkey': cmp_host_ipasshpubkey, # only seen with --all + 'l': cmp_host_l, + 'nshostlocation': cmp_host_nshostlocation, + 'nshardwareplatform': cmp_host_nshardwareplatform, + 'nsosversion': cmp_host_nsosversion, + 'description': cmp_host_description, + } + +elif args.subparser_name == 'service': + process = { + 'ipakrbauthzdata': cmp_service_ipakrbauthzdata, + } + +elif args.subparser_name == 'user': + process = { + 'givenname': cmp_user_givenname, + 'sn': cmp_user_sn, + 'cn': cmp_user_cn, + 'displayname': cmp_user_displayname, + 'initials': cmp_user_initials, + 'krbprincipalname': cmp_user_krbprincipalname, + 'mail': cmp_user_mail, + 'gecos': cmp_user_gecos, + 'uidnumber': cmp_user_uidnumber, + 'gidnumber': cmp_user_gidnumber, + 'loginshell': cmp_user_loginshell, + 'homedirectory': cmp_user_homedirectory, + 'ipasshpubkey': cmp_user_ipasshpubkey, + 'street': cmp_user_street, + 'l': cmp_user_l, + 'st': cmp_user_st, + 'postalcode': cmp_user_postalcode, + 'telephonenumber': cmp_user_telephonenumber, + 'mobile': cmp_user_mobile, + 'pager': cmp_user_pager, + 'facsimiletelephonenumber': cmp_user_facsimiletelephonenumber, + 'title': cmp_user_title, + 'ou': cmp_user_ou, + 'manager': cmp_user_manager, + 'carlicense': cmp_user_carlicense, + } + +try: + #output = ipalib.api.Command.host_show(fqdn=unicode(args.hostname)) + if args.subparser_name == 'host': + output = ipalib.api.Command.host_show(unicode(args.primarykey), all=True) + elif args.subparser_name == 'service': + output = ipalib.api.Command.service_show(unicode(args.primarykey), all=True) + elif args.subparser_name == 'user': + output = ipalib.api.Command.user_show(unicode(args.primarykey), all=True) + +except ipalib.errors.NotFound, e: + if args.debug: + print >> sys.stderr, 'Not found' + # NOTE: if we exit here, it's a bug in the puppet module because puppet + # should only be running this script for hosts that it believe exist... + sys.exit(2) + +result = output.get('result', {}) # the freeipa api returns a result key! +if args.debug: + print args + print result + +if args.subparser_name == 'host': + compare = cmp_host_primarykey + pk = result.get('fqdn', '') +elif args.subparser_name == 'service': + compare = cmp_service_primarykey + pk = result.get('krbprincipalname', '') +elif args.subparser_name == 'user': + compare = cmp_user_primarykey + pk = result.get('uid', '') + +# the pk gets untuples by the @left(untuple) compare decorators +assert compare(pk, args.primarykey), 'Primary key does not match!' + +# +# loop through all the keys to validate +# +for i in process.keys(): + + a = result.get(i, None) # value from ipa (in unicode) + b = getattr(args, i) # value from command line arg + + compare = process.get(i, cmp_default) # cmp function + compare = get(compare, cmp_default) # default None + + # NOTE: the a value (left) must always be the ipa data + # the b value (right) must correspond to the arg value + watch = (b is not None) # values of None are unmanaged + if watch and not(compare(a, b)): # run the cmp! + if args.debug: + # TODO: compare could return the post decorated x and y + # which we're actually comparing and print them here... + # this would give us more information about the unmatch + print >> sys.stderr, ('Unmatched on %s between %s and %s' % (i, a, b)) + if args.n: + sys.exit(0) + else: + sys.exit(1) + +if args.n: + sys.exit(1) +else: + sys.exit(0) # everything matches + diff --git a/ipa/lib/facter/ipa_host.rb b/ipa/lib/facter/ipa_host.rb new file mode 100644 index 000000000..b91ec65cc --- /dev/null +++ b/ipa/lib/facter/ipa_host.rb @@ -0,0 +1,35 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +require 'facter' +require 'resolv' + +# try and pick the _right_ ip that ipa should use by default... +fqdn = Facter.value('fqdn') +if not fqdn.nil? + ip = Resolv.getaddress "#{fqdn}" + if not ip.nil? + Facter.add('ipa_host_ip') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + ip + } + end + end +end + +# vim: ts=8 diff --git a/ipa/lib/facter/ipa_installed.rb b/ipa/lib/facter/ipa_installed.rb new file mode 100644 index 000000000..d236a459a --- /dev/null +++ b/ipa/lib/facter/ipa_installed.rb @@ -0,0 +1,57 @@ +# Simple ipa templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: this fact creates boolean string values that others can read for status +require 'facter' + +# find the module_vardir +dir = Facter.value('puppet_vardirtmp') # nil if missing +if dir.nil? # let puppet decide if present! + dir = Facter.value('puppet_vardir') + if dir.nil? + var = nil + else + var = dir.gsub(/\/$/, '')+'/'+'tmp/' # ensure trailing slash + end +else + var = dir.gsub(/\/$/, '')+'/' +end + +if var.nil? + # if we can't get a valid vardirtmp, then we can't collect... + valid_dir = nil +else + module_vardir = var+'ipa/' + valid_dir = module_vardir.gsub(/\/$/, '')+'/' +end + +if not(valid_dir.nil?) and File.directory?(valid_dir) + ['ipa_client_installed', 'ipa_server_installed'].each do |key| + f = valid_dir+''+key # the full file path + if File.exists?(f) + # NOTE: sadly, empty string facts don't work :( + Facter.add(key) do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode do + Facter::Util::Resolution.exec("/bin/cat '"+f+"'") + end + end + end + end +end + +# vim: ts=8 diff --git a/ipa/lib/facter/ipa_master.rb b/ipa/lib/facter/ipa_master.rb new file mode 100644 index 000000000..833013e9d --- /dev/null +++ b/ipa/lib/facter/ipa_master.rb @@ -0,0 +1,98 @@ +# Simple ipa templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: this fact creates boolean string values that others can read for status +require 'facter' + +# regexp to match an fqdn pattern, eg: ipa1.example.com +regexp = /^[a-zA-Z]{1}[a-zA-Z0-9\.\-]{0,}$/ # TODO: is this right ? +prefix = 'master_' + +# find the module_vardir +dir = Facter.value('puppet_vardirtmp') # nil if missing +if dir.nil? # let puppet decide if present! + dir = Facter.value('puppet_vardir') + if dir.nil? + var = nil + else + var = dir.gsub(/\/$/, '')+'/'+'tmp/' # ensure trailing slash + end +else + var = dir.gsub(/\/$/, '')+'/' +end + +if var.nil? + # if we can't get a valid vardirtmp, then we can't collect... + valid_dir = nil + masterdir = nil +else + module_vardir = var+'ipa/' + valid_dir = module_vardir.gsub(/\/$/, '')+'/' + masterdir = module_vardir+'replica/master/' +end + +# create fact from self-proclaimed master... needed to know who first installed +found = '' + +if not(masterdir.nil?) and File.directory?(masterdir) + Dir.glob(masterdir+prefix+'*').each do |f| + + b = File.basename(f) + # strip off leading prefix + fqdn = b[prefix.length, b.length-prefix.length] + + master = File.open(f, 'r').read.strip.downcase # read into str + if master.length > 0 and regexp.match(master) + # avoid: http://projects.puppetlabs.com/issues/22455 + + if master != fqdn + # FIXME: error: i think these should match... + puts 'ERROR' + end + + if found != '' + # FIXME: error, already found... + puts 'ERROR' + end + + found = master # save + #break # there should only be one, so no need to break + # TODO: print warning on else... + end + end +end + +# put the fqdn of the zero-th replica (the master) into a fact... look first in +# the local installation tag, and then in the found exported file if one exists +# it's inefficient to read the exported file a second time, but simpler to code +["#{valid_dir}ipa_server_replica_master", "#{masterdir}#{prefix}#{found}"].each do |key| + if File.exists?(key) + master = File.open(key, 'r').read.strip.downcase # read into str + if master.length > 0 and regexp.match(master) + + Facter.add('ipa_server_replica_master') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode do + Facter::Util::Resolution.exec("/bin/cat '"+key+"'") + end + end + break # there can only be one... + end + end +end + +# vim: ts=8 diff --git a/ipa/lib/facter/ipa_passwords.rb b/ipa/lib/facter/ipa_passwords.rb new file mode 100644 index 000000000..61cd7595f --- /dev/null +++ b/ipa/lib/facter/ipa_passwords.rb @@ -0,0 +1,78 @@ +# Simple ipa templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: this fact creates the one time password facts, needed to be exported... +require 'facter' + +suffix = '.password' +found = [] # create list of values + +# find the module_vardir +dir = Facter.value('puppet_vardirtmp') # nil if missing +if dir.nil? # let puppet decide if present! + dir = Facter.value('puppet_vardir') + if dir.nil? + var = nil + else + var = dir.gsub(/\/$/, '')+'/'+'tmp/' # ensure trailing slash + end +else + var = dir.gsub(/\/$/, '')+'/' +end + +if var.nil? + # if we can't get a valid vardirtmp, then we can't collect... + valid_hostpassdir = nil +else + module_vardir = var+'ipa/' + hostpassdir = module_vardir+'hosts/passwords/' + valid_hostpassdir = hostpassdir.gsub(/\/$/, '')+'/' +end + +if not(valid_hostpassdir.nil?) and File.directory?(valid_hostpassdir) + Dir.glob(valid_hostpassdir+'*'+suffix).each do |f| + b = File.basename(f) + g = b.split('.') # $name.password + if g.length >= 2 and ('.'+g.pop()) == suffix + x = g.join('.') # in case value had dots in it. + + #has_password = Facter::Util::Resolution.exec("/usr/bin/ipa host-show '"+x+"' --raw | /usr/bin/tr -d ' ' | /bin/grep '^has_password:' | /bin/cut -b 14- | /usr/bin/tr '[A-Z]' '[a-z]'") or 'nil' + key = ('ipa_host_'+x+'_password').gsub(/\./, '_') + found.push(key) # make a list of keys + + # NOTE: sadly, empty string facts don't work :( + Facter.add(key) do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode do + Facter::Util::Resolution.exec("/bin/cat '"+f+"'") + # single equals sign for test ! + #Facter::Util::Resolution.exec("/usr/bin/test \"`/usr/bin/ipa host-show '"+x+"' --raw | /usr/bin/tr -d ' ' | /bin/grep '^has_password:' | /bin/cut -b 14-`\" = 'True' && /bin/cat '"+f+"'") + end + end + end + end +end + +# make a list of keys... might be useful and helps to know this fact is working +Facter.add('ipa_host_passwords') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + found.join(',') + } +end + +# vim: ts=8 diff --git a/ipa/lib/facter/ipa_peering.rb b/ipa/lib/facter/ipa_peering.rb new file mode 100644 index 000000000..648c53ecb --- /dev/null +++ b/ipa/lib/facter/ipa_peering.rb @@ -0,0 +1,161 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +require 'facter' + +# uuid regexp +regexp = /^[a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12}$/ +prefix = 'peer_' + +# find the module_vardir +dir = Facter.value('puppet_vardirtmp') # nil if missing +if dir.nil? # let puppet decide if present! + dir = Facter.value('puppet_vardir') + if dir.nil? + var = nil + else + var = dir.gsub(/\/$/, '')+'/'+'tmp/' # ensure trailing slash + end +else + var = dir.gsub(/\/$/, '')+'/' +end + +if var.nil? + # if we can't get a valid vardirtmp, then we can't continue + module_vardir = nil + peerdir = nil + uuidfile = nil +else + module_vardir = var+'ipa/' + peerdir = module_vardir+'replica/peering/' + uuidfile = peerdir+'uuid' +end + +# NOTE: module specific mkdirs, needed to ensure there is no blocking/deadlock! +if not(var.nil?) and not File.directory?(var) + Dir::mkdir(var) +end + +if not(module_vardir.nil?) and not File.directory?(module_vardir) + Dir::mkdir(module_vardir) +end + +if not(peerdir.nil?) and not File.directory?(peerdir) + Dir::mkdir(File.expand_path('..', peerdir)) + Dir::mkdir(peerdir) +end + +# NOTE: each host is given a "uuidgen -t" based create time and they are sorted +# according to that time first, and alphabetically second. the idea is that the +# chronological order provides a consistent, but decentralized ordering that is +# needed so that subsequent joins are always sorted to the end of the uuid list + +# generate uuid and parent directory if they don't already exist... +if not(module_vardir.nil?) and File.directory?(module_vardir) + + create = false + # create a uuid and store it in our vardir if it doesn't already exist! + if File.directory?(peerdir) + + if File.exist?(uuidfile) + test = File.open(uuidfile, 'r').read.strip.downcase # read into str + # skip over uuid's of the wrong length or that don't match (security!!) + if test.length == 36 and regexp.match(test) + create = false + else + create = true + end + else + create = true + end + end + + if create + # NOTE: this is a time based uuid ! + result = system("/usr/bin/uuidgen -t > '" + uuidfile + "'") + if not(result) + # TODO: print warning + end + end +end + +# create the fact if the uuid file contains a valid uuid +if not(uuidfile.nil?) and File.exist?(uuidfile) + uuid = File.open(uuidfile, 'r').read.strip.downcase # read into str + # skip over uuid's of the wrong length or that don't match (security!!) + if uuid.length == 36 and regexp.match(uuid) + Facter.add('ipa_server_replica_uuid') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + # don't reuse uuid variable to avoid bug #: + # http://projects.puppetlabs.com/issues/22455 + uuid + } + end + # TODO: print warning on else... + end +end + +# create facts from externally collected peer files +peer = '' +found = {} +if not(peerdir.nil?) and File.directory?(peerdir) + Dir.glob(peerdir+prefix+'*').each do |f| + + b = File.basename(f) + # strip off leading prefix + fqdn = b[prefix.length, b.length-prefix.length] + + peer = File.open(f, 'r').read.strip.downcase # read into str + if peer.length > 0 and regexp.match(peer) + # avoid: http://projects.puppetlabs.com/issues/22455 + found[fqdn] = peer + # TODO: print warning on else... + end + end +end + +# FIXME: ensure that this properly sorts by uuidgen -t times... +# sort chronologically by time based uuid +# thanks to: Pádraig Brady for the sort implementation... +sorted = found.inject({}){ |h,(k,v)| h[k]=v.split('-'); h }.sort_by { |k,v| [v[2], v[1], v[0], v[3], v[4]] }.map { |x| x[0] } + +sorted.each do |x| + Facter.add('ipa_server_replica_uuid_'+x) do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + found[x] + } + end +end + +# list of generated ipa_server_replica_uuid's +Facter.add('ipa_server_replica_uuid_facts') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + sorted.collect {|x| 'ipa_server_replica_uuid_'+x }.join(',') + } +end + +Facter.add('ipa_server_replica_peers') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + sorted.join(',') + } +end + +# vim: ts=8 diff --git a/ipa/lib/facter/ipa_replica.rb b/ipa/lib/facter/ipa_replica.rb new file mode 100644 index 000000000..4e53bd855 --- /dev/null +++ b/ipa/lib/facter/ipa_replica.rb @@ -0,0 +1,71 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +require 'facter' + +# regexp to match an fqdn pattern, eg: ipa1.example.com +regexp = /^[a-zA-Z]{1}[a-zA-Z0-9\.\-]{0,}$/ # TODO: is this right ? +prefix = 'replica-info-' +ending = '.gpg' + +# find the module_vardir +dir = Facter.value('puppet_vardirtmp') # nil if missing +if dir.nil? # let puppet decide if present! + dir = Facter.value('puppet_vardir') + if dir.nil? + var = nil + else + var = dir.gsub(/\/$/, '')+'/'+'tmp/' # ensure trailing slash + end +else + var = dir.gsub(/\/$/, '')+'/' +end + +if var.nil? + # if we can't get a valid vardirtmp, then we can't continue + valid_replicadir = nil +else + module_vardir = var+'ipa/' + valid_replicadir = module_vardir.gsub(/\/$/, '')+'/replica/install/' +end + +found = [] + +if not(valid_replicadir.nil?) and File.directory?(valid_replicadir) + Dir.glob(valid_replicadir+prefix+'*'+ending).each do |f| + b = File.basename(f) + + g = b.slice(prefix.length, b.length-prefix.length-ending.length) + + if g.length > 0 and regexp.match(g) + if not found.include?(g) + found.push(g) + end + # TODO: print warning on else... + end + end +end + +Facter.add('ipa_replica_prepared_fqdns') do + #confine :operatingsystem => %w{CentOS, RedHat, Fedora} + setcode { + # TODO: facter should support native list types :) + found.sort.join(',') + } +end + +# vim: ts=8 diff --git a/ipa/lib/facter/ipa_version.rb b/ipa/lib/facter/ipa_version.rb new file mode 100644 index 000000000..f19761ec5 --- /dev/null +++ b/ipa/lib/facter/ipa_version.rb @@ -0,0 +1,45 @@ +# Simple ipa templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +require 'facter' + +# get the yum path. this fact can come from an external fact set in: params.pp +yum = Facter.value('ipa_program_yum').to_s.chomp +if yum == '' + yum = `which yum 2> /dev/null`.chomp + if yum == '' + yum = '/usr/bin/yum' + end +end + +ipa = Facter.value('ipa_package_ipa_server').to_s.chomp +if ipa == '' + ipa = 'ipa-server' +end + +#confine :operatingsystem => %w{CentOS, RedHat, Fedora} +# TODO: add a long TTL to avoid repeated yum noise +cmdout = Facter::Util::Resolution.exec(yum+" info "+ipa+" 2> /dev/null | /bin/grep '^Version' | /bin/awk -F ':' '{print $2}'") +if cmdout != nil + Facter.add('ipa_version') do + setcode { + cmdout.strip + } + end +end + +# vim: ts=8 diff --git a/ipa/lib/puppet/parser/functions/ipa_topology_flat.rb b/ipa/lib/puppet/parser/functions/ipa_topology_flat.rb new file mode 100644 index 000000000..299a1fb9a --- /dev/null +++ b/ipa/lib/puppet/parser/functions/ipa_topology_flat.rb @@ -0,0 +1,77 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# TODO: is 'flat' the correct topological name for what this algorithm outputs? + +module Puppet::Parser::Functions + newfunction(:ipa_topology_flat, :type => :rvalue, :doc => <<-'ENDHEREDOC') do |args| + Return an ipa N-N topology from a sorted list of hosts + + Example: + + $valid_peers = ipa_topology_flat($peers) + notice("valid peers is: ${valid_peers}") + + This function is used internally for building automatic topologies. + + ENDHEREDOC + + Puppet::Parser::Functions.function('warning') # load function + # signature: replica, bricks -> bricks + unless args.length == 1 + raise Puppet::ParseError, "ipa_topology_flat(): wrong number of arguments (#{args.length}; must be 1)" + end + if not(args[0].is_a?(Array)) + raise Puppet::ParseError, "ipa_topology_flat(): expects the first argument to be an array, got #{args[0].inspect} which is of type #{args[0].class}" + end + + peers = args[0] + + if peers.uniq.length != peers.length # there are duplicates! + raise Puppet::ParseError, "ipa_topology_flat(): duplicates were found in the first argument!" + end + + # NOTE: need at least one + if peers.length < 1 + function_warning(["ipa_topology_flat(): peer list is empty"]) + return {} + end + + # if we only have one peer, and it's me, then topology is empty + if peers.length == 1 and peers[0] == lookupvar('fqdn') + return {} + end + + result = {} + + peers.each do |x| + + same = peers.dup # copy... to not destroy peers! + if same.delete(x).nil? # normally returns the value... + # TODO: return programming error: delete failed + end + + # connect to every peer except yourself + result[x] = same + + end + + result # return + end +end + +# vim: ts=8 diff --git a/ipa/lib/puppet/parser/functions/ipa_topology_ring.rb b/ipa/lib/puppet/parser/functions/ipa_topology_ring.rb new file mode 100644 index 000000000..6a6c35a7b --- /dev/null +++ b/ipa/lib/puppet/parser/functions/ipa_topology_ring.rb @@ -0,0 +1,81 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# TODO: is 'ring' the correct topological name for what this algorithm outputs? + +module Puppet::Parser::Functions + newfunction(:ipa_topology_ring, :type => :rvalue, :doc => <<-'ENDHEREDOC') do |args| + Return an ipa ring topology from a sorted list of hosts + + Example: + + $valid_peers = ipa_topology_ring($peers) + notice("valid peers is: ${valid_peers}") + + This function is used internally for building automatic topologies. + + ENDHEREDOC + + Puppet::Parser::Functions.function('warning') # load function + # signature: replica, bricks -> bricks + unless args.length == 1 + raise Puppet::ParseError, "ipa_topology_ring(): wrong number of arguments (#{args.length}; must be 1)" + end + if not(args[0].is_a?(Array)) + raise Puppet::ParseError, "ipa_topology_ring(): expects the first argument to be an array, got #{args[0].inspect} which is of type #{args[0].class}" + end + + peers = args[0] + + if peers.uniq.length != peers.length # there are duplicates! + raise Puppet::ParseError, "ipa_topology_ring(): duplicates were found in the first argument!" + end + + # NOTE: need at least one + if peers.length < 1 + function_warning(["ipa_topology_ring(): peer list is empty"]) + return {} + end + + # if we only have one peer, and it's me, then topology is empty + if peers.length == 1 and peers[0] == lookupvar('fqdn') + return {} + end + + result = {} + + i = 0 + while i < peers.length do + + x = peers[i] # from + if i < peers.length-1 + y = peers[i+1] # to + else + y = peers[0] # wrap around + end + + # store value as a list, in the ring case of length 1 + result[x] = [y] + + i+=1 # i++ + end + + result # return + end +end + +# vim: ts=8 diff --git a/ipa/manifests/client.pp b/ipa/manifests/client.pp new file mode 100644 index 000000000..bae6a883d --- /dev/null +++ b/ipa/manifests/client.pp @@ -0,0 +1,213 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class ipa::client( + $name = '', # what define was called with... + $hostname = $::hostname, + $domain = $::domain, + $realm = '', # defaults to upcase($domain) + $server = '', # ipa server + $password = '', # seemingly no password restrictions... + + $admin = false, # should we get admin tools installed ? + $ssh = false, + $sshd = false, + $ntp = false, + $ntp_server = '', + + $shorewall = false, # TODO ? + $zone = 'net', + $allow = 'all', + $debug = false, + $ensure = present # TODO: support uninstall with 'absent' +) { + include ipa::vardir + + case $::osfamily { + 'RedHat': { + if $::operatingsystem == 'Fedora' { + $ipa_client_pkgname = 'freeipa-client' + $ipa_admintools_pkgname = 'freeipa-admintools' + } else { + $ipa_client_pkgname = 'ipa-client' + $ipa_admintools_pkgname = 'ipa-admintools' + } + } + default: { + fail('Unsupported OS') + } + } + + #$vardir = $::ipa::vardir::module_vardir # with trailing slash + $vardir = regsubst($::ipa::vardir::module_vardir, '\/$', '') + + $valid_domain = downcase($domain) # TODO: validate ? + $valid_realm = $realm ? { + '' => upcase($valid_domain), + default => upcase($realm), + } + + $valid_server = "${server}" ? { + '' => "ipa.${valid_domain}", # default if unspecified... + default => "${server}", + } + + if "${hostname}" != delete("${hostname}", '.') { + fail('The $hostname value must not contain periods. It is not the FQDN.') + } + + if "${valid_domain}" == '' { + fail('A $domain value is required.') + } + + $valid_name = "${name}" ? { + '' => "${hostname}.${domain}", # defaults to fqdn if empty... + default => "${name}", # this could be fqdn or not... + } + + if $debug { + # just used for debugging + $valid_fqdn = "${hostname}.${valid_domain}" + $valid_principal = "host/${valid_fqdn}@${valid_realm}" + notify { "ipa-client-host-${name}": + message => "Host: '${name}', principal: '${valid_principal}'", + } + } + + package { 'ipa-client': + name => $ipa_client_pkgname, + ensure => present, + } + + # an administrator machine requires the ipa-admintools package as well: + package { 'ipa-admintools': + ensure => $admin ? { + true => present, + false => absent, + }, + name => $ipa_admintools_pkgname, + require => Package['ipa-client'], + } + + # store the passwords in text files instead of having them on cmd line! + # TODO: storing plain text passwords is not good, so what should we do? + file { "${vardir}/password": + content => "${password}\n", # temporarily secret... + owner => root, + group => nobody, + mode => 600, # u=rw,go= + backup => false, + require => File["${vardir}/"], + ensure => present, + } + # these are the arguments to ipa-server-install in the prompted order + $args01 = "--hostname='${hostname}.${valid_domain}'" + $args02 = "--domain='${valid_domain}'" + $args03 = "--realm='${valid_realm}'" + $args04 = "--server='${valid_server}'" + #$args05 = "--password='${password}'" # password to join IPA realm + $args05 = "--password=`/bin/cat '${vardir}/password'`" + + $args06 = $ssh ? { + true => '', + default => '--no-ssh', + } + + $args07 = $sshd ? { + true => '', + default => '--no-sshd', + } + + $args08 = $ntp ? { + true => '', + default => '--no-ntp', + } + + $args09 = $ntp_server ? { + '' => '', + default => $ntp ? { + true => "--ntp-server=${ntp_server}", + default => '', + }, + } + + $arglist = ["${args01}", "${args02}", "${args03}", "${args04}", "${args05}", "${args06}", "${args07}", "${args08}", "${args09}"] + #$args = inline_template('<%= arglist.delete_if {|x| x.empty? }.join(" ") %>') + $args = join(delete($arglist, ''), ' ') + + # this makes the install wait if a valid password hasn't been exported! + # this happens because it takes a second run of the ipa puppet after it + # has configured the host, because, on this second puppet run, the fact + # will finally now see the password, and it can be properly exported... + $has_auth = "${password}" ? { + '' => 'false', + default => 'true', + } + $onlyif = "/usr/bin/test '${has_auth}' = 'true'" + $unless = "/usr/bin/python -c 'import sys,ipapython.sysrestore; sys.exit(0 if ipapython.sysrestore.FileStore(\"/var/lib/ipa-client/sysrestore\").has_files() else 1)'" + exec { "/usr/sbin/ipa-client-install ${args} --unattended": + logoutput => on_failure, + onlyif => "${onlyif}", # needs a password or authentication... + unless => "${unless}", # can't install if already installed... + require => [ + Package['ipa-client'], + File["${vardir}/password"], + ], + alias => 'ipa-install', # same alias as server to prevent both! + } + + # this file is a tag that lets nfs know that the ipa host is now ready! + file { "${vardir}/ipa_client_installed": + content => "true\n", + owner => root, + group => nobody, + mode => 600, # u=rw,go= + backup => false, + require => [ + File["${vardir}/"], + Exec['ipa-install'], + ], + ensure => present, + } + + # normally when this resource is created by collection, the password is + # exported which allows the client to boostrap itself without a ticket. + # once this host gets built, the password gets "used" on the ipa server + # which causes it to show 'has_password: False', which would cause that + # password to get regenerated, however this exported resource will stop + # that from happening when it gets collected on the server as a tag. if + # this client dissapears, then, the exported resource should eventually + # get removed when a client runs puppet, which will cause a new pass to + # be created for the new ipa client install if we happen to want one... + #if "${password}" == '' { + @@ipa::server::host::pwtag { "${valid_name}": + tag => "${valid_name}", # collection by name is buggy, use tag! + } + #} + + # send ssh keys back so that server updates its database if they change + @@ipa::server::host::sshpubkeys { "${valid_name}": + # FIXME: redo this resource so that we specify an array instead + # this is needed in case we decide to export other keys perhaps + # it's more important because static things aren't very elegant + rsa => "${::sshrsakey}", # built in fact + dsa => "${::sshdsakey}", # built in fact + tag => "${valid_name}", # same name as ipa::server::host + } +} + +# vim: ts=8 diff --git a/ipa/manifests/client/deploy.pp b/ipa/manifests/client/deploy.pp new file mode 100644 index 000000000..8e04a3fb3 --- /dev/null +++ b/ipa/manifests/client/deploy.pp @@ -0,0 +1,66 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: use this to deploy all the @@ipa::client::* exported resources on clients +# the $nametag variable should match the $name value of the server/client::host +class ipa::client::deploy( + $hostname = $::hostname, + $domain = $::domain, + $server = '', + $nametag = '', # pick a tag to collect... + $debug = false +) { + $valid_domain = downcase($domain) # TODO: validate ? + + # if $hostname has dots, then assume it's a fqdn, if not, we add $domain + $valid_fqdn = delete("${hostname}", '.') ? { + "${hostname}" => "${hostname}.${valid_domain}", # had no dots present + default => "${hostname}", # had dots present... + } + + # NOTE: the resource collects by fqdn; one good reason to use the fqdn! + # sure you can override this by choosing your own $name value, but why? + $valid_tag = "${nametag}" ? { + '' => "${valid_fqdn}", + default => "${nametag}", + } + + # TODO: if i had more than one arg to decide to override, then i would + # have to build a big tree of nested choices... this is one more place + # where puppet shows it's really not a mature language yet. oh well... + # the host field is also the argument passed to the exported resource, + # and it is the $valid_host variable that came from the server service + if "${server}" == '' { + Ipa::Client::Host <<| tag == "${valid_tag}" |>> { + debug => $debug, + } + Ipa::Client::Service <<| host == "${valid_tag}" |>> { + debug => $debug, + } + } else { + Ipa::Client::Host <<| tag == "${valid_tag}" |>> { + server => "${server}", # override... + debug => $debug, + } + Ipa::Client::Service <<| host == "${valid_tag}" |>> { + server => "${server}", # override... + debug => $debug, + } + } +} + +# vim: ts=8 diff --git a/ipa/manifests/client/host.pp b/ipa/manifests/client/host.pp new file mode 100644 index 000000000..bdbf017f6 --- /dev/null +++ b/ipa/manifests/client/host.pp @@ -0,0 +1,80 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +define ipa::client::host( + # NOTE: this should be a copy of most of the params from ipa::client + $domain = '', + $realm = '', + $server = '', + $password = '', + $admin = false, + $ssh = false, + $sshd = false, + $ntp = false, + $ntp_server = '', + $shorewall = false, + $zone = 'net', + $allow = 'all', + $debug = false, + $ensure = present # TODO +) { + # $name should be a fqdn, split it into the $hostname and $domain args! + # NOTE: a regexp wizard could possibly write something to match better! + #$r = '^([a-z][a-z0-9\-]*)\.([a-z0-9\.\-]*)$' + $r = '^([a-z][a-z0-9\-]*)(\.{0,1})([a-z0-9\.\-]*)$' + $h = regsubst("${name}", $r, '\1') + $x = regsubst("${name}", $r, '\2') # the dot + $d = regsubst("${name}", $r, '\3') + + $valid_hostname = "${h}" + $valid_domain = "${d}" ? { + '' => "${domain}" ? { + '' => "${::domain}", + default => "${domain}", + }, + default => "${d}" ? { # we need to check this matches $domain + "${domain}" => "${d}", # they match, okay phew + default => '', # no match, set '' to trigger an error! + }, + } + # this error condition is very important because '' is used as trigger! + if "${valid_domain}" == '' { + fail('A $domain inconsistency was found.') + } + + class { '::ipa::client': + # NOTE: this should transfer most of the params from ipa::client + name => $name, # often the fqdn, but necessarily + hostname => $valid_hostname, + domain => $valid_domain, + realm => $realm, + server => $server, + password => $password, + admin => $admin, + ssh => $ssh, + sshd => $sshd, + ntp => $ntp, + ntp_server => $ntp_server, + shorewall => $shorewall, + zone => $zone, + allow => $allow, + debug => $debug, + ensure => $ensure, + } +} + +# vim: ts=8 diff --git a/ipa/manifests/client/host/deploy.pp b/ipa/manifests/client/host/deploy.pp new file mode 100644 index 000000000..6dd89b35d --- /dev/null +++ b/ipa/manifests/client/host/deploy.pp @@ -0,0 +1,57 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: use this to deploy the exported resource @@ipa::client::host on clients +#define ipa::client::host::deploy( +class ipa::client::host::deploy( + $hostname = $::hostname, + $domain = $::domain, + $server = '', + $nametag = '', # pick a tag to collect... + $debug = false +) { + $valid_domain = downcase($domain) # TODO: validate ? + + # if $hostname has dots, then assume it's a fqdn, if not, we add $domain + $valid_fqdn = delete("${hostname}", '.') ? { + "${hostname}" => "${hostname}.${valid_domain}", # had no dots present + default => "${hostname}", # had dots present... + } + + # NOTE: the resource collects by fqdn; one good reason to use the fqdn! + # sure you can override this by choosing your own $name value, but why? + $valid_tag = "${nametag}" ? { + '' => "${valid_fqdn}", + default => "${nametag}", + } + + # TODO: if i had more than one arg to decide to override, then i would + # have to build a big tree of nested choices... this is one more place + # where puppet shows it's really not a mature language yet. oh well... + if "${server}" == '' { + Ipa::Client::Host <<| tag == "${valid_tag}" |>> { + debug => $debug, + } + } else { + Ipa::Client::Host <<| tag == "${valid_tag}" |>> { + server => "${server}", # override... + debug => $debug, + } + } +} + +# vim: ts=8 diff --git a/ipa/manifests/client/service.pp b/ipa/manifests/client/service.pp new file mode 100644 index 000000000..98ae77b1b --- /dev/null +++ b/ipa/manifests/client/service.pp @@ -0,0 +1,186 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# FIXME: if this resource is removed, how do we revoke the key from the keytab? +# FIXME: it seems that after a kdestroy/kinit cycle happens, it is then revoked +# FIXME: a freeipa expert should verify and confirm that it's safe/ok this way! +# this runs ipa-getkeytab magic, to setup the keytab, for a service on a client +define ipa::client::service( + $service = '', # nfs, HTTP, ldap + $host = '', # should match $name of ipa::client::host + $domain = '', # must be the empty string by default + $realm = '', + $principal = '', # after all that, you can override principal... + $server = '', # where the client will find the ipa server... + $keytab = '', # defaults to /etc/krb5.keytab + $comment = '', + $debug = false, + $ensure = present +) { + include ipa::vardir + #$vardir = $::ipa::vardir::module_vardir # with trailing slash + $vardir = regsubst($::ipa::vardir::module_vardir, '\/$', '') + + # NOTE: much of the following code is almost identical to that up above + # TODO: a better regexp magician could probably do a better job :) + # nfs/nfs.example.com@EXAMPLE.COM + $r = '^([a-zA-Z][a-zA-Z0-9]*)(/([a-z][a-z\.\-]*)(@([A-Z][A-Z\.\-]*)){0,1}){0,1}$' + + $a = regsubst("${name}", $r, '\1') # service (nfs) + $b = regsubst("${name}", $r, '\3') # fqdn (nfs.example.com) + $c = regsubst("${name}", $r, '\5') # realm (EXAMPLE.COM) + + # service: first try to get value from arg, then fall back to $a (name) + $valid_service = "${service}" ? { + '' => "${a}", # get from $name regexp + default => "${service}", + } + if "${valid_service}" == '' { + # NOTE: if we see this message it might be a regexp pattern bug + fail('The $service must be specified.') + } + + # host: first try to get value from arg, then fall back to $b + # this is not necessarily the fqdn, but it could be. both are possible! + $valid_host = "${host}" ? { + '' => "${b}", # get from $name regexp + default => "${host}", + } + # this error will probably prevent a later error in $valid_domain + if "${valid_host}" == '' { + fail('The $host must be specified.') + } + + # parse the fqdn from $valid_host + $r2 = '^([a-z][a-z0-9\-]*)(\.{0,1})([a-z0-9\.\-]*)$' + #$h = regsubst("${valid_host}", $r2, '\1') # hostname + $d = regsubst("${valid_host}", $r2, '\3') # domain + + $valid_domain = delete("${valid_host}", '.') ? { + "${valid_host}" => "${domain}" ? { # no dots, not an fqdn! + '' => "${ipa::client::domain}" ? { # NOTE: client! + '' => "${::domain}", # default to global val + default => "${ipa::client::domain}", # main! + }, + default => "${domain}", + }, + default => "${domain}" ? { # dots, it's an fqdn... + '' => "${d}", # okay, used parsed value, it had dots! + "${d}" => "${domain}", # they match, okay phew + default => '', # no match, set '' to trigger an error! + }, + } + + # this error condition is very important because '' is used as trigger! + if "${valid_domain}" == '' { + fail('The $domain must be specified.') + } + + $valid_fqdn = delete("${valid_host}", '.') ? { # does it have any dots + "${valid_host}" => "${valid_host}.${valid_domain}", + default => "${valid_host}", # it had dot(s) present + } + + $valid_realm = "${realm}" ? { + '' => "${c}" ? { # get from $name regexp + '' => upcase($valid_domain), # a backup plan default + default => "${c}", # got from $name regexp + }, + default => "${realm}", + } + + # sanity checking, this should probably not happen + if "${valid_realm}" == '' { + fail('The $realm must be specified.') + } + + $valid_server = "${server}" ? { + '' => "${ipa::client::valid_server}", + default => "${server}", + } + + # sanity checking, this should probably not happen + if "${valid_server}" == '' { + fail('The $server must be specified.') + } + + $valid_principal = "${principal}" ? { + '' => "${valid_service}/${valid_fqdn}@${valid_realm}", + default => "${principal}", # just do what you want + } + + $valid_keytab = "${keytab}" ? { # TODO: validate + '' => '/etc/krb5.keytab', + default => "${keytab}", + } + + if $debug { + notify { "ipa-client-service-${name}": + message => "Service: '${name}', principal: '${valid_principal}'", + } + } + + # TODO: it would be great to put this kinit code into a single class to + # be used by each service, but it's not easily possible if puppet stops + # us from declaring identical class objects when they're seen as dupes! + # there is ensure_resource, but it's a hack and class might not work... + # NOTE: i added a lifetime of 1 hour... no sense needing any longer + $rr = "krbtgt/${valid_realm}@${valid_realm}" + $tl = '900' # 60*15 => 15 minutes + $admin = "host/${valid_fqdn}@${valid_realm}" # use this principal... + exec { "/usr/bin/kinit -k -t '${valid_keytab}' ${admin} -l 1h": + logoutput => on_failure, + #unless => "/usr/bin/klist -s", # is there a credential cache + # NOTE: we need to check if the ticket has at least a certain + # amount of time left. if not, it could expire mid execution! + # this should definitely get patched, but in the meantime, we + # check that the current time is greater than the valid start + # time (in seconds) and that we have within $tl seconds left! + unless => "/usr/bin/klist -s && /usr/bin/test \$(( `/bin/date +%s` - `/usr/bin/klist | /bin/grep -F '${rr}' | /bin/awk '{print \$1\" \"\$2}' | /bin/date --file=- +%s` )) -gt 0 && /usr/bin/test \$(( `/usr/bin/klist | /bin/grep -F '${rr}' | /bin/awk '{print \$3\" \"\$4}' | /bin/date --file=- +%s` - `/bin/date +%s` )) -gt ${tl}", + require => [ + Package['ipa-client'], + Exec['ipa-install'], + Ipa::Client::Host["${valid_host}"], + ], + alias => "ipa-client-service-kinit-${name}", + } + + $args01 = "--server='${valid_server}'" # contact this KDC server (ipa) + $args02 = "--principal='${valid_principal}'" # the service principal + $args03 = "--keytab='${valid_keytab}'" + + $arglist = ["${args01}", "${args02}", "${args03}"] + $args = join(delete($arglist, ''), ' ') + + $kvno_bool = "/usr/bin/kvno -q '${valid_principal}'" + exec { "/usr/sbin/ipa-getkeytab ${args}": + logoutput => on_failure, + # check that the KDC has a valid ticket available there + # check that the ticket version no. matches our keytab! + unless => "${kvno_bool} && /usr/bin/klist -k -t '${valid_keytab}' | /bin/awk '{print \$4\": kvno = \"\$1}' | /bin/sort | /usr/bin/uniq | /bin/grep -F '${valid_principal}' | /bin/grep -qxF \"`/usr/bin/kvno '${valid_principal}'`\"", + require => [ + # these deps are done in the kinit + #Package['ipa-client'], + #Exec['ipa-install'], + #Ipa::Client::Host["${valid_host}"], + Exec["ipa-client-service-kinit-${name}"], + ], + #alias => "ipa-getkeytab-${name}", + } +} + +# vim: ts=8 diff --git a/ipa/manifests/client/service/deploy.pp b/ipa/manifests/client/service/deploy.pp new file mode 100644 index 000000000..9c8e053f3 --- /dev/null +++ b/ipa/manifests/client/service/deploy.pp @@ -0,0 +1,46 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: use this to deploy the exported resource @@ipa::client::service +class ipa::client::service::deploy( + $server = '', + $nametag = '', # pick a tag to collect... + $debug = false +) { + + # NOTE: the resource collects by fqdn; one good reason to use the fqdn! + # sure you can override this by choosing your own $name value, but why? + $valid_tag = "${nametag}" ? { + '' => "${::fqdn}", # if we're smart, this is what is used! + default => "${nametag}", + } + + # the host field is also the argument passed to the exported resource, + # and it is the $valid_host variable that came from the server service + if "${server}" == '' { + Ipa::Client::Service <<| host == "${valid_tag}" |>> { + debug => $debug, + } + } else { + Ipa::Client::Service <<| host == "${valid_tag}" |>> { + server => "${server}", # override... + debug => $debug, + } + } +} + +# vim: ts=8 diff --git a/ipa/manifests/common.pp b/ipa/manifests/common.pp new file mode 100644 index 000000000..8d0f16722 --- /dev/null +++ b/ipa/manifests/common.pp @@ -0,0 +1,27 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class ipa::common( + +) { + + # TODO: patch freeipa to provide this in a stable way... + $ipa_installed = "/usr/bin/python -c 'import sys,ipaserver.install.installutils; sys.exit(0 if ipaserver.install.installutils.is_ipa_configured() else 1)'" + +} + +# vim: ts=8 diff --git a/ipa/manifests/init.pp b/ipa/manifests/init.pp new file mode 100644 index 000000000..1fbc15f99 --- /dev/null +++ b/ipa/manifests/init.pp @@ -0,0 +1,60 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# README: this is a rather complicated module to understand. read the comments! + +# NOTE: if you ever see a puppet error where an ipa exec returns with: +# ipa: ERROR: no modifications to be performed +# then please report this as a bug. This puppet module is (supposed to be) +# smart enough to only run exec's when they are actually necessary. + +# NOTE: to hack your way into the ipa web ui with ssh port forwarding, when the +# computer you are using is completely isolated from the actual ipa server, you +# could fake the dns entry in your /etc/hosts file by adding/ensuring the line: +# 127.0.0.1 ipa.example.com ipa localhost.localdomain localhost +# exists (replace example.com with your ipa domain of course) and then running: +# sudo ssh root@ipa -L 80:localhost:80 -L 443:localhost:443 # (as root !) +# to force forwarding on priviledged ports, and then point your web browser to: +# https://ipa.example.com/ipa/ui/ +# and then accept the certificate. but don't do any of this, it's an evil hack! + +# NOTE: this expects mit kerberos: http://web.mit.edu/kerberos/krb5-latest/doc/ + +# NOTE: useful ipa docs at: https://access.redhat.com/site/documentation/en-US/ +# Red_Hat_Enterprise_Linux/6/html-single/Identity_Management_Guide/index.html + +# NOTE: if on client reinstall ipa-client-install complains with: +# freeipa LDAP Error: Connect error: TLS error -8054: You are attempting +# to import a cert with the same issuer/serial as an existing cert, but +# that is not the same cert. +# just: 'rm /etc/ipa/ca.crt', bug: https://fedorahosted.org/freeipa/ticket/3537 + +# NOTE: if you wish to use the $dns option, it must be enabled at first install +# subsequent enabling/disabling is currently not supported. this is because of: +# https://fedorahosted.org/freeipa/ticket/3726 +# (ipa-dns-install needs a --uninstall option) +# and also because the DM_PASSWORD might not be available if we gpg encrypt and +# email it out after randomly generating it. This is a security feature! (TODO) <- CHANGE TO (DONE) when finished! +# we could actually support install and uninstall if that bug was resolved, and +# if we either regenerated the password, or were able to circumvent it with our +# root powers somehow. this is actually quite plausible, but not worth the time + +# TODO: maybe we could have an exported resource that creates a .k5login in the +# root home dirs of machines to give access to other admins with their tickets? + +# TODO: a ...host::dns type or similar needs to be added to manage and host ips + diff --git a/ipa/manifests/rulewrapper.pp b/ipa/manifests/rulewrapper.pp new file mode 100644 index 000000000..d237113a4 --- /dev/null +++ b/ipa/manifests/rulewrapper.pp @@ -0,0 +1,47 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: this wraps shorewall::rule so that we can add on additional fake 'tags' +define ipa::rulewrapper( + $action = '', + $source = '', + $source_ips = [], + $dest = '', + $dest_ips = [], + $proto = '', + $port = [], + $sport = [], + $original = [], + $comment = '', + $ensure = present, + $match = '' # additional tag parameter +) { + shorewall::rule { "${name}": + action => "${action}", + source => "${source}", + source_ips => $source_ips, + dest => "${dest}", + dest_ips => $dest_ips, + proto => "${proto}", + port => $port, + sport => $sport, + comment => "${comment}", + ensure => $ensure, + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server.pp b/ipa/manifests/server.pp new file mode 100644 index 000000000..3e148ab16 --- /dev/null +++ b/ipa/manifests/server.pp @@ -0,0 +1,754 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class ipa::server( + $hostname = $::hostname, + $domain = $::domain, + $ipa_ipaddress = '', + $realm = '', # defaults to upcase($domain) + $vip = '', # virtual ip of the replica master host + $peers = {}, # specify the peering topology by fqdns + $topology = '', # specify the peering algorithm to use! + $topology_arguments = [], # list of additional arguments for algo + + # we generate these passwords locally to use for the install, but then + # we gpg encrypt and store locally and/or email to the root user. this + # requires an admin's public gpg key which is a sensible thing to have + # thanks to Jpmh from #gnupg for helping me find things in the manual! + $dm_password = '', # eight char minimum or auto-generated + $admin_password = '', # eight char minimum or auto-generated + + # if one of the above passwords is blank, you must use: $gpg_recipient + # with: $gpg_recipient, you must use: $gpg_publickey or $gpg_keyserver + $gpg_recipient = '', # must specify a valid -r value to use + $gpg_publickey = '', # can be the value or a puppet:/// uri + $gpg_keyserver = '', # use a uri like: hkp://keys.gnupg.net + $gpg_sendemail = false, # mail out the gpg encrypted password? + + $idstart = '16777216', # TODO: what is sensible? i picked 2^24 + $idmax = '', + $email_domain = '', # defaults to domain + $shell = true, # defaults to /bin/sh + $homes = true, # defaults to /home + + # packages products to install ? + $ntp = false, # opposite of ipa-server-install default + $dns = false, # must be set at install time to be used + $dogtag = false, + + $email = '', # defaults to root@domain, important... + + $vrrp = false, + $shorewall = false, + $zone = 'net', + $allow = 'all', + + # special + # NOTE: host_excludes is matched with bash regexp matching in: [[ =~ ]] + # if the string regexp passed contains quotes, string matching is done: + # $string='"hostname.example.com"' vs: $regexp='hostname.example.com' ! + # obviously, each pattern in the array is tried, and any match will do. + # invalid expressions might cause breakage! use this at your own risk!! + # remember that you are matching against the fqdn's, which have dots... + # a value of true, will automatically add the * character to match all. + $host_excludes = [], # never purge these host excludes... + $service_excludes = [], # never purge these service excludes... + $user_excludes = [], # never purge these user excludes... + $peer_excludes = [], # never purge these peer excludes... + $ensure = present # TODO: support uninstall with 'absent' +) { + $FW = '$FW' # make using $FW in shorewall easier... + + # TODO: should we always include the replica peering or only when used? + include ipa::server::replica::peering + include ipa::server::replica::master + include ipa::common + include ipa::vardir + #$vardir = $::ipa::vardir::module_vardir # with trailing slash + $vardir = regsubst($::ipa::vardir::module_vardir, '\/$', '') + + case $::osfamily { + 'RedHat': { + if $::operatingsystem == 'Fedora' { + $ipa_server_pkgname = 'freeipa-server' + } else { + $ipa_server_pkgname = 'ipa-server' + package { 'python-argparse': # used by diff.py + ensure => present, + before => [ + Package['ipa-server'], + File["${vardir}/diff.py"], + ], + } + } + } + default: { + fail('Unsupported OS') + } + } + + if "${vip}" != '' { + if ! ($vip =~ /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/) { + fail('You must specify a valid VIP to use.') + } + } + $valid_vip = "${vip}" + $vipif = inline_template("<%= @interfaces.split(',').find_all {|x| '${valid_vip}' == scope.lookupvar('ipaddress_'+x) }[0,1].join('') %>") + + # automatically setup vrrp on each host... + if $vrrp { + class { '::keepalived::simple': + #ip => '', + vip => "${valid_vip}", + shorewall => $shorewall, + zone => $zone, + #allow => $allow, + #password => '', + } + } + + # this is used for automatic peering... this is a list of every server! + $replica_peers_fact = "${::ipa_server_replica_peers}" # fact! + $replica_peers = split($replica_peers_fact, ',') # list! + + # NOTE: this algorithm transforms a sorted list of peers into a set of: + # from -> to pairs (as a hash), or from -> to and to -> from pairs that + # are symmetrical since peering is bi-directional... this list of hosts + # could either be determined automatically with "exported resources" or + # specified manually. just select an algorithm for automatic peering... + # the $key in the hash is the from value. the $value of the hash is the + # list of whichever hosts we should peer with, ordered by preference... + + # run the appropriate topology function here + $empty_hash = {} + $valid_peers = $topology ? { + 'flat' => ipa_topology_flat($replica_peers), + 'ring' => ipa_topology_ring($replica_peers), + #'manual' => $peers, + default => type($peers) ? { # 'manual' (default) peering... + 'hash' => $peers, # TODO: validate this data type + default => $empty_hash, # invalid data... + }, + } + + notice(inline_template('valid_peers: <%= @valid_peers.inspect %>')) + + # export the required firewalls... + if $shorewall { + # in the single host case, the topology should be an empty hash + if has_key($valid_peers, "${::fqdn}") { + ipa::server::replica::firewall { $valid_peers["${::fqdn}"]: + peer => "${::fqdn}", # match the manage type pattern + } + } + } + + $valid_hostname = "${hostname}" # TODO: validate ? + $valid_domain = downcase($domain) # TODO: validate ? + $valid_realm = $realm ? { + '' => upcase($valid_domain), + default => upcase($realm), + } + + $default_email_domain = "${email_domain}" ? { + '' => "${valid_domain}", + default => "${email_domain}", + } + ipa::server::config { 'emaildomain': + value => "${default_email_domain}", + } + + $default_shell = type($shell) ? { + 'boolean' => $shell ? { + false => false, # unmanaged + default => '/bin/sh', # the default + }, + default => "${shell}", + } + # we don't manage if value is false, otherwise it's good to go! + if ! (type($shell) == 'boolean' and (! $shell)) { + ipa::server::config { 'shell': + value => "${default_shell}", + } + } + + # TODO: the home stuff seems to not use trailing slashes. can i add it? + $default_homes = type($homes) ? { + 'boolean' => $homes ? { + false => false, # unmanaged + default => '/home', # the default + }, + default => "${homes}", + } + if ! (type($homes) == 'boolean' and (! $homes)) { + ipa::server::config { 'homes': + value => "${default_homes}", # XXX: remove trailing slash if present ? + } + } + + $valid_email = $email ? { + '' => "root@${default_email_domain}", + default => "${email}", + } + + if "${valid_hostname}" == '' { + fail('A $hostname value is required.') + } + + if "${valid_domain}" == '' { + fail('A $domain value is required.') + } + + $valid_fqdn = "${valid_hostname}.${valid_domain}" + + if $dns { + package { ['bind', 'bind-dyndb-ldap']: + ensure => present, + before => Package['ipa-server'], + } + } + + package { 'pwgen': # used to generate passwords + ensure => present, + before => Package['ipa-server'], + } + + package { 'ipa-server': + name => $ipa_server_pkgname, + ensure => present, + } + + file { "${vardir}/diff.py": # used by a few child classes + source => 'puppet:///modules/ipa/diff.py', + owner => root, + group => nobody, + mode => 700, # u=rwx + backup => false, # don't backup to filebucket + ensure => present, + require => [ + Package['ipa-server'], + File["${vardir}/"], + ], + } + + if "${dm_password}" == '' and "${gpg_recipient}" == '' { + fail('You must specify either a dm_password or a GPG id.') + } + + if "${admin_password}" == '' and "${gpg_recipient}" == '' { + fail('You must specify either an admin_password or a GPG id.') + } + + if "${gpg_recipient}" != '' { + if "${gpg_publickey}" == '' and "${gpg_keyserver}" == '' { + fail('You must specify either a keyserver or a public key.') + } + + if "${gpg_publickey}" != '' and "${gpg_keyserver}" != '' { + fail('You cannot specify a keyserver and a public key.') + } + } + + if "${gpg_recipient}" != '' { + file { "${vardir}/gpg/": + ensure => directory, # make sure this is a directory + recurse => true, # don't recurse into directory + purge => true, # don't purge unmanaged files + force => true, # don't purge subdirs and links + # group and other must not have perms or gpg complains! + mode => 600, # u=rw,go= + backup => false, + require => File["${vardir}/"], + } + + # tag + $dm_password_filename = "${vardir}/gpg/dm_password.gpg" + file { "${dm_password_filename}": + owner => root, + group => nobody, + mode => 600, # u=rw,go= + backup => false, + require => File["${vardir}/gpg/"], + ensure => present, + } + + # tag + $admin_password_filename = "${vardir}/gpg/admin_password.gpg" + file { "${admin_password_filename}": + owner => root, + group => nobody, + mode => 600, # u=rw,go= + backup => false, + require => File["${vardir}/gpg/"], + ensure => present, + } + + # tag + file { "${vardir}/gpg/pubring.gpg": + owner => root, + group => nobody, + mode => 600, # u=rw,go= + backup => false, + require => File["${vardir}/gpg/"], + ensure => present, + } + + file { "${vardir}/gpg/secring.gpg": + owner => root, + group => nobody, + mode => 600, # u=rw,go= + backup => false, + require => File["${vardir}/gpg/"], + ensure => present, + } + + # tag this file too, because the gpg 'unless' commands cause it + # get added when gpg sees that it's missing from the --homedir! + file { "${vardir}/gpg/trustdb.gpg": + owner => root, + group => nobody, + mode => 600, # u=rw,go= + backup => false, + require => File["${vardir}/gpg/"], + ensure => present, + } + } + + if "${gpg_publickey}" != '' { + $gpg_source = inline_template('<%= @gpg_publickey.start_with?("puppet:///") ? "true":"false" %>') + file { "${vardir}/gpg/pub.gpg": + content => "${gpg_source}" ? { + 'true' => undef, + default => "${gpg_publickey}", + }, + source => "${gpg_source}" ? { + 'true' => "${gpg_publickey}", + default => undef, + }, + owner => root, + group => nobody, + mode => 600, # u=rw,go= + backup => false, + before => Exec['ipa-gpg-import'], + require => File["${vardir}/gpg/"], + ensure => present, + } + } + + $gpg_cmd = "/usr/bin/gpg --homedir '${vardir}/gpg/'" # base gpg cmd! + + $gpg_import = "${gpg_publickey}" ? { + '' => "--keyserver '${gpg_keyserver}' --recv-keys '${gpg_recipient}'", + default => "--import '${vardir}/gpg/pub.gpg'", + } + + if "${gpg_recipient}" != '' { + + # check if key is already imported + $gpg_unless = "${gpg_cmd} --with-colons --fast-list-mode --list-public-keys '${gpg_recipient}'" + + exec { "${gpg_cmd} ${gpg_import}": + logoutput => on_failure, + unless => $gpg_unless, + before => Exec['ipa-install'], + require => File["${vardir}/gpg/"], + alias => 'ipa-gpg-import', + } + + # TODO: add checks + # * is key revoked ? + # * other sanity checks ? + + if $gpg_sendemail { + # if we email out the encrypted password, make sure its + # public key has the correct email address to match it! + $gpg_check_email = "${gpg_cmd} --with-colons --list-public-keys '${gpg_recipient}' | /bin/awk -F ':' '\$1 = /uid/ {print \$10}' | /bin/grep -qF '<${valid_email}>'" + exec { "${gpg_check_email}": + logoutput => on_failure, + unless => $gpg_unless, + before => Exec['ipa-install'], + require => Exec['ipa-gpg-import'], + alias => 'ipa-gpg-check', + } + } + } + + $pwgen_cmd = "/usr/bin/pwgen 16 1" + + $valid_dm_password = "${dm_password}" ? { + '' => "${pwgen_cmd}", + default => "/bin/cat '${vardir}/dm.password'", + } + + $valid_admin_password = "${admin_password}" ? { + '' => "${pwgen_cmd}", + default => "/bin/cat '${vardir}/admin.password'", + } + + # NOTE: we have to use '--trust-model always' or it prompts with: + # It is NOT certain that the key belongs to the person named + # in the user ID. If you *really* know what you are doing, + # you may answer the next question with yes. + $gpg_encrypt = "${gpg_cmd} --encrypt --trust-model always --recipient '${gpg_recipient}'" + $mail_send = "/bin/mailx -s 'Password for: ${valid_hostname}.${valid_domain}' '${valid_email}'" + + $dm_password_file = "${gpg_recipient}" ? { + '' => '/bin/cat', # pass through, no gpg key exists... + default => "/usr/bin/tee >( ${gpg_encrypt} > '${dm_password_filename}' )", + } + if "${gpg_recipient}" != '' and $gpg_sendemail { + $dm_password_mail = "/usr/bin/tee >( ${gpg_encrypt} | (/bin/echo 'GPG(DM password):'; /bin/cat) | ${mail_send} > /dev/null )" + } else { + $dm_password_mail = '/bin/cat' + } + $dm_password_exec = "${valid_dm_password} | ${dm_password_file} | ${dm_password_mail} | /bin/cat" + + $admin_password_file = "${gpg_recipient}" ? { + '' => '/bin/cat', + default => "/usr/bin/tee >( ${gpg_encrypt} > '${admin_password_filename}' )", + } + if "${gpg_recipient}" != '' and $gpg_sendemail { + $admin_password_mail = "/usr/bin/tee >( ${gpg_encrypt} | (/bin/echo 'GPG(admin password):'; /bin/cat) | ${mail_send} > /dev/null )" + } else { + $admin_password_mail = '/bin/cat' + } + $admin_password_exec = "${valid_admin_password} | ${admin_password_file} | ${admin_password_mail} | /bin/cat" + + # store the passwords in text files instead of having them on cmd line! + # even better is to let them get automatically generated and encrypted! + if "${dm_password}" != '' { + $dm_bool = inline_template('<%= @dm_password.length < 8 ? "false":"true" %>') + if "${dm_bool}" != 'true' { + fail('The dm_password must be at least eight characters in length.') + } + file { "${vardir}/dm.password": + content => "${dm_password}\n", # top top secret! + owner => root, + group => nobody, + mode => 600, # u=rw,go= + backup => false, + before => Exec['ipa-install'], + require => File["${vardir}/"], + ensure => present, + } + } + + if "${admin_password}" != '' { + $admin_bool = inline_template('<%= @admin_password.length < 8 ? "false":"true" %>') + if "${admin_bool}" != 'true' { + fail('The admin_password must be at least eight characters in length.') + } + file { "${vardir}/admin.password": + content => "${admin_password}\n", # top secret! + owner => root, + group => nobody, + mode => 600, # u=rw,go= + backup => false, + before => Exec['ipa-install'], + require => File["${vardir}/"], + ensure => present, + } + } + + # these are the arguments to ipa-server-install in the prompted order + $args01 = "--hostname='${valid_fqdn}'" + $args02 = "--domain='${valid_domain}'" + $args03 = "--realm='${valid_realm}'" + $args04 = "--ds-password=`${dm_password_exec}`" # Directory Manager + $args05 = "--admin-password=`${admin_password_exec}`" # IPA admin + # TODO: reconcile these options with the range settings: EXAMPLE.COM_id_range + # if that range is changed, should we watch for it and reset? yes we should if we specified one here... + $args06 = $idstart ? { + '' => '', + default => "--idstart=${idstart}", + } + $args07 = $idmax ? { + '' => '', + default => "--idmax=${idmax}", + } + + $args08 = $ntp ? { + true => '', # create ntp server... + default => '--no-ntp', + } + + $args09 = $dns ? { + true => '--setup-dns --no-forwarders', + default => '', + } + + $args10 = $dns ? { + true => "--zonemgr=${valid_email}", + default => '', + } + + # we check the version because the --selfsign option vanishes in 3.2.0 + # http://www.freeipa.org/page/Releases/3.2.0#Dropped_--selfsign_option + $versioncmp = versioncmp("${::ipa_version}", '3.2.0') + $args11 = $dogtag ? { + true => '', # TODO: setup dogtag + default => "${versioncmp}" ? { + # pre 3.2.0, you have to disable dogtag manually + '-1' => '--selfsign', # disable dogtag + # post 3.2.0, dogtag is not setup by default...! + default => '', + }, + } + + $args12 = $ipa_ipaddress ? { + '' => '', + default => $dns ? { + true => "--ip-address=${ipa_ipaddress} --no-host-dns", + default => "--ip-address=${ipa_ipaddress}", + }, + } + + $arglist = [ + "${args01}", + "${args02}", + "${args03}", + "${args04}", + "${args05}", + "${args06}", + "${args07}", + "${args08}", + "${args09}", + "${args10}", + "${args11}", + "${args12}", + ] + #$args = inline_template('<%= arglist.delete_if {|x| x.empty? }.join(" ") %>') + $args = join(delete($arglist, ''), ' ') + + # split ipa-server-install command into a separate file so that it runs + # as bash, and also so that it's available to run manually and inspect! + # if this installs successfully, tag it so we know which host was first + file { "${vardir}/ipa-server-install.sh": + content => inline_template("#!/bin/bash\n/usr/sbin/ipa-server-install ${args} --unattended && /bin/echo '${::fqdn}' > ${vardir}/ipa_server_replica_master\n"), + owner => root, + group => root, + mode => 700, + ensure => present, + require => File["${vardir}/"], + } + + if ("${valid_vip}" == '' or "${vipif}" != '') { + + exec { "${vardir}/ipa-server-install.sh": + logoutput => on_failure, + unless => "${::ipa::common::ipa_installed}", # can't install if installed... + timeout => 3600, # hope it doesn't take more than 1 hour + require => [ + Package['ipa-server'], + File["${vardir}/ipa-server-install.sh"], + ], + alias => 'ipa-install', # same alias as client to prevent both! + } + + # NOTE: this is useful to collect only on hosts that are installed or + # which are replicas that have been installed. ensure the type checks + # this prepares for any host we prepare for to potentially join us... + Ipa::Server::Replica::Prepare <<| title != "${::fqdn}" |>> { + + } + + } else { + + # NOTE: this is useful to export from any host that didn't install !!! + # this sends the message: "prepare for me to potentially join please!" + @@ipa::server::replica::prepare { "${valid_fqdn}": + } + + class { '::ipa::server::replica::install': + peers => $valid_peers, + } + + } + + # this file is a tag that lets you know which server was the first one! + file { "${vardir}/ipa_server_replica_master": + owner => root, + group => nobody, + mode => 600, # u=rw,go= + backup => false, + require => [ + File["${vardir}/"], + Exec['ipa-install'], + ], + ensure => present, + alias => 'ipa-server-master-flag', + } + + # this file is a tag that lets notify know it only needs to run once... + file { "${vardir}/ipa_server_installed": + #content => "true\n", + owner => root, + group => nobody, + mode => 600, # u=rw,go= + backup => false, + require => [ + File["${vardir}/"], + Exec['ipa-install'], + ], + ensure => present, + alias => 'ipa-server-installed-flag', + } + + # this sets the true value so that we know that ipa is installed first! + exec { "/bin/echo true > ${vardir}/ipa_server_installed": + logoutput => on_failure, + unless => "/usr/bin/test \"`/bin/cat ${vardir}/ipa_server_installed`\" = 'true'", + onlyif => "${::ipa::common::ipa_installed}", + require => File['ipa-server-installed-flag'], + } + + # check if we changed the dns state after initial install (unsupported) + # this is needed, because if dns was once setup, but the param is false + # then the host resource won't use --force and we'll get errors... this + # happens because of bug#: https://fedorahosted.org/freeipa/ticket/3726 + if ! $dns { + exec { '/bin/false': # fail so that we know about the change + logoutput => on_failure, + # thanks to 'ab' in #freeipa for help with the ipa api! + onlyif => "/usr/bin/python -c 'import sys,ipalib;ipalib.api.bootstrap_with_global_options(context=\"puppet\");ipalib.api.finalize();(ipalib.api.Backend.ldap2.connect(ccache=ipalib.api.Backend.krb.default_ccname()) if ipalib.api.env.in_server else ipalib.api.Backend.xmlclient.connect());sys.exit(0 if ipalib.api.Command.dns_is_enabled().get(\"result\") else 1)'", + require => Package['ipa-server'], + alias => 'ipa-dns-check', + } + } + + # TODO: add management of ipa services (ipa, httpd, krb5kdc, kadmin, etc...) run: ipactl status or service ipa status for more info + # TODO: add management (augeas?) of /etc/ipa/default.conf + + class { 'ipa::server::kinit': + realm => "${valid_realm}", + } + + # FIXME: consider allowing only certain ip's to the ipa server + # TODO: we could open ports per host when added with ipa::server::host + if $shorewall { + if $allow == 'all' or "${allow}" == '' { + $net = "${zone}" + } else { + $net = is_array($allow) ? { + true => sprintf("${zone}:%s", join($allow, ',')), + default => "${zone}:${allow}", + } + } + #################################################################### + #ACTION SOURCE DEST PROTO DEST SOURCE ORIGINAL + # PORT PORT(S) DEST + shorewall::rule { 'http': rule => " + HTTP/ACCEPT ${net} $FW + ", comment => 'Allow HTTP for webui'} + + shorewall::rule { 'https': rule => " + HTTPS/ACCEPT ${net} $FW + ", comment => 'Allow HTTPS for webui'} + + shorewall::rule { 'ldap': rule => " + LDAP/ACCEPT ${net} $FW + ", comment => 'Allow LDAP for 389 server on tcp port 389.'} + + shorewall::rule { 'ldaps': rule => " + LDAPS/ACCEPT ${net} $FW + ", comment => 'Allow LDAPS for 389 server on tcp port 636.'} + + shorewall::rule { 'kerberos': rule => " + Kerberos/ACCEPT ${net} $FW + ", comment => 'Allow Kerberos for krb5 server on tcp/udp port 88.'} + + # TODO: should i propose this as a shorewall macro ? + shorewall::rule { 'kpasswd': rule => " + ACCEPT ${net} $FW tcp 464 + ACCEPT ${net} $FW udp 464 + ", comment => 'Allow Kerberos for kpasswd on tcp/udp port 464.'} + + if $ntp { + shorewall::rule { 'ntp': rule => " + NTP/ACCEPT ${net} $FW + ", comment => 'Allow NTP on udp port 123.'} + } + + if $dns { + shorewall::rule { 'dns': rule => " + DNS/ACCEPT ${net} $FW + ", comment => 'Allow DNS on tcp/udp port 53.'} + } + + if $dogtag { + shorewall::rule { 'dogtag': rule => " + ACCEPT ${net} $FW tcp 7389 + ", comment => 'Allow dogtag certificate system on tcp port 7389.'} + } + } + + # in the single host case, the topology should be an empty hash + if has_key($valid_peers, "${::fqdn}") { + # ensure the topology has the right shape... + ipa::server::replica::manage { $valid_peers["${::fqdn}"]: # magic + peer => "${::fqdn}", + } + } + + # this fact gets created once the installation is complete... the first + # time that puppet runs, it won't be set. after installation it will :) + # this mechanism provides a way to only run the 'helpful' notifies once + if "${ipa_server_installed}" != 'true' { + # notify about password locations to be helpful + if "${gpg_recipient}" != '' { + if "${dm_password}" == '' { + $dm_password_msg = "The dm_password should be found in: ${dm_password_filename}." + notice("${dm_password_msg}") + notify {'ipa-notify-dm_password': + message => "${dm_password_msg}", + #stage => last, # TODO + require => Exec['ipa-install'], + } + if $gpg_sendemail { + $dm_password_email_msg = "The dm_password should be emailed to: ${valid_email}." + notice("${dm_password_email_msg}") + notify {'ipa-notify-email-dm_password': + message => "${dm_password_email_msg}", + #stage => last, # TODO + require => Exec['ipa-install'], + } + } + } + + if "${admin_password}" == '' { + $admin_password_msg = "The admin_password should be found in: ${admin_password_filename}." + notice("${admin_password_msg}") + notify {'ipa-notify-admin_password': + message => "${admin_password_msg}", + #stage => last, # TODO + require => Exec['ipa-install'], + } + if $gpg_sendemail { + $admin_password_email_msg = "The admin_password should be emailed to: ${valid_email}." + notice("${admin_password_email_msg}") + notify {'ipa-notify-email-admin_password': + message => "${admin_password_email_msg}", + #stage => last, # TODO + require => Exec['ipa-install'], + } + } + } + } + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server/config.pp b/ipa/manifests/server/config.pp new file mode 100644 index 000000000..ccaa9f2d2 --- /dev/null +++ b/ipa/manifests/server/config.pp @@ -0,0 +1,142 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# FIXME: some values have not been filled in yet. some are missing: --arguments +define ipa::server::config( + $value +) { + include ipa::common + + $key = "${name}" + + $etype = "${key}" ? { # expected type + #'?' => '', # FIXME: dn + #'?' => '', # --maxusername + 'homes' => 'string', + 'shell' => 'string', + #'?' => '', # --defaultgroup + 'emaildomain' => 'string', + #'?' => '', # --searchtimelimit + #'?' => '', # --searchrecordslimit + 'usersearch' => 'array', + 'groupsearch' => 'array', + 'migration' => 'boolean', + #'?' => '', # FIXME: ipacertificatesubjectbase + #'?' => '', # --groupobjectclasses + #'?' => '', # --userobjectclasses + #'?' => '', # --pwdexpnotify + #'?' => '', # --ipaconfigstring + #'?' => '', # --ipaselinuxusermaporder + #'?' => '', # --ipaselinuxusermapdefault + #'?' => '', # --pac-type + #'?' => '', # FIXME: cn + #'?' => '', # FIXME: objectclass + default => '', # missing + } + + $option = "${key}" ? { + #'?' => 'dn', FIXME + #'?' => '--maxusername=', + 'homes' => '--homedirectory=', + 'shell' => '--defaultshell=', + #'?' => '--defaultgroup=', + 'emaildomain' => '--emaildomain=', + #'?' => '--searchtimelimit=', + #'?' => '--searchrecordslimit=', + 'usersearch' => '--usersearch=', + 'groupsearch' => '--groupsearch=', + 'migration' => '--enable-migration=', + #'?' => 'ipacertificatesubjectbase', FIXME + #'?' => '--groupobjectclasses=', + #'?' => '--userobjectclasses=', + #'?' => '--pwdexpnotify=', + #'?' => '--ipaconfigstring=', + #'?' => '--ipaselinuxusermaporder=', + #'?' => '--ipaselinuxusermapdefault=', + #'?' => '--pac-type=', + #'?' => 'cn', FIXME + #'?' => 'objectclass', FIXME + default => '', # missing + } + + $rawkey = "${key}" ? { + #'?' => 'dn', + #'?' => 'ipamaxusernamelength', + 'homes' => 'ipahomesrootdir', + 'shell' => 'ipadefaultloginshell', + #'?' => 'ipadefaultprimarygroup', + 'emaildomain' => 'ipadefaultemaildomain', + #'?' => 'ipasearchtimelimit', + #'?' => 'ipasearchrecordslimit', + 'usersearch' => 'ipausersearchfields', + 'groupsearch' => 'ipagroupsearchfields', + 'migration' => 'ipamigrationenabled', + #'?' => 'ipacertificatesubjectbase', + #'?' => 'ipagroupobjectclasses', + #'?' => 'ipauserobjectclasses', + #'?' => 'ipapwdexpadvnotify', + #'?' => 'ipaconfigstring', + #'?' => 'ipaselinuxusermaporder', + #'?' => 'ipaselinuxusermapdefault', + #'?' => 'ipakrbauthzdata', + #'?' => 'cn', + #'?' => 'objectclass', + default => '', # missing + } + + if "${option}" == '' or "${etype}" == '' or "${rawkey}" == '' { + fail("Key '${key}' is invalid.") + } + + if type($value) != "${etype}" { + fail("Ipa::Server::Config[${key}] must be type: ${etype}.") + } + + # convert to correct type + if "${etype}" == 'string' { + $safe_value = shellquote($value) # TODO: is this right ? + $jchar = '' # pass through the paste binary + } elsif "${etype}" == 'array' { + $jchar = "${key}" ? { # join char + 'usersearch' => ',', + 'groupsearch' => ',', + default => '', + } + $safe_value = inline_template('<%= value.join(jchar) %>') + } elsif "${etype}" == 'boolean' { + $safe_value = $value ? { + true => 'TRUE', + default => 'FALSE', + } + $jchar = '' # pass through the paste binary + } else { + fail("Unknown type: ${etype}.") + } + + $cutlength = inline_template('<%= (rawkey.length+2).to_s %>') + exec { "/usr/bin/ipa config-mod ${option}'${safe_value}'": + logoutput => on_failure, + onlyif => "${::ipa::common::ipa_installed}", + unless => "/usr/bin/test \"`/usr/bin/ipa config-show --raw --all | /usr/bin/tr -d ' ' | /bin/grep '^${rawkey}:' | /bin/cut -b ${cutlength}- | /usr/bin/paste -sd '${jchar}'`\" = '${safe_value}'", + require => [ + Exec['ipa-install'], + Exec['ipa-server-kinit'], + ], + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server/host.pp b/ipa/manifests/server/host.pp new file mode 100644 index 000000000..4d13d9596 --- /dev/null +++ b/ipa/manifests/server/host.pp @@ -0,0 +1,404 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +define ipa::server::host( + $domain = $ipa::server::domain, # default to main domain + $server = '', # where the client will find the ipa server... + $macaddress = '', # TODO: this should be a list... + #$ipaddress = '', # NOTE: this is a bad fit here... + $sshpubkeys = true, # leave this at the default to get auto sshkeys + #$certificate = ???, # TODO ? + + $password = '', # one time password used for host provisioning! + $random = false, # or set this to true to have us generate it... + + # comment parameters... + $locality = '', # host locality (e.g. "Montreal, Canada") + $location = '', # host location (e.g. "Lab 42") + $platform = '', # host hardware platform (e.g. "Lenovo X201") + $osstring = '', # host operating system and version (e.g. "CentOS 6.4") + $comments = '', # host description (e.g. "NFS server") + + #$hosts = [], # TODO: add hosts managed by support + + # client specific parameters... + $admin = false, # should client get admin tools installed ? + + # special parameters... + $watch = true, # manage all changes to this resource, reverting others + $modify = true # modify this resource on puppet changes or not ? +) { + include ipa::server + include ipa::server::host::base + include ipa::vardir + #$vardir = $::ipa::vardir::module_vardir # with trailing slash + $vardir = regsubst($::ipa::vardir::module_vardir, '\/$', '') + + $dns = $ipa::server::dns # boolean from main obj + + $valid_domain = downcase($domain) + + $valid_server = "${server}" ? { + '' => "${::hostname}.${::domain}", + default => "${server}", + } + + # NOTE: the valid_fqdn is actually what ipa calls a hostname internally + # if $name has dots, then we assume it's a fqdn, if not, we add $domain + $valid_fqdn = delete("${name}", '.') ? { + "${name}" => "${name}.${valid_domain}", # had no dots present + default => "${name}", # had dots present... + } + + $valid_sshpubkeys = type($sshpubkeys) ? { + 'string' => "${sshpubkeys}" ? { + # BUG: lol: https://projects.puppetlabs.com/issues/15813 + '' => [], # assume managed but empty (rm sshkeys) + default => ["${sshpubkeys}"], + }, + 'boolean' => $sshpubkeys, + 'array' => $sshpubkeys, + default => '', # set an error... + } + if "${valid_sshpubkeys}" == '' { + fail('You must specify a valid type for $sshpubkeys.') + } + + if $watch and (! $modify) { + fail('You must be able to $modify to be able to $watch.') + } + + # NOTE: this is not a good fit for host-* it is part of the dns system, + # and not the host, and should be managed separately + #if $dns { + # $args00 = "${ipaddress}" ? { + # '' => '', + # default => "--ip-address='${ipaddress}'", + # } + #} else { + # $args00 = '' + # # TODO: allow this silently for now... + # #warning("Host: '${valid_fqdn}' is setting an IP without DNS.") + #} + + $args01 = "${macaddress}" ? { + '' => '', + default => "--macaddress='${macaddress}'", + } + + # array means: managed, set these keys exactly, and remove when it's [] + # boolean false means: unmanaged, don't set or get anything... empty '' + # boolean true means: managed, get the keys automatically (super magic) + $args02 = type($valid_sshpubkeys) ? { + # we always have to at least specify the '--sshpubkey=' if this + # is empty, because otherwise we have no way to remove old keys + 'array' => inline_template('<% if valid_sshpubkeys == [] %>--sshpubkey=<% else %><%= valid_sshpubkeys.map {|x| "--sshpubkey=\'"+x+"\'" }.join(" ") %><% end %>'), + default => $valid_sshpubkeys ? { # boolean + false => '', # unmanaged, do nothing + # this large beast loops through all the collected dirs + # and cats the contents of each file into an individual + # --sshpubkey argument. if no keys are found, the empty + # --sshpubkey argument is returned. this is all used to + # build the ipa commands. i hope this doesn't overflow! + default => "`a=(); for i in ${vardir}/hosts/sshpubkeys/${name}/*.pub; do [ -e \"\$i\" ] || break; a+=(\"--sshpubkey='\$(/bin/cat \$i)'\"); done; if [ \"\${a[*]}\" == '' ]; then /bin/echo \"--sshpubkey=\"; else /bin/echo \${a[@]}; fi`", + }, + } + + $args03 = "${locality}" ? { + '' => '', + default => "--locality='${locality}'", + } + $args04 = "${location}" ? { + '' => '', + default => "--location='${location}'", + } + $args05 = "${platform}" ? { + '' => '', + default => "--platform='${platform}'", + } + $args06 = "${osstring}" ? { + '' => '', + default => "--os='${osstring}'", + } + $args07 = "${comments}" ? { + '' => '', + default => "--desc='${comments}'", + } + + $arglist = ["${args01}", "${args02}", "${args03}", "${args04}", "${args05}", "${args06}", "${args07}"] + $args = join(delete($arglist, ''), ' ') + + if $random and ("${password}" != '') { + fail('Specify $random or $password, but not both.') + } + $argspass = "${password}" ? { + '' => $random ? { + true => '--random', + default => '', # no password specified + }, + #default => "--password='${password}'", # direct mode, (bad)! + default => "--password=`/bin/cat '${vardir}/hosts/passwords/${valid_fqdn}.password'`", + } + + $qarglist = ["${argspass}"] # NOTE: add any silent arg changes here + $qargs = join(delete($qarglist, ''), ' ') + + # if we're not modifying, we need to add on the qargs stuff to the add! + $xarglist = $modify ? { + false => concat($arglist, $qarglist), + default => $arglist, + } + $xargs = join(delete($xarglist, ''), ' ') + + # NOTE: this file is the subscribe destination for the modify exec when + # not using watch mode. it is separate from the qhost file (which is + # used for unwatchable changes), because if we had only one notify + # source, then a configuration transition from watch to unwatched would + # actually trigger a modification. this file is also the official file + # that is used by the clean script for determining which hosts need to + # be erased. please keep in mind that on accidental notification, or on + # system rebuild, the differing changes will be erased. + file { "${vardir}/hosts/${valid_fqdn}.host": + content => "${valid_fqdn}\n${args}\n", + owner => root, + group => nobody, + mode => 600, # u=rw,go= + require => File["${vardir}/hosts/"], + ensure => present, + } + + file { "${vardir}/hosts/${valid_fqdn}.qhost": + content => "${valid_fqdn}\n${qargs}\n", + owner => root, + group => nobody, + mode => 600, # u=rw,go= + require => File["${vardir}/hosts/"], + ensure => present, + } + + # NOTE: a custom fact, reads from these dirs and collects the passwords + if $random { + file { "${vardir}/hosts/passwords/${valid_fqdn}.password": + # no content! this is a tag, content comes in by echo ! + owner => root, + group => nobody, + mode => 600, # u=rw,go= + backup => false, + notify => $modify ? { + false => undef, # can't notify if not modifying + default => Exec["ipa-server-host-qmod-${name}"], + }, + require => File["${vardir}/hosts/passwords/"], + ensure => present, + } + } elsif "${password}" != '' { + file { "${vardir}/hosts/passwords/${valid_fqdn}.password": + content => "${password}\n", # top secret (briefly!) + owner => root, + group => nobody, + mode => 600, # u=rw,go= + backup => false, + notify => $modify ? { + false => undef, # can't notify if not modifying + default => Exec["ipa-server-host-qmod-${name}"], + }, + before => $modify ? { + false => undef, + default => Exec["ipa-server-host-qmod-${name}"], + }, + require => File["${vardir}/hosts/passwords/"], + ensure => present, + } + } + + file { "${vardir}/hosts/sshpubkeys/${name}/": # store host ssh keys + ensure => directory, # make sure this is a directory + recurse => true, # recursively manage directory + purge => true, # purge all unmanaged files + force => true, # also purge subdirs and links + owner => root, group => nobody, mode => 600, backup => false, + require => File["${vardir}/hosts/sshpubkeys/"], + } + + # collect host specific ssh keys + Ipa::Server::Host::Sshpubkeys <<| tag == "${name}" |>> { + #realname => "${name}", + #basedir => "${vardir}/hosts/sshpubkeys/${name}/", + } + + $exists = "/usr/bin/ipa host-show '${valid_fqdn}' > /dev/null 2>&1" + # NOTE: we don't need to set the password in the host-add, because the + # host-mod that deals specifically with password stuff will trigger it + # NOTE: --force is needed when dns is configured for ipa but we're not + # setting an ip address on host-add. this makes ipa sad, and it fails! + # NOTE: we don't seem to need --force for host-mod, as it hasn't erred + $force = "${xargs}" ? { # if args is empty + '' => '--force', # we have no args! + default => "${xargs} --force", # pixel perfect... + } + $fargs = $dns ? { # without the dns, + true => "${force}", # we don't need to + default => "${xargs}", # force everything + } + # NOTE: this runs when no host is present... + #exec { "/usr/bin/ipa host-add '${valid_fqdn}' ${fargs}": + exec { "ipa-server-host-add-${name}": # alias + # this has to be here because the command string gets too long + # for a puppet $name var and strange things start to happen... + command => "/usr/bin/ipa host-add '${valid_fqdn}' ${fargs}", + logoutput => on_failure, + unless => "${exists}", + require => $dns ? { + true => [ + Exec['ipa-server-kinit'], + File["${vardir}/hosts/sshpubkeys/${name}/"], + ], + default => [ + Exec['ipa-dns-check'], # avoid --force errors! + Exec['ipa-server-kinit'], + File["${vardir}/hosts/sshpubkeys/${name}/"], + ], + }, + #alias => "ipa-server-host-add-${name}", + } + + # NOTE: this runs when we detect that the attributes don't match (diff) + if $modify and ("${args}" != '') { # if there are changes to do... + #exec { "/usr/bin/ipa host-mod '${valid_fqdn}' ${args}": + exec { "ipa-server-host-mod-${name}": + command => "/usr/bin/ipa host-mod '${valid_fqdn}' ${args}", + logoutput => on_failure, + refreshonly => $watch ? { + false => true, # when not watching, we + default => undef, # refreshonly to change + }, + subscribe => $watch ? { + false => File["${vardir}/hosts/${valid_fqdn}.host"], + default => undef, + }, + onlyif => "${exists}", + unless => $watch ? { + false => undef, # don't run the diff checker... + default => "${exists} && ${vardir}/diff.py host '${valid_fqdn}' ${args}", + }, + before => "${qargs}" ? { # only if exec exists ! + '' => undef, + default => Exec["ipa-server-host-qmod-${name}"], + }, + require => [ + File["${vardir}/diff.py"], + Exec['ipa-server-kinit'], + Exec["ipa-server-host-add-${name}"], + File["${vardir}/hosts/sshpubkeys/${name}/"], + ], + #alias => "ipa-server-host-mod-${name}", + } + } + + # NOTE: this runs when there should be an attribute change we can't see + if $modify and ("${qargs}" != '') { # quiet q changes to do + + # this is a bonus to double check that a password entry exists! + # once a host is provisioned, it will reset the single use pass + # and this script would normally try and create a new one back, + # however if a pwtag is collected, then it won't run the notify + # this is pretty advanced stuff to understand, but it's useful! + if $random or ("${password}" != '') { + + # collect any password tags. note i used $name exactly! + Ipa::Server::Host::Pwtag <<| tag == "${name}" |>> { + } + exec { "ipa-host-verify-password-exists-${name}": # uid + command => '/bin/true', # i'm just here for the notify! + # do not run this if the password tag exists... + # if it dissapears, that means the host is gone + unless => "/usr/bin/test -e '${vardir}/hosts/passwords/${name}.pwtag'", + # only do this if machine is unenrolled, eg see + # https://git.fedorahosted.org/cgit/freeipa.git + # /tree/ipalib/plugins/host.py#n642 (approx...) + # NOTE: this uses a single equals sign for test + onlyif => [ + "/usr/bin/test \"`/usr/bin/ipa host-show '${valid_fqdn}' --raw | /usr/bin/tr -d ' ' | /bin/grep '^has_password:' | /bin/cut -b 14-`\" = 'False'", + "/usr/bin/test \"`/usr/bin/ipa host-show '${valid_fqdn}' --raw | /usr/bin/tr -d ' ' | /bin/grep '^has_keytab:' | /bin/cut -b 12-`\" = 'False'", + ], + logoutput => on_failure, + notify => Exec["ipa-server-host-qmod-${name}"], + # TODO: notify: Exec['again'] so that the facts + # get refreshed right away, and the password is + # exported without delay! now go and hack away! + before => Exec["ipa-server-host-qmod-${name}"], + require => [ + Exec['ipa-server-kinit'], + Exec["ipa-server-host-add-${name}"], + # this file require ensures that if the + # pwtag disappears (by that dir purge), + # that right away the new pass is made! + File["${vardir}/hosts/passwords/"], + ], + } + } + + # NOTE: if this runs before a pwtag can prevent it, on a random + # password it will succeed without error and wipe the password: + # invalid 'password': Password cannot be set on enrolled host. + # this isn't a big deal, it just has the side effect of erasing + # the stored temporary password from locally where it's unused. + # if this runs before a pwtag can prevent it, on a static pass, + # this will cause a transient error until the pwtag gets saved. + # to avoid both of these scenarios, the above exec runs a check + # to see if the host is unenrolled before running the notify :) + $qextra = $random ? { # save the generated password to a file + true => " --raw | /usr/bin/tr -d ' ' | /bin/grep '^randompassword:' | /bin/cut -b 16- > ${vardir}/hosts/passwords/${valid_fqdn}.password", + default => '', + } + exec { "/usr/bin/ipa host-mod '${valid_fqdn}' ${qargs}${qextra}": + logoutput => on_failure, + refreshonly => true, # needed because we can't "see" + subscribe => File["${vardir}/hosts/${valid_fqdn}.qhost"], + onlyif => "${exists}", + require => [ + Exec['ipa-server-kinit'], + Exec["ipa-server-host-add-${name}"], + ], + alias => "ipa-server-host-qmod-${name}", + } + } + + # use this password in an exported resource to deploy the ipa client... + $passfact = regsubst("ipa_host_${valid_fqdn}_password", '\.', '_', 'G') + $pass = getvar("${passfact}") + # NOTE: 'include ipa::client::host::deploy' to deploy the ipa client... + @@ipa::client::host { "${name}": # this is usually the fqdn + # NOTE: this should set all the client args it can safely assume + domain => $valid_domain, + realm => $realm, + server => "${valid_server}", + password => "${pass}", + admin => $admin, + #ssh => $ssh, + #sshd => $sshd, + #ntp => $ntp, + #ntp_server => $ntp_server, + #shorewall => $shorewall, + #zone => $zone, + #allow => $allow, + #ensure => $ensure, + tag => "${name}", # bonus + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server/host/base.pp b/ipa/manifests/server/host/base.pp new file mode 100644 index 000000000..5c8706719 --- /dev/null +++ b/ipa/manifests/server/host/base.pp @@ -0,0 +1,113 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class ipa::server::host::base { + include ipa::server + include ipa::vardir + #$vardir = $::ipa::vardir::module_vardir # with trailing slash + $vardir = regsubst($::ipa::vardir::module_vardir, '\/$', '') + + # we don't want to purge the freeipa entry, so we need to exclude it... + $valid_hostname = $ipa::server::valid_hostname + $valid_domain = $ipa::server::valid_domain + $host_always_ignore = ["${valid_hostname}.${valid_domain}"] + $host_excludes = $ipa::server::host_excludes + $valid_host_excludes = type($host_excludes) ? { + 'string' => [$host_excludes], + 'array' => $host_excludes, + 'boolean' => $host_excludes ? { + # TODO: there's probably a better fqdn match expression + # this is an expression to prevent all fqdn deletion... + #true => ['^[a-zA-Z0-9\.\-]*$'], + true => ['^[[:alpha:]]{1}[[:alnum:]-.]*$'], + default => false, + }, + default => false, # trigger error... + } + + if type($valid_host_excludes) != 'array' { + fail('The $host_excludes must be an array.') + } + + # directory of system tags which should exist (as managed by puppet) + file { "${vardir}/hosts/": + ensure => directory, # make sure this is a directory + recurse => true, # recursively manage directory + purge => true, # purge all unmanaged files + force => true, # also purge subdirs and links + owner => root, group => nobody, mode => 600, backup => false, + notify => Exec['ipa-clean-hosts'], + require => File["${vardir}/"], + } + + # these are template variables for the clean.sh.erb script + $id_dir = 'hosts' + $ls_cmd = '/usr/bin/ipa host-find --pkey-only --raw | /usr/bin/tr -d " " | /bin/grep "^fqdn:" | /bin/cut -b 6-' # show ipa hosts + # TODO: i don't understand all the implications of the --updatedns arg! + # we should probably change the dns arg based on if dns is on or not... + $rm_cmd = $dns ? { # delete ipa hosts + true => '/usr/bin/ipa host-del --updatedns ', + default => '/usr/bin/ipa host-del ', + } + $fs_chr = ' ' + $suffix = '.host' + $regexp = $valid_host_excludes + $ignore = $host_always_ignore + + # build the clean script + file { "${vardir}/clean-hosts.sh": + content => template('ipa/clean.sh.erb'), + owner => root, + group => nobody, + mode => 700, # u=rwx + backup => false, # don't backup to filebucket + ensure => present, + require => File["${vardir}/"], + } + + # run the cleanup + exec { "${vardir}/clean-hosts.sh": + logoutput => on_failure, + refreshonly => true, + require => [ + Exec['ipa-server-kinit'], + File["${vardir}/clean-hosts.sh"], + ], + alias => 'ipa-clean-hosts', + } + + # NOTE: it doesn't cause a problem that this dir is inside the hosts dir + file { "${vardir}/hosts/passwords/": + ensure => directory, # make sure this is a directory + recurse => true, # recursively manage directory + purge => true, # purge all unmanaged files + force => true, # also purge subdirs and links + owner => root, group => nobody, mode => 600, backup => false, + require => File["${vardir}/hosts/"], + } + + file { "${vardir}/hosts/sshpubkeys/": + ensure => directory, # make sure this is a directory + recurse => true, # recursively manage directory + purge => true, # purge all unmanaged files + force => true, # also purge subdirs and links + owner => root, group => nobody, mode => 600, backup => false, + require => File["${vardir}/hosts/"], + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server/host/pwtag.pp b/ipa/manifests/server/host/pwtag.pp new file mode 100644 index 000000000..a8c5e747f --- /dev/null +++ b/ipa/manifests/server/host/pwtag.pp @@ -0,0 +1,42 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: this should only be used by a freeipa client and as an exported resource +define ipa::server::host::pwtag() { + include ipa::vardir + #$vardir = $::ipa::vardir::module_vardir # with trailing slash + $vardir = regsubst($::ipa::vardir::module_vardir, '\/$', '') + + # the existence of this file means that an ipa client has exported it, + # and that the ipa server collected it and it means that a provisioned + # ipa client host is notifying the server that a new one time password + # does not need to be generated at this time. to reprovision the host, + # you must erase the exported resource that is sending this file here, + # or rather, in doing so, the ipa server will generate a new password! + file { "${vardir}/hosts/passwords/${name}.pwtag": + content => "# This is a password tag for: ${name}\n", + owner => root, + group => nobody, + mode => 600, # u=rw,go= + backup => false, + before => Exec["ipa-host-verify-password-exists-${name}"], + require => File["${vardir}/hosts/passwords/"], + ensure => present, + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server/host/sshpubkeys.pp b/ipa/manifests/server/host/sshpubkeys.pp new file mode 100644 index 000000000..d34396905 --- /dev/null +++ b/ipa/manifests/server/host/sshpubkeys.pp @@ -0,0 +1,63 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# NOTE: this is called by ipa::client internally and shouldn't be used manually + +define ipa::server::host::sshpubkeys( # $name matches ipa::server::host $name + $rsa = '', + $dsa = '' +) { + include ipa::vardir + #$vardir = $::ipa::vardir::module_vardir # with trailing slash + $vardir = regsubst($::ipa::vardir::module_vardir, '\/$', '') + + # FIXME: if i really cared, i would just have one argument, an array of + # keys, and i would loop through them creating each file... tempting... + if "${rsa}" != '' { + file { "${vardir}/hosts/sshpubkeys/${name}/rsa.pub": + content => "${rsa}\n", + owner => root, + group => nobody, + mode => 600, # u=rw,go= + backup => false, + # this before is essential, and it implies that it will also go + # before the "ipa-server-host-mod-${name}" exec, because of the + # relationship between those two types. mod might not always be + # present (if $modify is false) so don't directly reference it. + before => Exec["ipa-server-host-add-${name}"], + require => File["${vardir}/hosts/sshpubkeys/${name}/"], + ensure => present, + } + } + if "${dsa}" != '' { + file { "${vardir}/hosts/sshpubkeys/${name}/dsa.pub": + content => "${dsa}\n", + owner => root, + group => nobody, + mode => 600, # u=rw,go= + backup => false, + # this before is essential, and it implies that it will also go + # before the "ipa-server-host-mod-${name}" exec, because of the + # relationship between those two types. mod might not always be + # present (if $modify is false) so don't directly reference it. + before => Exec["ipa-server-host-add-${name}"], + require => File["${vardir}/hosts/sshpubkeys/${name}/"], + ensure => present, + } + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server/kinit.pp b/ipa/manifests/server/kinit.pp new file mode 100644 index 000000000..acd997a1a --- /dev/null +++ b/ipa/manifests/server/kinit.pp @@ -0,0 +1,57 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# FIXME: can we rename this to ipa::kinit and merge with the ipa client kinit ? + +class ipa::server::kinit( + $realm +) { + + include ipa::common + + $valid_realm = "${realm}" + + # since we're on the kdc, we can use our root access to get a ticket... + # < me> kaduk_: [...] is this an evil hack? [...] + # < kaduk_> [...] It's not really a hack, but things running on the KDC + # are always a bit special. + #exec { "/bin/cat '${vardir}/admin.password' | /usr/bin/kinit admin": + # NOTE: i added a lifetime of 1 hour... no sense needing any longer + $rr = "krbtgt/${valid_realm}@${valid_realm}" + $tl = '900' # 60*15 => 15 minutes + exec { "/usr/bin/kinit -k -t KDB: admin -l 1h": # thanks to: kaduk_ + logoutput => on_failure, + #unless => "/usr/bin/klist -s", # is there a credential cache + # NOTE: we need to check if the ticket has at least a certain + # amount of time left. if not, it could expire mid execution! + # this should definitely get patched, but in the meantime, we + # check that the current time is greater than the valid start + # time (in seconds) and that we have within $tl seconds left! + unless => "/usr/bin/klist -s && /usr/bin/test \$(( `/bin/date +%s` - `/usr/bin/klist | /bin/grep -F '${rr}' | /bin/awk '{print \$1\" \"\$2}' | /bin/date --file=- +%s` )) -gt 0 && /usr/bin/test \$(( `/usr/bin/klist | /bin/grep -F '${rr}' | /bin/awk '{print \$3\" \"\$4}' | /bin/date --file=- +%s` - `/bin/date +%s` )) -gt ${tl}", + onlyif => "${::ipa::common::ipa_installed}", + # NOTE: we need the 'require' to actually be a 'before', coming + # from the ipa-install exec since that may not exist right away + # if it's a replica that pulls in the exported resource exec... + require => [ + Exec['ipa-install'], + #File["${vardir}/admin.password"], + ], + alias => 'ipa-server-kinit', + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server/replica/base.pp b/ipa/manifests/server/replica/base.pp new file mode 100644 index 000000000..49df9a52d --- /dev/null +++ b/ipa/manifests/server/replica/base.pp @@ -0,0 +1,34 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class ipa::server::replica::base { + + include ipa::vardir + #$vardir = $::ipa::vardir::module_vardir # with trailing slash + $vardir = regsubst($::ipa::vardir::module_vardir, '\/$', '') + + file { "${vardir}/replica/": + ensure => directory, # make sure this is a directory + recurse => true, # recursively manage directory + purge => true, # purge all unmanaged files + force => true, # also purge subdirs and links + owner => root, group => nobody, mode => 600, backup => false, + require => File["${vardir}/"], + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server/replica/firewall.pp b/ipa/manifests/server/replica/firewall.pp new file mode 100644 index 000000000..1fd375430 --- /dev/null +++ b/ipa/manifests/server/replica/firewall.pp @@ -0,0 +1,186 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: all replication agreements are bi-directional for now due to FreeIPA... +# NOTE: in the future, it would be quite cool to allow uni-directional replicas +# NOTE: this type has been engineered to fit easily with the topology datatype: +# $ring = { # example flat topology as expressed in the std. format +# 'fqdn1': ['fqdn2', 'fqdn3'], +# 'fqdn2': ['fqdn3', 'fqdn1'], +# 'fqdn3': ['fqdn1', 'fqdn2'], +# } +# +# ipa::server::replica::firewall { $ring["${::fqdn}"]: # all automatic +# peer => "${::fqdn}", +# } +define ipa::server::replica::firewall( # to + $peer = '', # from (usually we run this on itself!) + $ip = '' # you can specify which ip address to use (if multiple) +) { + + include ipa::server::replica::firewall::base + + # NOTE: the peer vs. valid_peer names are by convention (but confusing) + $self = "${peer}" # from (a) + if "${self}" != "${::fqdn}" { + fail('Are you sure you want to run this on a different host ?') + } + $valid_peer = "${name}" # to (b) + + $zone = $::ipa::server::zone # firewall zone + $valid_ip = "${ip}" ? { + '' => "${::ipa_host_ip}" ? { # smart fact... + '' => "${::ipaddress}", # puppet picks! + default => "${::ipa_host_ip}", # smart + }, + default => "${ip}", # user selected + } + if "${valid_ip}" == '' { + fail('No valid IP exists!') + } + + # NOTE: an exported resource here says: "i would like to connect to you" + # this means the collector's (receiver) perspective source ip is *my* ip + + # NOTE: we need to add the $fqdn so that exported resources + # don't conflict... I'm not sure they should anyways though + + # Directory Service: Unsecure port (389) + @@ipa::rulewrapper { "ipa-server-replica-ldap-${name}-${::fqdn}": + action => 'LDAP/ACCEPT', + source => "${zone}", # override this on collect... + source_ips => ["${valid_ip}"], # i am the source ! + dest => '$FW', + #proto => 'tcp', + #port => '', # comma separated string or list + comment => "Allow incoming tcp:389 from ${::fqdn}.", + tag => 'ipa-server-replica', + match => "${name}", # used for collection + ensure => present, + } + + # Directory Service: Secure port (636) + @@ipa::rulewrapper { "ipa-server-replica-ldaps-${name}-${::fqdn}": + action => 'LDAPS/ACCEPT', + source => "${zone}", + source_ips => ["${valid_ip}"], + dest => '$FW', + comment => "Allow incoming tcp:636 from ${::fqdn}.", + tag => 'ipa-server-replica', + match => "${name}", + ensure => present, + } + + # TODO: this should work in a future version of shorewall... + # Kerberos KDC: TCP (88) / Kerberos KDC: UDP (88) + #@@ipa::rulewrapper { "ipa-server-replica-kerberos-${name}-${::fqdn}": + # action => 'Kerberos/ACCEPT', + # source => "${zone}", + # source_ips => ["${valid_ip}"], + # dest => '$FW', + # comment => "Allow incoming tcp/udp:88 from ${::fqdn}.", + # tag => 'ipa-server-replica', + # match => "${name}", + # ensure => present, + #} + + # TODO: until the Kerberos macro exists in shorewall, we do it manually + # Kerberos KDC: TCP (88) + @@ipa::rulewrapper { "ipa-server-replica-kerberos-tcp-${name}-${::fqdn}": + action => 'ACCEPT', + source => "${zone}", + source_ips => ["${valid_ip}"], + dest => '$FW', + proto => 'tcp', + port => ['88'], + comment => "Allow incoming tcp:88 from ${::fqdn}.", + tag => 'ipa-server-replica', + match => "${name}", + ensure => present, + } + + # Kerberos KDC: UDP (88) + @@ipa::rulewrapper { "ipa-server-replica-kerberos-udp-${name}-${::fqdn}": + action => 'ACCEPT', + source => "${zone}", + source_ips => ["${valid_ip}"], + dest => '$FW', + proto => 'udp', + port => ['88'], + comment => "Allow incoming udp:88 from ${::fqdn}.", + tag => 'ipa-server-replica', + match => "${name}", + ensure => present, + } + + # TODO: create a kpasswd macro, or use the 'macro.ActiveDir' one... + # Kerberos Kpasswd: TCP (464) + @@ipa::rulewrapper { "ipa-server-replica-kpasswd-tcp-${name}-${::fqdn}": + action => 'ACCEPT', + source => "${zone}", + source_ips => ["${valid_ip}"], + dest => '$FW', + proto => 'tcp', + port => ['464'], + comment => "Allow incoming tcp:464 from ${::fqdn}.", + tag => 'ipa-server-replica', + match => "${name}", + ensure => present, + } + + # Kerberos Kpasswd: UDP (464) + @@ipa::rulewrapper { "ipa-server-replica-kpasswd-udp-${name}-${::fqdn}": + action => 'ACCEPT', + source => "${zone}", + source_ips => ["${valid_ip}"], + dest => '$FW', + proto => 'udp', + port => ['464'], + comment => "Allow incoming udp:464 from ${::fqdn}.", + tag => 'ipa-server-replica', + match => "${name}", + ensure => present, + } + + # HTTP Server: Unsecure port (80) + @@ipa::rulewrapper { "ipa-server-replica-http-${name}-${::fqdn}": + action => 'HTTP/ACCEPT', + source => "${zone}", + source_ips => ["${valid_ip}"], + dest => '$FW', + comment => "Allow incoming tcp:80 from ${::fqdn}.", + tag => 'ipa-server-replica', + match => "${name}", + ensure => present, + } + + # HTTP Server: Secure port (443) + @@ipa::rulewrapper { "ipa-server-replica-https-${name}-${::fqdn}": + action => 'HTTPS/ACCEPT', + source => "${zone}", + source_ips => ["${valid_ip}"], + dest => '$FW', + comment => "Allow incoming tcp:443 from ${::fqdn}.", + tag => 'ipa-server-replica', + match => "${name}", + ensure => present, + } + + # FIXME: are all the necessary ports for ipa replication include here ? +} + +# vim: ts=8 diff --git a/ipa/manifests/server/replica/firewall/base.pp b/ipa/manifests/server/replica/firewall/base.pp new file mode 100644 index 000000000..4f95c5589 --- /dev/null +++ b/ipa/manifests/server/replica/firewall/base.pp @@ -0,0 +1,42 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class ipa::server::replica::firewall::base { + include ipa::server + + $zone = $::ipa::server::zone # firewall zone + $shorewall = $::ipa::server::shorewall # enable fw...? + + # open the firewall so that replicas can connect to what they will need + Ipa::Rulewrapper <<| tag == 'ipa-server-replica' and match == "${::fqdn}" |>> { + #Shorewall::Rule <<| tag == 'ipa-server-replica' and match == "${::fqdn}" |>> { + source => "${zone}", # use our source zone + # TODO: this below before is basically untested for usefulness! + before => Exec['ipa-install'], # open bi-directional fw first! + # TODO: the below require is basically untested for usefulness! + require => Exec['ipa-clean-peers'], # let the peers clean up first! + ensure => $shorewall ? { + absent => absent, + 'absent' => absent, + present => present, + 'present' => present, + default => present, + }, + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server/replica/install.pp b/ipa/manifests/server/replica/install.pp new file mode 100644 index 000000000..f8e786b9f --- /dev/null +++ b/ipa/manifests/server/replica/install.pp @@ -0,0 +1,107 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: this has to be a singleton (eg: class) because we can only install one! +# NOTE: topology connections and peering information can be non-singleton types TODO +class ipa::server::replica::install( + $peers = {} +) { + + include ipa::server::replica::install::base + include ipa::vardir + #$vardir = $::ipa::vardir::module_vardir # with trailing slash + $vardir = regsubst($::ipa::vardir::module_vardir, '\/$', '') + + # process possible replica masters that are available... + $replica_fqdns_fact = "${::ipa_replica_prepared_fqdns}" # fact! + $replica_fqdns = split($replica_fqdns_fact, ',') # list! + + # peering is always bidirectional for now :) + # $peers is a hash of fqdn1 => fqdn2 pairs... + + #if has_key($peers, "${::fqdn}") and member($replica_fqdns, $peers["${::fqdn}"]) { + # $valid_fqdn = $peers["${::fqdn}"] + if has_key($peers, "${::fqdn}") { + $intersection = intersection($replica_fqdns, $peers["${::fqdn}"]) + # NOTE use empty() because 'if []' returns true! + if empty($intersection) { + $valid_fqdn = '' + } else { + # pick the first in the list if there is more than one! + $valid_fqdn = pick($intersection, '') # first + } + } else { + $valid_fqdn = '' + } + + if "${ipa_server_installed}" != 'true' { + if "${valid_fqdn}" == '' { + warning("The requested peer: '${valid_fqdn}', isn't ready yet.") + } else { + info("The requested peer is: '${valid_fqdn}'.") + } + } + + $filename = "replica-info-${valid_fqdn}.gpg" + $filefrom = "replica-info-${::fqdn}.gpg" # name it with our fqdn + $valid_file = "${vardir}/replica/install/${filename}" + $valid_from = "${vardir}/replica/prepare/${filefrom}" + + # send to all prepared hosts, so the keys don't flip flop if vip moves! + ssh::send { $replica_fqdns: # fqdn of where i got this from... + + } + + # TODO: tag can be used as grouping + # NOTE: this could pull down multiple files... + # NOTE: this also matches against the file parameter from the exporting + # side. we do this so that we only pull in what is intended for us, and + # as a result, this avoids real duplicate resource conflicts. but NOTE: + # this currently depends on all hosts sharing the same value of $vardir + Ssh::File::Pull <<| tag == 'ipa-replica-prepare' and file == "${valid_from}" |>> { + path => "${vardir}/replica/install/", + verify => false, # rely on mtime + pair => false, # do it now so it happens fast! + # tag this file so it doesn't get purged + ensure => present, + owner => root, + group => nobody, + mode => 600, # u=rw + backup => false, # don't backup to filebucket + before => Exec['ipa-install'], + require => File["${vardir}/replica/install/"], + } + + # this exec is purposefully very similar to the ipa-server-install exec + # NOTE: the --admin-password is only useful for the connection check... + exec { "/usr/sbin/ipa-replica-install --password=`/bin/cat '${vardir}/dm.password'` --admin-password=`/bin/cat '${vardir}/admin.password'` --unattended ${valid_file}": + logoutput => on_failure, + onlyif => [ + "/usr/bin/test '${valid_fqdn}' != ''", # bonus safety! + "/usr/bin/test -s ${valid_file}", + ], + unless => "${::ipa::common::ipa_installed}", # can't install if installed... + timeout => 3600, # hope it doesn't take more than 1 hour + require => [ + File["${vardir}/"], + Package['ipa-server'], + ], + alias => 'ipa-install', # same alias as server to prevent both! + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server/replica/install/base.pp b/ipa/manifests/server/replica/install/base.pp new file mode 100644 index 000000000..32d1f8060 --- /dev/null +++ b/ipa/manifests/server/replica/install/base.pp @@ -0,0 +1,35 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class ipa::server::replica::install::base { + + include ipa::server::replica::base + include ipa::vardir + #$vardir = $::ipa::vardir::module_vardir # with trailing slash + $vardir = regsubst($::ipa::vardir::module_vardir, '\/$', '') + + file { "${vardir}/replica/install/": + ensure => directory, # make sure this is a directory + recurse => true, # recursively manage directory + purge => true, # purge all unmanaged files + force => true, # also purge subdirs and links + owner => root, group => nobody, mode => 600, backup => false, + require => File["${vardir}/replica/"], + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server/replica/manage.pp b/ipa/manifests/server/replica/manage.pp new file mode 100644 index 000000000..fd7af9757 --- /dev/null +++ b/ipa/manifests/server/replica/manage.pp @@ -0,0 +1,89 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: all replication agreements are bi-directional for now due to FreeIPA... +# NOTE: in the future, it would be quite cool to allow uni-directional replicas +# NOTE: this type has been engineered to fit easily with the topology datatype: +# $ring = { # example flat topology as expressed in the std. format +# 'fqdn1': ['fqdn2', 'fqdn3'], +# 'fqdn2': ['fqdn3', 'fqdn1'], +# 'fqdn3': ['fqdn1', 'fqdn2'], +# } +# +# ipa::server::replica::manage { $ring["${::fqdn}"]: # all automatic +# peer => "${::fqdn}", +# } +define ipa::server::replica::manage( # to + $peer = '' # from +) { + # TODO: this type could grow fancy name parsing to specify: to and from + + include ipa::server::replica::manage::base + include ipa::common + include ipa::vardir + #$vardir = $::ipa::vardir::module_vardir # with trailing slash + $vardir = regsubst($::ipa::vardir::module_vardir, '\/$', '') + + # NOTE: the peer vs. valid_peer names are by convention (but confusing) + $args = "${peer}" # from (a) + $valid_peer = "${name}" # to (b) + + # switch bad characters for file name friendly characters (unused atm!) + # this could be useful if we allow peers's with $ and others in them... + $valid_peer_file = regsubst("${valid_peer}", '\$', '-', 'G') + file { "${vardir}/replica/manage/peers/${valid_peer_file}.peer": + content => "${valid_peer}\n${args}\n", + owner => root, + group => nobody, + mode => 600, # u=rw,go= + require => File["${vardir}/replica/manage/peers/"], + ensure => present, + } + + # NOTE: this shouldn't depend on the VIP because it runs on each host... + exec { "/usr/sbin/ipa-replica-manage connect '${peer}' '${valid_peer}'": + logoutput => on_failure, + onlyif => [ + "${::ipa::common::ipa_installed}", # i am ready + # this check is used to see if my peer is "ready" to + # accept any ipa-replica-manage connect commands. if + # it is, then it must mean that ipa is installed and + # running, even though this check tool isn't exactly + # designed for this particular type of check case... + # NOTE: this avoids unnecessary 'ipa-replica-manage' + # calls which would error in 3.0.0 with the message: + # You cannot connect to a previously deleted master. + # INFO: https://fedorahosted.org/freeipa/ticket/3105 + "/usr/sbin/ipa-replica-conncheck -R '${valid_peer}'", + ], + unless => "/usr/sbin/ipa-replica-manage list '${peer}' | /bin/awk -F ':' '{print \$1}' | /bin/grep -qxF '${valid_peer}'", + timeout => 900, # hope it doesn't take more than 15 min + before => Exec['ipa-clean-peers'], # try to connect first! + require => [ + Exec['ipa-install'], # show for readability! + Exec['ipa-server-kinit'], # needs auth to work... + ], + # NOTE: these two aliases can be used to prevent reverse dupes! + # NOTE: remove these if FreeIPA ever supports unidirectionality + alias => [ + "${peer} -> ${valid_peer}", + "${valid_peer} -> ${peer}", + ], + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server/replica/manage/base.pp b/ipa/manifests/server/replica/manage/base.pp new file mode 100644 index 000000000..235718e1e --- /dev/null +++ b/ipa/manifests/server/replica/manage/base.pp @@ -0,0 +1,97 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class ipa::server::replica::manage::base { + include ipa::server + include ipa::vardir + #$vardir = $::ipa::vardir::module_vardir # with trailing slash + $vardir = regsubst($::ipa::vardir::module_vardir, '\/$', '') + + # TODO: do we need this extra nesting here, or should we use it below ? + file { "${vardir}/replica/manage/": + ensure => directory, # make sure this is a directory + recurse => true, # recursively manage directory + purge => true, # purge all unmanaged files + force => true, # also purge subdirs and links + owner => root, group => nobody, mode => 600, backup => false, + require => File["${vardir}/replica/"], + } + + # since we don't want to purge them, we need to exclude them... + $peer_always_ignore = ["${::fqdn}"] # never try and purge yourself! + $peer_excludes = $ipa::server::peer_excludes + $valid_peer_excludes = type($peer_excludes) ? { + 'string' => [$peer_excludes], + 'array' => $peer_excludes, + 'boolean' => $peer_excludes ? { + # TODO: there's probably a better peer match expression + # this is an expression to prevent all peer deletion... + #true => ['^[a-zA-Z0-9]*$'], + true => ['^[[:alpha:]]{1}[[:alnum:]]*$'], + default => false, + }, + default => false, # trigger error... + } + + if type($valid_peer_excludes) != 'array' { + fail('The $peer_excludes must be an array.') + } + + # directory of system tags which should exist (as managed by puppet) + file { "${vardir}/replica/manage/peers/": + ensure => directory, # make sure this is a directory + recurse => true, # recursively manage directory + purge => true, # purge all unmanaged files + force => true, # also purge subdirs and links + owner => root, group => nobody, mode => 600, backup => false, + notify => Exec['ipa-clean-peers'], + require => File["${vardir}/replica/manage/"], + } + + # these are template variables for the clean.sh.erb script + $id_dir = 'replica/manage/peers' + $ls_cmd = "/usr/sbin/ipa-replica-manage list '${::fqdn}'" # show ipa peers + $rm_cmd = "/usr/sbin/ipa-replica-manage disconnect '${::fqdn}' " # disconnect ipa peers + $fs_chr = ':' # remove the ':replica' suffix + $suffix = '.peer' + $regexp = $valid_peer_excludes + $ignore = $peer_always_ignore + + # build the clean script + file { "${vardir}/clean-peers.sh": + content => template('ipa/clean.sh.erb'), + owner => root, + group => nobody, + mode => 700, # u=rwx + backup => false, # don't backup to filebucket + ensure => present, + require => File["${vardir}/"], + } + + # run the cleanup + exec { "${vardir}/clean-peers.sh": + logoutput => on_failure, + refreshonly => true, + require => [ + Exec['ipa-server-kinit'], + File["${vardir}/clean-peers.sh"], + ], + alias => 'ipa-clean-peers', + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server/replica/master.pp b/ipa/manifests/server/replica/master.pp new file mode 100644 index 000000000..34c32a84e --- /dev/null +++ b/ipa/manifests/server/replica/master.pp @@ -0,0 +1,43 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class ipa::server::replica::master( +) { + + include ipa::server::replica::master::base + include ipa::vardir + #$vardir = $::ipa::vardir::module_vardir # with trailing slash + $vardir = regsubst($::ipa::vardir::module_vardir, '\/$', '') + + # fact from data in: ${vardir}/ipa_server_replica_master + $valid_master = "${::ipa_server_replica_master}" + + @@file { "${vardir}/replica/master/master_${::fqdn}": + content => "${valid_master}\n", + tag => 'ipa-server-replica-master', + owner => root, + group => nobody, + mode => 600, + ensure => present, + } + + # collect to make facts + File <<| tag == 'ipa-server-replica-master' |>> { + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server/replica/master/base.pp b/ipa/manifests/server/replica/master/base.pp new file mode 100644 index 000000000..94c39882c --- /dev/null +++ b/ipa/manifests/server/replica/master/base.pp @@ -0,0 +1,35 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class ipa::server::replica::master::base { + + include ipa::server::replica::base + include ipa::vardir + #$vardir = $::ipa::vardir::module_vardir # with trailing slash + $vardir = regsubst($::ipa::vardir::module_vardir, '\/$', '') + + file { "${vardir}/replica/master/": + ensure => directory, # make sure this is a directory + recurse => true, # recursively manage directory + purge => true, # purge all unmanaged files + force => true, # also purge subdirs and links + owner => root, group => nobody, mode => 600, backup => false, + require => File["${vardir}/replica/"], + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server/replica/peering.pp b/ipa/manifests/server/replica/peering.pp new file mode 100644 index 000000000..fbe9ff3dd --- /dev/null +++ b/ipa/manifests/server/replica/peering.pp @@ -0,0 +1,69 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class ipa::server::replica::peering( + # NOTE: these are *time* based uuid's, eg as generated with: uuidgen -t + $uuid = '', # if empty, puppet will attempt to use the uuidgen fact +) { + + include ipa::server::replica::peering::base + include ipa::vardir + #$vardir = $::ipa::vardir::module_vardir # with trailing slash + $vardir = regsubst($::ipa::vardir::module_vardir, '\/$', '') + + if ("${uuid}" != '') and (! ("${uuid}" =~ /^[a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12}$/)) { + fail("The chosen UUID: '${uuid}' is not valid.") + } + + # if we manually *pick* a uuid, then store it too, so that it + # sticks if we ever go back to using automatic uuids. this is + # useful if a user wants to initially import uuids by picking + # them manually, and then letting puppet take over afterwards + file { "${vardir}/replica/peering/uuid": + # this file object needs to always exist to avoid us purging... + content => "${uuid}" ? { + '' => undef, + default => "${uuid}\n", + }, + owner => root, + group => nobody, + mode => 600, # might as well... + ensure => present, + require => File["${vardir}/replica/peering/"], + } + + $valid_uuid = "${uuid}" ? { + # fact from data generated in: ${vardir}/replica/peering/uuid + '' => "${::ipa_server_replica_uuid}", + default => "${uuid}", + } + + @@file { "${vardir}/replica/peering/peer_${::fqdn}": + content => "${valid_uuid}\n", + tag => 'ipa-server-replica-peering', + owner => root, + group => nobody, + mode => 600, + ensure => present, + } + + # collect to make facts + File <<| tag == 'ipa-server-replica-peering' |>> { + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server/replica/peering/base.pp b/ipa/manifests/server/replica/peering/base.pp new file mode 100644 index 000000000..d192faf0c --- /dev/null +++ b/ipa/manifests/server/replica/peering/base.pp @@ -0,0 +1,35 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class ipa::server::replica::peering::base { + + include ipa::server::replica::base + include ipa::vardir + #$vardir = $::ipa::vardir::module_vardir # with trailing slash + $vardir = regsubst($::ipa::vardir::module_vardir, '\/$', '') + + file { "${vardir}/replica/peering/": + ensure => directory, # make sure this is a directory + recurse => true, # recursively manage directory + purge => true, # purge all unmanaged files + force => true, # also purge subdirs and links + owner => root, group => nobody, mode => 600, backup => false, + require => File["${vardir}/replica/"], + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server/replica/prepare.pp b/ipa/manifests/server/replica/prepare.pp new file mode 100644 index 000000000..c86ecf710 --- /dev/null +++ b/ipa/manifests/server/replica/prepare.pp @@ -0,0 +1,74 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# $name is the fqdn of the server we are preparing for +define ipa::server::replica::prepare( +) { + + include ipa::server::replica::prepare::base + include ipa::common + include ipa::vardir + #$vardir = $::ipa::vardir::module_vardir # with trailing slash + $vardir = regsubst($::ipa::vardir::module_vardir, '\/$', '') + + $valid_fqdn = "${name}" # TODO: validate + + $filename = "replica-info-${valid_fqdn}.gpg" + $filedest = "replica-info-${::fqdn}.gpg" # name it with our fqdn + $prepared = "/var/lib/ipa/${filename}" + $valid_file = "${vardir}/replica/prepare/${filename}" + + # TODO: ipa-replica-prepare should allow you to pick output path/file + exec { "/usr/sbin/ipa-replica-prepare --password=`/bin/cat '${vardir}/dm.password'` ${valid_fqdn} && /bin/mv -f '${prepared}' '${valid_file}'": + logoutput => on_failure, + creates => "${valid_file}", + onlyif => "${::ipa::common::ipa_installed}", + # ipa-server-install or ipa-replica-install must execute first! + require => Exec['ipa-install'], # same alias for either install + alias => "ipa-replica-prepare-${name}", + } + + # tag this file so it doesn't get purged + file { "${valid_file}": + owner => root, + group => nobody, + mode => 600, # u=rw + backup => false, # don't backup to filebucket + ensure => present, + require => Exec["ipa-replica-prepare-${name}"], + } + + # add this manually so we don't have to wait for the exported resources + ssh::recv { "${valid_fqdn}": + + } + + # use a pull, so the remote path is decided over *there* + # export (@@) the pull, so that it knows the file is already here... + @@ssh::file::pull { "ipa-replica-prepare-${::fqdn}-${name}": + user => 'root', # src user + host => "${::fqdn}", # src host + file => "${valid_file}", # src file + path => "${vardir}/replica/install/", # dest path; overridden + dest => "${filedest}", # dest file + verify => false, # rely on mtime + pair => false, # do it now so it happens fast! + tag => 'ipa-replica-prepare', # TODO: can be used as grouping + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server/replica/prepare/base.pp b/ipa/manifests/server/replica/prepare/base.pp new file mode 100644 index 000000000..69503dac2 --- /dev/null +++ b/ipa/manifests/server/replica/prepare/base.pp @@ -0,0 +1,35 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class ipa::server::replica::prepare::base { + + include ipa::server::replica::base + include ipa::vardir + #$vardir = $::ipa::vardir::module_vardir # with trailing slash + $vardir = regsubst($::ipa::vardir::module_vardir, '\/$', '') + + file { "${vardir}/replica/prepare/": + ensure => directory, # make sure this is a directory + recurse => true, # recursively manage directory + purge => true, # purge all unmanaged files + force => true, # also purge subdirs and links + owner => root, group => nobody, mode => 600, backup => false, + require => File["${vardir}/replica/"], + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server/service.pp b/ipa/manifests/server/service.pp new file mode 100644 index 000000000..b12e545f8 --- /dev/null +++ b/ipa/manifests/server/service.pp @@ -0,0 +1,230 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +define ipa::server::service( + $service = '', # nfs, HTTP, ldap + $host = '', # should match $name of ipa::server::host + $domain = '', # must be the empty string by default + $realm = '', + $principal = '', # after all that, you can override principal... + $server = '', # where the client will find the ipa server... + + # args + $pactype = [], # bad values are silently discarded, [] is NONE + + #$hosts = [], # TODO: add hosts managed by support + + # special parameters... + $watch = true, # manage all changes to this resource, reverting others + $modify = true, # modify this resource on puppet changes or not ? + $comment = '', + $ensure = present # TODO +) { + include ipa::server + include ipa::server::service::base + include ipa::vardir + #$vardir = $::ipa::vardir::module_vardir # with trailing slash + $vardir = regsubst($::ipa::vardir::module_vardir, '\/$', '') + + $dns = $ipa::server::dns # boolean from main obj + + # TODO: a better regexp magician could probably do a better job :) + # nfs/nfs.example.com@EXAMPLE.COM + $r = '^([a-zA-Z][a-zA-Z0-9]*)(/([a-z0-9][a-z0-9\.\-]*)(@([A-Z][A-Z\.\-]*)){0,1}){0,1}$' + + $a = regsubst("${name}", $r, '\1') # service (nfs) + $b = regsubst("${name}", $r, '\3') # fqdn (nfs.example.com) + $c = regsubst("${name}", $r, '\5') # realm (EXAMPLE.COM) + + # service: first try to get value from arg, then fall back to $a (name) + $valid_service = "${service}" ? { + '' => "${a}", # get from $name regexp + default => "${service}", + } + if "${valid_service}" == '' { + # NOTE: if we see this message it might be a regexp pattern bug + fail('The $service must be specified.') + } + + # host: first try to get value from arg, then fall back to $b + # this is not necessarily the fqdn, but it could be. both are possible! + $valid_host = "${host}" ? { + '' => "${b}", # get from $name regexp + default => "${host}", + } + # this error will probably prevent a later error in $valid_domain + if "${valid_host}" == '' { + fail('The $host must be specified.') + } + + # parse the fqdn from $valid_host + $r2 = '^([a-z][a-z0-9\-]*)(\.{0,1})([a-z0-9\.\-]*)$' + #$h = regsubst("${valid_host}", $r2, '\1') # hostname + $d = regsubst("${valid_host}", $r2, '\3') # domain + + $valid_domain = delete("${valid_host}", '.') ? { + "${valid_host}" => "${domain}" ? { # no dots, not an fqdn! + '' => "${ipa::server::domain}" ? { # NOTE: server! + '' => "${::domain}", # default to global val + default => "${ipa::server::domain}", # main! + }, + default => "${domain}", + }, + default => "${domain}" ? { # dots, it's an fqdn... + '' => "${d}", # okay, used parsed value, it had dots! + "${d}" => "${domain}", # they match, okay phew + default => '', # no match, set '' to trigger an error! + }, + } + + # this error condition is very important because '' is used as trigger! + if "${valid_domain}" == '' { + fail('The $domain must be specified.') + } + + $valid_fqdn = delete("${valid_host}", '.') ? { # does it have any dots + "${valid_host}" => "${valid_host}.${valid_domain}", + default => "${valid_host}", # it had dot(s) present + } + + $valid_realm = "${realm}" ? { + '' => "${c}" ? { # get from $name regexp + '' => upcase($valid_domain), # a backup plan default + default => "${c}", # got from $name regexp + }, + default => "${realm}", + } + + # sanity checking, this should probably not happen + if "${valid_realm}" == '' { + fail('The $realm must be specified.') + } + + $valid_server = "${server}" ? { + '' => "${::hostname}.${::domain}", + default => "${server}", + } + + # sanity checking, this should probably not happen + if "${valid_server}" == '' { + fail('The $server must be specified.') + } + + $valid_principal = "${principal}" ? { + '' => "${valid_service}/${valid_fqdn}@${valid_realm}", + default => "${principal}", # just do what you want + } + + if $watch and (! $modify) { + fail('You must be able to $modify to be able to $watch.') + } + + $pactype_valid = ['MS-PAC', 'PAD'] # or 'NONE' + $pactype_array = type($pactype) ? { + 'array' => $pactype, + 'string' => ["${pactype}"], + default => [], # will become 'NONE' + } + $valid_pactype = split(inline_template('<%= ((pactype_array.delete_if {|x| not pactype_valid.include?(x)}.length == 0) ? ["NONE"] : pactype_array.delete_if {|x| not pactype_valid.include?(x)}).join("#") %>'), '#') + + $args01 = sprintf("--pac-type='%s'", join($valid_pactype, ',')) + + $arglist = ["${args01}"] # future expansion available :) + $args = join(delete($arglist, ''), ' ') + + # switch the slashes for a file name friendly character + $valid_principal_file = regsubst("${valid_principal}", '/', '-', 'G') + file { "${vardir}/services/${valid_principal_file}.service": + content => "${valid_principal}\n${args}\n", + owner => root, + group => nobody, + mode => 600, # u=rw,go= + require => File["${vardir}/services/"], + ensure => present, + } + + $exists = "/usr/bin/ipa service-show '${valid_principal}' > /dev/null 2>&1" + $force = "${args}" ? { # if args is empty + '' => '--force', # we have no args! + default => "${args} --force", # pixel perfect... + } + $fargs = $dns ? { # without the dns, + true => "${force}", # we don't need to + default => "${args}", # force everything + } + # NOTE: this runs when no service is present... + exec { "ipa-server-service-add-${name}": # alias + # this has to be here because the command string gets too long + # for a puppet $name var and strange things start to happen... + command => "/usr/bin/ipa service-add '${valid_principal}' ${fargs}", + logoutput => on_failure, + unless => "${exists}", + require => $dns ? { + true => [ + Exec['ipa-server-kinit'], + ], + default => [ + Exec['ipa-dns-check'], # avoid --force errors! + Exec['ipa-server-kinit'], + ], + }, + } + + # NOTE: this runs when we detect that the attributes don't match (diff) + if $modify and ("${args}" != '') { # if there are changes to do... + #exec { "/usr/bin/ipa service-mod '${valid_principal}' ${args}": + exec { "ipa-server-service-mod-${name}": + command => "/usr/bin/ipa service-mod '${valid_principal}' ${args}", + logoutput => on_failure, + refreshonly => $watch ? { + false => true, # when not watching, we + default => undef, # refreshonly to change + }, + subscribe => $watch ? { + false => File["${vardir}/services/${valid_principal_file}.service"], + default => undef, + }, + onlyif => "${exists}", + unless => $watch ? { + false => undef, # don't run the diff checker... + default => "${exists} && ${vardir}/diff.py service '${valid_principal}' ${args}", + }, + require => [ + File["${vardir}/diff.py"], + Exec['ipa-server-kinit'], + Exec["ipa-server-service-add-${name}"], + ], + #alias => "ipa-server-service-mod-${name}", + } + } + + @@ipa::client::service { "${name}": # this is usually the principal + # NOTE: this should set all the client args it can safely assume + service => "${valid_service}", + host => "${valid_host}", # this value is used to collect + domain => "${valid_domain}", + realm => "${valid_realm}", + principal => "${valid_principal}", + server => "${valid_server}", + comment => "${comment}", + ensure => $ensure, + require => Ipa::Client::Host["${name}"], # should match! +# TODO: FIXME: tag => "${name}", # bonus + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server/service/base.pp b/ipa/manifests/server/service/base.pp new file mode 100644 index 000000000..94e4399f2 --- /dev/null +++ b/ipa/manifests/server/service/base.pp @@ -0,0 +1,98 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class ipa::server::service::base { + include ipa::server + include ipa::vardir + #$vardir = $::ipa::vardir::module_vardir # with trailing slash + $vardir = regsubst($::ipa::vardir::module_vardir, '\/$', '') + + # by default, the following services get installed with freeipa: + # DNS/ipa.example.com@EXAMPLE.COM + # dogtagldap/ipa.example.com@EXAMPLE.COM + # HTTP/ipa.example.com@EXAMPLE.COM + # ldap/ipa.example.com@EXAMPLE.COM + # since we don't want to purge them, we need to exclude them... + $prefix = ['DNS', 'dogtagldap', 'HTTP', 'ldap'] + $valid_hostname = $ipa::server::valid_hostname + $valid_domain = $ipa::server::valid_domain + $valid_realm = $ipa::server::valid_realm + $append = "/${valid_hostname}.${valid_domain}@${valid_realm}" + $service_always_ignore = suffix($prefix, $append) + + $service_excludes = $ipa::server::service_excludes + $valid_service_excludes = type($service_excludes) ? { + 'string' => [$service_excludes], + 'array' => $service_excludes, + 'boolean' => $service_excludes ? { + # TODO: there's probably a better fqdn match expression + # this is an expression to prevent all fqdn deletion... + #true => ['^[a-zA-Z0-9\.\-]*$'], + true => ['^[[:alpha:]]{1}[[:alnum:]-.]*$'], + default => false, + }, + default => false, # trigger error... + } + + if type($valid_service_excludes) != 'array' { + fail('The $service_excludes must be an array.') + } + + # directory of system tags which should exist (as managed by puppet) + file { "${vardir}/services/": + ensure => directory, # make sure this is a directory + recurse => true, # recursively manage directory + purge => true, # purge all unmanaged files + force => true, # also purge subdirs and links + owner => root, group => nobody, mode => 600, backup => false, + notify => Exec['ipa-clean-services'], + require => File["${vardir}/"], + } + + # these are template variables for the clean.sh.erb script + $id_dir = 'services' + $ls_cmd = '/usr/bin/ipa service-find --pkey-only --raw | /usr/bin/tr -d " " | /bin/grep "^krbprincipalname:" | /bin/cut -b 18-' # show ipa services + $rm_cmd = '/usr/bin/ipa service-del ' # delete ipa services + $fs_chr = ' ' + $suffix = '.service' + $regexp = $valid_service_excludes + $ignore = $service_always_ignore + + # build the clean script + file { "${vardir}/clean-services.sh": + content => template('ipa/clean.sh.erb'), + owner => root, + group => nobody, + mode => 700, # u=rwx + backup => false, # don't backup to filebucket + ensure => present, + require => File["${vardir}/"], + } + + # run the cleanup + exec { "${vardir}/clean-services.sh": + logoutput => on_failure, + refreshonly => true, + require => [ + Exec['ipa-server-kinit'], + File["${vardir}/clean-services.sh"], + ], + alias => 'ipa-clean-services', + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server/user.pp b/ipa/manifests/server/user.pp new file mode 100644 index 000000000..4a288313e --- /dev/null +++ b/ipa/manifests/server/user.pp @@ -0,0 +1,543 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +define ipa::server::user( # $login or principal as a unique id + $login = '', # usually the same as $name, but set manually + $instance = '', # as in: user/instance@REALM + $domain = '', # must be the empty string by default + $realm = '', + $principal = true, # after all that, you can override principal... + + # name args + $first = '', # required + $last = '', # required + $cn = true, # full name, defaults to "$first $last" + $displayname = true, # defaults to "$first $last" + $initials = true, # defaults to $first[0]+$last[0] + + # some of these parameters can be strings, arrays, or boolean specials! + $email = true, # comes with a sensible default (false = no) + $gecos = true, # old style passwd field, can be set manually + + # special characteristics + $uid = true, # either pick a value, or let system assign it! + $gid = true, # true means try to match $uid value on create! + $shell = true, + $home = true, + $sshpubkeys = false, + + # password + $random = false, # set to true to have the password generated... + $password_file = false, # save to file in ${vardir}/ipa/users/passwords/ + $password_mail = false, # TODO: mail a gpg encrypted password to admin! + + # mailing address section (just plain strings, false is unmanaged) + $street = false, # street address + $city = false, # city + $state = false, # state/province + $postalcode = false, # zip/postal code + + # these four accept arrays or a string. false means unmanaged... + $phone = false, # telephone number + $mobile = false, # mobile telephone number + $pager = false, # pager number + $fax = false, # fax number + + # other information + $jobtitle = false, # job title + $orgunit = false, # org. unit (department) + $manager = false, # manager (should match an existing user $name) + $carlicense = false, # car license (who cares?) + + #$hosts = [], # TODO: add hosts managed by support if exists! + + # special parameters... + $watch = true, # manage all changes to this resource, reverting others + $modify = true, # modify this resource on puppet changes or not ? + $comment = '', + $ensure = present # TODO +) { + include ipa::server + include ipa::server::user::base + include ipa::vardir + #$vardir = $::ipa::vardir::module_vardir # with trailing slash + $vardir = regsubst($::ipa::vardir::module_vardir, '\/$', '') + + # TODO: a better regexp magician could probably do a better job :) + # james/admin@EXAMPLE.COM + # james@EXAMPLE.COM + # james + $r = '^([a-zA-Z][a-zA-Z0-9]*)((/([a-zA-Z][a-zA-Z0-9]*)){0,1}@([A-Z][A-Z\.\-]*)){0,1}$' + + $a = regsubst("${name}", $r, '\1') # login (james) + $b = regsubst("${name}", $r, '\4') # instance (admin) + $c = regsubst("${name}", $r, '\5') # realm (EXAMPLE.COM) + + # user: first try to get value from arg, then fall back to $a (name) + $valid_login = "${login}" ? { + '' => "${a}", # get from $name regexp + default => "${login}", + } + if "${valid_login}" == '' { + # NOTE: if we see this message it might be a regexp pattern bug + fail('The $login must be specified.') + } + + # host: first try to get value from arg, then fall back to $b + # this is not necessarily the group, but it could be. both are possible + # empty values are allowed and possibly even common :) + $valid_instance = "${instance}" ? { + '' => "${b}", # get from $name regexp + default => "${instance}", + } + + $valid_domain = "${domain}" ? { + '' => "${ipa::server::domain}" ? { # NOTE: server! + '' => "${::domain}", # default to global val + default => "${ipa::server::domain}", # main! + }, + default => "${domain}", + } + + # this error condition is very important because '' is used as trigger! + if "${valid_domain}" == '' { + fail('The $domain must be specified.') + } + + $valid_realm = "${realm}" ? { + '' => "${c}" ? { # get from $name regexp + '' => upcase($valid_domain), # a backup plan default + default => "${c}", # got from $name regexp + }, + default => "${realm}", + } + + # sanity checking, this should probably not happen + if "${valid_realm}" == '' { + fail('The $realm must be specified.') + } + + # to be used if principal is generated from the available entered data! + $auto_principal = "${valid_instance}" ? { + '' => "${valid_login}@${valid_realm}", # no instance ! + default => "${valid_login}/${valid_instance}@${valid_realm}", + } + + $valid_principal = type($principal) ? { + 'string' => "${principal}" ? { + '' => "${auto_principal}", + default => "${principal}", # just do what you want + }, + 'boolean' => $principal ? { + false => '', # don't use a principal + default => "${auto_principal}", + }, + default => '', + } + + if $watch and (! $modify) { + fail('You must be able to $modify to be able to $watch.') + } + + if "${first}" == '' { + fail("The first name is required for: '${valid_login}'.") + } + if "${last}" == '' { + fail("The last name is required for: '${valid_login}'.") + } + + $args01 = "${first}" ? { + '' => '', + default => "--first='${first}'", + } + $args02 = "${last}" ? { + '' => '', + default => "--last='${last}'", + } + + $args03 = type($cn) ? { + 'string' => "--cn='${cn}'", + 'boolean' => $cn ? { + false => '', + default => "--cn='${first} ${last}'", + }, + default => '', + } + + $args04 = type($displayname) ? { + 'string' => "--displayname='${displayname}'", + 'boolean' => $displayname ? { + false => '', + default => "--displayname='${first} ${last}'", + }, + default => '', + } + + $args05 = type($initials) ? { + 'string' => "--initials='${displayname}'", + 'boolean' => $initials ? { + false => '', + # NOTE: [0,1] is a version robust way to get index 0... + default => sprintf("--initials='%s'", inline_template('<%= first[0,1]+last[0,1] %>')), + }, + default => '', + } + + # email can provide a sensible default + $default_email_domain = $ipa::server::default_email_domain + $valid_email = type($email) ? { + 'string' => "${email}" ? { + '' => [], # assume managed but empty (rm values) + default => ["${email}"], + }, + 'array' => $email, + 'boolean' => $email ? { + false => '', # unmanaged + default => ["${valid_login}@${default_email_domain}"], # sensible default + }, + default => '', # unmanaged + } + $args06 = type($valid_email) ? { + 'array' => inline_template('<% if valid_email == [] %>--email=<% else %><%= valid_email.map {|x| "--email=\'"+x+"\'" }.join(" ") %><% end %>'), + default => '', # unmanaged + } + + $args07 = type($gecos) ? { + 'string' => "--gecos='${gecos}'", + 'boolean' => $gecos ? { + false => '', + default => "--gecos='${first} ${last}'", + }, + default => '', + } + + # TODO: validate id ranges ? + $args08 = type($uid) ? { + 'string' => "--uid='${uid}'", + 'integer' => "--uid='${uid}'", + default => '', + } + + # TODO: validate id ranges ? + $args09 = type($gid) ? { + 'string' => "--gidnumber='${gid}'", + 'integer' => "--gidnumber='${gid}'", + 'boolean' => $gid ? { + false => '', + default => type($uid) ? { # auto try to match uid + 'string' => "--gidnumber='${uid}'", # uid ! + 'integer' => "--gidnumber='${uid}'", # uid ! + default => '', # auto + }, + }, + default => '', + } + + $default_shell = $ipa::server::default_shell + $args10 = type($shell) ? { + 'string' => "--shell='${shell}'", + 'boolean' => $shell ? { + false => '', + default => "--shell='${default_shell}'", + }, + default => '', + } + + # TODO: the home stuff seems to not use trailing slashes. can i add it? + $default_homes = $ipa::server::default_homes + $args11 = type($home) ? { + 'string' => sprintf("--homedir='%s'", regsubst("${home}" , '\/$', '')), + 'boolean' => $home ? { + false => '', + default => type($default_homes) ? { + 'string' => sprintf("--homedir='%s/${valid_login}'", regsubst("${default_homes}" , '\/$', '')), + # TODO: warning ? + default => '', # can't manage, parent is false + }, + }, + default => '', + } + + # users individual ssh public keys + $valid_sshpubkeys = type($sshpubkeys) ? { + 'string' => "${sshpubkeys}" ? { + '' => [], # assume managed but empty (rm values) + default => ["${sshpubkeys}"], + }, + 'array' => $sshpubkeys, + default => '', # unmanaged + } + $args12 = type($valid_sshpubkeys) ? { + 'array' => inline_template('<% if valid_sshpubkeys == [] %>--sshpubkey=<% else %><%= valid_sshpubkeys.map {|x| "--sshpubkey=\'"+x+"\'" }.join(" ") %><% end %>'), + default => '', # unmanaged + } + + # mailing address section + $args13 = type($street) ? { + 'string' => "--street='${street}'", + 'boolean' => $street ? { + true => '--street=', # managed + default => '', # unmanaged + }, + default => '', # whatever and unmanaged + } + + $args14 = type($city) ? { + 'string' => "--city='${city}'", + 'boolean' => $city ? { + true => '--city=', + default => '', + }, + default => '', + } + + $args15 = type($state) ? { # or province + 'string' => "--state='${state}'", + 'boolean' => $state ? { + true => '--state=', + default => '', + }, + default => '', + } + + $args16 = type($postalcode) ? { + 'string' => "--postalcode='${postalcode}'", + 'boolean' => $postalcode ? { + true => '--postalcode=', + default => '', + }, + default => '', + } + + # the following four phone number types can be arrays + $valid_phone = type($phone) ? { + 'string' => "${phone}" ? { + '' => [], # assume managed but empty (rm values) + default => ["${phone}"], + }, + 'array' => $phone, + default => '', # unmanaged + } + $args17 = type($valid_phone) ? { + 'array' => inline_template('<% if valid_phone == [] %>--phone=<% else %><%= valid_phone.map {|x| "--phone=\'"+x+"\'" }.join(" ") %><% end %>'), + default => '', # unmanaged + } + + $valid_mobile = type($mobile) ? { + 'string' => "${mobile}" ? { + '' => [], # assume managed but empty (rm values) + default => ["${mobile}"], + }, + 'array' => $mobile, + default => '', # unmanaged + } + $args18 = type($valid_mobile) ? { + 'array' => inline_template('<% if valid_mobile == [] %>--mobile=<% else %><%= valid_mobile.map {|x| "--mobile=\'"+x+"\'" }.join(" ") %><% end %>'), + default => '', # unmanaged + } + + $valid_pager = type($pager) ? { + 'string' => "${pager}" ? { + '' => [], # assume managed but empty (rm values) + default => ["${pager}"], + }, + 'array' => $pager, + default => '', # unmanaged + } + $args19 = type($valid_pager) ? { + 'array' => inline_template('<% if valid_pager == [] %>--pager=<% else %><%= valid_pager.map {|x| "--pager=\'"+x+"\'" }.join(" ") %><% end %>'), + default => '', # unmanaged + } + + $valid_fax = type($fax) ? { + 'string' => "${fax}" ? { + '' => [], # assume managed but empty (rm values) + default => ["${fax}"], + }, + 'array' => $fax, + default => '', # unmanaged + } + $args20 = type($valid_fax) ? { + 'array' => inline_template('<% if valid_fax == [] %>--fax=<% else %><%= valid_fax.map {|x| "--fax=\'"+x+"\'" }.join(" ") %><% end %>'), + default => '', # unmanaged + } + + # other information + $args21 = type($jobtitle) ? { # job title + 'string' => "--title='${jobtitle}'", + 'boolean' => $jobtitle ? { + true => '--title=', + default => '', + }, + default => '', + } + + $args22 = type($orgunit) ? { + 'string' => "--orgunit='${orgunit}'", + 'boolean' => $orgunit ? { + true => '--orgunit=', + default => '', + }, + default => '', + } + + # manager requires user exists... this lets us match a user principal + $valid_manager = regsubst("${manager}", $r, '\1') # login (james) + $args23 = type($manager) ? { # this has to match an existing user... + 'string' => "--manager='${valid_manager}'", + 'boolean' => $manager ? { + true => '--manager=', + default => '', + }, + default => '', + } + + $args24 = type($carlicense) ? { + 'string' => "--carlicense='${carlicense}'", + 'boolean' => $carlicense ? { + true => '--carlicense=', + default => '', + }, + default => '', + } + + $arglist = ["${args01}", "${args02}", "${args03}", "${args04}", "${args05}", "${args06}", "${args07}", "${args08}", "${args09}", "${args10}", "${args11}", "${args12}", "${args13}", "${args14}", "${args15}", "${args16}", "${args17}", "${args18}", "${args19}", "${args20}", "${args21}", "${args22}", "${args23}", "${args24}"] + $args = join(delete($arglist, ''), ' ') + + # switch bad characters for file name friendly characters (unused atm!) + # this could be useful if we allow login's with $ and others in them... + $valid_login_file = regsubst("${valid_login}", '\$', '-', 'G') + file { "${vardir}/users/${valid_login_file}.user": + content => "${valid_login}\n${args}\n", + owner => root, + group => nobody, + mode => 600, # u=rw,go= + require => File["${vardir}/users/"], + ensure => present, + } + + if $random and $password_file { + file { "${vardir}/users/passwords/${valid_login}.password": + # no content! this is a tag, content comes in by echo ! + owner => root, + group => nobody, + mode => 600, # u=rw,go= + backup => false, + require => File["${vardir}/users/passwords/"], + ensure => present, + } + } + + $exists = "/usr/bin/ipa user-show '${valid_login}' > /dev/null 2>&1" + # this requires ensures the $manager user exists when we can check that + # this melds together the kinit require which is needed by the user add + $requires = type($manager) ? { + 'string' => "${manager}" ? { + '' => Exec['ipa-server-kinit'], + default => $watch ? { + false => Exec['ipa-server-kinit'], + default => [ + Exec['ipa-server-kinit'], + Ipa::Server::User["${manager}"], + ], + }, + }, + default => Exec['ipa-server-kinit'], + } + + # principal is only set on user add... it can't be edited afaict + $principal_arg = "${valid_principal}" ? { # not shown in ipa gui! + '' => '', + default => "--principal='${valid_principal}'", + } + + $aargs = "${principal_arg}" ? { # principal exists + '' => "${args}", # just normal args + default => "${principal_arg} ${args}", # pixel perfect... + } + + # NOTE: this runs when no user is present... + exec { "ipa-server-user-add-${name}": # alias + # this has to be here because the command string gets too long + # for a puppet $name var and strange things start to happen... + command => "/usr/bin/ipa user-add '${valid_login}' ${aargs}", + logoutput => on_failure, + unless => "${exists}", + require => $requires, + } + + # NOTE: this runs when we detect that the attributes don't match (diff) + if $modify and ("${args}" != '') { # if there are changes to do... + #exec { "/usr/bin/ipa user-mod '${valid_login}' ${args}": + exec { "ipa-server-user-mod-${name}": + command => "/usr/bin/ipa user-mod '${valid_login}' ${args}", + logoutput => on_failure, + refreshonly => $watch ? { + false => true, # when not watching, we + default => undef, # refreshonly to change + }, + subscribe => $watch ? { + false => File["${vardir}/users/${valid_login_file}.user"], + default => undef, + }, + onlyif => "${exists}", + unless => $watch ? { + false => undef, # don't run the diff checker... + default => "${exists} && ${vardir}/diff.py user '${valid_login}' ${args}", + }, + require => [ + File["${vardir}/diff.py"], + Exec['ipa-server-kinit'], + # this user-add exec pulls in manager $requires + Exec["ipa-server-user-add-${name}"], + ], + #alias => "ipa-server-user-mod-${name}", + } + } + + $prog01 = $password_file ? { + true => "/bin/cat > ${vardir}/users/passwords/${valid_login}.password", + default => '', + } + + $gpg_email = $ipa::server::valid_email # admin email + #$gpg_key = $ipa::server::TODO + $prog02 = $password_mail ? { + #true => "/bin/cat | /usr/bin/gpg TODO | /bin/mailx -s 'GPG encrypted password' '${gpg_email}'", # FIXME: add this code! + default => '', + } + + if $modify and $random { + $proglist = ["${prog01}", "${prog02}"] + # eg /usr/bin/tee /dev/null >(prog1) >(prog2) >(progN) + $progs = join(suffix(prefix(delete($proglist, ''), '>('), ')'), ' ') + exec { "ipa-server-user-qmod-${name}": + # bash -c is needed because this command uses bashisms! + command => "/bin/bash -c \"/usr/bin/ipa user-mod '${valid_login}' --raw --random | /usr/bin/tr -d ' ' | /bin/grep '^randompassword:' | /bin/cut -b 16- | /usr/bin/tee /dev/null ${progs}\"", + logoutput => on_failure, + onlyif => "/usr/bin/test \"`/usr/bin/ipa user-show '${valid_login}' --raw | /usr/bin/tr -d ' ' | /bin/grep '^has_password:' | /bin/cut -b 14-`\" = 'False'", + require => [ + Exec['ipa-server-kinit'], + Exec["ipa-server-user-add-${name}"], + #Exec["ipa-server-user-mod-${name}"], # not needed... + ], + #alias => "ipa-server-user-qmod-${name}", + } + } +} + +# vim: ts=8 diff --git a/ipa/manifests/server/user/base.pp b/ipa/manifests/server/user/base.pp new file mode 100644 index 000000000..542e5be5c --- /dev/null +++ b/ipa/manifests/server/user/base.pp @@ -0,0 +1,98 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class ipa::server::user::base { + include ipa::server + include ipa::vardir + #$vardir = $::ipa::vardir::module_vardir # with trailing slash + $vardir = regsubst($::ipa::vardir::module_vardir, '\/$', '') + + # by default, the following users get installed with freeipa: + # admin + # since we don't want to purge them, we need to exclude them... + $user_always_ignore = ['admin'] + $user_excludes = $ipa::server::user_excludes + $valid_user_excludes = type($user_excludes) ? { + 'string' => [$user_excludes], + 'array' => $user_excludes, + 'boolean' => $user_excludes ? { + # TODO: there's probably a better user match expression + # this is an expression to prevent all user deletion... + #true => ['^[a-zA-Z0-9]*$'], + true => ['^[[:alpha:]]{1}[[:alnum:]]*$'], + default => false, + }, + default => false, # trigger error... + } + + if type($valid_user_excludes) != 'array' { + fail('The $user_excludes must be an array.') + } + + # directory of system tags which should exist (as managed by puppet) + file { "${vardir}/users/": + ensure => directory, # make sure this is a directory + recurse => true, # recursively manage directory + purge => true, # purge all unmanaged files + force => true, # also purge subdirs and links + owner => root, group => nobody, mode => 600, backup => false, + notify => Exec['ipa-clean-users'], + require => File["${vardir}/"], + } + + # these are template variables for the clean.sh.erb script + $id_dir = 'users' + $ls_cmd = '/usr/bin/ipa user-find --pkey-only --raw | /usr/bin/tr -d " " | /bin/grep "^uid:" | /bin/cut -b 5-' # show ipa users + $rm_cmd = '/usr/bin/ipa user-del ' # delete ipa users + $fs_chr = ' ' + $suffix = '.user' + $regexp = $valid_user_excludes + $ignore = $user_always_ignore + + # build the clean script + file { "${vardir}/clean-users.sh": + content => template('ipa/clean.sh.erb'), + owner => root, + group => nobody, + mode => 700, # u=rwx + backup => false, # don't backup to filebucket + ensure => present, + require => File["${vardir}/"], + } + + # run the cleanup + exec { "${vardir}/clean-users.sh": + logoutput => on_failure, + refreshonly => true, + require => [ + Exec['ipa-server-kinit'], + File["${vardir}/clean-users.sh"], + ], + alias => 'ipa-clean-users', + } + + file { "${vardir}/users/passwords/": # for storing random passwords + ensure => directory, # make sure this is a directory + recurse => true, # recursively manage directory + purge => true, # purge all unmanaged files + force => true, # also purge subdirs and links + owner => root, group => nobody, mode => 600, backup => false, + require => File["${vardir}/users/"], + } +} + +# vim: ts=8 diff --git a/ipa/manifests/vardir.pp b/ipa/manifests/vardir.pp new file mode 100644 index 000000000..e033549f4 --- /dev/null +++ b/ipa/manifests/vardir.pp @@ -0,0 +1,51 @@ +# FreeIPA templating module by James +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +class ipa::vardir { # module vardir snippet + if "${::puppet_vardirtmp}" == '' { + if "${::puppet_vardir}" == '' { + # here, we require that the puppetlabs fact exist! + fail('Fact: $puppet_vardir is missing!') + } + $tmp = sprintf("%s/tmp/", regsubst($::puppet_vardir, '\/$', '')) + # base directory where puppet modules can work and namespace in + file { "${tmp}": + ensure => directory, # make sure this is a directory + recurse => false, # don't recurse into directory + purge => true, # purge all unmanaged files + force => true, # also purge subdirs and links + owner => root, + group => nobody, + mode => 600, + backup => false, # don't backup to filebucket + before => File["${module_vardir}"], + #require => Package['puppet'], # no puppet module seen + } + } else { + $tmp = sprintf("%s/", regsubst($::puppet_vardirtmp, '\/$', '')) + } + $module_vardir = sprintf("%s/ipa/", regsubst($tmp, '\/$', '')) + file { "${module_vardir}": # /var/lib/puppet/tmp/ipa/ + ensure => directory, # make sure this is a directory + recurse => true, # recursively manage directory + purge => true, # purge all unmanaged files + force => true, # also purge subdirs and links + owner => root, group => nobody, mode => 600, backup => false, + } +} + +# vim: ts=8 diff --git a/ipa/metadata.json b/ipa/metadata.json new file mode 100644 index 000000000..89d5c0939 --- /dev/null +++ b/ipa/metadata.json @@ -0,0 +1,15 @@ +{ + "name": "purpleidea-ipa", + "version": "0.0.4", + "author": "James Shubin", + "summary": "FreeIPA templating module by James", + "license": "GNU Affero General Public License, Version 3.0+", + "source": "https://github.com/purpleidea/puppet-ipa/", + "project_page": "https://github.com/purpleidea/puppet-ipa/", + "issues_url": null, + "description": "UNKNOWN", + "dependencies": [ + { "name": "puppetlabs/stdlib", "version_requirement": ">= 4.0.0" } + ] +} + diff --git a/ipa/puppet-ipa.spec.in b/ipa/puppet-ipa.spec.in new file mode 100644 index 000000000..dca48da16 --- /dev/null +++ b/ipa/puppet-ipa.spec.in @@ -0,0 +1,49 @@ +# special thanks to kkeithley for using his wizard rpm skills to get this going +%global puppet_module_version __VERSION__ + +Name: puppet-ipa +Version: __VERSION__ +#Release: __RELEASE__%{?dist} # use this to make dist specific builds +Release: __RELEASE__ +Summary: A puppet module for FreeIPA +License: AGPLv3+ +URL: https://github.com/purpleidea/puppet-ipa +Source0: https://download.gluster.org/pub/gluster/purpleidea/puppet-ipa/SOURCES/puppet-ipa-%{puppet_module_version}.tar.bz2 +BuildArch: noarch + +Requires: puppet >= 3.0.0 +Requires: puppetlabs-stdlib >= 4.1.0 + +# these should be 'Suggests:' or similar, since they aren't absolutely required +#Requires: puppetlabs-apt >= 1.4.0 +Requires: puppet-keepalived >= 0.0.1 +Requires: puppet-puppet >= 0.0.1 +Requires: puppet-shorewall >= 0.0.1 +Requires: puppet-ssh >= 0.0.1 +Requires: puppet-yum >= 0.0.1 + +%description +A puppet module used for installing, configuring and managing FreeIPA. + +%prep +%setup -c -q -T -D -a 0 + +find %{_builddir} -type f -name ".*" -exec rm {} + +find %{_builddir} -size 0 -exec rm {} + +find %{_builddir} \( -name "*.pl" -o -name "*.sh" \) -exec chmod +x {} + +find %{_builddir} \( -name "*.pp" -o -name "*.py" \) -exec chmod -x {} + +find %{_builddir} \( -name "*.rb" -o -name "*.erb" \) -exec chmod -x {} + -exec sed -i "/^#!/{d;q}" {} + + +%build + +%install +rm -rf %{buildroot} +# _datadir is typically /usr/share/ +install -d -m 0755 %{buildroot}/%{_datadir}/puppet/modules/ +cp -r puppet-ipa-%{puppet_module_version} %{buildroot}/%{_datadir}/puppet/modules/ipa + +%files +%{_datadir}/puppet/modules/* + +# this changelog is auto-generated by git log +%changelog diff --git a/ipa/spec/spec_helper.rb b/ipa/spec/spec_helper.rb new file mode 100644 index 000000000..b51de3a7e --- /dev/null +++ b/ipa/spec/spec_helper.rb @@ -0,0 +1,18 @@ +dir = File.expand_path(File.dirname(__FILE__)) +$LOAD_PATH.unshift File.join(dir, 'lib') + +require 'mocha' +require 'puppet' +require 'rspec' +require 'spec/autorun' + +Spec::Runner.configure do |config| + config.mock_with :mocha +end + +# We need this because the RAL uses 'should' as a method. This +# allows us the same behaviour but with a different method name. +class Object + alias :must :should +end + diff --git a/ipa/templates/clean.sh.erb b/ipa/templates/clean.sh.erb new file mode 100644 index 000000000..6c1382be5 --- /dev/null +++ b/ipa/templates/clean.sh.erb @@ -0,0 +1,58 @@ +#!/bin/bash + +# README: +# This script loops through listed items and removes those which are not known. +# This is so that when puppet removes the 'tag' file the actual item gets rm'd. +# An optional list of bash matches can be provided to spare removal if matched. +# +# NOTE: +# The following variables were used to build this script: +# id_dir: <%= id_dir %> +# ls_cmd: <%= ls_cmd %> +# rm_cmd: <%= rm_cmd %> +# fs_chr: <%= fs_chr %> +# suffix: <%= suffix %> +# regexp: <%= regexp.join(' ') %> +# ignore: <%= ignore.join(' ') %> +# + +for i in `<%= ls_cmd %> | /bin/awk -F '<%= fs_chr %>' '{print $1}'`; do + #echo "$i" + found=false + # this section is essentially an in_array() + for j in <%= scope.lookupvar('::ipa::vardir::module_vardir').gsub(/\/$/, '') %>/<%= id_dir %>/*<%= suffix %>; do + [ -e "$j" ] || break # loop in bash properly + #echo "found tag: $j" + # compare against first line of the file + n=`/usr/bin/head -1 "$j"` + if [ "$i" == "$n" ]; then + found=true # found it -- it's safe + break + fi + done + +<% if ignore.is_a?(Array) and ignore != [] -%> + # check against built in ignores +<% ignore.each do |x| -%> + if [ "$i" == '<%= x %>' ]; then + found=true + fi +<% end -%> + +<% end -%> +<% if regexp.is_a?(Array) and regexp != [] -%> + # quoting the match pattern indicate a string match (bash v3.2+) +<% regexp.each do |m| -%> + if [[ "$i" =~ <%= m %> ]]; then + found=true + fi +<% end -%> + +<% end -%> + # if not found, not matched, and not ignored, then it should be removed + if ! $found; then + # echo "Removing: $i" + <%= rm_cmd %>"$i" + fi +done + diff --git a/ipa/vagrant/.gitignore b/ipa/vagrant/.gitignore new file mode 100644 index 000000000..cb41b90cb --- /dev/null +++ b/ipa/vagrant/.gitignore @@ -0,0 +1,3 @@ +puppet-ipa.yaml +.vagrant/ +.ssh/ diff --git a/ipa/vagrant/Vagrantfile b/ipa/vagrant/Vagrantfile new file mode 100644 index 000000000..64246847d --- /dev/null +++ b/ipa/vagrant/Vagrantfile @@ -0,0 +1,570 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# Vagrantfile for FreeIPA using Puppet-Ipa +# Copyright (C) 2012-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: vagrant-libvirt needs to run in series (not in parallel) to avoid trying +# to create the network twice, eg: 'vagrant up --no-parallel'. alternatively you +# can build the first host (puppet server) manually, and then the hosts next eg: +# 'vagrant up puppet && vagrant up' which ensures the puppet server is up first! +# NOTE: https://github.com/pradels/vagrant-libvirt/issues/104 is the open bug... + +# README: https://ttboj.wordpress.com/2013/12/09/vagrant-on-fedora-with-libvirt/ +# README: https://ttboj.wordpress.com/2013/12/21/vagrant-vsftp-and-other-tricks/ +# ALSO: https://ttboj.wordpress.com/2014/01/02/vagrant-clustered-ssh-and-screen/ + +# NOTE: this will not work properly on Fedora 19 or anything that does not have +# the libvirt broadcast patch included. You can check which libvirt version you +# have and see if that version tag is in the libvirt git or in your distro. eg: +# git tag --contains 51e184e9821c3740ac9b52055860d683f27b0ab6 | grep +# this is because old libvirt broke the vrrp broadcast packets from keepalived! + +# TODO: the /etc/hosts DNS setup is less than ideal, but I didn't implement +# anything better yet. Please feel free to suggest something else! + +# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! +VAGRANTFILE_API_VERSION = '2' + +require 'ipaddr' +require 'yaml' + +# +# globals +# +domain = 'example.com' +network = IPAddr.new '192.168.144.0/24' +range = network.to_range.to_a +cidr = (32-(Math.log(range.length)/Math.log(2))).to_i +offset = 100 # start hosts after here +#puts range[0].to_s # network +#puts range[1].to_s # router (reserved) +#puts range[2].to_s # puppetmaster +#puts range[3].to_s # vip + +network2 = IPAddr.new '192.168.145.0/24' +range2 = network2.to_range.to_a +cidr2 = (32-(Math.log(range2.length)/Math.log(2))).to_i +offset2 = 2 +netmask2 = IPAddr.new('255.255.255.255').mask(cidr2).to_s + +# mutable by ARGV and settings file +count = 1 # default number of ipa hosts to build +# NOTE: this is the default topology that makes sense for puppet-ipa+vagrant... +# FIXME: what topology makes the most sense for most replica host counts of N ? +topology = 'ring' # default topology +version = '' # default ipa version (empty string means latest!) +firewall = false # default firewall enabled (FIXME: default to true when keepalived bug is fixed) +recipient = '' # default gpg recipient to use +clients = 1 # default number of XXX clients to build +sync = 'rsync' # default sync type +cachier = false # default cachier usage + +# +# ARGV parsing +# +projectdir = File.expand_path File.dirname(__FILE__) # vagrant project dir!! +f = File.join(projectdir, 'puppet-ipa.yaml') + +# load settings +if File.exist?(f) + settings = YAML::load_file f + count = settings[:count] + topology = settings[:topology] + version = settings[:version] + firewall = settings[:firewall] + recipient = settings[:recipient] + clients = settings[:clients] + sync = settings[:sync] + cachier = settings[:cachier] +end + +# ARGV parser +skip = 0 +while skip < ARGV.length + #puts "#{skip}, #{ARGV[skip]}" # debug + if ARGV[skip].start_with?(arg='--ipa-count=') + v = ARGV.delete_at(skip).dup + v.slice! arg + #puts "#{arg}, #{v}" # debug + + count = v.to_i # set ipa host count + + elsif ARGV[skip].start_with?(arg='--ipa-topology=') + v = ARGV.delete_at(skip).dup + v.slice! arg + + topology = v.to_s # set ipa topology + + elsif ARGV[skip].start_with?(arg='--ipa-version=') + v = ARGV.delete_at(skip).dup + v.slice! arg + + version = v.to_s # set ipa version + + elsif ARGV[skip].start_with?(arg='--ipa-firewall=') + v = ARGV.delete_at(skip).dup + v.slice! arg + + firewall = v.to_s # set firewall flag + if ['false', 'no'].include?(firewall.downcase) + firewall = false + else + firewall = true + end + + elsif ARGV[skip].start_with?(arg='--ipa-recipient=') + v = ARGV.delete_at(skip).dup + v.slice! arg + + recipient = v.to_s # set gpg recipient + + elsif ARGV[skip].start_with?(arg='--ipa-clients=') + v = ARGV.delete_at(skip).dup + v.slice! arg + + clients = v.to_i # set ipa client count + + elsif ARGV[skip].start_with?(arg='--ipa-sync=') + v = ARGV.delete_at(skip).dup + v.slice! arg + + sync = v.to_s # set sync type + + elsif ARGV[skip].start_with?(arg='--ipa-cachier=') + v = ARGV.delete_at(skip).dup + v.slice! arg + + cachier = v.to_s # set cachier flag + if ['true', 'yes'].include?(cachier.downcase) + cachier = true + else + cachier = false + end + + else # skip over "official" vagrant args + skip = skip + 1 + end +end + +# save settings (ARGV overrides) +settings = { + :count => count, + :topology => topology, + :version => version, + :firewall => firewall, + :recipient => recipient, + :clients => clients, + :sync => sync, + :cachier => cachier +} +File.open(f, 'w') do |file| + file.write settings.to_yaml +end + +#puts "ARGV: #{ARGV}" # debug + +# erase host information from puppet so that the user can do partial rebuilds +snoop = ARGV.select { |x| !x.start_with?('-') } +if snoop.length > 1 and snoop[0] == 'destroy' + snoop.shift # left over array snoop should be list of hosts + if snoop.include?('puppet') # doesn't matter then... + snoop = [] + end +else + # important! clear snoop because we're not using 'destroy' + snoop = [] +end + +# figure out which hosts are getting destroyed +destroy = ARGV.select { |x| !x.start_with?('-') } +if destroy.length > 0 and destroy[0] == 'destroy' + destroy.shift # left over array destroy should be list of hosts or [] + if destroy.length == 0 + destroy = true # destroy everything + end +else + destroy = false # destroy nothing +end + +# figure out which hosts are getting provisioned +provision = ARGV.select { |x| !x.start_with?('-') } +if provision.length > 0 and ['up', 'provision'].include?(provision[0]) + provision.shift # left over array provision should be list of hosts or [] + if provision.length == 0 + provision = true # provision everything + end +else + provision = false # provision nothing +end + +# XXX: workaround for: https://github.com/mitchellh/vagrant/issues/2447 +# only run on 'vagrant init' or if it's the first time running vagrant +if sync == 'nfs' and ((ARGV.length > 0 and ARGV[0] == 'init') or not(File.exist?(f))) + `sudo systemctl restart nfs-server` + `firewall-cmd --permanent --zone public --add-service mountd` + `firewall-cmd --permanent --zone public --add-service rpc-bind` + `firewall-cmd --permanent --zone public --add-service nfs` + `firewall-cmd --reload` +end + +Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| + + #config.landrush.enable # TODO ? + + # + # box (pre-built base image) + # + config.vm.box = 'centos-6' # i built it + + # box source url + # TODO: this box should be GPG signed + config.vm.box_url = 'https://download.gluster.org/pub/gluster/purpleidea/vagrant/centos-6/centos-6.box' + + # + # sync type + # + config.vm.synced_folder './', '/vagrant', type: sync # nfs, rsync + + # + # cache + # + # NOTE: you should probably erase the cache between rebuilds if you are + # installing older package versions. This is because the newer packages + # will get cached, and then subsequently might get silently installed!! + if cachier + # TODO: this doesn't cache metadata, full offline operation not possible + config.cache.auto_detect = true + config.cache.enable :yum + #config.cache.enable :apt + if not ARGV.include?('--no-parallel') # when running in parallel, + config.cache.scope = :machine # use the per machine cache + end + if sync == 'nfs' # TODO: support other sync types here... + config.cache.enable_nfs = true # sets nfs => true on the synced_folder + # the nolock option is required, otherwise the NFSv3 client will try to + # access the NLM sideband protocol to lock files needed for /var/cache/ + # all of this can be avoided by using NFSv4 everywhere. die NFSv3, die! + config.cache.mount_options = ['rw', 'vers=3', 'tcp', 'nolock'] + end + end + + # + # vip + # + vip_ip = range[3].to_s + vip_hostname = 'ipa' + + # + # puppetmaster + # + puppet_ip = range[2].to_s + puppet_hostname = 'puppet' + fv = File.join(projectdir, '.vagrant', "#{puppet_hostname}-hosts.done") + if destroy.is_a?(TrueClass) or (destroy.is_a?(Array) and destroy.include?(puppet_hostname)) + if File.exists?(fv) # safety + puts "Unlocking shell provisioning for: #{puppet_hostname}..." + File.delete(fv) # delete hosts token + end + end + + #puppet_fqdn = "#{puppet_hostname}.#{domain}" + config.vm.define :puppet, :primary => true do |vm| + vm.vm.hostname = puppet_hostname + # red herring network so that management happens here... + vm.vm.network :private_network, + :ip => range2[2].to_s, + :libvirt__netmask => netmask2, + #:libvirt__dhcp_enabled => false, # XXX: not allowed here + :libvirt__network_name => 'default' + + # this is the real network that we'll use... + vm.vm.network :private_network, + :ip => puppet_ip, + :libvirt__dhcp_enabled => false, + :libvirt__network_name => 'ipa' + + #vm.landrush.host puppet_hostname, puppet_ip # TODO ? + + # ensure the ipa module is present for provisioning... + if provision.is_a?(TrueClass) or (provision.is_a?(Array) and provision.include?(puppet_hostname)) + cwd = `pwd` + mod = File.join(projectdir, 'puppet', 'modules') + `cd #{mod} && make ipa &> /dev/null; cd #{cwd}` + end + + # + # shell + # + if not File.exists?(fv) # only modify /etc/hosts once + if provision.is_a?(TrueClass) or (provision.is_a?(Array) and provision.include?(puppet_hostname)) + File.open(fv, 'w') {} # touch + end + vm.vm.provision 'shell', inline: 'puppet resource host localhost.localdomain ip=127.0.0.1 host_aliases=localhost' + vm.vm.provision 'shell', inline: "puppet resource host #{puppet_hostname} ensure=absent" # so that fqdn works + + vm.vm.provision 'shell', inline: "puppet resource host #{vip_hostname}.#{domain} ip=#{vip_ip} host_aliases=#{vip_hostname} ensure=present" + vm.vm.provision 'shell', inline: "puppet resource host #{puppet_hostname}.#{domain} ip=#{puppet_ip} host_aliases=#{puppet_hostname} ensure=present" + (1..count).each do |i| + h = "ipa#{i}" + ip = range[offset+i].to_s + vm.vm.provision 'shell', inline: "puppet resource host #{h}.#{domain} ip=#{ip} host_aliases=#{h} ensure=present" + end + + # hosts entries for all clients + (1..clients).each do |i| + h = "client#{i}" + ip = range[offset+count+i].to_s + vm.vm.provision 'shell', inline: "puppet resource host #{h}.#{domain} ip=#{ip} host_aliases=#{h} ensure=present" + end + end + # + # puppet (apply) + # + vm.vm.provision :puppet do |puppet| + puppet.module_path = 'puppet/modules' + puppet.manifests_path = 'puppet/manifests' + puppet.manifest_file = 'site.pp' + # custom fact + puppet.facter = { + 'vagrant' => '1', + 'vagrant_ipa_firewall' => firewall ? 'true' : 'false', + 'vagrant_ipa_allow' => (1..count).map{|z| range[offset+z].to_s}.join(','), + } + puppet.synced_folder_type = sync + end + + vm.vm.provider :libvirt do |libvirt| + libvirt.cpus = 2 + libvirt.memory = 1024 + end + end + + # + # ipa, et al... + # + (1..count).each do |i| + h = "ipa#{i}" + ip = range[offset+i].to_s + #fqdn = "#{h}.#{domain}" + fvx = File.join(projectdir, '.vagrant', "#{h}-hosts.done") + if destroy.is_a?(TrueClass) or (destroy.is_a?(Array) and destroy.include?(h)) + if File.exists?(fvx) # safety + puts "Unlocking shell provisioning for: #{h}..." + File.delete(fvx) # delete hosts token + end + end + + if snoop.include?(h) # should we clean this machine? + cmd = "puppet cert clean #{h}.#{domain}" + puts "Running 'puppet cert clean' for: #{h}..." + `vagrant ssh #{puppet_hostname} -c 'sudo #{cmd}'` + cmd = "puppet node deactivate #{h}.#{domain}" + puts "Running 'puppet node deactivate' for: #{h}..." + `vagrant ssh #{puppet_hostname} -c 'sudo #{cmd}'` + end + + config.vm.define h.to_sym do |vm| + vm.vm.hostname = h + # red herring network so that management happens here... + vm.vm.network :private_network, + :ip => range2[offset2+i].to_s, + :libvirt__netmask => netmask2, + :libvirt__network_name => 'default' + + # this is the real network that we'll use... + vm.vm.network :private_network, + :ip => ip, + :libvirt__dhcp_enabled => false, + :libvirt__network_name => 'ipa' + + # use a specialized freeipa box if it exists :) + if `vagrant box list | grep -q '^centos-6-freeipa' && echo -n found` != '' + vm.vm.box = 'centos-6-freeipa' + end + # vagrant won't bind to these ports if not run as root! + # if vagrant-libvirt includes this patch, it is better: + # https://github.com/purpleidea/vagrant-libvirt/tree/feat/sudo-forward + vm.vm.network 'forwarded_port', guest: 80, host: 80 + vm.vm.network 'forwarded_port', guest: 443, host: 443 + + #vm.landrush.host h, ip # TODO ? + + # + # shell + # + if not File.exists?(fvx) # only modify /etc/hosts once + if provision.is_a?(TrueClass) or (provision.is_a?(Array) and provision.include?(h)) + File.open(fvx, 'w') {} # touch + end + vm.vm.provision 'shell', inline: 'puppet resource host localhost.localdomain ip=127.0.0.1 host_aliases=localhost' + vm.vm.provision 'shell', inline: "puppet resource host #{h} ensure=absent" # so that fqdn works + + vm.vm.provision 'shell', inline: "puppet resource host #{vip_hostname}.#{domain} ip=#{vip_ip} host_aliases=#{vip_hostname} ensure=present" + vm.vm.provision 'shell', inline: "puppet resource host #{puppet_hostname}.#{domain} ip=#{puppet_ip} host_aliases=#{puppet_hostname} ensure=present" + #vm.vm.provision 'shell', inline: "[ ! -e /root/puppet-cert-is-clean ] && ssh -o 'StrictHostKeyChecking=no' #{puppet_hostname} puppet cert clean #{h}.#{domain} ; touch /root/puppet-cert-is-clean" + # hosts entries for all hosts + (1..count).each do |j| + oh = "ipa#{j}" + oip = range[offset+j].to_s # eg: "192.168.142.#{100+i}" + vm.vm.provision 'shell', inline: "puppet resource host #{oh}.#{domain} ip=#{oip} host_aliases=#{oh} ensure=present" + end + + # hosts entries for all clients + (1..clients).each do |j| + oh = "client#{j}" + oip = range[offset+count+j].to_s + vm.vm.provision 'shell', inline: "puppet resource host #{oh}.#{domain} ip=#{oip} host_aliases=#{oh} ensure=present" + end + end + # + # puppet (agent) + # + vm.vm.provision :puppet_server do |puppet| + #puppet.puppet_node = "#{h}" # redundant + #puppet.puppet_server = "#{puppet_hostname}.#{domain}" + puppet.puppet_server = puppet_hostname + #puppet.options = '--verbose --debug' + puppet.options = '--test' # see the output + puppet.facter = { + 'vagrant' => '1', + 'vagrant_ipa_vip' => vip_ip, + 'vagrant_ipa_vip_fqdn' => "#{vip_hostname}.#{domain}", + 'vagrant_ipa_firewall' => firewall ? 'true' : 'false', + 'vagrant_ipa_topology' => topology.to_s, + 'vagrant_ipa_recipient' => recipient.to_s, + 'vagrant_ipa_version' => version, + } + end + + vm.vm.provider :libvirt do |libvirt| + # add additional disks to the os + #(1..disks).each do |j| # if disks is 0, this passes :) + # #print "disk: #{j}" + # libvirt.storage :file, + # #:path => '', # auto! + # #:device => 'vdb', # auto! + # #:size => '10G', # auto! + # :type => 'qcow2' + # + #end + end + end + end + + # + # client + # + (1..clients).each do |i| + h = "client#{i}" + ip = range[offset+count+i].to_s + #fqdn = "ipa#{i}.#{domain}" + fvy = File.join(projectdir, '.vagrant', "#{h}-hosts.done") + if destroy.is_a?(TrueClass) or (destroy.is_a?(Array) and destroy.include?(h)) + if File.exists?(fvy) # safety + puts "Unlocking shell provisioning for: #{h}..." + File.delete(fvy) # delete hosts token + end + end + + if snoop.include?(h) # should we clean this machine? + cmd = "puppet cert clean #{h}.#{domain}" + puts "Running 'puppet cert clean' for: #{h}..." + `vagrant ssh #{puppet_hostname} -c 'sudo #{cmd}'` + cmd = "puppet node deactivate #{h}.#{domain}" + puts "Running 'puppet node deactivate' for: #{h}..." + `vagrant ssh #{puppet_hostname} -c 'sudo #{cmd}'` + end + + config.vm.define h.to_sym do |vm| + vm.vm.hostname = h + # red herring network so that management happens here... + vm.vm.network :private_network, + :ip => range2[offset2+count+i].to_s, + :libvirt__netmask => netmask2, + :libvirt__network_name => 'default' + + # this is the real network that we'll use... + vm.vm.network :private_network, + :ip => ip, + :libvirt__dhcp_enabled => false, + :libvirt__network_name => 'ipa' + + #vm.landrush.host h, ip # TODO ? + + # + # shell + # + if not File.exists?(fvy) # only modify /etc/hosts once + if provision.is_a?(TrueClass) or (provision.is_a?(Array) and provision.include?(h)) + File.open(fvy, 'w') {} # touch + end + vm.vm.provision 'shell', inline: 'puppet resource host localhost.localdomain ip=127.0.0.1 host_aliases=localhost' + vm.vm.provision 'shell', inline: "puppet resource host #{h} ensure=absent" # so that fqdn works + + vm.vm.provision 'shell', inline: "puppet resource host #{vip_hostname}.#{domain} ip=#{vip_ip} host_aliases=#{vip_hostname} ensure=present" + vm.vm.provision 'shell', inline: "puppet resource host #{puppet_hostname}.#{domain} ip=#{puppet_ip} host_aliases=#{puppet_hostname} ensure=present" + # hosts entries for all hosts + (1..count).each do |j| + oh = "ipa#{j}" + oip = range[offset+j].to_s # eg: "192.168.142.#{100+i}" + vm.vm.provision 'shell', inline: "puppet resource host #{oh}.#{domain} ip=#{oip} host_aliases=#{oh} ensure=present" + end + + # hosts entries for all clients + (1..clients).each do |j| + oh = "client#{j}" + oip = range[offset+count+j].to_s + vm.vm.provision 'shell', inline: "puppet resource host #{oh}.#{domain} ip=#{oip} host_aliases=#{oh} ensure=present" + end + end + # + # puppet (agent) + # + vm.vm.provision :puppet_server do |puppet| + #puppet.puppet_node = "#{h}" # redundant + #puppet.puppet_server = "#{puppet_hostname}.#{domain}" + puppet.puppet_server = puppet_hostname + #puppet.options = '--verbose --debug' + puppet.options = '--test' # see the output + puppet.facter = { + 'vagrant' => '1', + 'vagrant_ipa_vip' => vip_ip, + 'vagrant_ipa_vip_fqdn' => "#{vip_hostname}.#{domain}", + 'vagrant_ipa_firewall' => firewall ? 'true' : 'false', + 'vagrant_ipa_version' => version, + } + end + end + end + + # + # libvirt + # + config.vm.provider :libvirt do |libvirt| + libvirt.driver = 'kvm' # needed for kvm performance benefits ! + # leave out to connect directly with qemu:///system + #libvirt.host = 'localhost' + libvirt.connect_via_ssh = false + libvirt.username = 'root' + libvirt.storage_pool_name = 'default' + #libvirt.default_network = 'default' # XXX: this does nothing + libvirt.default_prefix = 'ipa' # set a prefix for your vm's... + end + +end + diff --git a/ipa/vagrant/puppet/files/README b/ipa/vagrant/puppet/files/README new file mode 100644 index 000000000..ae49d54bf --- /dev/null +++ b/ipa/vagrant/puppet/files/README @@ -0,0 +1,2 @@ +This is Puppet-Ipa+Vagrant! (https://ttboj.wordpress.com/) + diff --git a/ipa/vagrant/puppet/hiera.yaml b/ipa/vagrant/puppet/hiera.yaml new file mode 100644 index 000000000..5aaf25d18 --- /dev/null +++ b/ipa/vagrant/puppet/hiera.yaml @@ -0,0 +1,7 @@ +--- +:backends: + - yaml +:yaml: + :datadir: /etc/puppet/hieradata/ +:hierarchy: + - common diff --git a/ipa/vagrant/puppet/hieradata/common.yaml b/ipa/vagrant/puppet/hieradata/common.yaml new file mode 100644 index 000000000..03416fb3d --- /dev/null +++ b/ipa/vagrant/puppet/hieradata/common.yaml @@ -0,0 +1,3 @@ +--- +welcome: 'This is Puppet-Ipa+Vagrant! (https://ttboj.wordpress.com/)' +# vim:expandtab ts=8 sw=8 sta diff --git a/ipa/vagrant/puppet/manifests/site.pp b/ipa/vagrant/puppet/manifests/site.pp new file mode 100644 index 000000000..aa788acab --- /dev/null +++ b/ipa/vagrant/puppet/manifests/site.pp @@ -0,0 +1,166 @@ +node default { + # this will get put on every host... + $url = 'https://ttboj.wordpress.com/' + file { '/etc/motd': + content => "This is Puppet-Ipa+Vagrant! (${url})\n", + } +} + +# puppetmaster +node puppet inherits default { + + if "${::vagrant_ipa_firewall}" != 'false' { + include firewall + } + + $allow = split("${::vagrant_ipa_allow}", ',') # ip list fact + + class { '::puppet::server': + pluginsync => true, # do we want to enable pluginsync? + storeconfigs => true, # do we want to enable storeconfigs? + autosign => [ + '*', # FIXME: this is a temporary solution + #"*.${domain}", # FIXME: this is a temporary solution + ], + #allow_duplicate_certs => true, # redeploy without cert clean + allow => $allow, # also used in fileserver.conf + repo => true, # automatic repos + shorewall => "${::vagrant_ipa_firewall}" ? { + 'false' => false, + default => true, + }, + start => true, + } + + class { '::puppet::deploy': + path => '/vagrant/puppet/', # puppet folder is put here... + backup => false, # don't use puppet to backup... + } +} + +node /^ipa\d+$/ inherits default { # ipa{1,2,..N} + + if "${::vagrant_ipa_firewall}" != 'false' { + include firewall + } + + class { '::puppet::client': + #start => true, + start => false, # useful for testing manually... + } + + if "${::vagrant_ipa_recipient}" == '' { + # if no recipient is specified, we use a password of 'password' + warning("The IPA recipient is empty. This is unsafe!") + } + + $domain = $::domain + class { '::ipa::server': + domain => "${domain}", + vip => "${::vagrant_ipa_vip}", + topology => "${::vagrant_ipa_topology}" ? { + '' => undef, + default => "${::vagrant_ipa_topology}", + }, + dm_password => "${::vagrant_ipa_recipient}" ? { + '' => 'password', # unsafe !!! + default => undef, + }, + admin_password => "${::vagrant_ipa_recipient}" ? { + '' => 'password', # unsafe !!! + default => undef, + }, + # NOTE: email must exist in the public key if we use gpg_sendemail + #email => 'root@example.com', + gpg_recipient => "${::vagrant_ipa_recipient}" ? { + '' => undef, + default => "${::vagrant_ipa_recipient}", + }, + #gpg_publickey => '', + gpg_keyserver => 'hkp://keys.gnupg.net', # TODO: variable + gpg_sendemail => false, + vrrp => true, + shorewall => "${::vagrant_ipa_firewall}" ? { + 'false' => false, + default => true, + }, + } + +} + +node /^client\d+$/ inherits default { # client{1,2,..N} + + if "${::vagrant_ipa_firewall}" != 'false' { + include firewall + } + + class { '::puppet::client': + #start => true, + start => false, # useful for testing manually... + } + +} + +class firewall { + + $FW = '$FW' # make using $FW in shorewall easier + + class { '::shorewall::configuration': + # NOTE: no configuration specifics are needed at the moment + } + + shorewall::zone { ['net', 'man']: + type => 'ipv4', + options => [], # these aren't really needed right now + } + + # management zone interface used by vagrant-libvirt + shorewall::interface { 'man': + interface => 'MAN_IF', + broadcast => 'detect', + physical => 'eth0', # XXX: set manually! + options => ['dhcp', 'tcpflags', 'routefilter', 'nosmurfs', 'logmartians'], + comment => 'Management zone.', # FIXME: verify options + } + + # XXX: eth1 'dummy' zone to trick vagrant-libvirt into leaving me alone + # + + # net zone that ipa uses to communicate + shorewall::interface { 'net': + interface => 'NET_IF', + broadcast => 'detect', + physical => 'eth2', # XXX: set manually! + options => ['tcpflags', 'routefilter', 'nosmurfs', 'logmartians'], + comment => 'Public internet zone.', # FIXME: verify options + } + + # TODO: is this policy really what we want ? can we try to limit this ? + shorewall::policy { '$FW-net': + policy => 'ACCEPT', # TODO: shouldn't we whitelist? + } + + shorewall::policy { '$FW-man': + policy => 'ACCEPT', # TODO: shouldn't we whitelist? + } + + #################################################################### + #ACTION SOURCE DEST PROTO DEST SOURCE ORIGINAL + # PORT PORT(S) DEST + shorewall::rule { 'ssh': rule => " + SSH/ACCEPT net $FW + SSH/ACCEPT man $FW + ", comment => 'Allow SSH'} + + shorewall::rule { 'ping': rule => " + #Ping/DROP net $FW + Ping/ACCEPT net $FW + Ping/ACCEPT man $FW + ", comment => 'Allow ping from the `bad` net zone'} + + shorewall::rule { 'icmp': rule => " + ACCEPT $FW net icmp + ACCEPT $FW man icmp + ", comment => 'Allow icmp from the firewall zone'} +} + diff --git a/ipa/vagrant/puppet/modules/.gitignore b/ipa/vagrant/puppet/modules/.gitignore new file mode 100644 index 000000000..bf8c6ef16 --- /dev/null +++ b/ipa/vagrant/puppet/modules/.gitignore @@ -0,0 +1 @@ +ipa/ diff --git a/ipa/vagrant/puppet/modules/Makefile b/ipa/vagrant/puppet/modules/Makefile new file mode 100644 index 000000000..3737d497a --- /dev/null +++ b/ipa/vagrant/puppet/modules/Makefile @@ -0,0 +1,63 @@ +# Makefile for pulling in git modules for Vagrant deployment for Puppet-Ipa +# Copyright (C) 2010-2013+ James Shubin +# Written by James Shubin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# NOTE: if we remove a module, it won't get purged from the destination! + +# NOTE: this script can sync puppet-ipa to a specific sha1sum commit, or it +# can sync all of the repos to git master. This option can be useful for devel. + +BASE = 'https://github.com/purpleidea' +MODULES := \ + puppet-ipa \ + puppet-module-data \ + puppet-puppet \ + puppet-shorewall \ + puppetlabs-stdlib +# NOTE: set to a git commit id if we need an specific commit for vagrant builds +# NOTE: remember that new commits to master should change this to a specific id +# if they will break the vagrant build process. hopefully we don't forget this! +#SHA1SUM := master +SHA1SUM := $(shell git rev-parse --verify HEAD) # goto whatever the main tree is at + +.PHONY: all modules ipa +.SILENT: all modules ipa + +all: + +# +# modules +# +# clone, and then pull +modules: + basename `pwd` | grep -q '^modules' || exit 1 # run in a modules dir! + for i in $(MODULES); do \ + j=`echo $$i | awk -F '-' '{print $$2}'`; \ + [ -d "$$j" ] || git clone --depth 1 $(BASE)/$$i.git $$j; \ + [ -d "$$j" ] && cd $$j && git pull; cd ..; \ + done + +# +# ipa +# +# just clone and pull this one +ipa: + basename `pwd` | grep -q '^modules' || exit 1 # run in a modules dir! + i='puppet-ipa'; \ + j=`echo $$i | awk -F '-' '{print $$2}'`; \ + [ -d "$$j" ] || git clone ../../../. $$j; \ + [ -d "$$j" ] && cd $$j && git checkout master && git pull && git checkout $(SHA1SUM); cd .. + diff --git a/ipa/vagrant/puppet/modules/keepalived b/ipa/vagrant/puppet/modules/keepalived new file mode 160000 index 000000000..4e3609580 --- /dev/null +++ b/ipa/vagrant/puppet/modules/keepalived @@ -0,0 +1 @@ +Subproject commit 4e3609580f9e2e2e73a386f1dd52d83dd1b37b84 diff --git a/ipa/vagrant/puppet/modules/module-data b/ipa/vagrant/puppet/modules/module-data new file mode 160000 index 000000000..4b3ad1cc2 --- /dev/null +++ b/ipa/vagrant/puppet/modules/module-data @@ -0,0 +1 @@ +Subproject commit 4b3ad1cc239d7831616e69796184e400de0f5fe4 diff --git a/ipa/vagrant/puppet/modules/puppet b/ipa/vagrant/puppet/modules/puppet new file mode 160000 index 000000000..f139d0b7c --- /dev/null +++ b/ipa/vagrant/puppet/modules/puppet @@ -0,0 +1 @@ +Subproject commit f139d0b7cfe6d55c0848d0d338e19fe640a961f2 diff --git a/ipa/vagrant/puppet/modules/shorewall b/ipa/vagrant/puppet/modules/shorewall new file mode 160000 index 000000000..fbc7c6576 --- /dev/null +++ b/ipa/vagrant/puppet/modules/shorewall @@ -0,0 +1 @@ +Subproject commit fbc7c6576092ceaf81b837989e086cb7fcc071d8 diff --git a/ipa/vagrant/puppet/modules/ssh b/ipa/vagrant/puppet/modules/ssh new file mode 160000 index 000000000..0063501b6 --- /dev/null +++ b/ipa/vagrant/puppet/modules/ssh @@ -0,0 +1 @@ +Subproject commit 0063501b6b8d58961b7b7273780973d95fd6d2e3 diff --git a/ipa/vagrant/puppet/modules/stdlib b/ipa/vagrant/puppet/modules/stdlib new file mode 160000 index 000000000..44c181ec0 --- /dev/null +++ b/ipa/vagrant/puppet/modules/stdlib @@ -0,0 +1 @@ +Subproject commit 44c181ec0e230768b8dce10de57f9b32638e66e1 diff --git a/ipa/vagrant/puppet/modules/yum b/ipa/vagrant/puppet/modules/yum new file mode 160000 index 000000000..d098f6de9 --- /dev/null +++ b/ipa/vagrant/puppet/modules/yum @@ -0,0 +1 @@ +Subproject commit d098f6de9a38055b3482325d371722835b576976