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