propellor 0.5.1 → 0.5.3
raw patch · 133 files changed
+6869/−7254 lines, 133 filesnew-component:exe:wrapperPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
- Propellor.Property.SiteSpecific.GitAnnexBuilder: builder :: Architecture -> CronTimes -> Bool -> Property
+ Propellor.Property: doNothing :: Property
+ Propellor.Property.Docker: expose :: String -> Property
+ Propellor.Property.HostingProvider.CloudAtCost: decruft :: Property
+ Propellor.Property.HostingProvider.DigitalOcean: distroKernel :: Property
+ Propellor.Property.SiteSpecific.GitAnnexBuilder: androidAutoBuilderContainer :: (System -> Image) -> CronTimes -> TimeOut -> Host
+ Propellor.Property.SiteSpecific.GitAnnexBuilder: androidContainer :: (System -> Image) -> ContainerName -> Property -> FilePath -> Host
+ Propellor.Property.SiteSpecific.GitAnnexBuilder: armelAutoBuilderContainer :: (System -> Image) -> CronTimes -> TimeOut -> Host
+ Propellor.Property.SiteSpecific.GitAnnexBuilder: armelCompanionContainer :: (System -> Image) -> Host
+ Propellor.Property.SiteSpecific.GitAnnexBuilder: autobuilder :: CronTimes -> TimeOut -> Bool -> Property
+ Propellor.Property.SiteSpecific.GitAnnexBuilder: buildDepsApt :: Property
+ Propellor.Property.SiteSpecific.GitAnnexBuilder: buildDepsFewHaskellLibs :: Property
+ Propellor.Property.SiteSpecific.GitAnnexBuilder: buildDepsNoHaskellLibs :: Property
+ Propellor.Property.SiteSpecific.GitAnnexBuilder: cabalDeps :: Property
+ Propellor.Property.SiteSpecific.GitAnnexBuilder: standardAutoBuilderContainer :: (System -> Image) -> Architecture -> Int -> TimeOut -> Host
+ Propellor.Property.SiteSpecific.GitAnnexBuilder: tree :: Architecture -> Property
+ Propellor.Property.SiteSpecific.GitAnnexBuilder: type TimeOut = String
+ Propellor.Property.SiteSpecific.JoeySites: ircBouncer :: Property
Files
- CHANGELOG +21/−0
- GPL +0/−674
- LICENSE +22/−0
- Makefile +4/−2
- Propellor.hs +0/−77
- Propellor/Attr.hs +0/−111
- Propellor/CmdLine.hs +0/−383
- Propellor/Engine.hs +0/−37
- Propellor/Exception.hs +0/−18
- Propellor/Message.hs +0/−51
- Propellor/PrivData.hs +0/−91
- Propellor/Property.hs +0/−163
- Propellor/Property/Apache.hs +0/−62
- Propellor/Property/Apt.hs +0/−256
- Propellor/Property/Cmd.hs +0/−49
- Propellor/Property/Cron.hs +0/−49
- Propellor/Property/Dns.hs +0/−405
- Propellor/Property/Docker.hs +0/−459
- Propellor/Property/Docker/Shim.hs +0/−61
- Propellor/Property/File.hs +0/−94
- Propellor/Property/Git.hs +0/−89
- Propellor/Property/Gpg.hs +0/−41
- Propellor/Property/Hostname.hs +0/−33
- Propellor/Property/Network.hs +0/−30
- Propellor/Property/Obnam.hs +0/−155
- Propellor/Property/OpenId.hs +0/−29
- Propellor/Property/Postfix.hs +0/−25
- Propellor/Property/Reboot.hs +0/−7
- Propellor/Property/Scheduled.hs +0/−67
- Propellor/Property/Service.hs +0/−31
- Propellor/Property/SiteSpecific/GitAnnexBuilder.hs +0/−57
- Propellor/Property/SiteSpecific/GitHome.hs +0/−34
- Propellor/Property/SiteSpecific/JoeySites.hs +0/−295
- Propellor/Property/Ssh.hs +0/−152
- Propellor/Property/Sudo.hs +0/−32
- Propellor/Property/Tor.hs +0/−19
- Propellor/Property/User.hs +0/−61
- Propellor/SimpleSh.hs +0/−97
- Propellor/Types.hs +0/−153
- Propellor/Types/Attr.hs +0/−48
- Propellor/Types/Dns.hs +0/−92
- Propellor/Types/OS.hs +0/−27
- README.md +6/−4
- Utility/Applicative.hs +0/−16
- Utility/Data.hs +0/−17
- Utility/Directory.hs +0/−135
- Utility/Env.hs +0/−81
- Utility/Exception.hs +0/−59
- Utility/FileMode.hs +0/−157
- Utility/FileSystemEncoding.hs +0/−132
- Utility/LinuxMkLibs.hs +0/−61
- Utility/Misc.hs +0/−148
- Utility/Monad.hs +0/−69
- Utility/PartialPrelude.hs +0/−68
- Utility/Path.hs +0/−293
- Utility/PosixFiles.hs +0/−33
- Utility/Process.hs +0/−360
- Utility/QuickCheck.hs +0/−52
- Utility/SafeCommand.hs +0/−120
- Utility/Scheduled.hs +0/−396
- Utility/ThreadScheduler.hs +0/−73
- Utility/Tmp.hs +0/−100
- Utility/UserInfo.hs +0/−55
- config-joey.hs +42/−57
- config.hs +0/−46
- debian/changelog +21/−0
- debian/copyright +22/−5
- debian/lintian-overrides +0/−1
- doc/README.mdwn +6/−4
- propellor.cabal +10/−5
- propellor.hs +0/−91
- src/Propellor.hs +77/−0
- src/Propellor/Attr.hs +111/−0
- src/Propellor/CmdLine.hs +392/−0
- src/Propellor/Engine.hs +37/−0
- src/Propellor/Exception.hs +18/−0
- src/Propellor/Message.hs +51/−0
- src/Propellor/PrivData.hs +91/−0
- src/Propellor/Property.hs +166/−0
- src/Propellor/Property/Apache.hs +62/−0
- src/Propellor/Property/Apt.hs +256/−0
- src/Propellor/Property/Cmd.hs +49/−0
- src/Propellor/Property/Cron.hs +49/−0
- src/Propellor/Property/Dns.hs +405/−0
- src/Propellor/Property/Docker.hs +460/−0
- src/Propellor/Property/Docker/Shim.hs +61/−0
- src/Propellor/Property/File.hs +94/−0
- src/Propellor/Property/Git.hs +93/−0
- src/Propellor/Property/Gpg.hs +41/−0
- src/Propellor/Property/HostingProvider/CloudAtCost.hs +24/−0
- src/Propellor/Property/HostingProvider/DigitalOcean.hs +21/−0
- src/Propellor/Property/Hostname.hs +33/−0
- src/Propellor/Property/Network.hs +30/−0
- src/Propellor/Property/Obnam.hs +155/−0
- src/Propellor/Property/OpenId.hs +29/−0
- src/Propellor/Property/Postfix.hs +25/−0
- src/Propellor/Property/Reboot.hs +7/−0
- src/Propellor/Property/Scheduled.hs +67/−0
- src/Propellor/Property/Service.hs +31/−0
- src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs +176/−0
- src/Propellor/Property/SiteSpecific/GitHome.hs +34/−0
- src/Propellor/Property/SiteSpecific/JoeySites.hs +319/−0
- src/Propellor/Property/Ssh.hs +157/−0
- src/Propellor/Property/Sudo.hs +32/−0
- src/Propellor/Property/Tor.hs +19/−0
- src/Propellor/Property/User.hs +61/−0
- src/Propellor/SimpleSh.hs +101/−0
- src/Propellor/Types.hs +154/−0
- src/Propellor/Types/Attr.hs +48/−0
- src/Propellor/Types/Dns.hs +92/−0
- src/Propellor/Types/OS.hs +27/−0
- src/Utility/Applicative.hs +16/−0
- src/Utility/Data.hs +17/−0
- src/Utility/Directory.hs +135/−0
- src/Utility/Env.hs +81/−0
- src/Utility/Exception.hs +59/−0
- src/Utility/FileMode.hs +158/−0
- src/Utility/FileSystemEncoding.hs +132/−0
- src/Utility/LinuxMkLibs.hs +61/−0
- src/Utility/Misc.hs +148/−0
- src/Utility/Monad.hs +69/−0
- src/Utility/PartialPrelude.hs +68/−0
- src/Utility/Path.hs +293/−0
- src/Utility/PosixFiles.hs +33/−0
- src/Utility/Process.hs +353/−0
- src/Utility/QuickCheck.hs +52/−0
- src/Utility/SafeCommand.hs +120/−0
- src/Utility/Scheduled.hs +396/−0
- src/Utility/ThreadScheduler.hs +75/−0
- src/Utility/Tmp.hs +100/−0
- src/Utility/UserInfo.hs +55/−0
- src/config.hs +46/−0
- src/wrapper.hs +93/−0
CHANGELOG view
@@ -1,3 +1,24 @@+propellor (0.5.3) unstable; urgency=medium++ * Fix unattended-upgrades config for !stable.+ * Ensure that kernel hostname is same as /etc/hostname when configuring+ hostname.+ * Added modules for some hosting providers (DigitalOcean, CloudAtCost).++ -- Joey Hess <joeyh@debian.org> Thu, 29 May 2014 14:29:53 -0400++propellor (0.5.2) unstable; urgency=medium++ * A bug that caused propellor to hang when updating a running docker+ container appears to have been fixed. Note that since it affects+ the propellor process that serves as "init" of docker containers,+ they have to be restarted for the fix to take effect.+ * Licence changed from GPL to BSD.+ * A few changes to allow building Propellor on OSX. One user reports+ successfully using it there.++ -- Joey Hess <joeyh@debian.org> Sat, 17 May 2014 16:42:55 -0400+ propellor (0.5.1) unstable; urgency=medium * Primary DNS servers now have allow-transfer automatically populated
− GPL
@@ -1,674 +0,0 @@- GNU GENERAL PUBLIC LICENSE- Version 3, 29 June 2007-- Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>- Everyone is permitted to copy and distribute verbatim copies- of this license document, but changing it is not allowed.-- Preamble-- The GNU General Public License is a free, copyleft license for-software and other kinds of works.-- The licenses for most software and other practical works are designed-to take away your freedom to share and change the works. By contrast,-the GNU General Public License is intended to guarantee your freedom to-share and change all versions of a program--to make sure it remains free-software for all its users. We, the Free Software Foundation, use the-GNU General Public License for most of our software; it applies also to-any other work released this way by its authors. You can apply it to-your programs, too.-- When we speak of free software, we are referring to freedom, not-price. Our General Public Licenses are designed to make sure that you-have the freedom to distribute copies of free software (and charge for-them if you wish), that you receive source code or can get it if you-want it, that you can change the software or use pieces of it in new-free programs, and that you know you can do these things.-- To protect your rights, we need to prevent others from denying you-these rights or asking you to surrender the rights. Therefore, you have-certain responsibilities if you distribute copies of the software, or if-you modify it: responsibilities to respect the freedom of others.-- For example, if you distribute copies of such a program, whether-gratis or for a fee, you must pass on to the recipients the same-freedoms that you received. You must make sure that they, too, receive-or can get the source code. And you must show them these terms so they-know their rights.-- Developers that use the GNU GPL protect your rights with two steps:-(1) assert copyright on the software, and (2) offer you this License-giving you legal permission to copy, distribute and/or modify it.-- For the developers' and authors' protection, the GPL clearly explains-that there is no warranty for this free software. For both users' and-authors' sake, the GPL requires that modified versions be marked as-changed, so that their problems will not be attributed erroneously to-authors of previous versions.-- Some devices are designed to deny users access to install or run-modified versions of the software inside them, although the manufacturer-can do so. This is fundamentally incompatible with the aim of-protecting users' freedom to change the software. The systematic-pattern of such abuse occurs in the area of products for individuals to-use, which is precisely where it is most unacceptable. Therefore, we-have designed this version of the GPL to prohibit the practice for those-products. If such problems arise substantially in other domains, we-stand ready to extend this provision to those domains in future versions-of the GPL, as needed to protect the freedom of users.-- Finally, every program is threatened constantly by software patents.-States should not allow patents to restrict development and use of-software on general-purpose computers, but in those that do, we wish to-avoid the special danger that patents applied to a free program could-make it effectively proprietary. To prevent this, the GPL assures that-patents cannot be used to render the program non-free.-- The precise terms and conditions for copying, distribution and-modification follow.-- TERMS AND CONDITIONS-- 0. Definitions.-- "This License" refers to version 3 of the GNU General Public License.-- "Copyright" also means copyright-like laws that apply to other kinds of-works, such as semiconductor masks.-- "The Program" refers to any copyrightable work licensed under this-License. Each licensee is addressed as "you". "Licensees" and-"recipients" may be individuals or organizations.-- To "modify" a work means to copy from or adapt all or part of the work-in a fashion requiring copyright permission, other than the making of an-exact copy. The resulting work is called a "modified version" of the-earlier work or a work "based on" the earlier work.-- A "covered work" means either the unmodified Program or a work based-on the Program.-- To "propagate" a work means to do anything with it that, without-permission, would make you directly or secondarily liable for-infringement under applicable copyright law, except executing it on a-computer or modifying a private copy. Propagation includes copying,-distribution (with or without modification), making available to the-public, and in some countries other activities as well.-- To "convey" a work means any kind of propagation that enables other-parties to make or receive copies. Mere interaction with a user through-a computer network, with no transfer of a copy, is not conveying.-- An interactive user interface displays "Appropriate Legal Notices"-to the extent that it includes a convenient and prominently visible-feature that (1) displays an appropriate copyright notice, and (2)-tells the user that there is no warranty for the work (except to the-extent that warranties are provided), that licensees may convey the-work under this License, and how to view a copy of this License. If-the interface presents a list of user commands or options, such as a-menu, a prominent item in the list meets this criterion.-- 1. Source Code.-- The "source code" for a work means the preferred form of the work-for making modifications to it. "Object code" means any non-source-form of a work.-- A "Standard Interface" means an interface that either is an official-standard defined by a recognized standards body, or, in the case of-interfaces specified for a particular programming language, one that-is widely used among developers working in that language.-- The "System Libraries" of an executable work include anything, other-than the work as a whole, that (a) is included in the normal form of-packaging a Major Component, but which is not part of that Major-Component, and (b) serves only to enable use of the work with that-Major Component, or to implement a Standard Interface for which an-implementation is available to the public in source code form. A-"Major Component", in this context, means a major essential component-(kernel, window system, and so on) of the specific operating system-(if any) on which the executable work runs, or a compiler used to-produce the work, or an object code interpreter used to run it.-- The "Corresponding Source" for a work in object code form means all-the source code needed to generate, install, and (for an executable-work) run the object code and to modify the work, including scripts to-control those activities. However, it does not include the work's-System Libraries, or general-purpose tools or generally available free-programs which are used unmodified in performing those activities but-which are not part of the work. For example, Corresponding Source-includes interface definition files associated with source files for-the work, and the source code for shared libraries and dynamically-linked subprograms that the work is specifically designed to require,-such as by intimate data communication or control flow between those-subprograms and other parts of the work.-- The Corresponding Source need not include anything that users-can regenerate automatically from other parts of the Corresponding-Source.-- The Corresponding Source for a work in source code form is that-same work.-- 2. Basic Permissions.-- All rights granted under this License are granted for the term of-copyright on the Program, and are irrevocable provided the stated-conditions are met. This License explicitly affirms your unlimited-permission to run the unmodified Program. The output from running a-covered work is covered by this License only if the output, given its-content, constitutes a covered work. This License acknowledges your-rights of fair use or other equivalent, as provided by copyright law.-- You may make, run and propagate covered works that you do not-convey, without conditions so long as your license otherwise remains-in force. You may convey covered works to others for the sole purpose-of having them make modifications exclusively for you, or provide you-with facilities for running those works, provided that you comply with-the terms of this License in conveying all material for which you do-not control copyright. Those thus making or running the covered works-for you must do so exclusively on your behalf, under your direction-and control, on terms that prohibit them from making any copies of-your copyrighted material outside their relationship with you.-- Conveying under any other circumstances is permitted solely under-the conditions stated below. Sublicensing is not allowed; section 10-makes it unnecessary.-- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.-- No covered work shall be deemed part of an effective technological-measure under any applicable law fulfilling obligations under article-11 of the WIPO copyright treaty adopted on 20 December 1996, or-similar laws prohibiting or restricting circumvention of such-measures.-- When you convey a covered work, you waive any legal power to forbid-circumvention of technological measures to the extent such circumvention-is effected by exercising rights under this License with respect to-the covered work, and you disclaim any intention to limit operation or-modification of the work as a means of enforcing, against the work's-users, your or third parties' legal rights to forbid circumvention of-technological measures.-- 4. Conveying Verbatim Copies.-- You may convey verbatim copies of the Program's source code as you-receive it, in any medium, provided that you conspicuously and-appropriately publish on each copy an appropriate copyright notice;-keep intact all notices stating that this License and any-non-permissive terms added in accord with section 7 apply to the code;-keep intact all notices of the absence of any warranty; and give all-recipients a copy of this License along with the Program.-- You may charge any price or no price for each copy that you convey,-and you may offer support or warranty protection for a fee.-- 5. Conveying Modified Source Versions.-- You may convey a work based on the Program, or the modifications to-produce it from the Program, in the form of source code under the-terms of section 4, provided that you also meet all of these conditions:-- a) The work must carry prominent notices stating that you modified- it, and giving a relevant date.-- b) The work must carry prominent notices stating that it is- released under this License and any conditions added under section- 7. This requirement modifies the requirement in section 4 to- "keep intact all notices".-- c) You must license the entire work, as a whole, under this- License to anyone who comes into possession of a copy. This- License will therefore apply, along with any applicable section 7- additional terms, to the whole of the work, and all its parts,- regardless of how they are packaged. This License gives no- permission to license the work in any other way, but it does not- invalidate such permission if you have separately received it.-- d) If the work has interactive user interfaces, each must display- Appropriate Legal Notices; however, if the Program has interactive- interfaces that do not display Appropriate Legal Notices, your- work need not make them do so.-- A compilation of a covered work with other separate and independent-works, which are not by their nature extensions of the covered work,-and which are not combined with it such as to form a larger program,-in or on a volume of a storage or distribution medium, is called an-"aggregate" if the compilation and its resulting copyright are not-used to limit the access or legal rights of the compilation's users-beyond what the individual works permit. Inclusion of a covered work-in an aggregate does not cause this License to apply to the other-parts of the aggregate.-- 6. Conveying Non-Source Forms.-- You may convey a covered work in object code form under the terms-of sections 4 and 5, provided that you also convey the-machine-readable Corresponding Source under the terms of this License,-in one of these ways:-- a) Convey the object code in, or embodied in, a physical product- (including a physical distribution medium), accompanied by the- Corresponding Source fixed on a durable physical medium- customarily used for software interchange.-- b) Convey the object code in, or embodied in, a physical product- (including a physical distribution medium), accompanied by a- written offer, valid for at least three years and valid for as- long as you offer spare parts or customer support for that product- model, to give anyone who possesses the object code either (1) a- copy of the Corresponding Source for all the software in the- product that is covered by this License, on a durable physical- medium customarily used for software interchange, for a price no- more than your reasonable cost of physically performing this- conveying of source, or (2) access to copy the- Corresponding Source from a network server at no charge.-- c) Convey individual copies of the object code with a copy of the- written offer to provide the Corresponding Source. This- alternative is allowed only occasionally and noncommercially, and- only if you received the object code with such an offer, in accord- with subsection 6b.-- d) Convey the object code by offering access from a designated- place (gratis or for a charge), and offer equivalent access to the- Corresponding Source in the same way through the same place at no- further charge. You need not require recipients to copy the- Corresponding Source along with the object code. If the place to- copy the object code is a network server, the Corresponding Source- may be on a different server (operated by you or a third party)- that supports equivalent copying facilities, provided you maintain- clear directions next to the object code saying where to find the- Corresponding Source. Regardless of what server hosts the- Corresponding Source, you remain obligated to ensure that it is- available for as long as needed to satisfy these requirements.-- e) Convey the object code using peer-to-peer transmission, provided- you inform other peers where the object code and Corresponding- Source of the work are being offered to the general public at no- charge under subsection 6d.-- A separable portion of the object code, whose source code is excluded-from the Corresponding Source as a System Library, need not be-included in conveying the object code work.-- A "User Product" is either (1) a "consumer product", which means any-tangible personal property which is normally used for personal, family,-or household purposes, or (2) anything designed or sold for incorporation-into a dwelling. In determining whether a product is a consumer product,-doubtful cases shall be resolved in favor of coverage. For a particular-product received by a particular user, "normally used" refers to a-typical or common use of that class of product, regardless of the status-of the particular user or of the way in which the particular user-actually uses, or expects or is expected to use, the product. A product-is a consumer product regardless of whether the product has substantial-commercial, industrial or non-consumer uses, unless such uses represent-the only significant mode of use of the product.-- "Installation Information" for a User Product means any methods,-procedures, authorization keys, or other information required to install-and execute modified versions of a covered work in that User Product from-a modified version of its Corresponding Source. The information must-suffice to ensure that the continued functioning of the modified object-code is in no case prevented or interfered with solely because-modification has been made.-- If you convey an object code work under this section in, or with, or-specifically for use in, a User Product, and the conveying occurs as-part of a transaction in which the right of possession and use of the-User Product is transferred to the recipient in perpetuity or for a-fixed term (regardless of how the transaction is characterized), the-Corresponding Source conveyed under this section must be accompanied-by the Installation Information. But this requirement does not apply-if neither you nor any third party retains the ability to install-modified object code on the User Product (for example, the work has-been installed in ROM).-- The requirement to provide Installation Information does not include a-requirement to continue to provide support service, warranty, or updates-for a work that has been modified or installed by the recipient, or for-the User Product in which it has been modified or installed. Access to a-network may be denied when the modification itself materially and-adversely affects the operation of the network or violates the rules and-protocols for communication across the network.-- Corresponding Source conveyed, and Installation Information provided,-in accord with this section must be in a format that is publicly-documented (and with an implementation available to the public in-source code form), and must require no special password or key for-unpacking, reading or copying.-- 7. Additional Terms.-- "Additional permissions" are terms that supplement the terms of this-License by making exceptions from one or more of its conditions.-Additional permissions that are applicable to the entire Program shall-be treated as though they were included in this License, to the extent-that they are valid under applicable law. If additional permissions-apply only to part of the Program, that part may be used separately-under those permissions, but the entire Program remains governed by-this License without regard to the additional permissions.-- When you convey a copy of a covered work, you may at your option-remove any additional permissions from that copy, or from any part of-it. (Additional permissions may be written to require their own-removal in certain cases when you modify the work.) You may place-additional permissions on material, added by you to a covered work,-for which you have or can give appropriate copyright permission.-- Notwithstanding any other provision of this License, for material you-add to a covered work, you may (if authorized by the copyright holders of-that material) supplement the terms of this License with terms:-- a) Disclaiming warranty or limiting liability differently from the- terms of sections 15 and 16 of this License; or-- b) Requiring preservation of specified reasonable legal notices or- author attributions in that material or in the Appropriate Legal- Notices displayed by works containing it; or-- c) Prohibiting misrepresentation of the origin of that material, or- requiring that modified versions of such material be marked in- reasonable ways as different from the original version; or-- d) Limiting the use for publicity purposes of names of licensors or- authors of the material; or-- e) Declining to grant rights under trademark law for use of some- trade names, trademarks, or service marks; or-- f) Requiring indemnification of licensors and authors of that- material by anyone who conveys the material (or modified versions of- it) with contractual assumptions of liability to the recipient, for- any liability that these contractual assumptions directly impose on- those licensors and authors.-- All other non-permissive additional terms are considered "further-restrictions" within the meaning of section 10. If the Program as you-received it, or any part of it, contains a notice stating that it is-governed by this License along with a term that is a further-restriction, you may remove that term. If a license document contains-a further restriction but permits relicensing or conveying under this-License, you may add to a covered work material governed by the terms-of that license document, provided that the further restriction does-not survive such relicensing or conveying.-- If you add terms to a covered work in accord with this section, you-must place, in the relevant source files, a statement of the-additional terms that apply to those files, or a notice indicating-where to find the applicable terms.-- Additional terms, permissive or non-permissive, may be stated in the-form of a separately written license, or stated as exceptions;-the above requirements apply either way.-- 8. Termination.-- You may not propagate or modify a covered work except as expressly-provided under this License. Any attempt otherwise to propagate or-modify it is void, and will automatically terminate your rights under-this License (including any patent licenses granted under the third-paragraph of section 11).-- However, if you cease all violation of this License, then your-license from a particular copyright holder is reinstated (a)-provisionally, unless and until the copyright holder explicitly and-finally terminates your license, and (b) permanently, if the copyright-holder fails to notify you of the violation by some reasonable means-prior to 60 days after the cessation.-- Moreover, your license from a particular copyright holder is-reinstated permanently if the copyright holder notifies you of the-violation by some reasonable means, this is the first time you have-received notice of violation of this License (for any work) from that-copyright holder, and you cure the violation prior to 30 days after-your receipt of the notice.-- Termination of your rights under this section does not terminate the-licenses of parties who have received copies or rights from you under-this License. If your rights have been terminated and not permanently-reinstated, you do not qualify to receive new licenses for the same-material under section 10.-- 9. Acceptance Not Required for Having Copies.-- You are not required to accept this License in order to receive or-run a copy of the Program. Ancillary propagation of a covered work-occurring solely as a consequence of using peer-to-peer transmission-to receive a copy likewise does not require acceptance. However,-nothing other than this License grants you permission to propagate or-modify any covered work. These actions infringe copyright if you do-not accept this License. Therefore, by modifying or propagating a-covered work, you indicate your acceptance of this License to do so.-- 10. Automatic Licensing of Downstream Recipients.-- Each time you convey a covered work, the recipient automatically-receives a license from the original licensors, to run, modify and-propagate that work, subject to this License. You are not responsible-for enforcing compliance by third parties with this License.-- An "entity transaction" is a transaction transferring control of an-organization, or substantially all assets of one, or subdividing an-organization, or merging organizations. If propagation of a covered-work results from an entity transaction, each party to that-transaction who receives a copy of the work also receives whatever-licenses to the work the party's predecessor in interest had or could-give under the previous paragraph, plus a right to possession of the-Corresponding Source of the work from the predecessor in interest, if-the predecessor has it or can get it with reasonable efforts.-- You may not impose any further restrictions on the exercise of the-rights granted or affirmed under this License. For example, you may-not impose a license fee, royalty, or other charge for exercise of-rights granted under this License, and you may not initiate litigation-(including a cross-claim or counterclaim in a lawsuit) alleging that-any patent claim is infringed by making, using, selling, offering for-sale, or importing the Program or any portion of it.-- 11. Patents.-- A "contributor" is a copyright holder who authorizes use under this-License of the Program or a work on which the Program is based. The-work thus licensed is called the contributor's "contributor version".-- A contributor's "essential patent claims" are all patent claims-owned or controlled by the contributor, whether already acquired or-hereafter acquired, that would be infringed by some manner, permitted-by this License, of making, using, or selling its contributor version,-but do not include claims that would be infringed only as a-consequence of further modification of the contributor version. For-purposes of this definition, "control" includes the right to grant-patent sublicenses in a manner consistent with the requirements of-this License.-- Each contributor grants you a non-exclusive, worldwide, royalty-free-patent license under the contributor's essential patent claims, to-make, use, sell, offer for sale, import and otherwise run, modify and-propagate the contents of its contributor version.-- In the following three paragraphs, a "patent license" is any express-agreement or commitment, however denominated, not to enforce a patent-(such as an express permission to practice a patent or covenant not to-sue for patent infringement). To "grant" such a patent license to a-party means to make such an agreement or commitment not to enforce a-patent against the party.-- If you convey a covered work, knowingly relying on a patent license,-and the Corresponding Source of the work is not available for anyone-to copy, free of charge and under the terms of this License, through a-publicly available network server or other readily accessible means,-then you must either (1) cause the Corresponding Source to be so-available, or (2) arrange to deprive yourself of the benefit of the-patent license for this particular work, or (3) arrange, in a manner-consistent with the requirements of this License, to extend the patent-license to downstream recipients. "Knowingly relying" means you have-actual knowledge that, but for the patent license, your conveying the-covered work in a country, or your recipient's use of the covered work-in a country, would infringe one or more identifiable patents in that-country that you have reason to believe are valid.-- If, pursuant to or in connection with a single transaction or-arrangement, you convey, or propagate by procuring conveyance of, a-covered work, and grant a patent license to some of the parties-receiving the covered work authorizing them to use, propagate, modify-or convey a specific copy of the covered work, then the patent license-you grant is automatically extended to all recipients of the covered-work and works based on it.-- A patent license is "discriminatory" if it does not include within-the scope of its coverage, prohibits the exercise of, or is-conditioned on the non-exercise of one or more of the rights that are-specifically granted under this License. You may not convey a covered-work if you are a party to an arrangement with a third party that is-in the business of distributing software, under which you make payment-to the third party based on the extent of your activity of conveying-the work, and under which the third party grants, to any of the-parties who would receive the covered work from you, a discriminatory-patent license (a) in connection with copies of the covered work-conveyed by you (or copies made from those copies), or (b) primarily-for and in connection with specific products or compilations that-contain the covered work, unless you entered into that arrangement,-or that patent license was granted, prior to 28 March 2007.-- Nothing in this License shall be construed as excluding or limiting-any implied license or other defenses to infringement that may-otherwise be available to you under applicable patent law.-- 12. No Surrender of Others' Freedom.-- If conditions are imposed on you (whether by court order, agreement or-otherwise) that contradict the conditions of this License, they do not-excuse you from the conditions of this License. If you cannot convey a-covered work so as to satisfy simultaneously your obligations under this-License and any other pertinent obligations, then as a consequence you may-not convey it at all. For example, if you agree to terms that obligate you-to collect a royalty for further conveying from those to whom you convey-the Program, the only way you could satisfy both those terms and this-License would be to refrain entirely from conveying the Program.-- 13. Use with the GNU Affero General Public License.-- Notwithstanding any other provision of this License, you have-permission to link or combine any covered work with a work licensed-under version 3 of the GNU Affero General Public License into a single-combined work, and to convey the resulting work. The terms of this-License will continue to apply to the part which is the covered work,-but the special requirements of the GNU Affero General Public License,-section 13, concerning interaction through a network will apply to the-combination as such.-- 14. Revised Versions of this License.-- The Free Software Foundation may publish revised and/or new versions of-the GNU General Public License from time to time. Such new versions will-be similar in spirit to the present version, but may differ in detail to-address new problems or concerns.-- Each version is given a distinguishing version number. If the-Program specifies that a certain numbered version of the GNU General-Public License "or any later version" applies to it, you have the-option of following the terms and conditions either of that numbered-version or of any later version published by the Free Software-Foundation. If the Program does not specify a version number of the-GNU General Public License, you may choose any version ever published-by the Free Software Foundation.-- If the Program specifies that a proxy can decide which future-versions of the GNU General Public License can be used, that proxy's-public statement of acceptance of a version permanently authorizes you-to choose that version for the Program.-- Later license versions may give you additional or different-permissions. However, no additional obligations are imposed on any-author or copyright holder as a result of your choosing to follow a-later version.-- 15. Disclaimer of Warranty.-- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.-- 16. Limitation of Liability.-- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF-SUCH DAMAGES.-- 17. Interpretation of Sections 15 and 16.-- If the disclaimer of warranty and limitation of liability provided-above cannot be given local legal effect according to their terms,-reviewing courts shall apply local law that most closely approximates-an absolute waiver of all civil liability in connection with the-Program, unless a warranty or assumption of liability accompanies a-copy of the Program in return for a fee.-- END OF TERMS AND CONDITIONS-- How to Apply These Terms to Your New Programs-- If you develop a new program, and you want it to be of the greatest-possible use to the public, the best way to achieve this is to make it-free software which everyone can redistribute and change under these terms.-- To do so, attach the following notices to the program. It is safest-to attach them to the start of each source file to most effectively-state the exclusion of warranty; and each file should have at least-the "copyright" line and a pointer to where the full notice is found.-- <one line to give the program's name and a brief idea of what it does.>- Copyright (C) <year> <name of author>-- This program is free software: you can redistribute it and/or modify- it under the terms of the GNU General Public License as published by- the Free Software Foundation, either version 3 of the License, or- (at your option) any later version.-- This program is distributed in the hope that it will be useful,- but WITHOUT ANY WARRANTY; without even the implied warranty of- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the- GNU General Public License for more details.-- You should have received a copy of the GNU General Public License- along with this program. If not, see <http://www.gnu.org/licenses/>.--Also add information on how to contact you by electronic and paper mail.-- If the program does terminal interaction, make it output a short-notice like this when it starts in an interactive mode:-- <program> Copyright (C) <year> <name of author>- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.- This is free software, and you are welcome to redistribute it- under certain conditions; type `show c' for details.--The hypothetical commands `show w' and `show c' should show the appropriate-parts of the General Public License. Of course, your program's commands-might be different; for a GUI interface, you would use an "about box".-- You should also get your employer (if you work as a programmer) or school,-if any, to sign a "copyright disclaimer" for the program, if necessary.-For more information on this, and how to apply and follow the GNU GPL, see-<http://www.gnu.org/licenses/>.-- The GNU General Public License does not permit incorporating your program-into proprietary programs. If your program is a subroutine library, you-may consider it more useful to permit linking proprietary applications with-the library. If this is what you want to do, use the GNU Lesser General-Public License instead of this License. But first, please read-<http://www.gnu.org/philosophy/why-not-lgpl.html>.
+ LICENSE view
@@ -0,0 +1,22 @@+Copyright 2014 Joey Hess <joeyh@debian.org> and contributors.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions+are met:+1. Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.+2. Redistributions in binary form must reproduce the above copyright+ notice, this list of conditions and the following disclaimer in the+ documentation and/or other materials provided with the distribution.++THIS SOFTWARE IS PROVIDED BY AUTHORS AND CONTRIBUTORS ``AS IS'' AND+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE+ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF+SUCH DAMAGE.
Makefile view
@@ -19,7 +19,7 @@ install: install -d $(DESTDIR)/usr/bin $(DESTDIR)/usr/src/propellor- install -s dist/build/propellor/propellor $(DESTDIR)/usr/bin+ install -s dist/build/wrapper/wrapper $(DESTDIR)/usr/bin/propellor $(CABAL) sdist cat dist/propellor-*.tar.gz | \ (cd $(DESTDIR)/usr/src/propellor && tar zx --strip-components=1)@@ -30,8 +30,10 @@ find -name \*.hi -exec rm {} \; # hothasktags chokes on some template haskell etc, so ignore errors+# duplicate tags with Propellor.Property. removed from the start, as we+# often import qualified by just the module base name. tags:- find . | grep -v /.git/ | grep -v /tmp/ | grep -v /dist/ | grep -v /doc/ | egrep '\.hs$$' | xargs hothasktags > tags 2>/dev/null+ find . | grep -v /.git/ | grep -v /tmp/ | grep -v /dist/ | grep -v /doc/ | egrep '\.hs$$' | xargs hothasktags | perl -ne 'print; s/Propellor\.Property\.//; print' | sort > tags 2>/dev/null # Upload to hackage. hackage:
− Propellor.hs
@@ -1,77 +0,0 @@-{-# LANGUAGE PackageImports #-}---- | Pulls in lots of useful modules for building and using Properties.--- --- When propellor runs on a Host, it ensures that its list of Properties--- is satisfied, taking action as necessary when a Property is not--- currently satisfied.------ A simple propellor program example:------ > import Propellor--- > import Propellor.CmdLine--- > import qualified Propellor.Property.File as File--- > import qualified Propellor.Property.Apt as Apt--- > --- > main :: IO ()--- > main = defaultMain hosts--- > --- > hosts :: [Host]--- > hosts =--- > [ host "example.com"--- > & Apt.installed ["mydaemon"]--- > & "/etc/mydaemon.conf" `File.containsLine` "secure=1"--- > `onChange` cmdProperty "service" ["mydaemon", "restart"]--- > ! Apt.installed ["unwantedpackage"]--- > ]------ See config.hs for a more complete example, and clone Propellor's--- git repository for a deployable system using Propellor:--- git clone <git://git.kitenet.net/propellor>--module Propellor (- module Propellor.Types- , module Propellor.Property- , module Propellor.Property.Cmd- , module Propellor.Attr- , module Propellor.PrivData- , module Propellor.Engine- , module Propellor.Exception- , module Propellor.Message- , localdir-- , module X-) where--import Propellor.Types-import Propellor.Property-import Propellor.Engine-import Propellor.Property.Cmd-import Propellor.PrivData-import Propellor.Message-import Propellor.Exception-import Propellor.Attr--import Utility.PartialPrelude as X-import Utility.Process as X-import Utility.Exception as X-import Utility.Env as X-import Utility.Directory as X-import Utility.Tmp as X-import Utility.Monad as X-import Utility.Misc as X--import System.Directory as X-import System.IO as X-import System.FilePath as X-import Data.Maybe as X-import Data.Either as X-import Control.Applicative as X-import Control.Monad as X-import Data.Monoid as X-import Control.Monad.IfElse as X-import "mtl" Control.Monad.Reader as X---- | This is where propellor installs itself when deploying a host.-localdir :: FilePath-localdir = "/usr/local/propellor"
− Propellor/Attr.hs
@@ -1,111 +0,0 @@-{-# LANGUAGE PackageImports #-}--module Propellor.Attr where--import Propellor.Types-import Propellor.Types.Attr--import "mtl" Control.Monad.Reader-import qualified Data.Set as S-import qualified Data.Map as M-import Data.Maybe-import Control.Applicative--pureAttrProperty :: Desc -> SetAttr -> Property -pureAttrProperty desc = Property ("has " ++ desc) (return NoChange)--hostname :: HostName -> Property-hostname name = pureAttrProperty ("hostname " ++ name) $- \d -> d { _hostname = name }--getHostName :: Propellor HostName-getHostName = asks _hostname--os :: System -> Property-os system = pureAttrProperty ("Operating " ++ show system) $- \d -> d { _os = Just system }--getOS :: Propellor (Maybe System)-getOS = asks _os---- | Indidate that a host has an A record in the DNS.------ TODO check at run time if the host really has this address.--- (Can't change the host's address, but as a sanity check.)-ipv4 :: String -> Property-ipv4 addr = pureAttrProperty ("ipv4 " ++ addr)- (addDNS $ Address $ IPv4 addr)---- | Indidate that a host has an AAAA record in the DNS.-ipv6 :: String -> Property-ipv6 addr = pureAttrProperty ("ipv6 " ++ addr)- (addDNS $ Address $ IPv6 addr)---- | Indicates another name for the host in the DNS.-alias :: Domain -> Property-alias domain = pureAttrProperty ("alias " ++ domain)- (addDNS $ CNAME $ AbsDomain domain)--addDNS :: Record -> SetAttr-addDNS record d = d { _dns = S.insert record (_dns d) }---- | Adds a DNS NamedConf stanza.------ Note that adding a Master stanza for a domain always overrides an--- existing Secondary stanza, while a Secondary stanza is only added--- when there is no existing Master stanza.-addNamedConf :: NamedConf -> SetAttr-addNamedConf conf d = d { _namedconf = new }- where- m = _namedconf d- domain = confDomain conf- new = case (confDnsServerType conf, confDnsServerType <$> M.lookup domain m) of- (Secondary, Just Master) -> m- _ -> M.insert domain conf m--getNamedConf :: Propellor (M.Map Domain NamedConf)-getNamedConf = asks _namedconf--sshPubKey :: String -> Property-sshPubKey k = pureAttrProperty ("ssh pubkey known") $- \d -> d { _sshPubKey = Just k }--getSshPubKey :: Propellor (Maybe String)-getSshPubKey = asks _sshPubKey--hostnameless :: Attr-hostnameless = newAttr (error "hostname Attr not specified")--hostAttr :: Host -> Attr-hostAttr (Host _ mkattrs) = mkattrs hostnameless--hostProperties :: Host -> [Property]-hostProperties (Host ps _) = ps--hostMap :: [Host] -> M.Map HostName Host-hostMap l = M.fromList $ zip (map (_hostname . hostAttr) l) l --hostAttrMap :: [Host] -> M.Map HostName Attr-hostAttrMap l = M.fromList $ zip (map _hostname attrs) attrs- where- attrs = map hostAttr l--findHost :: [Host] -> HostName -> Maybe Host-findHost l hn = M.lookup hn (hostMap l)--getAddresses :: Attr -> [IPAddr]-getAddresses = mapMaybe getIPAddr . S.toList . _dns--hostAddresses :: HostName -> [Host] -> [IPAddr]-hostAddresses hn hosts = case hostAttr <$> findHost hosts hn of- Nothing -> []- Just attr -> mapMaybe getIPAddr $ S.toList $ _dns attr---- | Lifts an action into a different host.------ For example, `fromHost hosts "otherhost" getSshPubKey`-fromHost :: [Host] -> HostName -> Propellor a -> Propellor (Maybe a)-fromHost l hn getter = case findHost l hn of- Nothing -> return Nothing- Just h -> liftIO $ Just <$>- runReaderT (runWithAttr getter) (hostAttr h)
− Propellor/CmdLine.hs
@@ -1,383 +0,0 @@-module Propellor.CmdLine where--import System.Environment (getArgs)-import Data.List-import System.Exit-import System.Log.Logger-import System.Log.Formatter-import System.Log.Handler (setFormatter, LogHandler)-import System.Log.Handler.Simple-import System.PosixCompat-import Control.Exception (bracket)-import System.Posix.IO-import Data.Time.Clock.POSIX--import Propellor-import qualified Propellor.Property.Docker as Docker-import qualified Propellor.Property.Docker.Shim as DockerShim-import Utility.FileMode-import Utility.SafeCommand-import Utility.UserInfo--usage :: IO a-usage = do- putStrLn $ unlines - [ "Usage:"- , " propellor"- , " propellor hostname"- , " propellor --spin hostname"- , " propellor --set hostname field"- , " propellor --add-key keyid"- ]- exitFailure--processCmdLine :: IO CmdLine-processCmdLine = go =<< getArgs- where- go ("--help":_) = usage- go ("--spin":h:[]) = return $ Spin h- go ("--boot":h:[]) = return $ Boot h- go ("--add-key":k:[]) = return $ AddKey k- go ("--set":h:f:[]) = case readish f of- Just pf -> return $ Set h pf- Nothing -> errorMessage $ "Unknown privdata field " ++ f- go ("--continue":s:[]) = case readish s of- Just cmdline -> return $ Continue cmdline- Nothing -> errorMessage "--continue serialization failure"- go ("--chain":h:[]) = return $ Chain h- go ("--docker":h:[]) = return $ Docker h- go (h:[])- | "--" `isPrefixOf` h = usage- | otherwise = return $ Run h- go [] = do- s <- takeWhile (/= '\n') <$> readProcess "hostname" ["-f"]- if null s- then errorMessage "Cannot determine hostname! Pass it on the command line."- else return $ Run s- go _ = usage--defaultMain :: [Host] -> IO ()-defaultMain hostlist = do- DockerShim.cleanEnv- checkDebugMode- cmdline <- processCmdLine- debug ["command line: ", show cmdline]- go True cmdline- where- go _ (Continue cmdline) = go False cmdline- go _ (Set hn field) = setPrivData hn field- go _ (AddKey keyid) = addKey keyid- go _ (Chain hn) = withprops hn $ \attr ps -> do- r <- runPropellor attr $ ensureProperties ps- putStrLn $ "\n" ++ show r- go _ (Docker hn) = Docker.chain hn- go True cmdline@(Spin _) = buildFirst cmdline $ go False cmdline- go True cmdline = updateFirst cmdline $ go False cmdline- go False (Spin hn) = withprops hn $ const . const $ spin hn- go False (Run hn) = ifM ((==) 0 <$> getRealUserID)- ( onlyProcess $ withprops hn mainProperties- , go True (Spin hn)- )- go False (Boot hn) = onlyProcess $ withprops hn boot-- withprops :: HostName -> (Attr -> [Property] -> IO ()) -> IO ()- withprops hn a = maybe- (unknownhost hn)- (\h -> a (hostAttr h) (hostProperties h))- (findHost hostlist hn)--onlyProcess :: IO a -> IO a-onlyProcess a = bracket lock unlock (const a)- where- lock = do- l <- createFile lockfile stdFileMode- setLock l (WriteLock, AbsoluteSeek, 0, 0)- `catchIO` const alreadyrunning- return l- unlock = closeFd- alreadyrunning = error "Propellor is already running on this host!"- lockfile = localdir </> ".lock"--unknownhost :: HostName -> IO a-unknownhost h = errorMessage $ unlines- [ "Propellor does not know about host: " ++ h- , "(Perhaps you should specify the real hostname on the command line?)"- , "(Or, edit propellor's config.hs to configure this host)"- ]--buildFirst :: CmdLine -> IO () -> IO ()-buildFirst cmdline next = do- oldtime <- getmtime- ifM (actionMessage "Propellor build" $ boolSystem "make" [Param "build"])- ( do- newtime <- getmtime- if newtime == oldtime- then next- else void $ boolSystem "./propellor" [Param "--continue", Param (show cmdline)]- , errorMessage "Propellor build failed!" - )- where- getmtime = catchMaybeIO $ getModificationTime "propellor"--getCurrentBranch :: IO String-getCurrentBranch = takeWhile (/= '\n') - <$> readProcess "git" ["symbolic-ref", "--short", "HEAD"]--updateFirst :: CmdLine -> IO () -> IO ()-updateFirst cmdline next = do- branchref <- getCurrentBranch- let originbranch = "origin" </> branchref-- void $ actionMessage "Git fetch" $ boolSystem "git" [Param "fetch"]- - whenM (doesFileExist keyring) $ do- {- To verify origin branch commit's signature, have to- - convince gpg to use our keyring. While running git log.- - Which has no way to pass options to gpg.- - Argh! -}- let gpgconf = privDataDir </> "gpg.conf"- writeFile gpgconf $ unlines- [ " keyring " ++ keyring- , "no-auto-check-trustdb"- ]- -- gpg is picky about perms- modifyFileMode privDataDir (removeModes otherGroupModes)- s <- readProcessEnv "git" ["log", "-n", "1", "--format=%G?", originbranch]- (Just [("GNUPGHOME", privDataDir)])- nukeFile $ privDataDir </> "trustdb.gpg"- nukeFile $ privDataDir </> "pubring.gpg"- nukeFile $ privDataDir </> "gpg.conf"- if s == "U\n" || s == "G\n"- then do- putStrLn $ "git branch " ++ originbranch ++ " gpg signature verified; merging"- hFlush stdout- else errorMessage $ "git branch " ++ originbranch ++ " is not signed with a trusted gpg key; refusing to deploy it!"- - oldsha <- getCurrentGitSha1 branchref- void $ boolSystem "git" [Param "merge", Param originbranch]- newsha <- getCurrentGitSha1 branchref-- if oldsha == newsha- then next- else ifM (actionMessage "Propellor build" $ boolSystem "make" [Param "build"])- ( void $ boolSystem "./propellor" [Param "--continue", Param (show cmdline)]- , errorMessage "Propellor build failed!" - )--getCurrentGitSha1 :: String -> IO String-getCurrentGitSha1 branchref = readProcess "git" ["show-ref", "--hash", branchref]--spin :: HostName -> IO ()-spin hn = do- url <- getUrl- void $ gitCommit [Param "--allow-empty", Param "-a", Param "-m", Param "propellor spin"]- void $ boolSystem "git" [Param "push"]- cacheparams <- toCommand <$> sshCachingParams hn- go cacheparams url =<< gpgDecrypt (privDataFile hn)- where- go cacheparams url privdata = withBothHandles createProcessSuccess (proc "ssh" $ cacheparams ++ [user, bootstrapcmd]) $ \(toh, fromh) -> do- let finish = do- senddata toh (privDataFile hn) privDataMarker privdata- hClose toh- - -- Display remaining output.- void $ tryIO $ forever $- showremote =<< hGetLine fromh- hClose fromh- status <- getstatus fromh `catchIO` (const $ errorMessage "protocol error (perhaps the remote propellor failed to run?)")- case status of- Ready -> finish- NeedGitClone -> do- hClose toh- hClose fromh- sendGitClone hn url- go cacheparams url privdata- - user = "root@"++hn-- bootstrapcmd = shellWrap $ intercalate " ; "- [ "if [ ! -d " ++ localdir ++ " ]"- , "then " ++ intercalate " && "- [ "apt-get --no-install-recommends --no-upgrade -y install git make"- , "echo " ++ toMarked statusMarker (show NeedGitClone)- ]- , "else " ++ intercalate " && "- [ "cd " ++ localdir- , "if ! test -x ./propellor; then make deps build; fi"- , "./propellor --boot " ++ hn- ]- , "fi"- ]-- getstatus :: Handle -> IO BootStrapStatus- getstatus h = do- l <- hGetLine h- case readish =<< fromMarked statusMarker l of- Nothing -> do- showremote l- getstatus h- Just status -> return status- - showremote s = putStrLn s- senddata toh f marker s = void $- actionMessage ("Sending " ++ f ++ " (" ++ show (length s) ++ " bytes) to " ++ hn) $ do- sendMarked toh marker s- return True--sendGitClone :: HostName -> String -> IO ()-sendGitClone hn url = void $ actionMessage ("Pushing git repository to " ++ hn) $ do- branch <- getCurrentBranch- cacheparams <- sshCachingParams hn- withTmpFile "propellor.git" $ \tmp _ -> allM id- [ boolSystem "git" [Param "bundle", Param "create", File tmp, Param "HEAD"]- , boolSystem "scp" $ cacheparams ++ [File tmp, Param ("root@"++hn++":"++remotebundle)]- , boolSystem "ssh" $ cacheparams ++ [Param ("root@"++hn), Param $ unpackcmd branch]- ]- where- remotebundle = "/usr/local/propellor.git"- unpackcmd branch = shellWrap $ intercalate " && "- [ "git clone " ++ remotebundle ++ " " ++ localdir- , "cd " ++ localdir- , "git checkout -b " ++ branch- , "git remote rm origin"- , "rm -f " ++ remotebundle- , "git remote add origin " ++ url- -- same as --set-upstream-to, except origin branch- -- has not been pulled yet- , "git config branch."++branch++".remote origin"- , "git config branch."++branch++".merge refs/heads/"++branch- ]--data BootStrapStatus = Ready | NeedGitClone- deriving (Read, Show, Eq)--type Marker = String-type Marked = String--statusMarker :: Marker-statusMarker = "STATUS"--privDataMarker :: String-privDataMarker = "PRIVDATA "--toMarked :: Marker -> String -> String-toMarked marker = intercalate "\n" . map (marker ++) . lines--sendMarked :: Handle -> Marker -> String -> IO ()-sendMarked h marker s = do- -- Prefix string with newline because sometimes a- -- incomplete line is output.- hPutStrLn h ("\n" ++ toMarked marker s)- hFlush h--fromMarked :: Marker -> Marked -> Maybe String-fromMarked marker s- | null matches = Nothing- | otherwise = Just $ intercalate "\n" $- map (drop len) matches- where- len = length marker- matches = filter (marker `isPrefixOf`) $ lines s--boot :: Attr -> [Property] -> IO ()-boot attr ps = do- sendMarked stdout statusMarker $ show Ready- reply <- hGetContentsStrict stdin-- makePrivDataDir- maybe noop (writeFileProtected privDataLocal) $- fromMarked privDataMarker reply- mainProperties attr ps--addKey :: String -> IO ()-addKey keyid = exitBool =<< allM id [ gpg, gitadd, gitcommit ]- where- gpg = boolSystem "sh"- [ Param "-c"- , Param $ "gpg --export " ++ keyid ++ " | gpg " ++- unwords (gpgopts ++ ["--import"])- ]- gitadd = boolSystem "git"- [ Param "add"- , File keyring- ]- gitcommit = gitCommit- [ File keyring- , Param "-m"- , Param "propellor addkey"- ]--{- Automatically sign the commit if there'a a keyring. -}-gitCommit :: [CommandParam] -> IO Bool-gitCommit ps = do- k <- doesFileExist keyring- boolSystem "git" $ catMaybes $- [ Just (Param "commit")- , if k then Just (Param "--gpg-sign") else Nothing- ] ++ map Just ps--keyring :: FilePath-keyring = privDataDir </> "keyring.gpg"--gpgopts :: [String]-gpgopts = ["--options", "/dev/null", "--no-default-keyring", "--keyring", keyring]--getUrl :: IO String-getUrl = maybe nourl return =<< getM get urls- where- urls = ["remote.deploy.url", "remote.origin.url"]- nourl = errorMessage $ "Cannot find deploy url in " ++ show urls- get u = do- v <- catchMaybeIO $ - takeWhile (/= '\n') - <$> readProcess "git" ["config", u]- return $ case v of- Just url | not (null url) -> Just url- _ -> Nothing--checkDebugMode :: IO ()-checkDebugMode = go =<< getEnv "PROPELLOR_DEBUG"- where- go (Just s)- | s == "1" = do- f <- setFormatter- <$> streamHandler stderr DEBUG- <*> pure (simpleLogFormatter "[$time] $msg")- updateGlobalLogger rootLoggerName $ - setLevel DEBUG . setHandlers [f]- go _ = noop---- Parameters can be passed to both ssh and scp, to enable a ssh connection--- caching socket.------ If the socket already exists, check if its mtime is older than 10--- minutes, and if so stop that ssh process, in order to not try to--- use an old stale connection. (atime would be nicer, but there's--- a good chance a laptop uses noatime)-sshCachingParams :: HostName -> IO [CommandParam]-sshCachingParams hn = do- home <- myHomeDir- let cachedir = home </> ".ssh" </> "propellor"- createDirectoryIfMissing False cachedir- let socketfile = cachedir </> hn ++ ".sock"- let ps = - [ Param "-o", Param ("ControlPath=" ++ socketfile)- , Params "-o ControlMaster=auto -o ControlPersist=yes"- ]-- maybe noop (expireold ps socketfile)- =<< catchMaybeIO (getFileStatus socketfile)- - return ps- - where- expireold ps f s = do- now <- truncate <$> getPOSIXTime :: IO Integer- if modificationTime s > fromIntegral now - tenminutes- then touchFile f- else do- void $ boolSystem "ssh" $- [ Params "-O stop" ] ++ ps ++- [ Param "localhost" ]- nukeFile f- tenminutes = 600
− Propellor/Engine.hs
@@ -1,37 +0,0 @@-{-# LANGUAGE PackageImports #-}--module Propellor.Engine where--import System.Exit-import System.IO-import Data.Monoid-import System.Console.ANSI-import "mtl" Control.Monad.Reader--import Propellor.Types-import Propellor.Message-import Propellor.Exception--runPropellor :: Attr -> Propellor a -> IO a-runPropellor attr a = runReaderT (runWithAttr a) attr--mainProperties :: Attr -> [Property] -> IO ()-mainProperties attr ps = do- r <- runPropellor attr $- ensureProperties [Property "overall" (ensureProperties ps) id]- setTitle "propellor: done"- hFlush stdout- case r of- FailedChange -> exitWith (ExitFailure 1)- _ -> exitWith ExitSuccess--ensureProperties :: [Property] -> Propellor Result-ensureProperties ps = ensure ps NoChange- where- ensure [] rs = return rs- ensure (l:ls) rs = do- r <- actionMessage (propertyDesc l) (ensureProperty l)- ensure ls (r <> rs)--ensureProperty :: Property -> Propellor Result-ensureProperty = catchPropellor . propertySatisfy
− Propellor/Exception.hs
@@ -1,18 +0,0 @@-{-# LANGUAGE PackageImports #-}--module Propellor.Exception where--import qualified "MonadCatchIO-transformers" Control.Monad.CatchIO as M-import Control.Exception--import Propellor.Types-import Propellor.Message---- | Catches IO exceptions and returns FailedChange.-catchPropellor :: Propellor Result -> Propellor Result-catchPropellor a = either err return =<< tryPropellor a- where- err e = warningMessage (show e) >> return FailedChange--tryPropellor :: Propellor a -> Propellor (Either IOException a)-tryPropellor = M.try
− Propellor/Message.hs
@@ -1,51 +0,0 @@-{-# LANGUAGE PackageImports #-}--module Propellor.Message where--import System.Console.ANSI-import System.IO-import System.Log.Logger-import "mtl" Control.Monad.Reader--import Propellor.Types---- | Shows a message while performing an action, with a colored status--- display.-actionMessage :: (MonadIO m, ActionResult r) => Desc -> m r -> m r-actionMessage desc a = do- liftIO $ do- setTitle $ "propellor: " ++ desc- hFlush stdout-- r <- a-- liftIO $ do- setTitle "propellor: running"- let (msg, intensity, color) = getActionResult r- putStr $ desc ++ " ... "- colorLine intensity color msg- hFlush stdout-- return r--warningMessage :: MonadIO m => String -> m ()-warningMessage s = liftIO $ colorLine Vivid Magenta $ "** warning: " ++ s--colorLine :: ColorIntensity -> Color -> String -> IO ()-colorLine intensity color msg = do- setSGR [SetColor Foreground intensity color]- putStr msg- setSGR []- -- Note this comes after the color is reset, so that- -- the color set and reset happen in the same line.- putStrLn ""- hFlush stdout--errorMessage :: String -> IO a-errorMessage s = do- liftIO $ colorLine Vivid Red $ "** error: " ++ s- error "Cannot continue!"---- | Causes a debug message to be displayed when PROPELLOR_DEBUG=1-debug :: [String] -> IO ()-debug = debugM "propellor" . unwords
− Propellor/PrivData.hs
@@ -1,91 +0,0 @@-{-# LANGUAGE PackageImports #-}--module Propellor.PrivData where--import qualified Data.Map as M-import Control.Applicative-import System.FilePath-import System.IO-import System.Directory-import Data.Maybe-import Data.List-import Control.Monad-import "mtl" Control.Monad.Reader--import Propellor.Types-import Propellor.Attr-import Propellor.Message-import Utility.Monad-import Utility.PartialPrelude-import Utility.Exception-import Utility.Process-import Utility.Tmp-import Utility.SafeCommand-import Utility.Misc---- | When the specified PrivDataField is available on the host Propellor--- is provisioning, it provies the data to the action. Otherwise, it prints--- a message to help the user make the necessary private data available.-withPrivData :: PrivDataField -> (String -> Propellor Result) -> Propellor Result-withPrivData field a = maybe missing a =<< liftIO (getPrivData field)- where- missing = do- host <- getHostName- let host' = if ".docker" `isSuffixOf` host- then "$parent_host"- else host- liftIO $ do- warningMessage $ "Missing privdata " ++ show field- putStrLn $ "Fix this by running: propellor --set "++host'++" '" ++ show field ++ "'"- return FailedChange--getPrivData :: PrivDataField -> IO (Maybe String)-getPrivData field = do- m <- catchDefaultIO Nothing $ readish <$> readFile privDataLocal- return $ maybe Nothing (M.lookup field) m--setPrivData :: HostName -> PrivDataField -> IO ()-setPrivData host field = do- putStrLn "Enter private data on stdin; ctrl-D when done:"- value <- chomp <$> hGetContentsStrict stdin- makePrivDataDir- let f = privDataFile host- m <- fromMaybe M.empty . readish <$> gpgDecrypt f- let m' = M.insert field value m- gpgEncrypt f (show m')- putStrLn "Private data set."- void $ boolSystem "git" [Param "add", File f]- where- chomp s- | end s == "\n" = chomp (beginning s)- | otherwise = s--makePrivDataDir :: IO ()-makePrivDataDir = createDirectoryIfMissing False privDataDir--privDataDir :: FilePath-privDataDir = "privdata"--privDataFile :: HostName -> FilePath-privDataFile host = privDataDir </> host ++ ".gpg"--privDataLocal :: FilePath-privDataLocal = privDataDir </> "local"--gpgDecrypt :: FilePath -> IO String-gpgDecrypt f = ifM (doesFileExist f)- ( readProcess "gpg" ["--decrypt", f]- , return ""- )--gpgEncrypt :: FilePath -> String -> IO ()-gpgEncrypt f s = do- encrypted <- writeReadProcessEnv "gpg"- [ "--default-recipient-self"- , "--armor"- , "--encrypt"- ]- Nothing- (Just $ flip hPutStr s)- Nothing- viaTmp writeFile f encrypted
− Propellor/Property.hs
@@ -1,163 +0,0 @@-{-# LANGUAGE PackageImports #-}--module Propellor.Property where--import System.Directory-import Control.Monad-import Data.Monoid-import Data.List-import Control.Monad.IfElse-import "mtl" Control.Monad.Reader--import Propellor.Types-import Propellor.Types.Attr-import Propellor.Attr-import Propellor.Engine-import Utility.Monad-import System.FilePath---- Constructs a Property.-property :: Desc -> Propellor Result -> Property-property d s = Property d s id---- | Combines a list of properties, resulting in a single property--- that when run will run each property in the list in turn,--- and print out the description of each as it's run. Does not stop--- on failure; does propigate overall success/failure.-propertyList :: Desc -> [Property] -> Property-propertyList desc ps = Property desc (ensureProperties ps) (combineSetAttrs ps)---- | Combines a list of properties, resulting in one property that--- ensures each in turn, stopping on failure.-combineProperties :: Desc -> [Property] -> Property-combineProperties desc ps = Property desc (go ps NoChange) (combineSetAttrs ps)- where- go [] rs = return rs- go (l:ls) rs = do- r <- ensureProperty l- case r of- FailedChange -> return FailedChange- _ -> go ls (r <> rs)---- | Combines together two properties, resulting in one property--- that ensures the first, and if the first succeeds, ensures the second.--- The property uses the description of the first property.-before :: Property -> Property -> Property-p1 `before` p2 = p2 `requires` p1- `describe` (propertyDesc p1)---- | Makes a perhaps non-idempotent Property be idempotent by using a flag--- file to indicate whether it has run before.--- Use with caution.-flagFile :: Property -> FilePath -> Property-flagFile p = flagFile' p . return--flagFile' :: Property -> IO FilePath -> Property-flagFile' p getflagfile = adjustProperty p $ \satisfy -> do- flagfile <- liftIO getflagfile- go satisfy flagfile =<< liftIO (doesFileExist flagfile)- where- go _ _ True = return NoChange- go satisfy flagfile False = do- r <- satisfy- when (r == MadeChange) $ liftIO $ - unlessM (doesFileExist flagfile) $ do- createDirectoryIfMissing True (takeDirectory flagfile)- writeFile flagfile ""- return r----- | Whenever a change has to be made for a Property, causes a hook--- Property to also be run, but not otherwise.-onChange :: Property -> Property -> Property-p `onChange` hook = Property (propertyDesc p) satisfy (combineSetAttr p hook)- where- satisfy = do- r <- ensureProperty p- case r of- MadeChange -> do- r' <- ensureProperty hook- return $ r <> r'- _ -> return r--(==>) :: Desc -> Property -> Property-(==>) = flip describe-infixl 1 ==>---- | Makes a Property only need to do anything when a test succeeds.-check :: IO Bool -> Property -> Property-check c p = adjustProperty p $ \satisfy -> ifM (liftIO c)- ( satisfy- , return NoChange- )---- | Marks a Property as trivial. It can only return FailedChange or--- NoChange. ------ Useful when it's just as expensive to check if a change needs--- to be made as it is to just idempotently assure the property is--- satisfied. For example, chmodding a file.-trivial :: Property -> Property-trivial p = adjustProperty p $ \satisfy -> do- r <- satisfy- if r == MadeChange- then return NoChange- else return r---- | Makes a property that is satisfied differently depending on the host's--- operating system. ------ Note that the operating system may not be declared for some hosts.-withOS :: Desc -> (Maybe System -> Propellor Result) -> Property-withOS desc a = property desc $ a =<< getOS--boolProperty :: Desc -> IO Bool -> Property-boolProperty desc a = property desc $ ifM (liftIO a)- ( return MadeChange- , return FailedChange- )---- | Undoes the effect of a property.-revert :: RevertableProperty -> RevertableProperty-revert (RevertableProperty p1 p2) = RevertableProperty p2 p1---- | Starts accumulating the properties of a Host.------ > host "example.com"--- > & someproperty--- > ! oldproperty--- > & otherproperty-host :: HostName -> Host-host hn = Host [] (\_ -> newAttr hn)---- | Adds a property to a Host------ Can add Properties and RevertableProperties-(&) :: IsProp p => Host -> p -> Host-(Host ps as) & p = Host (ps ++ [toProp p]) (setAttr p . as)--infixl 1 &---- | Adds a property to the Host in reverted form.-(!) :: Host -> RevertableProperty -> Host-(Host ps as) ! p = Host (ps ++ [toProp q]) (setAttr q . as)- where- q = revert p--infixl 1 !---- Changes the action that is performed to satisfy a property. -adjustProperty :: Property -> (Propellor Result -> Propellor Result) -> Property-adjustProperty p f = p { propertySatisfy = f (propertySatisfy p) }---- Combines the Attr settings of two properties.-combineSetAttr :: (IsProp p, IsProp q) => p -> q -> SetAttr-combineSetAttr p q = setAttr p . setAttr q--combineSetAttrs :: IsProp p => [p] -> SetAttr-combineSetAttrs = foldl' (.) id . map setAttr--makeChange :: IO () -> Propellor Result-makeChange a = liftIO a >> return MadeChange--noChange :: Propellor Result-noChange = return NoChange
− Propellor/Property/Apache.hs
@@ -1,62 +0,0 @@-module Propellor.Property.Apache where--import Propellor-import qualified Propellor.Property.File as File-import qualified Propellor.Property.Apt as Apt-import qualified Propellor.Property.Service as Service--type ConfigFile = [String]--siteEnabled :: HostName -> ConfigFile -> RevertableProperty-siteEnabled hn cf = RevertableProperty enable disable- where- enable = trivial $ cmdProperty "a2ensite" ["--quiet", hn]- `describe` ("apache site enabled " ++ hn)- `requires` siteAvailable hn cf- `requires` installed- `onChange` reloaded- disable = trivial $ File.notPresent (siteCfg hn)- `describe` ("apache site disabled " ++ hn)- `onChange` cmdProperty "a2dissite" ["--quiet", hn]- `requires` installed- `onChange` reloaded--siteAvailable :: HostName -> ConfigFile -> Property-siteAvailable hn cf = siteCfg hn `File.hasContent` (comment:cf)- `describe` ("apache site available " ++ hn)- where- comment = "# deployed with propellor, do not modify"--modEnabled :: String -> RevertableProperty-modEnabled modname = RevertableProperty enable disable- where- enable = trivial $ cmdProperty "a2enmod" ["--quiet", modname]- `describe` ("apache module enabled " ++ modname)- `requires` installed- `onChange` reloaded- disable = trivial $ cmdProperty "a2dismod" ["--quiet", modname]- `describe` ("apache module disabled " ++ modname)- `requires` installed- `onChange` reloaded--siteCfg :: HostName -> FilePath-siteCfg hn = "/etc/apache2/sites-available/" ++ hn--installed :: Property-installed = Apt.installed ["apache2"]--restarted :: Property-restarted = cmdProperty "service" ["apache2", "restart"]--reloaded :: Property-reloaded = Service.reloaded "apache2"---- | Configure apache to use SNI to differentiate between--- https hosts.-multiSSL :: Property-multiSSL = "/etc/apache2/conf.d/ssl" `File.hasContent`- [ "NameVirtualHost *:443"- , "SSLStrictSNIVHostCheck off"- ]- `describe` "apache SNI enabled"- `onChange` reloaded
− Propellor/Property/Apt.hs
@@ -1,256 +0,0 @@-module Propellor.Property.Apt where--import Data.Maybe-import Control.Applicative-import Data.List-import System.IO-import Control.Monad--import Propellor-import qualified Propellor.Property.File as File-import qualified Propellor.Property.Service as Service-import Propellor.Property.File (Line)--sourcesList :: FilePath-sourcesList = "/etc/apt/sources.list"--type Url = String-type Section = String--type SourcesGenerator = DebianSuite -> [Line]--showSuite :: DebianSuite -> String-showSuite Stable = "stable"-showSuite Testing = "testing"-showSuite Unstable = "unstable"-showSuite Experimental = "experimental"-showSuite (DebianRelease r) = r--backportSuite :: String-backportSuite = showSuite stableRelease ++ "-backports"--debLine :: String -> Url -> [Section] -> Line-debLine suite mirror sections = unwords $- ["deb", mirror, suite] ++ sections--srcLine :: Line -> Line-srcLine l = case words l of- ("deb":rest) -> unwords $ "deb-src" : rest- _ -> ""--stdSections :: [Section]-stdSections = ["main", "contrib", "non-free"]--binandsrc :: String -> SourcesGenerator-binandsrc url suite- | isStable suite = [l, srcLine l, bl, srcLine bl]- | otherwise = [l, srcLine l]- where- l = debLine (showSuite suite) url stdSections- bl = debLine backportSuite url stdSections--debCdn :: SourcesGenerator-debCdn = binandsrc "http://cdn.debian.net/debian"--kernelOrg :: SourcesGenerator-kernelOrg = binandsrc "http://mirrors.kernel.org/debian"---- | Only available for Stable and Testing-securityUpdates :: SourcesGenerator-securityUpdates suite- | isStable suite || suite == Testing =- let l = "deb http://security.debian.org/ " ++ showSuite suite ++ "/updates " ++ unwords stdSections- in [l, srcLine l]- | otherwise = []---- | Makes sources.list have a standard content using the mirror CDN,--- with a particular DebianSuite.------ Since the CDN is sometimes unreliable, also adds backup lines using--- kernel.org.-stdSourcesList :: DebianSuite -> Property-stdSourcesList suite = stdSourcesList' suite []---- | Adds additional sources.list generators.------ Note that if a Property needs to enable an apt source, it's better--- to do so via a separate file in /etc/apt/sources.list.d/-stdSourcesList' :: DebianSuite -> [SourcesGenerator] -> Property-stdSourcesList' suite more = setSourcesList- (concatMap (\gen -> gen suite) generators)- `describe` ("standard sources.list for " ++ show suite)- where- generators = [debCdn, kernelOrg, securityUpdates] ++ more--setSourcesList :: [Line] -> Property-setSourcesList ls = sourcesList `File.hasContent` ls `onChange` update--setSourcesListD :: [Line] -> FilePath -> Property-setSourcesListD ls basename = f `File.hasContent` ls `onChange` update- where- f = "/etc/apt/sources.list.d/" ++ basename ++ ".list"--runApt :: [String] -> Property-runApt ps = cmdProperty' "apt-get" ps noninteractiveEnv--noninteractiveEnv :: [(String, String)]-noninteractiveEnv =- [ ("DEBIAN_FRONTEND", "noninteractive")- , ("APT_LISTCHANGES_FRONTEND", "none")- ]--update :: Property-update = runApt ["update"]- `describe` "apt update"--upgrade :: Property-upgrade = runApt ["-y", "dist-upgrade"]- `describe` "apt dist-upgrade"--type Package = String--installed :: [Package] -> Property-installed = installed' ["-y"]--installed' :: [String] -> [Package] -> Property-installed' params ps = robustly $ check (isInstallable ps) go- `describe` (unwords $ "apt installed":ps)- where- go = runApt $ params ++ ["install"] ++ ps--installedBackport :: [Package] -> Property-installedBackport ps = trivial $ withOS desc $ \o -> case o of- Nothing -> error "cannot install backports; os not declared"- (Just (System (Debian suite) _))- | isStable suite -> - ensureProperty $ runApt $ - ["install", "-t", backportSuite, "-y"] ++ ps- _ -> error $ "backports not supported on " ++ show o- where- desc = (unwords $ "apt installed backport":ps)---- | Minimal install of package, without recommends.-installedMin :: [Package] -> Property-installedMin = installed' ["--no-install-recommends", "-y"]--removed :: [Package] -> Property-removed ps = check (or <$> isInstalled' ps) go- `describe` (unwords $ "apt removed":ps)- where- go = runApt $ ["-y", "remove"] ++ ps--buildDep :: [Package] -> Property-buildDep ps = robustly go- `describe` (unwords $ "apt build-dep":ps)- where- go = runApt $ ["-y", "build-dep"] ++ ps---- | Installs the build deps for the source package unpacked--- in the specifed directory, with a dummy package also--- installed so that autoRemove won't remove them.-buildDepIn :: FilePath -> Property-buildDepIn dir = go `requires` installedMin ["devscripts", "equivs"]- where- go = cmdProperty' "sh" ["-c", "cd '" ++ dir ++ "' && mk-build-deps debian/control --install --tool 'apt-get -y --no-install-recommends' --remove"]- noninteractiveEnv---- | Package installation may fail becuse the archive has changed.--- Run an update in that case and retry.-robustly :: Property -> Property-robustly p = adjustProperty p $ \satisfy -> do- r <- satisfy- if r == FailedChange- then ensureProperty $ p `requires` update- else return r--isInstallable :: [Package] -> IO Bool-isInstallable ps = do- l <- isInstalled' ps- return $ any (== False) l && not (null l)--isInstalled :: Package -> IO Bool-isInstalled p = (== [True]) <$> isInstalled' [p]---- | Note that the order of the returned list will not always--- correspond to the order of the input list. The number of items may--- even vary. If apt does not know about a package at all, it will not--- be included in the result list.-isInstalled' :: [Package] -> IO [Bool]-isInstalled' ps = catMaybes . map parse . lines- <$> readProcess "apt-cache" ("policy":ps)- where- parse l- | "Installed: (none)" `isInfixOf` l = Just False- | "Installed: " `isInfixOf` l = Just True- | otherwise = Nothing--autoRemove :: Property-autoRemove = runApt ["-y", "autoremove"]- `describe` "apt autoremove"---- | Enables unattended upgrades. Revert to disable.-unattendedUpgrades :: RevertableProperty-unattendedUpgrades = RevertableProperty enable disable- where- enable = setup True- `before` Service.running "cron"- `before` configure- disable = setup False-- setup enabled = (if enabled then installed else removed) ["unattended-upgrades"]- `onChange` reConfigure "unattended-upgrades"- [("unattended-upgrades/enable_auto_updates" , "boolean", v)]- `describe` ("unattended upgrades " ++ v)- where- v- | enabled = "true"- | otherwise = "false"- - configure = withOS "unattended upgrades configured" $ \o ->- case o of- -- the package defaults to only upgrading stable- (Just (System (Debian suite) _))- | not (isStable suite) -> ensureProperty $- "/etc/apt/apt.conf.d/50unattended-upgrades"- `File.containsLine`- ("\t\"o=Debian,a="++showSuite suite++"\";")- _ -> noChange---- | Preseeds debconf values and reconfigures the package so it takes--- effect.-reConfigure :: Package -> [(String, String, String)] -> Property-reConfigure package vals = reconfigure `requires` setselections- `describe` ("reconfigure " ++ package)- where- setselections = property "preseed" $ makeChange $- withHandle StdinHandle createProcessSuccess- (proc "debconf-set-selections" []) $ \h -> do- forM_ vals $ \(tmpl, tmpltype, value) ->- hPutStrLn h $ unwords [package, tmpl, tmpltype, value]- hClose h- reconfigure = cmdProperty' "dpkg-reconfigure" ["-fnone", package] noninteractiveEnv---- | Ensures that a service is installed and running.------ Assumes that there is a 1:1 mapping between service names and apt--- package names.-serviceInstalledRunning :: Package -> Property-serviceInstalledRunning svc = Service.running svc `requires` installed [svc]--data AptKey = AptKey- { keyname :: String- , pubkey :: String- }--trustsKey :: AptKey -> RevertableProperty-trustsKey k = RevertableProperty trust untrust- where- desc = "apt trusts key " ++ keyname k- f = "/etc/apt/trusted.gpg.d" </> keyname k ++ ".gpg"- untrust = File.notPresent f- trust = check (not <$> doesFileExist f) $ property desc $ makeChange $ do- withHandle StdinHandle createProcessSuccess- (proc "gpg" ["--no-default-keyring", "--keyring", f, "--import", "-"]) $ \h -> do- hPutStr h (pubkey k)- hClose h- nukeFile $ f ++ "~" -- gpg dropping
− Propellor/Property/Cmd.hs
@@ -1,49 +0,0 @@-{-# LANGUAGE PackageImports #-}--module Propellor.Property.Cmd (- cmdProperty,- cmdProperty',- scriptProperty,- userScriptProperty,-) where--import Control.Applicative-import Data.List-import "mtl" Control.Monad.Reader--import Propellor.Types-import Propellor.Property-import Utility.Monad-import Utility.SafeCommand-import Utility.Env---- | A property that can be satisfied by running a command.------ The command must exit 0 on success.-cmdProperty :: String -> [String] -> Property-cmdProperty cmd params = cmdProperty' cmd params []---- | A property that can be satisfied by running a command,--- with added environment.-cmdProperty' :: String -> [String] -> [(String, String)] -> Property-cmdProperty' cmd params env = property desc $ liftIO $ do- env' <- addEntries env <$> getEnvironment- ifM (boolSystemEnv cmd (map Param params) (Just env'))- ( return MadeChange- , return FailedChange- )- where- desc = unwords $ cmd : params---- | A property that can be satisfied by running a series of shell commands.-scriptProperty :: [String] -> Property-scriptProperty script = cmdProperty "sh" ["-c", shellcmd]- where- shellcmd = intercalate " ; " ("set -e" : script)---- | A property that can satisfied by running a series of shell commands,--- as user (cd'd to their home directory).-userScriptProperty :: UserName -> [String] -> Property-userScriptProperty user script = cmdProperty "su" ["-c", shellcmd, user]- where- shellcmd = intercalate " ; " ("set -e" : "cd" : script)
− Propellor/Property/Cron.hs
@@ -1,49 +0,0 @@-module Propellor.Property.Cron where--import Propellor-import qualified Propellor.Property.File as File-import qualified Propellor.Property.Apt as Apt-import Utility.SafeCommand--import Data.Char--type CronTimes = String---- | Installs a cron job, run as a specified user, in a particular--- directory. Note that the Desc must be unique, as it is used for the --- cron.d/ filename.--- --- Only one instance of the cron job is allowed to run at a time, no matter--- how long it runs. This is accomplished using flock locking of the cron--- job file.------ The cron job's output will only be emailed if it exits nonzero.-job :: Desc -> CronTimes -> UserName -> FilePath -> String -> Property-job desc times user cddir command = cronjobfile `File.hasContent`- [ "# Generated by propellor"- , ""- , "SHELL=/bin/sh"- , "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin"- , ""- , times ++ "\t" ++ user ++ "\t"- ++ "chronic flock -n " ++ shellEscape cronjobfile- ++ " sh -c " ++ shellEscape cmdline- ]- `requires` Apt.serviceInstalledRunning "cron"- `requires` Apt.installed ["util-linux", "moreutils"]- `describe` ("cronned " ++ desc)- where- cmdline = "cd " ++ cddir ++ " && ( " ++ command ++ " )"- cronjobfile = "/etc/cron.d/" ++ map sanitize desc- sanitize c- | isAlphaNum c = c- | otherwise = '_'---- | Installs a cron job, and runs it niced and ioniced.-niceJob :: Desc -> CronTimes -> UserName -> FilePath -> String -> Property-niceJob desc times user cddir command = job desc times user cddir- ("nice ionice -c 3 " ++ command)---- | Installs a cron job to run propellor.-runPropellor :: CronTimes -> Property-runPropellor times = niceJob "propellor" times "root" localdir "make"
− Propellor/Property/Dns.hs
@@ -1,405 +0,0 @@-module Propellor.Property.Dns (- module Propellor.Types.Dns,- primary,- secondary,- secondaryFor,- mkSOA,- writeZoneFile,- nextSerialNumber,- adjustSerialNumber,- serialNumberOffset,- WarningMessage,- genZone,-) where--import Propellor-import Propellor.Types.Dns-import Propellor.Property.File-import Propellor.Types.Attr-import qualified Propellor.Property.Apt as Apt-import qualified Propellor.Property.Service as Service-import Utility.Applicative--import qualified Data.Map as M-import qualified Data.Set as S-import Data.List---- | Primary dns server for a domain.------ Most of the content of the zone file is configured by setting properties--- of hosts. For example,------ > host "foo.example.com"--- > & ipv4 "192.168.1.1"--- > & alias "mail.exmaple.com"------ Will cause that hostmame and its alias to appear in the zone file,--- with the configured IP address.------ The [(BindDomain, Record)] list can be used for additional records--- that cannot be configured elsewhere. This often includes NS records,--- TXT records and perhaps CNAMEs pointing at hosts that propellor does--- not control.------ The primary server is configured to only allow zone transfers to--- secondary dns servers. These are determined in two ways:------ 1. By looking at the properties of other hosts, to find hosts that--- are configured as the secondary dns server.------ 2. By looking for NS Records in the passed list of records.------ In either case, the secondary dns server Host should have an ipv4 and/or--- ipv6 property defined.-primary :: [Host] -> Domain -> SOA -> [(BindDomain, Record)] -> RevertableProperty-primary hosts domain soa rs = RevertableProperty setup cleanup- where- setup = withwarnings (check needupdate baseprop)- `requires` servingZones- `onChange` Service.reloaded "bind9"- cleanup = check (doesFileExist zonefile) $- property ("removed dns primary for " ++ domain)- (makeChange $ removeZoneFile zonefile)- `requires` namedConfWritten- `onChange` Service.reloaded "bind9"-- (partialzone, zonewarnings) = genZone hosts domain soa- zone = partialzone { zHosts = zHosts partialzone ++ rs }- zonefile = "/etc/bind/propellor/db." ++ domain- baseprop = Property ("dns primary for " ++ domain)- (makeChange $ writeZoneFile zone zonefile)- (addNamedConf conf)- withwarnings p = adjustProperty p $ \satisfy -> do- mapM_ warningMessage $ zonewarnings ++ secondarywarnings- satisfy- conf = NamedConf- { confDomain = domain- , confDnsServerType = Master- , confFile = zonefile- , confMasters = []- , confAllowTransfer = nub $- concatMap (\h -> hostAddresses h hosts) $- secondaries ++ nssecondaries- , confLines = []- }- secondaries = otherServers Secondary hosts domain- secondarywarnings = map (\h -> "No IP address defined for DNS seconary " ++ h) $- filter (\h -> null (hostAddresses h hosts)) secondaries- nssecondaries = mapMaybe (domainHostName <=< getNS) rootRecords- rootRecords = map snd $- filter (\(d, _r) -> d == RootDomain || d == AbsDomain domain) rs- needupdate = do- v <- readZonePropellorFile zonefile- return $ case v of- Nothing -> True- Just oldzone ->- -- compare everything except serial- let oldserial = sSerial (zSOA oldzone)- z = zone { zSOA = (zSOA zone) { sSerial = oldserial } }- in z /= oldzone || oldserial < sSerial (zSOA zone)---- | Secondary dns server for a domain.------ The primary server is determined by looking at the properties of other--- hosts to find which one is configured as the primary.------ Note that if a host is declared to be a primary and a secondary dns--- server for the same domain, the primary server config always wins.-secondary :: [Host] -> Domain -> RevertableProperty-secondary hosts domain = secondaryFor (otherServers Master hosts domain) hosts domain---- | This variant is useful if the primary server does not have its DNS--- configured via propellor.-secondaryFor :: [HostName] -> [Host] -> Domain -> RevertableProperty-secondaryFor masters hosts domain = RevertableProperty setup cleanup- where- setup = pureAttrProperty desc (addNamedConf conf)- `requires` servingZones- cleanup = namedConfWritten-- desc = "dns secondary for " ++ domain- conf = NamedConf- { confDomain = domain- , confDnsServerType = Secondary- , confFile = "db." ++ domain- , confMasters = concatMap (\m -> hostAddresses m hosts) masters- , confAllowTransfer = []- , confLines = []- }--otherServers :: DnsServerType -> [Host] -> Domain -> [HostName]-otherServers wantedtype hosts domain =- M.keys $ M.filter wanted $ hostAttrMap hosts- where- wanted attr = case M.lookup domain (_namedconf attr) of- Nothing -> False- Just conf -> confDnsServerType conf == wantedtype- && confDomain conf == domain---- | Rewrites the whole named.conf.local file to serve the zones--- configured by `primary` and `secondary`, and ensures that bind9 is--- running.-servingZones :: Property-servingZones = namedConfWritten- `onChange` Service.reloaded "bind9"- `requires` Apt.serviceInstalledRunning "bind9"--namedConfWritten :: Property-namedConfWritten = property "named.conf configured" $ do- zs <- getNamedConf- ensureProperty $- hasContent namedConfFile $- concatMap confStanza $ M.elems zs--confStanza :: NamedConf -> [Line]-confStanza c =- [ "// automatically generated by propellor"- , "zone \"" ++ confDomain c ++ "\" {"- , cfgline "type" (if confDnsServerType c == Master then "master" else "slave")- , cfgline "file" ("\"" ++ confFile c ++ "\"")- ] ++- mastersblock ++- allowtransferblock ++- (map (\l -> "\t" ++ l ++ ";") (confLines c)) ++- [ "};"- , ""- ]- where- cfgline f v = "\t" ++ f ++ " " ++ v ++ ";"- ipblock name l = - [ "\t" ++ name ++ " {" ] ++- (map (\ip -> "\t\t" ++ fromIPAddr ip ++ ";") l) ++- [ "\t};" ]- mastersblock- | null (confMasters c) = []- | otherwise = ipblock "masters" (confMasters c)- -- an empty block prohibits any transfers- allowtransferblock = ipblock "allow-transfer" (confAllowTransfer c)--namedConfFile :: FilePath-namedConfFile = "/etc/bind/named.conf.local"---- | Generates a SOA with some fairly sane numbers in it.------ The Domain is the domain to use in the SOA record. Typically--- something like ns1.example.com. So, not the domain that this is the SOA--- record for.------ The SerialNumber can be whatever serial number was used by the domain--- before propellor started managing it. Or 0 if the domain has only ever--- been managed by propellor.------ You do not need to increment the SerialNumber when making changes!--- Propellor will automatically add the number of commits in the git--- repository to the SerialNumber.-mkSOA :: Domain -> SerialNumber -> SOA-mkSOA d sn = SOA- { sDomain = AbsDomain d- , sSerial = sn- , sRefresh = hours 4- , sRetry = hours 1- , sExpire = 2419200 -- 4 weeks- , sNegativeCacheTTL = hours 8- }- where- hours n = n * 60 * 60--dValue :: BindDomain -> String-dValue (RelDomain d) = d-dValue (AbsDomain d) = d ++ "."-dValue (RootDomain) = "@"--rField :: Record -> String-rField (Address (IPv4 _)) = "A"-rField (Address (IPv6 _)) = "AAAA"-rField (CNAME _) = "CNAME"-rField (MX _ _) = "MX"-rField (NS _) = "NS"-rField (TXT _) = "TXT"-rField (SRV _ _ _ _) = "SRV"--rValue :: Record -> String-rValue (Address (IPv4 addr)) = addr-rValue (Address (IPv6 addr)) = addr-rValue (CNAME d) = dValue d-rValue (MX pri d) = show pri ++ " " ++ dValue d-rValue (NS d) = dValue d-rValue (SRV priority weight port target) = unwords- [ show priority- , show weight- , show port- , dValue target- ]-rValue (TXT s) = [q] ++ filter (/= q) s ++ [q]- where- q = '"'---- | Adjusts the serial number of the zone to always be larger--- than the serial number in the Zone record,--- and always be larger than the passed SerialNumber.-nextSerialNumber :: Zone -> SerialNumber -> Zone-nextSerialNumber z serial = adjustSerialNumber z $ \sn -> succ $ max sn serial--adjustSerialNumber :: Zone -> (SerialNumber -> SerialNumber) -> Zone-adjustSerialNumber (Zone d soa l) f = Zone d soa' l- where- soa' = soa { sSerial = f (sSerial soa) }---- | Count the number of git commits made to the current branch.-serialNumberOffset :: IO SerialNumber-serialNumberOffset = fromIntegral . length . lines- <$> readProcess "git" ["log", "--pretty=%H"]---- | Write a Zone out to a to a file.------ The serial number in the Zone automatically has the serialNumberOffset--- added to it. Also, just in case, the old serial number used in the zone--- file is checked, and if it is somehow larger, its succ is used.-writeZoneFile :: Zone -> FilePath -> IO ()-writeZoneFile z f = do- oldserial <- oldZoneFileSerialNumber f- offset <- serialNumberOffset- let z' = nextSerialNumber- (adjustSerialNumber z (+ offset))- oldserial- createDirectoryIfMissing True (takeDirectory f)- writeFile f (genZoneFile z')- writeZonePropellorFile f z'--removeZoneFile :: FilePath -> IO ()-removeZoneFile f = do- nukeFile f- nukeFile (zonePropellorFile f)---- | Next to the zone file, is a ".propellor" file, which contains--- the serialized Zone. This saves the bother of parsing--- the horrible bind zone file format.-zonePropellorFile :: FilePath -> FilePath-zonePropellorFile f = f ++ ".propellor"--oldZoneFileSerialNumber :: FilePath -> IO SerialNumber-oldZoneFileSerialNumber = maybe 0 (sSerial . zSOA) <$$> readZonePropellorFile--writeZonePropellorFile :: FilePath -> Zone -> IO ()-writeZonePropellorFile f z = writeFile (zonePropellorFile f) (show z)--readZonePropellorFile :: FilePath -> IO (Maybe Zone)-readZonePropellorFile f = catchDefaultIO Nothing $- readish <$> readFileStrict (zonePropellorFile f)---- | Generating a zone file.-genZoneFile :: Zone -> String-genZoneFile (Zone zdomain soa rs) = unlines $- header : genSOA soa ++ map (genRecord zdomain) rs- where- header = com $ "BIND zone file for " ++ zdomain ++ ". Generated by propellor, do not edit."--genRecord :: Domain -> (BindDomain, Record) -> String-genRecord zdomain (domain, record) = intercalate "\t"- [ domainHost zdomain domain- , "IN"- , rField record- , rValue record- ]--genSOA :: SOA -> [String]-genSOA soa = - -- "@ IN SOA ns1.example.com. root ("- [ intercalate "\t"- [ dValue RootDomain - , "IN"- , "SOA"- , dValue (sDomain soa)- , "root"- , "("- ]- , headerline sSerial "Serial"- , headerline sRefresh "Refresh"- , headerline sRetry "Retry"- , headerline sExpire "Expire"- , headerline sNegativeCacheTTL "Negative Cache TTL"- , inheader ")"- ]- where- headerline r comment = inheader $ show (r soa) ++ "\t\t" ++ com comment- inheader l = "\t\t\t" ++ l---- | Comment line in a zone file.-com :: String -> String-com s = "; " ++ s--type WarningMessage = String---- | Generates a Zone for a particular Domain from the DNS properies of all--- hosts that propellor knows about that are in that Domain.-genZone :: [Host] -> Domain -> SOA -> (Zone, [WarningMessage])-genZone hosts zdomain soa =- let (warnings, zhosts) = partitionEithers $ concat $ map concat- [ map hostips inzdomain- , map hostrecords inzdomain- , map addcnames (M.elems m)- ]- in (Zone zdomain soa (nub zhosts), warnings)- where- m = hostAttrMap hosts- -- Known hosts with hostname located in the zone's domain.- inzdomain = M.elems $ M.filterWithKey (\hn _ -> inDomain zdomain $ AbsDomain $ hn) m- - -- Each host with a hostname located in the zdomain- -- should have 1 or more IPAddrs in its Attr.- --- -- If a host lacks any IPAddr, it's probably a misconfiguration,- -- so warn.- hostips :: Attr -> [Either WarningMessage (BindDomain, Record)]- hostips attr- | null l = [Left $ "no IP address defined for host " ++ _hostname attr]- | otherwise = map Right l- where- l = zip (repeat $ AbsDomain $ _hostname attr)- (map Address $ getAddresses attr)-- -- Any host, whether its hostname is in the zdomain or not,- -- may have cnames which are in the zdomain. The cname may even be- -- the same as the root of the zdomain, which is a nice way to- -- specify IP addresses for a SOA record.- --- -- Add Records for those.. But not actually, usually, cnames!- -- Why not? Well, using cnames doesn't allow doing some things,- -- including MX and round robin DNS, and certianly CNAMES- -- shouldn't be used in SOA records.- --- -- We typically know the host's IPAddrs anyway.- -- So we can just use the IPAddrs.- addcnames :: Attr -> [Either WarningMessage (BindDomain, Record)]- addcnames attr = concatMap gen $ filter (inDomain zdomain) $- mapMaybe getCNAME $ S.toList (_dns attr)- where- gen c = case getAddresses attr of- [] -> [ret (CNAME c)]- l -> map (ret . Address) l- where- ret record = Right (c, record)- - -- Adds any other DNS records for a host located in the zdomain.- hostrecords :: Attr -> [Either WarningMessage (BindDomain, Record)]- hostrecords attr = map Right l- where- l = zip (repeat $ AbsDomain $ _hostname attr)- (S.toList $ S.filter (\r -> isNothing (getIPAddr r) && isNothing (getCNAME r)) (_dns attr))--inDomain :: Domain -> BindDomain -> Bool-inDomain domain (AbsDomain d) = domain == d || ('.':domain) `isSuffixOf` d-inDomain _ _ = False -- can't tell, so assume not---- | Gets the hostname of the second domain, relative to the first domain,--- suitable for using in a zone file.-domainHost :: Domain -> BindDomain -> String-domainHost _ (RelDomain d) = d-domainHost _ RootDomain = "@"-domainHost base (AbsDomain d)- | dotbase `isSuffixOf` d = take (length d - length dotbase) d- | base == d = "@"- | otherwise = d- where- dotbase = '.':base-
− Propellor/Property/Docker.hs
@@ -1,459 +0,0 @@-{-# LANGUAGE BangPatterns #-}---- | Docker support for propellor------ The existance of a docker container is just another Property of a system,--- which propellor can set up. See config.hs for an example.--module Propellor.Property.Docker where--import Propellor-import Propellor.SimpleSh-import Propellor.Types.Attr-import qualified Propellor.Property.File as File-import qualified Propellor.Property.Apt as Apt-import qualified Propellor.Property.Docker.Shim as Shim-import Utility.SafeCommand-import Utility.Path--import Control.Concurrent.Async-import System.Posix.Directory-import System.Posix.Process-import Data.List-import Data.List.Utils---- | Configures docker with an authentication file, so that images can be--- pushed to index.docker.io.-configured :: Property-configured = property "docker configured" go `requires` installed- where- go = withPrivData DockerAuthentication $ \cfg -> ensureProperty $ - "/root/.dockercfg" `File.hasContent` (lines cfg)--installed :: Property-installed = Apt.installed ["docker.io"]---- | A short descriptive name for a container.--- Should not contain whitespace or other unusual characters,--- only [a-zA-Z0-9_-] are allowed-type ContainerName = String---- | Starts accumulating the properties of a Docker container.------ > container "web-server" "debian"--- > & publish "80:80"--- > & Apt.installed {"apache2"]--- > & ...-container :: ContainerName -> Image -> Host-container cn image = Host [] (\_ -> attr)- where- attr = (newAttr (cn2hn cn)) { _dockerImage = Just image }--cn2hn :: ContainerName -> HostName-cn2hn cn = cn ++ ".docker"---- | Ensures that a docker container is set up and running. The container--- has its own Properties which are handled by running propellor--- inside the container.------ Reverting this property ensures that the container is stopped and--- removed.-docked- :: [Host]- -> ContainerName- -> RevertableProperty-docked hosts cn = RevertableProperty (go "docked" setup) (go "undocked" teardown)- where- go desc a = property (desc ++ " " ++ cn) $ do- hn <- getHostName- let cid = ContainerId hn cn- ensureProperties [findContainer hosts cid cn $ a cid]-- setup cid (Container image runparams) =- provisionContainer cid- `requires`- runningContainer cid image runparams- `requires`- installed-- teardown cid (Container image _runparams) =- combineProperties ("undocked " ++ fromContainerId cid)- [ stoppedContainer cid- , property ("cleaned up " ++ fromContainerId cid) $- liftIO $ report <$> mapM id- [ removeContainer cid- , removeImage image- ]- ]--findContainer- :: [Host]- -> ContainerId- -> ContainerName- -> (Container -> Property)- -> Property-findContainer hosts cid cn mk = case findHost hosts (cn2hn cn) of- Nothing -> cantfind- Just h -> maybe cantfind mk (mkContainer cid h)- where- cantfind = containerDesc cid $ property "" $ do- liftIO $ warningMessage $- "missing definition for docker container \"" ++ cn2hn cn- return FailedChange--mkContainer :: ContainerId -> Host -> Maybe Container-mkContainer cid@(ContainerId hn _cn) h = Container- <$> _dockerImage attr- <*> pure (map (\a -> a hn) (_dockerRunParams attr))- where- attr = hostAttr h'- h' = h- -- expose propellor directory inside the container- & volume (localdir++":"++localdir)- -- name the container in a predictable way so we- -- and the user can easily find it later- & name (fromContainerId cid)---- | Causes *any* docker images that are not in use by running containers to--- be deleted. And deletes any containers that propellor has set up--- before that are not currently running. Does not delete any containers--- that were not set up using propellor.------ Generally, should come after the properties for the desired containers.-garbageCollected :: Property-garbageCollected = propertyList "docker garbage collected"- [ gccontainers- , gcimages- ]- where- gccontainers = property "docker containers garbage collected" $- liftIO $ report <$> (mapM removeContainer =<< listContainers AllContainers)- gcimages = property "docker images garbage collected" $ do- liftIO $ report <$> (mapM removeImage =<< listImages)--data Container = Container Image [RunParam]---- | Parameters to pass to `docker run` when creating a container.-type RunParam = String---- | A docker image, that can be used to run a container.-type Image = String---- | Set custom dns server for container.-dns :: String -> Property-dns = runProp "dns"---- | Set container host name.-hostname :: String -> Property-hostname = runProp "hostname"---- | Set name for container. (Normally done automatically.)-name :: String -> Property-name = runProp "name"---- | Publish a container's port to the host--- (format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort)-publish :: String -> Property-publish = runProp "publish"---- | Username or UID for container.-user :: String -> Property-user = runProp "user"---- | Mount a volume--- Create a bind mount with: [host-dir]:[container-dir]:[rw|ro]--- With just a directory, creates a volume in the container.-volume :: String -> Property-volume = runProp "volume"---- | Mount a volume from the specified container into the current--- container.-volumes_from :: ContainerName -> Property-volumes_from cn = genProp "volumes-from" $ \hn ->- fromContainerId (ContainerId hn cn)---- | Work dir inside the container. -workdir :: String -> Property-workdir = runProp "workdir"---- | Memory limit for container.---Format: <number><optional unit>, where unit = b, k, m or g-memory :: String -> Property-memory = runProp "memory"---- | Link with another container on the same host.-link :: ContainerName -> ContainerAlias -> Property-link linkwith calias = genProp "link" $ \hn ->- fromContainerId (ContainerId hn linkwith) ++ ":" ++ calias---- | A short alias for a linked container.--- Each container has its own alias namespace.-type ContainerAlias = String---- | A container is identified by its name, and the host--- on which it's deployed.-data ContainerId = ContainerId HostName ContainerName- deriving (Eq, Read, Show)---- | Two containers with the same ContainerIdent were started from--- the same base image (possibly a different version though), and--- with the same RunParams.-data ContainerIdent = ContainerIdent Image HostName ContainerName [RunParam]- deriving (Read, Show, Eq)--ident2id :: ContainerIdent -> ContainerId-ident2id (ContainerIdent _ hn cn _) = ContainerId hn cn--toContainerId :: String -> Maybe ContainerId-toContainerId s- | myContainerSuffix `isSuffixOf` s = case separate (== '.') (desuffix s) of- (cn, hn)- | null hn || null cn -> Nothing- | otherwise -> Just $ ContainerId hn cn- | otherwise = Nothing- where- desuffix = reverse . drop len . reverse- len = length myContainerSuffix--fromContainerId :: ContainerId -> String-fromContainerId (ContainerId hn cn) = cn++"."++hn++myContainerSuffix--containerHostName :: ContainerId -> HostName-containerHostName (ContainerId _ cn) = cn2hn cn--myContainerSuffix :: String-myContainerSuffix = ".propellor"--containerDesc :: ContainerId -> Property -> Property-containerDesc cid p = p `describe` desc- where- desc = "[" ++ fromContainerId cid ++ "] " ++ propertyDesc p--runningContainer :: ContainerId -> Image -> [RunParam] -> Property-runningContainer cid@(ContainerId hn cn) image runps = containerDesc cid $ property "running" $ do- l <- liftIO $ listContainers RunningContainers- if cid `elem` l- then do- -- Check if the ident has changed; if so the- -- parameters of the container differ and it must- -- be restarted.- runningident <- liftIO $ getrunningident- if runningident == Just ident- then noChange- else do- void $ liftIO $ stopContainer cid- restartcontainer- else ifM (liftIO $ elem cid <$> listContainers AllContainers)- ( restartcontainer- , go image- )- where- ident = ContainerIdent image hn cn runps-- restartcontainer = do- oldimage <- liftIO $ fromMaybe image <$> commitContainer cid- void $ liftIO $ removeContainer cid- go oldimage-- getrunningident :: IO (Maybe ContainerIdent)- getrunningident = simpleShClient (namedPipe cid) "cat" [propellorIdent] $ \rs -> do- let !v = extractident rs- return v-- extractident :: [Resp] -> Maybe ContainerIdent- extractident = headMaybe . catMaybes . map readish . catMaybes . map getStdout-- go img = do- liftIO $ do- clearProvisionedFlag cid- createDirectoryIfMissing True (takeDirectory $ identFile cid)- shim <- liftIO $ Shim.setup (localdir </> "propellor") (localdir </> shimdir cid)- liftIO $ writeFile (identFile cid) (show ident)- ensureProperty $ boolProperty "run" $ runContainer img- (runps ++ ["-i", "-d", "-t"])- [shim, "--docker", fromContainerId cid]---- | Called when propellor is running inside a docker container.--- The string should be the container's ContainerId.------ This process is effectively init inside the container.--- It even needs to wait on zombie processes!------ Fork a thread to run the SimpleSh server in the background.--- In the foreground, run an interactive bash (or sh) shell,--- so that the user can interact with it when attached to the container.------ When the system reboots, docker restarts the container, and this is run--- again. So, to make the necessary services get started on boot, this needs--- to provision the container then. However, if the container is already--- being provisioned by the calling propellor, it would be redundant and--- problimatic to also provisoon it here.------ The solution is a flag file. If the flag file exists, then the container--- was already provisioned. So, it must be a reboot, and time to provision--- again. If the flag file doesn't exist, don't provision here.-chain :: String -> IO ()-chain s = case toContainerId s of- Nothing -> error $ "Invalid ContainerId: " ++ s- Just cid -> do- changeWorkingDirectory localdir- writeFile propellorIdent . show =<< readIdentFile cid- -- Run boot provisioning before starting simpleSh,- -- to avoid ever provisioning twice at the same time.- whenM (checkProvisionedFlag cid) $ do- let shim = Shim.file (localdir </> "propellor") (localdir </> shimdir cid)- unlessM (boolSystem shim [Param "--continue", Param $ show $ Chain $ containerHostName cid]) $- warningMessage "Boot provision failed!"- void $ async $ job reapzombies- void $ async $ job $ simpleSh $ namedPipe cid- job $ do- void $ tryIO $ ifM (inPath "bash")- ( boolSystem "bash" [Param "-l"]- , boolSystem "/bin/sh" []- )- putStrLn "Container is still running. Press ^P^Q to detach."- where- job = forever . void . tryIO- reapzombies = void $ getAnyProcessStatus True False---- | Once a container is running, propellor can be run inside--- it to provision it.------ Note that there is a race here, between the simplesh--- server starting up in the container, and this property--- being run. So, retry connections to the client for up to--- 1 minute.-provisionContainer :: ContainerId -> Property-provisionContainer cid = containerDesc cid $ property "provision" $ liftIO $ do- let shim = Shim.file (localdir </> "propellor") (localdir </> shimdir cid)- r <- simpleShClientRetry 60 (namedPipe cid) shim params (go Nothing)- when (r /= FailedChange) $- setProvisionedFlag cid - return r- where- params = ["--continue", show $ Chain $ containerHostName cid]-- go lastline (v:rest) = case v of- StdoutLine s -> do- debug ["stdout: ", show s]- maybe noop putStrLn lastline- hFlush stdout- go (Just s) rest- StderrLine s -> do- debug ["stderr: ", show s]- maybe noop putStrLn lastline- hFlush stdout- hPutStrLn stderr s- hFlush stderr- go Nothing rest- Done -> ret lastline- go lastline [] = ret lastline-- ret lastline = return $ fromMaybe FailedChange $- readish =<< lastline--stopContainer :: ContainerId -> IO Bool-stopContainer cid = boolSystem dockercmd [Param "stop", Param $ fromContainerId cid ]--stoppedContainer :: ContainerId -> Property-stoppedContainer cid = containerDesc cid $ property desc $ - ifM (liftIO $ elem cid <$> listContainers RunningContainers)- ( liftIO cleanup `after` ensureProperty - (boolProperty desc $ stopContainer cid)- , return NoChange- )- where- desc = "stopped"- cleanup = do- nukeFile $ namedPipe cid- nukeFile $ identFile cid- removeDirectoryRecursive $ shimdir cid- clearProvisionedFlag cid--removeContainer :: ContainerId -> IO Bool-removeContainer cid = catchBoolIO $- snd <$> processTranscript dockercmd ["rm", fromContainerId cid ] Nothing--removeImage :: Image -> IO Bool-removeImage image = catchBoolIO $- snd <$> processTranscript dockercmd ["rmi", image ] Nothing--runContainer :: Image -> [RunParam] -> [String] -> IO Bool-runContainer image ps cmd = boolSystem dockercmd $ map Param $- "run" : (ps ++ image : cmd)--commitContainer :: ContainerId -> IO (Maybe Image)-commitContainer cid = catchMaybeIO $- takeWhile (/= '\n') - <$> readProcess dockercmd ["commit", fromContainerId cid]--data ContainerFilter = RunningContainers | AllContainers- deriving (Eq)---- | Only lists propellor managed containers.-listContainers :: ContainerFilter -> IO [ContainerId]-listContainers status = - catMaybes . map toContainerId . concat . map (split ",")- . catMaybes . map (lastMaybe . words) . lines- <$> readProcess dockercmd ps- where- ps- | status == AllContainers = baseps ++ ["--all"]- | otherwise = baseps- baseps = ["ps", "--no-trunc"]--listImages :: IO [Image]-listImages = lines <$> readProcess dockercmd ["images", "--all", "--quiet"]--runProp :: String -> RunParam -> Property-runProp field val = pureAttrProperty (param) $ \attr ->- attr { _dockerRunParams = _dockerRunParams attr ++ [\_ -> "--"++param] }- where- param = field++"="++val--genProp :: String -> (HostName -> RunParam) -> Property-genProp field mkval = pureAttrProperty field $ \attr ->- attr { _dockerRunParams = _dockerRunParams attr ++ [\hn -> "--"++field++"=" ++ mkval hn] }---- | The ContainerIdent of a container is written to--- /.propellor-ident inside it. This can be checked to see if--- the container has the same ident later.-propellorIdent :: FilePath-propellorIdent = "/.propellor-ident"---- | Named pipe used for communication with the container.-namedPipe :: ContainerId -> FilePath-namedPipe cid = "docker" </> fromContainerId cid--provisionedFlag :: ContainerId -> FilePath-provisionedFlag cid = "docker" </> fromContainerId cid ++ ".provisioned"--clearProvisionedFlag :: ContainerId -> IO ()-clearProvisionedFlag = nukeFile . provisionedFlag--setProvisionedFlag :: ContainerId -> IO ()-setProvisionedFlag cid = do- createDirectoryIfMissing True (takeDirectory (provisionedFlag cid))- writeFile (provisionedFlag cid) "1"--checkProvisionedFlag :: ContainerId -> IO Bool-checkProvisionedFlag = doesFileExist . provisionedFlag--shimdir :: ContainerId -> FilePath-shimdir cid = "docker" </> fromContainerId cid ++ ".shim"--identFile :: ContainerId -> FilePath-identFile cid = "docker" </> fromContainerId cid ++ ".ident"--readIdentFile :: ContainerId -> IO ContainerIdent-readIdentFile cid = fromMaybe (error "bad ident in identFile")- . readish <$> readFile (identFile cid)--dockercmd :: String-dockercmd = "docker.io"--report :: [Bool] -> Result-report rmed- | or rmed = MadeChange- | otherwise = NoChange-
− Propellor/Property/Docker/Shim.hs
@@ -1,61 +0,0 @@--- | Support for running propellor, as built outside a docker container,--- inside the container.------ Note: This is currently Debian specific, due to glibcLibs.--module Propellor.Property.Docker.Shim (setup, cleanEnv, file) where--import Propellor-import Utility.LinuxMkLibs-import Utility.SafeCommand-import Utility.Path-import Utility.FileMode--import Data.List-import System.Posix.Files---- | Sets up a shimmed version of the program, in a directory, and--- returns its path.-setup :: FilePath -> FilePath -> IO FilePath-setup propellorbin dest = do- createDirectoryIfMissing True dest-- libs <- parseLdd <$> readProcess "ldd" [propellorbin]- glibclibs <- glibcLibs- let libs' = nub $ libs ++ glibclibs- libdirs <- map (dest ++) . nub . catMaybes- <$> mapM (installLib installFile dest) libs'- - let linker = (dest ++) $ - fromMaybe (error "cannot find ld-linux linker") $- headMaybe $ filter ("ld-linux" `isInfixOf`) libs'- let gconvdir = (dest ++) $ parentDir $- fromMaybe (error "cannot find gconv directory") $- headMaybe $ filter ("/gconv/" `isInfixOf`) glibclibs- let linkerparams = ["--library-path", intercalate ":" libdirs ]- let shim = file propellorbin dest- writeFile shim $ unlines- [ "#!/bin/sh"- , "GCONV_PATH=" ++ shellEscape gconvdir- , "export GCONV_PATH"- , "exec " ++ unwords (map shellEscape $ linker : linkerparams) ++ - " " ++ shellEscape propellorbin ++ " \"$@\""- ]- modifyFileMode shim (addModes executeModes)- return shim--cleanEnv :: IO ()-cleanEnv = void $ unsetEnv "GCONV_PATH"--file :: FilePath -> FilePath -> FilePath-file propellorbin dest = dest </> takeFileName propellorbin--installFile :: FilePath -> FilePath -> IO ()-installFile top f = do- createDirectoryIfMissing True destdir- nukeFile dest- createLink f dest `catchIO` (const copy)- where- copy = void $ boolSystem "cp" [Param "-a", Param f, Param dest]- destdir = inTop top $ parentDir f- dest = inTop top f
− Propellor/Property/File.hs
@@ -1,94 +0,0 @@-module Propellor.Property.File where--import Propellor-import Utility.FileMode--import System.Posix.Files-import System.PosixCompat.Types--type Line = String---- | Replaces all the content of a file.-hasContent :: FilePath -> [Line] -> Property-f `hasContent` newcontent = fileProperty ("replace " ++ f)- (\_oldcontent -> newcontent) f---- | Ensures a file has contents that comes from PrivData.------ The file's permissions are preserved if the file already existed.--- Otherwise, they're set to 600.-hasPrivContent :: FilePath -> Property-hasPrivContent f = property desc $ withPrivData (PrivFile f) $ \privcontent -> - ensureProperty $ fileProperty' writeFileProtected desc- (\_oldcontent -> lines privcontent) f- where- desc = "privcontent " ++ f---- | Leaves the file world-readable.-hasPrivContentExposed :: FilePath -> Property-hasPrivContentExposed f = hasPrivContent f `onChange`- mode f (combineModes (ownerWriteMode:readModes))---- | Ensures that a line is present in a file, adding it to the end if not.-containsLine :: FilePath -> Line -> Property-f `containsLine` l = f `containsLines` [l]--containsLines :: FilePath -> [Line] -> Property-f `containsLines` l = fileProperty (f ++ " contains:" ++ show l) go f- where- go ls- | all (`elem` ls) l = ls- | otherwise = ls++l---- | Ensures that a line is not present in a file.--- Note that the file is ensured to exist, so if it doesn't, an empty--- file will be written.-lacksLine :: FilePath -> Line -> Property-f `lacksLine` l = fileProperty (f ++ " remove: " ++ l) (filter (/= l)) f---- | Removes a file. Does not remove symlinks or non-plain-files.-notPresent :: FilePath -> Property-notPresent f = check (doesFileExist f) $ property (f ++ " not present") $ - makeChange $ nukeFile f--fileProperty :: Desc -> ([Line] -> [Line]) -> FilePath -> Property-fileProperty = fileProperty' writeFile-fileProperty' :: (FilePath -> String -> IO ()) -> Desc -> ([Line] -> [Line]) -> FilePath -> Property-fileProperty' writer desc a f = property desc $ go =<< liftIO (doesFileExist f)- where- go True = do- ls <- liftIO $ lines <$> readFile f- let ls' = a ls- if ls' == ls- then noChange- else makeChange $ viaTmp updatefile f (unlines ls')- go False = makeChange $ writer f (unlines $ a [])-- -- viaTmp makes the temp file mode 600.- -- Replicate the original file's owner and mode.- updatefile f' content = do- writer f' content- s <- getFileStatus f- setFileMode f' (fileMode s)- setOwnerAndGroup f' (fileOwner s) (fileGroup s)---- | Ensures a directory exists.-dirExists :: FilePath -> Property-dirExists d = check (not <$> doesDirectoryExist d) $ property (d ++ " exists") $- makeChange $ createDirectoryIfMissing True d---- | Ensures that a file/dir has the specified owner and group.-ownerGroup :: FilePath -> UserName -> GroupName -> Property-ownerGroup f owner group = property (f ++ " owner " ++ og) $ do- r <- ensureProperty $ cmdProperty "chown" [og, f]- if r == FailedChange- then return r- else noChange- where- og = owner ++ ":" ++ group---- | Ensures that a file/dir has the specfied mode.-mode :: FilePath -> FileMode -> Property-mode f v = property (f ++ " mode " ++ show v) $ do- liftIO $ modifyFileMode f (\_old -> v)- noChange
− Propellor/Property/Git.hs
@@ -1,89 +0,0 @@-module Propellor.Property.Git where--import Propellor-import Propellor.Property.File-import qualified Propellor.Property.Apt as Apt-import qualified Propellor.Property.Service as Service-import Utility.SafeCommand--import Data.List---- | Exports all git repos in a directory (that user nobody can read)--- using git-daemon, run from inetd.------ Note that reverting this property does not remove or stop inetd.-daemonRunning :: FilePath -> RevertableProperty-daemonRunning exportdir = RevertableProperty setup unsetup- where- setup = containsLine conf (mkl "tcp4")- `requires`- containsLine conf (mkl "tcp6")- `requires`- dirExists exportdir- `requires`- Apt.serviceInstalledRunning "openbsd-inetd"- `onChange`- Service.running "openbsd-inetd"- `describe` ("git-daemon exporting " ++ exportdir)- unsetup = lacksLine conf (mkl "tcp4")- `requires`- lacksLine conf (mkl "tcp6")- `onChange`- Service.reloaded "openbsd-inetd"-- conf = "/etc/inetd.conf"-- mkl tcpv = intercalate "\t"- [ "git"- , "stream"- , tcpv- , "nowait"- , "nobody"- , "/usr/bin/git"- , "git"- , "daemon"- , "--inetd"- , "--export-all"- , "--base-path=" ++ exportdir- , exportdir- ]--installed :: Property-installed = Apt.installed ["git"]--type RepoUrl = String--type Branch = String---- | Specified git repository is cloned to the specified directory.------ If the firectory exists with some other content, it will be recursively--- deleted.------ A branch can be specified, to check out.-cloned :: UserName -> RepoUrl -> FilePath -> Maybe Branch -> Property-cloned owner url dir mbranch = check originurl (property desc checkout)- `requires` installed- where- desc = "git cloned " ++ url ++ " to " ++ dir- gitconfig = dir </> ".git/config"- originurl = ifM (doesFileExist gitconfig)- ( do- v <- catchDefaultIO Nothing $ headMaybe . lines <$>- readProcess "git" ["config", "--file", gitconfig, "remote.origin.url"]- return (v /= Just url)- , return True- )- checkout = do- liftIO $ do- whenM (doesDirectoryExist dir) $- removeDirectoryRecursive dir- createDirectoryIfMissing True (takeDirectory dir)- ensureProperty $ userScriptProperty owner $ catMaybes- -- The </dev/null fixes an intermittent- -- "fatal: read error: Bad file descriptor"- -- when run across ssh with propellor --spin- [ Just $ "git clone " ++ shellEscape url ++ " " ++ shellEscape dir ++ " < /dev/null"- , Just $ "cd " ++ shellEscape dir- , ("git checkout " ++) <$> mbranch- ]
− Propellor/Property/Gpg.hs
@@ -1,41 +0,0 @@-module Propellor.Property.Gpg where--import Propellor-import qualified Propellor.Property.Apt as Apt-import Utility.FileSystemEncoding--import System.PosixCompat--installed :: Property-installed = Apt.installed ["gnupg"]---- | Sets up a user with a gpg key from the privdata.------ Note that if a secret key is exported using gpg -a --export-secret-key,--- the public key is also included. Or just a public key could be--- exported, and this would set it up just as well.------ Recommend only using this for low-value dedicated role keys.--- No attempt has been made to scrub the key out of memory once it's used.------ The GpgKeyId does not have to be a numeric id; it can just as easily--- be a description of the key.-keyImported :: GpgKeyId -> UserName -> Property-keyImported keyid user = flagFile' (property desc go) genflag- `requires` installed- where- desc = user ++ " has gpg key " ++ show keyid- genflag = do- d <- dotDir user- return $ d </> ".propellor-imported-keyid-" ++ keyid- go = withPrivData (GpgKey keyid) $ \key -> makeChange $- withHandle StdinHandle createProcessSuccess- (proc "su" ["-c", "gpg --import", user]) $ \h -> do- fileEncoding h- hPutStr h key- hClose h--dotDir :: UserName -> IO FilePath-dotDir user = do- home <- homeDirectory <$> getUserEntryForName user- return $ home </> ".gnupg"
− Propellor/Property/Hostname.hs
@@ -1,33 +0,0 @@-module Propellor.Property.Hostname where--import Propellor-import qualified Propellor.Property.File as File---- | Ensures that the hostname is set to the HostAttr value.--- Configures /etc/hostname and the current hostname.------ A FQDN also configures /etc/hosts, with an entry for 127.0.1.1, which is--- standard at least on Debian to set the FDQN (127.0.0.1 is localhost).-sane :: Property-sane = property ("sane hostname") (ensureProperty . setTo =<< getHostName)--setTo :: HostName -> Property-setTo hn = combineProperties desc go- `onChange` cmdProperty "hostname" [basehost]- where- desc = "hostname " ++ hn- (basehost, domain) = separate (== '.') hn-- go = catMaybes- [ Just $ "/etc/hostname" `File.hasContent` [basehost]- , if null domain- then Nothing - else Just $ File.fileProperty desc- addhostline "/etc/hosts"- ]- - hostip = "127.0.1.1"- hostline = hostip ++ "\t" ++ hn ++ " " ++ basehost-- addhostline ls = hostline : filter (not . hashostip) ls- hashostip l = headMaybe (words l) == Just hostip
− Propellor/Property/Network.hs
@@ -1,30 +0,0 @@-module Propellor.Property.Network where--import Propellor-import Propellor.Property.File--interfaces :: FilePath-interfaces = "/etc/network/interfaces"---- | 6to4 ipv6 connection, should work anywhere-ipv6to4 :: Property-ipv6to4 = fileProperty "ipv6to4" go interfaces- `onChange` ifUp "sit0"- where- go ls- | all (`elem` ls) stanza = ls- | otherwise = ls ++ stanza- stanza =- [ "# Automatically added by propeller"- , "iface sit0 inet6 static"- , "\taddress 2002:5044:5531::1"- , "\tnetmask 64"- , "\tgateway ::192.88.99.1"- , "auto sit0"- , "# End automatically added by propeller"- ]--type Interface = String--ifUp :: Interface -> Property-ifUp iface = cmdProperty "ifup" [iface]
− Propellor/Property/Obnam.hs
@@ -1,155 +0,0 @@-module Propellor.Property.Obnam where--import Propellor-import qualified Propellor.Property.Apt as Apt-import qualified Propellor.Property.Cron as Cron-import Utility.SafeCommand--import Data.List--type ObnamParam = String---- | An obnam repository can be used by multiple clients. Obnam uses--- locking to allow only one client to write at a time. Since stale lock--- files can prevent backups from happening, it's more robust, if you know--- a repository has only one client, to force the lock before starting a--- backup. Using OnlyClient allows propellor to do so when running obnam.-data NumClients = OnlyClient | MultipleClients- deriving (Eq)---- | Installs a cron job that causes a given directory to be backed--- up, by running obnam with some parameters.------ If the directory does not exist, or exists but is completely empty,--- this Property will immediately restore it from an existing backup.------ So, this property can be used to deploy a directory of content--- to a host, while also ensuring any changes made to it get backed up.--- And since Obnam encrypts, just make this property depend on a gpg--- key, and tell obnam to use the key, and your data will be backed--- up securely. For example: ------ > & Obnam.backup "/srv/git" "33 3 * * *"--- > [ "--repository=sftp://2318@usw-s002.rsync.net/~/mygitrepos.obnam"--- > , "--encrypt-with=1B169BE1"--- > ] Obnam.OnlyClient--- > `requires` Gpg.keyImported "1B169BE1" "root"--- > `requires` Ssh.keyImported SshRsa "root"------ How awesome is that?-backup :: FilePath -> Cron.CronTimes -> [ObnamParam] -> NumClients -> Property-backup dir crontimes params numclients = cronjob `describe` desc- `requires` restored dir params- where- desc = dir ++ " backed up by obnam"- cronjob = Cron.niceJob ("obnam_backup" ++ dir) crontimes "root" "/" $- intercalate ";" $ catMaybes- [ if numclients == OnlyClient- then Just $ unwords $- [ "obnam"- , "force-lock"- ] ++ map shellEscape params- else Nothing- , Just $ unwords $- [ "obnam"- , "backup"- , shellEscape dir- ] ++ map shellEscape params- ]---- | Restores a directory from an obnam backup.------ Only does anything if the directory does not exist, or exists,--- but is completely empty.------ The restore is performed atomically; restoring to a temp directory--- and then moving it to the directory.-restored :: FilePath -> [ObnamParam] -> Property-restored dir params = property (dir ++ " restored by obnam") go- `requires` installed- where- go = ifM (liftIO needsRestore)- ( do- warningMessage $ dir ++ " is empty/missing; restoring from backup ..."- liftIO restore- , noChange- )-- needsRestore = null <$> catchDefaultIO [] (dirContents dir)-- restore = withTmpDirIn (takeDirectory dir) "obnam-restore" $ \tmpdir -> do- ok <- boolSystem "obnam" $- [ Param "restore"- , Param "--to"- , Param tmpdir- ] ++ map Param params- let restoreddir = tmpdir ++ "/" ++ dir- ifM (pure ok <&&> doesDirectoryExist restoreddir)- ( do- void $ tryIO $ removeDirectory dir- renameDirectory restoreddir dir- return MadeChange- , return FailedChange- )--installed :: Property-installed = Apt.installed ["obnam"]---- | Ensures that a recent version of obnam gets installed.------ Only does anything for Debian Stable.-latestVersion :: Property-latestVersion = withOS "obnam latest version" $ \o -> case o of- (Just (System (Debian suite) _)) | isStable suite -> ensureProperty $- Apt.setSourcesListD (sources suite) "obnam"- `requires` toProp (Apt.trustsKey key)- _ -> noChange- where- sources suite = - [ "deb http://code.liw.fi/debian " ++ Apt.showSuite suite ++ " main"- ]- -- gpg key used by the code.liw.fi repository.- key = Apt.AptKey "obnam" $ unlines- [ "-----BEGIN PGP PUBLIC KEY BLOCK-----"- , "Version: GnuPG v1.4.9 (GNU/Linux)"- , ""- , "mQGiBEfzuTgRBACcVNG/H6QJqLx5qiQs2zmPe6D6BWOWHfgNgG4IWzNstm21YDxb"- , "KqwFG0gxcnZJGHkXAhkSfqTokYd0lc5eBemcA1pkceNjzMEX8wwiZ810HzJD4eEH"- , "sjoWR8+qKrZeixzZqReAfqztcXoBGKQ0u1R1vpg1txUa75OM4BUqaUbsmwCgmS4x"- , "DjMxSaUSPuu6vQ7ZGZBXSP0D/RQw8DBHMfsv3DiaqFqk8tkuUkpMFPIekHidSHlO"- , "EACbncqbbyHksyCpFNVNcQIDHrOLjOZK9BAXkSd8I3ww7U+nLdDcCblrW8CZnJtm"- , "ZYrxfaXaHZ/It9/RCAsQ+c8xtmyUPjsf//4Vf8olxNQHzgBSe5/LJRi4Vd53he+K"- , "YP4LA/9IZbjvVmm8+8Y0pQrTHlI6nTImtzdBXHc4+T3lLBj9XODHLozC2kSBOQky"- , "q/EisTITHTXL8vYg4NsKm5RTbPAuBwdtxcny8CXfOqKtGOdrebmKotGllTozzdPv"- , "9p53cuce6oJ2oMUodc074JOGTWwDSgLiJX4nViGcU1wy/vtQnrQkY29kZS5saXcu"- , "ZmkgYXJjaGl2ZSBrZXkgPGxpd0BsaXcuZmk+iGAEExECACAFAkfzuTgCGwMGCwkI"- , "BwMCBBUCCAMEFgIDAQIeAQIXgAAKCRBG53tJR95LscKrAJ0ZtKqa2x6Kplwa2mzx"- , "ItImbIGMJACdETqofDYzUN91yLAFlOnxAyrE+UyIRgQQEQIABgUCSFd5GgAKCRAf"- , "u5W/LZrMjqr8AJ4xPVHpW8ZNlgMwDSVb075RnA2DiACgg2SR69jAHFQOWV6xfLRr"- , "vh0bLKGJAhwEEAEIAAYFAktEyIwACgkQ61zh116FEfm7Lg//Wiy3TjWAk8YHUddv"- , "zOioYzCxQ985GsVhJGAVPqSGOc9vfTWBJZ8J3l0NnYTRpEGucmbF9G+mAt9iGXu6"- , "7yZkxyFdvbo7EDsqMU1wLOM6PiU+Un63MKlbTNmFn7OKE8aXPRAFgcyUO/qjdqoD"- , "sa9FgU5Z0f60m9qah6BPXH6IzMLHYoiP7t8rCBIwLgyl3w2w+Fjt1DFpbW9Kb7jz"- , "i8jFvC8jPmxV8xh2OSgVZyNk4qg6hIV8GVQY7AJt8OurZSckgQd7ifHK9JTGohtF"- , "tXCiqeDEvnMF4A9HI/TcXJBzonZ8ds1JCq42nSSKmL+8TyjtUSD/xHygazuc0CK0"- , "hFnQWBub60IfyV6F0oTagJ8cmARv2sezHAeHDkzPHE8RdjgktazH1eJrA4LheEd6"- , "KeSnVtYWpw8dgMv5PleFyQiAj/t3C/N50fd15tUyfnH15G7nFjMQV2Yx35uwSxOj"- , "376OWnDN/YGTNk283XXULbyVJYR8Q2unso20XQ94yQ2A5EpHHPrHoLxrL/ydM08d"- , "nvKstLZIZtal1seiMkymtlSiGz25A5oqsclwS6VZCKdWA8HO/wlElOMcaHyl6Y1y"- , "gYP7y9O5yFYKFOrCH0nFjJbwmkRiBLsxuuWsYgJigVGq/atSrtawkHdshpCw0HCY"- , "N/RFcWkJ864BdsO0C0sDzueNkQO5Ag0ER/O5RBAIAJiwPH9tyJTgXcC2Y4XWboOq"- , "rx5CkOnr5b45oS9cK2eIJ8TKxE3XgKLxUr3mIH0QR2kZgDOwNl0WY+7/CXjn+Spn"- , "BokPg54rafEUePodGpGdUXdgrHhAMHYjh8fXFJ1SlQcg46/zc1wDI7jBCkGrK3V8"- , "5cXDqwTFTN5LcjoSRWeM4Voa6pEfDdL3rMlnOw9R9gDHRBBb6CDSjWXqM86pR889"- , "5QrR0SDwiJNrMoyxSjMXFKGBQAsYHJ82myZrlbuZbroZjVp5Uh7eB1ZiPljNVtcr"- , "sksACIWBCo1rvLzrPXsLYOeV3cDDtYAkSwGfuzC1Etbe+qgfIroFTOqdefMw4s8A"- , "AwUH/0KLXm4MS54QQspg3evu4Q4U/E8Hem5/FqB0GhBCitQ4rUsucKyY8/ItpUn5"- , "ismLE60bQqka+Mzd/Zw18TCTzImv0ozAaZ2sNtBado7f6jcC8EDfY5zzK1ukcsAr"- , "Qc5hdLHYuTQW5KpA6fKaW969OUzIwPbdVaCOLOBpxKC6N6iBspQYd6uiQtLw6EUO"- , "50oQqUiJABf0eOocvdw5e2KQQpuC3205+VMYtyl4w3pdJihK8NK0AikGXzDVsbQt"- , "l8kmB5ZrN4WIKhMke1FxbqQC5Q3XATvYRzpzzisZb/HYGNti8W6du5EUwJ0D2NRh"- , "cu+twocOzW0VKfmrDApfifJ9OsSISQQYEQIACQUCR/O5RAIbDAAKCRBG53tJR95L"- , "seQOAJ95KUyzjRjdYgZkDC69Mgu25L86UACdGduINUaRly43ag4kwUXxpqswBBM="- , "=i2c3"- , "-----END PGP PUBLIC KEY BLOCK-----"- ]
− Propellor/Property/OpenId.hs
@@ -1,29 +0,0 @@-module Propellor.Property.OpenId where--import Propellor-import qualified Propellor.Property.File as File-import qualified Propellor.Property.Apt as Apt-import qualified Propellor.Property.Service as Service--import Data.List--providerFor :: [UserName] -> String -> Property-providerFor users baseurl = propertyList desc $- [ Apt.serviceInstalledRunning "apache2"- , Apt.installed ["simpleid"]- `onChange` Service.restarted "apache2"- , File.fileProperty (desc ++ " configured")- (map setbaseurl) "/etc/simpleid/config.inc"- ] ++ map identfile users- where- url = "http://"++baseurl++"/simpleid"- desc = "openid provider " ++ url- setbaseurl l- | "SIMPLEID_BASE_URL" `isInfixOf` l = - "define('SIMPLEID_BASE_URL', '"++url++"');"- | otherwise = l- - -- the identitites directory controls access, so open up- -- file mode- identfile u = File.hasPrivContentExposed $- concat $ [ "/var/lib/simpleid/identities/", u, ".identity" ]
− Propellor/Property/Postfix.hs
@@ -1,25 +0,0 @@-module Propellor.Property.Postfix where--import Propellor-import qualified Propellor.Property.Apt as Apt--installed :: Property-installed = Apt.serviceInstalledRunning "postfix"---- | Configures postfix as a satellite system, which --- relats all mail through a relay host, which defaults to smtp.domain. ------ The smarthost may refuse to relay mail on to other domains, without--- futher coniguration/keys. But this should be enough to get cron job--- mail flowing to a place where it will be seen.-satellite :: Property-satellite = setup `requires` installed- where- setup = trivial $ property "postfix satellite system" $ do- hn <- getHostName- ensureProperty $ Apt.reConfigure "postfix"- [ ("postfix/main_mailer_type", "select", "Satellite system")- , ("postfix/root_address", "string", "root")- , ("postfix/destinations", "string", " ")- , ("postfix/mailname", "string", hn)- ]
− Propellor/Property/Reboot.hs
@@ -1,7 +0,0 @@-module Propellor.Property.Reboot where--import Propellor--now :: Property-now = cmdProperty "reboot" []- `describe` "reboot now"
− Propellor/Property/Scheduled.hs
@@ -1,67 +0,0 @@-module Propellor.Property.Scheduled- ( period- , periodParse- , Recurrance(..)- , WeekDay- , MonthDay- , YearDay- ) where--import Propellor-import Utility.Scheduled--import Data.Time.Clock-import Data.Time.LocalTime-import qualified Data.Map as M---- | Makes a Property only be checked every so often.------ This uses the description of the Property to keep track of when it was--- last run.-period :: Property -> Recurrance -> Property-period prop recurrance = flip describe desc $ adjustProperty prop $ \satisfy -> do- lasttime <- liftIO $ getLastChecked (propertyDesc prop)- nexttime <- liftIO $ fmap startTime <$> nextTime schedule lasttime- t <- liftIO localNow- if Just t >= nexttime- then do- r <- satisfy- liftIO $ setLastChecked t (propertyDesc prop)- return r- else noChange- where- schedule = Schedule recurrance AnyTime- desc = propertyDesc prop ++ " (period " ++ fromRecurrance recurrance ++ ")"---- | Like period, but parse a human-friendly string.-periodParse :: Property -> String -> Property-periodParse prop s = case toRecurrance s of- Just recurrance -> period prop recurrance- Nothing -> property "periodParse" $ do- liftIO $ warningMessage $ "failed periodParse: " ++ s- noChange--lastCheckedFile :: FilePath-lastCheckedFile = localdir </> ".lastchecked"--getLastChecked :: Desc -> IO (Maybe LocalTime)-getLastChecked desc = M.lookup desc <$> readLastChecked--localNow :: IO LocalTime-localNow = do- now <- getCurrentTime- tz <- getTimeZone now- return $ utcToLocalTime tz now--setLastChecked :: LocalTime -> Desc -> IO ()-setLastChecked time desc = do- m <- readLastChecked- writeLastChecked (M.insert desc time m)--readLastChecked :: IO (M.Map Desc LocalTime)-readLastChecked = fromMaybe M.empty <$> catchDefaultIO Nothing go- where- go = readish <$> readFileStrict lastCheckedFile--writeLastChecked :: M.Map Desc LocalTime -> IO ()-writeLastChecked = writeFile lastCheckedFile . show
− Propellor/Property/Service.hs
@@ -1,31 +0,0 @@-module Propellor.Property.Service where--import Propellor-import Utility.SafeCommand--type ServiceName = String---- | Ensures that a service is running. Does not ensure that--- any package providing that service is installed. See--- Apt.serviceInstalledRunning------ Note that due to the general poor state of init scripts, the best--- we can do is try to start the service, and if it fails, assume--- this means it's already running.-running :: ServiceName -> Property-running svc = property ("running " ++ svc) $ do- void $ ensureProperty $- scriptProperty ["service " ++ shellEscape svc ++ " start >/dev/null 2>&1 || true"]- return NoChange--restarted :: ServiceName -> Property-restarted svc = property ("restarted " ++ svc) $ do- void $ ensureProperty $- scriptProperty ["service " ++ shellEscape svc ++ " restart >/dev/null 2>&1 || true"]- return NoChange--reloaded :: ServiceName -> Property-reloaded svc = property ("reloaded " ++ svc) $ do- void $ ensureProperty $- scriptProperty ["service " ++ shellEscape svc ++ " reload >/dev/null 2>&1 || true"]- return NoChange
− Propellor/Property/SiteSpecific/GitAnnexBuilder.hs
@@ -1,57 +0,0 @@-module Propellor.Property.SiteSpecific.GitAnnexBuilder where--import Propellor-import qualified Propellor.Property.Apt as Apt-import qualified Propellor.Property.User as User-import qualified Propellor.Property.Cron as Cron-import Propellor.Property.Cron (CronTimes)--builduser :: UserName-builduser = "builder"--homedir :: FilePath-homedir = "/home/builder"--gitbuilderdir :: FilePath-gitbuilderdir = homedir </> "gitbuilder"--builddir :: FilePath-builddir = gitbuilderdir </> "build"--builder :: Architecture -> CronTimes -> Bool -> Property-builder arch crontimes rsyncupload = combineProperties "gitannexbuilder"- [ Apt.stdSourcesList Unstable- , Apt.buildDep ["git-annex"]- , Apt.installed ["git", "rsync", "moreutils", "ca-certificates",- "liblockfile-simple-perl", "cabal-install", "vim", "less"]- , Apt.serviceInstalledRunning "cron"- , User.accountFor builduser- , check (not <$> doesDirectoryExist gitbuilderdir) $ userScriptProperty builduser- [ "git clone git://git.kitenet.net/gitannexbuilder " ++ gitbuilderdir- , "cd " ++ gitbuilderdir- , "git checkout " ++ arch- ]- `describe` "gitbuilder setup"- , check (not <$> doesDirectoryExist builddir) $ userScriptProperty builduser- [ "git clone git://git-annex.branchable.com/ " ++ builddir- ]- , "git-annex source build deps installed" ==> Apt.buildDepIn builddir- , Cron.niceJob "gitannexbuilder" crontimes builduser gitbuilderdir "git pull ; ./autobuild"- -- The builduser account does not have a password set,- -- instead use the password privdata to hold the rsync server- -- password used to upload the built image.- , property "rsync password" $ do- let f = homedir </> "rsyncpassword"- if rsyncupload - then withPrivData (Password builduser) $ \p -> do- oldp <- liftIO $ catchDefaultIO "" $- readFileStrict f- if p /= oldp- then makeChange $ writeFile f p- else noChange- else do- ifM (liftIO $ doesFileExist f)- ( noChange- , makeChange $ writeFile f "no password configured"- )- ]
− Propellor/Property/SiteSpecific/GitHome.hs
@@ -1,34 +0,0 @@-module Propellor.Property.SiteSpecific.GitHome where--import Propellor-import qualified Propellor.Property.Apt as Apt-import Propellor.Property.User-import Utility.SafeCommand---- | Clones Joey Hess's git home directory, and runs its fixups script.-installedFor :: UserName -> Property-installedFor user = check (not <$> hasGitDir user) $ - property ("githome " ++ user) (go =<< liftIO (homedir user))- `requires` Apt.installed ["git"]- where- go home = do- let tmpdir = home </> "githome"- ensureProperty $ combineProperties "githome setup"- [ userScriptProperty user ["git clone " ++ url ++ " " ++ tmpdir]- , property "moveout" $ makeChange $ void $- moveout tmpdir home- , property "rmdir" $ makeChange $ void $- catchMaybeIO $ removeDirectory tmpdir- , userScriptProperty user ["rm -rf .aptitude/ .bashrc .profile; bin/mr checkout; bin/fixups"]- ]- moveout tmpdir home = do- fs <- dirContents tmpdir- forM fs $ \f -> boolSystem "mv" [File f, File home]--url :: String-url = "git://git.kitenet.net/joey/home"--hasGitDir :: UserName -> IO Bool-hasGitDir user = go =<< homedir user- where- go home = doesDirectoryExist (home </> ".git")
− Propellor/Property/SiteSpecific/JoeySites.hs
@@ -1,295 +0,0 @@--- | Specific configuation for Joey Hess's sites. Probably not useful to--- others except as an example.--module Propellor.Property.SiteSpecific.JoeySites where--import Propellor-import qualified Propellor.Property.Apt as Apt-import qualified Propellor.Property.File as File-import qualified Propellor.Property.Gpg as Gpg-import qualified Propellor.Property.Ssh as Ssh-import qualified Propellor.Property.Git as Git-import qualified Propellor.Property.Cron as Cron-import qualified Propellor.Property.Service as Service-import qualified Propellor.Property.User as User-import qualified Propellor.Property.Obnam as Obnam-import qualified Propellor.Property.Apache as Apache-import Utility.SafeCommand--import Data.List-import System.Posix.Files--oldUseNetServer :: [Host] -> Property-oldUseNetServer hosts = propertyList ("olduse.net server")- [ oldUseNetInstalled "oldusenet-server"- , Obnam.latestVersion- , Obnam.backup datadir "33 4 * * *"- [ "--repository=sftp://2318@usw-s002.rsync.net/~/olduse.net"- , "--client-name=spool"- ] Obnam.OnlyClient- `requires` Ssh.keyImported SshRsa "root"- `requires` Ssh.knownHost hosts "usw-s002.rsync.net" "root"- , check (not . isSymbolicLink <$> getSymbolicLinkStatus newsspool) $- property "olduse.net spool in place" $ makeChange $ do- removeDirectoryRecursive newsspool- createSymbolicLink (datadir </> "news") newsspool- , Apt.installed ["leafnode"]- , "/etc/news/leafnode/config" `File.hasContent` - [ "# olduse.net configuration (deployed by propellor)"- , "expire = 1000000" -- no expiry via texpire- , "server = " -- no upstream server- , "debugmode = 1"- , "allowSTRANGERS = 42" -- lets anyone connect- , "nopost = 1" -- no new posting (just gather them)- ]- , "/etc/hosts.deny" `File.lacksLine` "leafnode: ALL"- , Apt.serviceInstalledRunning "openbsd-inetd"- , File.notPresent "/etc/cron.daily/leafnode"- , File.notPresent "/etc/cron.d/leafnode"- , Cron.niceJob "oldusenet-expire" "11 1 * * *" "news" newsspool $ intercalate ";"- [ "find \\( -path ./out.going -or -path ./interesting.groups -or -path './*/.overview' \\) -prune -or -type f -ctime +60 -print | xargs --no-run-if-empty rm"- , "find -type d -empty | xargs --no-run-if-empty rmdir"- ]- , Cron.niceJob "oldusenet-uucp" "*/5 * * * *" "news" "/" $- "/usr/bin/uucp " ++ datadir- , toProp $ Apache.siteEnabled "nntp.olduse.net" $ apachecfg "nntp.olduse.net" False- [ " DocumentRoot " ++ datadir ++ "/"- , " <Directory " ++ datadir ++ "/>"- , " Options Indexes FollowSymlinks"- , " AllowOverride None"- -- I had this in the file before.- -- This may be needed by a newer version of apache?- --, " Require all granted"- , " </Directory>"- ]- ]- where- newsspool = "/var/spool/news"- datadir = "/var/spool/oldusenet"--oldUseNetShellBox :: Property-oldUseNetShellBox = oldUseNetInstalled "oldusenet"--oldUseNetInstalled :: Apt.Package -> Property-oldUseNetInstalled pkg = check (not <$> Apt.isInstalled pkg) $- propertyList ("olduse.net " ++ pkg)- [ Apt.installed (words "build-essential devscripts debhelper git libncursesw5-dev libpcre3-dev pkg-config bison libicu-dev libidn11-dev libcanlock2-dev libuu-dev ghc libghc-strptime-dev libghc-hamlet-dev libghc-ifelse-dev libghc-hxt-dev libghc-utf8-string-dev libghc-missingh-dev libghc-sha-dev")- `describe` "olduse.net build deps"- , scriptProperty- [ "rm -rf /root/tmp/oldusenet" -- idenpotency- , "git clone git://olduse.net/ /root/tmp/oldusenet/source"- , "cd /root/tmp/oldusenet/source/"- , "dpkg-buildpackage -us -uc"- , "dpkg -i ../" ++ pkg ++ "_*.deb || true"- , "apt-get -fy install" -- dependencies- , "rm -rf /root/tmp/oldusenet"- ] `describe` "olduse.net built"- ]---kgbServer :: Property-kgbServer = withOS desc $ \o -> case o of- (Just (System (Debian Unstable) _)) ->- ensureProperty $ propertyList desc- [ Apt.serviceInstalledRunning "kgb-bot"- , File.hasPrivContent "/etc/kgb-bot/kgb.conf"- `onChange` Service.restarted "kgb-bot"- , "/etc/default/kgb-bot" `File.containsLine` "BOT_ENABLED=1"- `describe` "kgb bot enabled"- `onChange` Service.running "kgb-bot"- ]- _ -> error "kgb server needs Debian unstable (for kgb-bot 1.31+)"- where- desc = "kgb.kitenet.net setup"--mumbleServer :: [Host] -> Property-mumbleServer hosts = combineProperties "mumble.debian.net" - [ Obnam.latestVersion- , Obnam.backup "/var/lib/mumble-server" "55 5 * * *"- [ "--repository=sftp://joey@turtle.kitenet.net/~/lib/backup/mumble.debian.net.obnam"- , "--client-name=mumble"- ] Obnam.OnlyClient- `requires` Ssh.keyImported SshRsa "root"- `requires` Ssh.knownHost hosts "turtle.kitenet.net" "root"- , trivial $ cmdProperty "chown" ["-R", "mumble-server:mumble-server", "/var/lib/mumble-server"]- , Apt.serviceInstalledRunning "mumble-server"- ]--obnamLowMem :: Property-obnamLowMem = combineProperties "obnam tuned for low memory use"- [ Obnam.latestVersion- , "/etc/obnam.conf" `File.containsLines`- [ "[config]"- , "# Suggested by liw to keep Obnam memory consumption down (at some speed cost)."- , "upload-queue-size = 128"- , "lru-size = 128"- ]- ]---- git.kitenet.net and git.joeyh.name-gitServer :: [Host] -> Property-gitServer hosts = propertyList "git.kitenet.net setup"- [ Obnam.latestVersion- , Obnam.backup "/srv/git" "33 3 * * *"- [ "--repository=sftp://2318@usw-s002.rsync.net/~/git.kitenet.net"- , "--encrypt-with=1B169BE1"- , "--client-name=wren"- ] Obnam.OnlyClient- `requires` Gpg.keyImported "1B169BE1" "root"- `requires` Ssh.keyImported SshRsa "root"- `requires` Ssh.knownHost hosts "usw-s002.rsync.net" "root"- `requires` Ssh.authorizedKeys "family"- `requires` User.accountFor "family"- , Apt.installed ["git", "rsync", "kgb-client-git", "gitweb"]- , Apt.installedBackport ["git-annex"]- , File.hasPrivContentExposed "/etc/kgb-bot/kgb-client.conf"- , toProp $ Git.daemonRunning "/srv/git"- , "/etc/gitweb.conf" `File.containsLines`- [ "$projectroot = '/srv/git';"- , "@git_base_url_list = ('git://git.kitenet.net', 'http://git.kitenet.net/git', 'https://git.kitenet.net/git', 'ssh://git.kitenet.net/srv/git');"- , "# disable snapshot download; overloads server"- , "$feature{'snapshot'}{'default'} = [];"- ]- `describe` "gitweb configured"- -- Repos push on to github.- , Ssh.knownHost hosts "github.com" "joey"- -- I keep the website used for gitweb checked into git..- , Git.cloned "root" "/srv/git/joey/git.kitenet.net.git" "/srv/web/git.kitenet.net" Nothing- , website "git.kitenet.net"- , website "git.joeyh.name"- , toProp $ Apache.modEnabled "cgi"- ]- where- website hn = toProp $ Apache.siteEnabled hn $ apachecfg hn True- [ " DocumentRoot /srv/web/git.kitenet.net/"- , " <Directory /srv/web/git.kitenet.net/>"- , " Options Indexes ExecCGI FollowSymlinks"- , " AllowOverride None"- , " AddHandler cgi-script .cgi"- , " DirectoryIndex index.cgi"- , " </Directory>"- , ""- , " ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/"- , " <Directory /usr/lib/cgi-bin>"- , " SetHandler cgi-script"- , " Options ExecCGI"- , " </Directory>"- ]--type AnnexUUID = String---- | A website, with files coming from a git-annex repository.-annexWebSite :: [Host] -> Git.RepoUrl -> HostName -> AnnexUUID -> [(String, Git.RepoUrl)] -> Property-annexWebSite hosts origin hn uuid remotes = propertyList (hn ++" website using git-annex")- [ Git.cloned "joey" origin dir Nothing- `onChange` setup- , setupapache- ]- where- dir = "/srv/web/" ++ hn- setup = userScriptProperty "joey" setupscript- `requires` Ssh.keyImported SshRsa "joey"- `requires` Ssh.knownHost hosts "turtle.kitenet.net" "joey"- setupscript = - [ "cd " ++ shellEscape dir- , "git config annex.uuid " ++ shellEscape uuid- ] ++ map addremote remotes ++- [ "git annex get"- ]- addremote (name, url) = "git remote add " ++ shellEscape name ++ " " ++ shellEscape url- setupapache = toProp $ Apache.siteEnabled hn $ apachecfg hn True $ - [ " ServerAlias www."++hn- , ""- , " DocumentRoot /srv/web/"++hn- , " <Directory /srv/web/"++hn++">"- , " Options FollowSymLinks"- , " AllowOverride None"- , " </Directory>"- , " <Directory /srv/web/"++hn++">"- , " Options Indexes FollowSymLinks ExecCGI"- , " AllowOverride None"- , " AddHandler cgi-script .cgi"- , " DirectoryIndex index.html index.cgi"- , " Order allow,deny"- , " allow from all"- , " </Directory>"- ]--apachecfg :: HostName -> Bool -> Apache.ConfigFile -> Apache.ConfigFile-apachecfg hn withssl middle- | withssl = vhost False ++ vhost True- | otherwise = vhost False- where- vhost ssl = - [ "<VirtualHost *:"++show port++">"- , " ServerAdmin grue@joeyh.name"- , " ServerName "++hn++":"++show port- ]- ++ mainhttpscert ssl- ++ middle ++- [ ""- , " ErrorLog /var/log/apache2/error.log"- , " LogLevel warn"- , " CustomLog /var/log/apache2/access.log combined"- , " ServerSignature On"- , " "- , " <Directory \"/usr/share/apache2/icons\">"- , " Options Indexes MultiViews"- , " AllowOverride None"- , " Order allow,deny"- , " Allow from all"- , " </Directory>"- , "</VirtualHost>"- ]- where- port = if ssl then 443 else 80 :: Int--mainhttpscert :: Bool -> Apache.ConfigFile-mainhttpscert False = []-mainhttpscert True = - [ " SSLEngine on"- , " SSLCertificateFile /etc/ssl/certs/web.pem"- , " SSLCertificateKeyFile /etc/ssl/private/web.pem"- , " SSLCertificateChainFile /etc/ssl/certs/startssl.pem"- ]- -gitAnnexDistributor :: Property-gitAnnexDistributor = combineProperties "git-annex distributor, including rsync server and signer"- [ Apt.installed ["rsync"]- , File.hasPrivContent "/etc/rsyncd.conf"- , File.hasPrivContent "/etc/rsyncd.secrets"- , "/etc/default/rsync" `File.containsLine` "RSYNC_ENABLE=true"- `onChange` Service.running "rsync"- , endpoint "/srv/web/downloads.kitenet.net/git-annex/autobuild"- , endpoint "/srv/web/downloads.kitenet.net/git-annex/autobuild/x86_64-apple-mavericks"- -- git-annex distribution signing key- , Gpg.keyImported "89C809CB" "joey"- ]- where- endpoint d = combineProperties ("endpoint " ++ d)- [ File.dirExists d- , File.ownerGroup d "joey" "joey"- ]---- Twitter, you kill us.-twitRss :: Property-twitRss = combineProperties "twitter rss"- [ Git.cloned "joey" "git://git.kitenet.net/twitrss.git" dir Nothing- , check (not <$> doesFileExist (dir </> "twitRss")) $- userScriptProperty "joey"- [ "cd " ++ dir- , "ghc --make twitRss" - ]- `requires` Apt.installed- [ "libghc-xml-dev"- , "libghc-feed-dev"- , "libghc-tagsoup-dev"- ]- , feed "http://twitter.com/search/realtime?q=git-annex" "git-annex-twitter"- , feed "http://twitter.com/search/realtime?q=olduse+OR+git-annex+OR+debhelper+OR+etckeeper+OR+ikiwiki+-ashley_ikiwiki" "twittergrep"- ]- where- dir = "/srv/web/tmp.kitenet.net/twitrss"- crontime = "15 * * * *"- feed url desc = Cron.job desc crontime "joey" dir $- "./twitRss " ++ shellEscape url ++ " > " ++ shellEscape ("../" ++ desc ++ ".rss")
− Propellor/Property/Ssh.hs
@@ -1,152 +0,0 @@-module Propellor.Property.Ssh (- setSshdConfig,- permitRootLogin,- passwordAuthentication,- hasAuthorizedKeys,- restartSshd,- randomHostKeys,- hostKey,- keyImported,- knownHost,- authorizedKeys-) where--import Propellor-import qualified Propellor.Property.File as File-import Propellor.Property.User-import Utility.SafeCommand-import Utility.FileMode--import System.PosixCompat--sshBool :: Bool -> String-sshBool True = "yes"-sshBool False = "no"--sshdConfig :: FilePath-sshdConfig = "/etc/ssh/sshd_config"--setSshdConfig :: String -> Bool -> Property-setSshdConfig setting allowed = combineProperties "sshd config"- [ sshdConfig `File.lacksLine` (sshline $ not allowed)- , sshdConfig `File.containsLine` (sshline allowed)- ]- `onChange` restartSshd- `describe` unwords [ "ssh config:", setting, sshBool allowed ]- where- sshline v = setting ++ " " ++ sshBool v--permitRootLogin :: Bool -> Property-permitRootLogin = setSshdConfig "PermitRootLogin"--passwordAuthentication :: Bool -> Property-passwordAuthentication = setSshdConfig "PasswordAuthentication"--dotDir :: UserName -> IO FilePath-dotDir user = do- h <- homedir user- return $ h </> ".ssh"--dotFile :: FilePath -> UserName -> IO FilePath-dotFile f user = do- d <- dotDir user- return $ d </> f--hasAuthorizedKeys :: UserName -> IO Bool-hasAuthorizedKeys = go <=< dotFile "authorized_keys"- where- go f = not . null <$> catchDefaultIO "" (readFile f)--restartSshd :: Property-restartSshd = cmdProperty "service" ["ssh", "restart"]---- | Blows away existing host keys and make new ones.--- Useful for systems installed from an image that might reuse host keys.--- A flag file is used to only ever do this once.-randomHostKeys :: Property-randomHostKeys = flagFile prop "/etc/ssh/.unique_host_keys"- `onChange` restartSshd- where- prop = property "ssh random host keys" $ do- void $ liftIO $ boolSystem "sh"- [ Param "-c"- , Param "rm -f /etc/ssh/ssh_host_*"- ]- ensureProperty $- cmdProperty "/var/lib/dpkg/info/openssh-server.postinst"- ["configure"]---- | Sets ssh host keys from the site's PrivData.--- --- (Uses a null username for host keys.)-hostKey :: SshKeyType -> Property-hostKey keytype = combineProperties desc- [ property desc (install writeFile (SshPubKey keytype "") ".pub")- , property desc (install writeFileProtected (SshPrivKey keytype "") "")- ]- `onChange` restartSshd- where- desc = "known ssh host key (" ++ fromKeyType keytype ++ ")"- install writer p ext = withPrivData p $ \key -> do- let f = "/etc/ssh/ssh_host_" ++ fromKeyType keytype ++ "_key" ++ ext- s <- liftIO $ readFileStrict f- if s == key- then noChange- else makeChange $ writer f key---- | Sets up a user with a ssh private key and public key pair--- from the site's PrivData.-keyImported :: SshKeyType -> UserName -> Property-keyImported keytype user = combineProperties desc- [ property desc (install writeFile (SshPubKey keytype user) ".pub")- , property desc (install writeFileProtected (SshPrivKey keytype user) "")- ]- where- desc = user ++ " has ssh key (" ++ fromKeyType keytype ++ ")"- install writer p ext = do- f <- liftIO $ keyfile ext- ifM (liftIO $ doesFileExist f)- ( noChange- , ensureProperty $ combineProperties desc- [ property desc $ - withPrivData p $ \key -> makeChange $- writer f key- , File.ownerGroup f user user- ]- )- keyfile ext = do- home <- homeDirectory <$> getUserEntryForName user- return $ home </> ".ssh" </> "id_" ++ fromKeyType keytype ++ ext--fromKeyType :: SshKeyType -> String-fromKeyType SshRsa = "rsa"-fromKeyType SshDsa = "dsa"-fromKeyType SshEcdsa = "ecdsa"-fromKeyType SshEd25519 = "ed25519"---- | Puts some host's ssh public key into the known_hosts file for a user.-knownHost :: [Host] -> HostName -> UserName -> Property-knownHost hosts hn user = property desc $- go =<< fromHost hosts hn getSshPubKey- where- desc = user ++ " knows ssh key for " ++ hn- go (Just (Just k)) = do- f <- liftIO $ dotFile "known_hosts" user- ensureProperty $ combineProperties desc- [ File.dirExists (takeDirectory f)- , f `File.containsLine` (hn ++ " " ++ k)- , File.ownerGroup f user user- ]- go _ = do- warningMessage $ "no configred sshPubKey for " ++ hn- return FailedChange---- | Makes a user have authorized_keys from the PrivData-authorizedKeys :: UserName -> Property-authorizedKeys user = property (user ++ " has authorized_keys") $- withPrivData (SshAuthorizedKeys user) $ \v -> do- f <- liftIO $ dotFile "authorized_keys" user- liftIO $ do- createDirectoryIfMissing True (takeDirectory f)- writeFileProtected f v- ensureProperty $ File.ownerGroup f user user
− Propellor/Property/Sudo.hs
@@ -1,32 +0,0 @@-module Propellor.Property.Sudo where--import Data.List--import Propellor-import Propellor.Property.File-import qualified Propellor.Property.Apt as Apt-import Propellor.Property.User---- | Allows a user to sudo. If the user has a password, sudo is configured--- to require it. If not, NOPASSWORD is enabled for the user.-enabledFor :: UserName -> Property-enabledFor user = property desc go `requires` Apt.installed ["sudo"]- where- go = do- locked <- liftIO $ isLockedPassword user- ensureProperty $- fileProperty desc- (modify locked . filter (wanted locked))- "/etc/sudoers"- desc = user ++ " is sudoer"- sudobaseline = user ++ " ALL=(ALL:ALL)"- sudoline True = sudobaseline ++ " NOPASSWD:ALL"- sudoline False = sudobaseline ++ " ALL"- wanted locked l- -- TOOD: Full sudoers file format parse.. - | not (sudobaseline `isPrefixOf` l) = True- | "NOPASSWD" `isInfixOf` l = locked- | otherwise = True- modify locked ls- | sudoline locked `elem` ls = ls- | otherwise = ls ++ [sudoline locked]
− Propellor/Property/Tor.hs
@@ -1,19 +0,0 @@-module Propellor.Property.Tor where--import Propellor-import qualified Propellor.Property.File as File-import qualified Propellor.Property.Apt as Apt--isBridge :: Property-isBridge = setup `requires` Apt.installed ["tor"]- `describe` "tor bridge"- where- setup = "/etc/tor/torrc" `File.hasContent`- [ "SocksPort 0"- , "ORPort 443"- , "BridgeRelay 1"- , "Exitpolicy reject *:*"- ] `onChange` restartTor--restartTor :: Property-restartTor = cmdProperty "service" ["tor", "restart"]
− Propellor/Property/User.hs
@@ -1,61 +0,0 @@-module Propellor.Property.User where--import System.Posix--import Propellor--data Eep = YesReallyDeleteHome--accountFor :: UserName -> Property-accountFor user = check (isNothing <$> catchMaybeIO (homedir user)) $ cmdProperty "adduser"- [ "--disabled-password"- , "--gecos", ""- , user- ]- `describe` ("account for " ++ user)---- | Removes user home directory!! Use with caution.-nuked :: UserName -> Eep -> Property-nuked user _ = check (isJust <$> catchMaybeIO (homedir user)) $ cmdProperty "userdel"- [ "-r"- , user- ]- `describe` ("nuked user " ++ user)---- | Only ensures that the user has some password set. It may or may--- not be the password from the PrivData.-hasSomePassword :: UserName -> Property-hasSomePassword user = check ((/= HasPassword) <$> getPasswordStatus user) $- hasPassword user--hasPassword :: UserName -> Property-hasPassword user = property (user ++ " has password") $- withPrivData (Password user) $ \password -> makeChange $- withHandle StdinHandle createProcessSuccess- (proc "chpasswd" []) $ \h -> do- hPutStrLn h $ user ++ ":" ++ password- hClose h--lockedPassword :: UserName -> Property-lockedPassword user = check (not <$> isLockedPassword user) $ cmdProperty "passwd"- [ "--lock"- , user- ]- `describe` ("locked " ++ user ++ " password")--data PasswordStatus = NoPassword | LockedPassword | HasPassword- deriving (Eq)--getPasswordStatus :: UserName -> IO PasswordStatus-getPasswordStatus user = parse . words <$> readProcess "passwd" ["-S", user]- where- parse (_:"L":_) = LockedPassword- parse (_:"NP":_) = NoPassword- parse (_:"P":_) = HasPassword- parse _ = NoPassword--isLockedPassword :: UserName -> IO Bool-isLockedPassword user = (== LockedPassword) <$> getPasswordStatus user--homedir :: UserName -> IO FilePath-homedir user = homeDirectory <$> getUserEntryForName user
− Propellor/SimpleSh.hs
@@ -1,97 +0,0 @@--- | Simple server, using a named pipe. Client connects, sends a command,--- and gets back all the output from the command, in a stream.------ This is useful for eg, docker.--module Propellor.SimpleSh where--import Network.Socket-import Control.Concurrent.Chan-import Control.Concurrent.Async-import System.Process (std_in, std_out, std_err)--import Propellor-import Utility.FileMode-import Utility.ThreadScheduler--data Cmd = Cmd String [String]- deriving (Read, Show)--data Resp = StdoutLine String | StderrLine String | Done- deriving (Read, Show)--simpleSh :: FilePath -> IO ()-simpleSh namedpipe = do- nukeFile namedpipe- let dir = takeDirectory namedpipe- createDirectoryIfMissing True dir- modifyFileMode dir (removeModes otherGroupModes)- s <- socket AF_UNIX Stream defaultProtocol- bindSocket s (SockAddrUnix namedpipe)- listen s 2- forever $ do- (client, _addr) <- accept s- h <- socketToHandle client ReadWriteMode- maybe noop (run h) . readish =<< hGetLine h- where- run h (Cmd cmd params) = do- let p = (proc cmd params)- { std_in = Inherit- , std_out = CreatePipe- , std_err = CreatePipe- }- (Nothing, Just outh, Just errh, pid) <- createProcess p- chan <- newChan-- let runwriter = do- v <- readChan chan- hPutStrLn h (show v)- hFlush h- case v of- Done -> noop- _ -> runwriter- writer <- async runwriter-- let mkreader t from = maybe noop (const $ mkreader t from) - =<< catchMaybeIO (writeChan chan . t =<< hGetLine from)- void $ concurrently- (mkreader StdoutLine outh)- (mkreader StderrLine errh)- - void $ tryIO $ waitForProcess pid-- writeChan chan Done-- wait writer-- hClose outh- hClose errh- hClose h--simpleShClient :: FilePath -> String -> [String] -> ([Resp] -> IO a) -> IO a-simpleShClient namedpipe cmd params handler = do- s <- socket AF_UNIX Stream defaultProtocol- connect s (SockAddrUnix namedpipe)- h <- socketToHandle s ReadWriteMode- hPutStrLn h $ show $ Cmd cmd params- hFlush h- resps <- catMaybes . map readish . lines <$> hGetContents h- hClose h `after` handler resps--simpleShClientRetry :: Int -> FilePath -> String -> [String] -> ([Resp] -> IO a) -> IO a-simpleShClientRetry retries namedpipe cmd params handler = go retries- where- run = simpleShClient namedpipe cmd params handler- go n- | n < 1 = run- | otherwise = do- v <- tryIO run- case v of- Right r -> return r- Left _ -> do- threadDelaySeconds (Seconds 1)- go (n - 1)--getStdout :: Resp -> Maybe String-getStdout (StdoutLine s) = Just s-getStdout _ = Nothing
− Propellor/Types.hs
@@ -1,153 +0,0 @@-{-# LANGUAGE PackageImports #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE ExistentialQuantification #-}--module Propellor.Types- ( Host(..)- , Attr- , SetAttr- , Propellor(..)- , Property(..)- , RevertableProperty(..)- , IsProp- , describe- , toProp- , setAttr- , requires- , Desc- , Result(..)- , ActionResult(..)- , CmdLine(..)- , PrivDataField(..)- , GpgKeyId- , SshKeyType(..)- , module Propellor.Types.OS- , module Propellor.Types.Dns- ) where--import Data.Monoid-import Control.Applicative-import System.Console.ANSI-import "mtl" Control.Monad.Reader-import "MonadCatchIO-transformers" Control.Monad.CatchIO--import Propellor.Types.Attr-import Propellor.Types.OS-import Propellor.Types.Dns--data Host = Host [Property] SetAttr---- | Propellor's monad provides read-only access to attributes of the--- system.-newtype Propellor p = Propellor { runWithAttr :: ReaderT Attr IO p }- deriving- ( Monad- , Functor- , Applicative- , MonadReader Attr - , MonadIO- , MonadCatchIO- )---- | The core data type of Propellor, this represents a property--- that the system should have, and an action to ensure it has the--- property.-data Property = Property- { propertyDesc :: Desc- , propertySatisfy :: Propellor Result- -- ^ must be idempotent; may run repeatedly- , propertyAttr :: SetAttr- -- ^ a property can set an Attr on the host that has the property.- }---- | A property that can be reverted.-data RevertableProperty = RevertableProperty Property Property--class IsProp p where- -- | Sets description.- describe :: p -> Desc -> p- toProp :: p -> Property- -- | Indicates that the first property can only be satisfied- -- once the second one is.- requires :: p -> Property -> p- setAttr :: p -> SetAttr--instance IsProp Property where- describe p d = p { propertyDesc = d }- toProp p = p- setAttr = propertyAttr- x `requires` y = Property (propertyDesc x) satisfy attr- where- attr = propertyAttr x . propertyAttr y- satisfy = do- r <- propertySatisfy y- case r of- FailedChange -> return FailedChange- _ -> propertySatisfy x- --instance IsProp RevertableProperty where- -- | Sets the description of both sides.- describe (RevertableProperty p1 p2) d = - RevertableProperty (describe p1 d) (describe p2 ("not " ++ d))- toProp (RevertableProperty p1 _) = p1- (RevertableProperty p1 p2) `requires` y =- RevertableProperty (p1 `requires` y) p2- -- | Return the SetAttr of the currently active side.- setAttr (RevertableProperty p1 _p2) = setAttr p1--type Desc = String--data Result = NoChange | MadeChange | FailedChange- deriving (Read, Show, Eq)--instance Monoid Result where- mempty = NoChange-- mappend FailedChange _ = FailedChange- mappend _ FailedChange = FailedChange- mappend MadeChange _ = MadeChange- mappend _ MadeChange = MadeChange- mappend NoChange NoChange = NoChange---- | Results of actions, with color.-class ActionResult a where- getActionResult :: a -> (String, ColorIntensity, Color)--instance ActionResult Bool where- getActionResult False = ("failed", Vivid, Red)- getActionResult True = ("done", Dull, Green)--instance ActionResult Result where- getActionResult NoChange = ("ok", Dull, Green)- getActionResult MadeChange = ("done", Vivid, Green)- getActionResult FailedChange = ("failed", Vivid, Red)--data CmdLine- = Run HostName- | Spin HostName- | Boot HostName- | Set HostName PrivDataField- | AddKey String- | Continue CmdLine- | Chain HostName- | Docker HostName- deriving (Read, Show, Eq)---- | Note that removing or changing field names will break the--- serialized privdata files, so don't do that!--- It's fine to add new fields.-data PrivDataField- = DockerAuthentication- | SshPubKey SshKeyType UserName- | SshPrivKey SshKeyType UserName- | SshAuthorizedKeys UserName- | Password UserName- | PrivFile FilePath- | GpgKey GpgKeyId- deriving (Read, Show, Ord, Eq)--type GpgKeyId = String--data SshKeyType = SshRsa | SshDsa | SshEcdsa | SshEd25519- deriving (Read, Show, Ord, Eq)
− Propellor/Types/Attr.hs
@@ -1,48 +0,0 @@-module Propellor.Types.Attr where--import Propellor.Types.OS-import qualified Propellor.Types.Dns as Dns--import qualified Data.Set as S-import qualified Data.Map as M---- | The attributes of a host. For example, its hostname.-data Attr = Attr- { _hostname :: HostName- , _os :: Maybe System- , _sshPubKey :: Maybe String- , _dns :: S.Set Dns.Record- , _namedconf :: M.Map Dns.Domain Dns.NamedConf-- , _dockerImage :: Maybe String- , _dockerRunParams :: [HostName -> String]- }--instance Eq Attr where- x == y = and- [ _hostname x == _hostname y- , _os x == _os y- , _dns x == _dns y- , _namedconf x == _namedconf y- , _sshPubKey x == _sshPubKey y-- , _dockerImage x == _dockerImage y- , let simpl v = map (\a -> a "") (_dockerRunParams v)- in simpl x == simpl y- ]--instance Show Attr where- show a = unlines- [ "hostname " ++ _hostname a- , "OS " ++ show (_os a)- , "sshPubKey " ++ show (_sshPubKey a)- , "dns " ++ show (_dns a)- , "namedconf " ++ show (_namedconf a)- , "docker image " ++ show (_dockerImage a)- , "docker run params " ++ show (map (\mk -> mk "") (_dockerRunParams a))- ]--newAttr :: HostName -> Attr-newAttr hn = Attr hn Nothing Nothing S.empty M.empty Nothing []--type SetAttr = Attr -> Attr
− Propellor/Types/Dns.hs
@@ -1,92 +0,0 @@-module Propellor.Types.Dns where--import Propellor.Types.OS (HostName)--import Data.Word--type Domain = String--data IPAddr = IPv4 String | IPv6 String- deriving (Read, Show, Eq, Ord)--fromIPAddr :: IPAddr -> String-fromIPAddr (IPv4 addr) = addr-fromIPAddr (IPv6 addr) = addr---- | Represents a bind 9 named.conf file.-data NamedConf = NamedConf- { confDomain :: Domain- , confDnsServerType :: DnsServerType- , confFile :: FilePath- , confMasters :: [IPAddr]- , confAllowTransfer :: [IPAddr]- , confLines :: [String]- }- deriving (Show, Eq, Ord)--data DnsServerType = Master | Secondary- deriving (Show, Eq, Ord)---- | Represents a bind 9 zone file.-data Zone = Zone- { zDomain :: Domain- , zSOA :: SOA- , zHosts :: [(BindDomain, Record)]- }- deriving (Read, Show, Eq)---- | Every domain has a SOA record, which is big and complicated.-data SOA = SOA- { sDomain :: BindDomain- -- ^ Typically ns1.your.domain- , sSerial :: SerialNumber- -- ^ The most important parameter is the serial number,- -- which must increase after each change.- , sRefresh :: Integer- , sRetry :: Integer- , sExpire :: Integer- , sNegativeCacheTTL :: Integer- }- deriving (Read, Show, Eq)---- | Types of DNS records.------ This is not a complete list, more can be added.-data Record- = Address IPAddr- | CNAME BindDomain- | MX Int BindDomain- | NS BindDomain- | TXT String- | SRV Word16 Word16 Word16 BindDomain- deriving (Read, Show, Eq, Ord)--getIPAddr :: Record -> Maybe IPAddr-getIPAddr (Address addr) = Just addr-getIPAddr _ = Nothing--getCNAME :: Record -> Maybe BindDomain-getCNAME (CNAME d) = Just d-getCNAME _ = Nothing--getNS :: Record -> Maybe BindDomain-getNS (NS d) = Just d-getNS _ = Nothing---- | Bind serial numbers are unsigned, 32 bit integers.-type SerialNumber = Word32---- | Domains in the zone file must end with a period if they are absolute.------ Let's use a type to keep absolute domains straight from relative--- domains.------ The RootDomain refers to the top level of the domain, so can be used--- to add nameservers, MX's, etc to a domain.-data BindDomain = RelDomain Domain | AbsDomain Domain | RootDomain- deriving (Read, Show, Eq, Ord)--domainHostName :: BindDomain -> Maybe HostName-domainHostName (RelDomain d) = Just d-domainHostName (AbsDomain d) = Just d-domainHostName RootDomain = Nothing
− Propellor/Types/OS.hs
@@ -1,27 +0,0 @@-module Propellor.Types.OS where--type HostName = String-type UserName = String-type GroupName = String---- | High level descritption of a operating system.-data System = System Distribution Architecture- deriving (Show, Eq)--data Distribution- = Debian DebianSuite- | Ubuntu Release- deriving (Show, Eq)--data DebianSuite = Experimental | Unstable | Testing | Stable | DebianRelease Release- deriving (Show, Eq)---- | The release that currently corresponds to stable.-stableRelease :: DebianSuite-stableRelease = DebianRelease "wheezy"--isStable :: DebianSuite -> Bool-isStable s = s == Stable || s == stableRelease--type Release = String-type Architecture = String
README.md view
@@ -6,7 +6,8 @@ Propellor is configured via a git repository, which typically lives in `~/.propellor/` on your development machine. Propellor clones the repository to each host it manages, in a -[secure](http://propellor.branchable.com/security/) way.+[secure](http://propellor.branchable.com/security/) way. The git repository+contains the full source code to Propellor, along with its config file. Properties are defined using Haskell. Edit `~/.propellor/config.hs` to get started. There is fairly complete @@ -40,11 +41,12 @@ `apt-get install propellor` 2. Run propellor for the first time. It will set up a `~/.propellor/` git repository for you.-3. `cd ~/.propellor/`; use git to push the repository to a central+3. If you don't have a gpg private key already, generate one: `gpg --gen-key`+4. Run: `propellor --add-key $KEYID`, which will make propellor trust+ your gpg key, and will sign your `~/.propellor` repository using it.+5. `cd ~/.propellor/`; use git to push the repository to a central server (github, or your own git server). Configure that central server as the origin remote of the repository.-4. If you don't have a gpg private key, generate one: `gpg --gen-key`-5. Run: `propellor --add-key $KEYID` 6. Edit `~/.propellor/config.hs`, and add a host you want to manage. You can start by not adding any properties, or only a few. 7. Pick a host and run: `propellor --spin $HOST`
− Utility/Applicative.hs
@@ -1,16 +0,0 @@-{- applicative stuff- -- - Copyright 2012 Joey Hess <joey@kitenet.net>- -- - Licensed under the GNU GPL version 3 or higher.- -}--module Utility.Applicative where--{- Like <$> , but supports one level of currying.- - - - foo v = bar <$> action v == foo = bar <$$> action- -}-(<$$>) :: Functor f => (a -> b) -> (c -> f a) -> c -> f b-f <$$> v = fmap f . v-infixr 4 <$$>
− Utility/Data.hs
@@ -1,17 +0,0 @@-{- utilities for simple data types- -- - Copyright 2013 Joey Hess <joey@kitenet.net>- -- - Licensed under the GNU GPL version 3 or higher.- -}--module Utility.Data where--{- First item in the list that is not Nothing. -}-firstJust :: Eq a => [Maybe a] -> Maybe a-firstJust ms = case dropWhile (== Nothing) ms of- [] -> Nothing- (md:_) -> md--eitherToMaybe :: Either a b -> Maybe b-eitherToMaybe = either (const Nothing) Just
− Utility/Directory.hs
@@ -1,135 +0,0 @@-{- directory manipulation- -- - Copyright 2011-2014 Joey Hess <joey@kitenet.net>- -- - Licensed under the GNU GPL version 3 or higher.- -}--{-# LANGUAGE CPP #-}--module Utility.Directory where--import System.IO.Error-import System.Directory-import Control.Exception (throw)-import Control.Monad-import Control.Monad.IfElse-import System.FilePath-import Control.Applicative-import System.IO.Unsafe (unsafeInterleaveIO)--import Utility.PosixFiles-import Utility.SafeCommand-import Utility.Tmp-import Utility.Exception-import Utility.Monad-import Utility.Applicative--dirCruft :: FilePath -> Bool-dirCruft "." = True-dirCruft ".." = True-dirCruft _ = False--{- Lists the contents of a directory.- - Unlike getDirectoryContents, paths are not relative to the directory. -}-dirContents :: FilePath -> IO [FilePath]-dirContents d = map (d </>) . filter (not . dirCruft) <$> getDirectoryContents d--{- Gets files in a directory, and then its subdirectories, recursively,- - and lazily.- -- - Does not follow symlinks to other subdirectories.- -- - When the directory does not exist, no exception is thrown,- - instead, [] is returned. -}-dirContentsRecursive :: FilePath -> IO [FilePath]-dirContentsRecursive topdir = dirContentsRecursiveSkipping (const False) True topdir--{- Skips directories whose basenames match the skipdir. -}-dirContentsRecursiveSkipping :: (FilePath -> Bool) -> Bool -> FilePath -> IO [FilePath]-dirContentsRecursiveSkipping skipdir followsubdirsymlinks topdir = go [topdir]- where- go [] = return []- go (dir:dirs)- | skipdir (takeFileName dir) = go dirs- | otherwise = unsafeInterleaveIO $ do- (files, dirs') <- collect [] []- =<< catchDefaultIO [] (dirContents dir)- files' <- go (dirs' ++ dirs)- return (files ++ files')- collect files dirs' [] = return (reverse files, reverse dirs')- collect files dirs' (entry:entries)- | dirCruft entry = collect files dirs' entries- | otherwise = do- let skip = collect (entry:files) dirs' entries- let recurse = collect files (entry:dirs') entries- ms <- catchMaybeIO $ getSymbolicLinkStatus entry- case ms of- (Just s) - | isDirectory s -> recurse- | isSymbolicLink s && followsubdirsymlinks ->- ifM (doesDirectoryExist entry)- ( recurse- , skip- )- _ -> skip--{- Gets the directory tree from a point, recursively and lazily,- - with leaf directories **first**, skipping any whose basenames- - match the skipdir. Does not follow symlinks. -}-dirTreeRecursiveSkipping :: (FilePath -> Bool) -> FilePath -> IO [FilePath]-dirTreeRecursiveSkipping skipdir topdir = go [] [topdir]- where- go c [] = return c- go c (dir:dirs)- | skipdir (takeFileName dir) = go c dirs- | otherwise = unsafeInterleaveIO $ do- subdirs <- go c- =<< filterM (isDirectory <$$> getSymbolicLinkStatus)- =<< catchDefaultIO [] (dirContents dir)- go (subdirs++[dir]) dirs--{- Moves one filename to another.- - First tries a rename, but falls back to moving across devices if needed. -}-moveFile :: FilePath -> FilePath -> IO ()-moveFile src dest = tryIO (rename src dest) >>= onrename- where- onrename (Right _) = noop- onrename (Left e)- | isPermissionError e = rethrow- | isDoesNotExistError e = rethrow- | otherwise = do- -- copyFile is likely not as optimised as- -- the mv command, so we'll use the latter.- -- But, mv will move into a directory if- -- dest is one, which is not desired.- whenM (isdir dest) rethrow- viaTmp mv dest undefined- where- rethrow = throw e- mv tmp _ = do- ok <- boolSystem "mv" [Param "-f", Param src, Param tmp]- unless ok $ do- -- delete any partial- _ <- tryIO $ removeFile tmp- rethrow-- isdir f = do- r <- tryIO $ getFileStatus f- case r of- (Left _) -> return False- (Right s) -> return $ isDirectory s--{- Removes a file, which may or may not exist, and does not have to- - be a regular file.- -- - Note that an exception is thrown if the file exists but- - cannot be removed. -}-nukeFile :: FilePath -> IO ()-nukeFile file = void $ tryWhenExists go- where-#ifndef mingw32_HOST_OS- go = removeLink file-#else- go = removeFile file-#endif
− Utility/Env.hs
@@ -1,81 +0,0 @@-{- portable environment variables- -- - Copyright 2013 Joey Hess <joey@kitenet.net>- -- - Licensed under the GNU GPL version 3 or higher.- -}--{-# LANGUAGE CPP #-}--module Utility.Env where--#ifdef mingw32_HOST_OS-import Utility.Exception-import Control.Applicative-import Data.Maybe-import qualified System.Environment as E-#else-import qualified System.Posix.Env as PE-#endif--getEnv :: String -> IO (Maybe String)-#ifndef mingw32_HOST_OS-getEnv = PE.getEnv-#else-getEnv = catchMaybeIO . E.getEnv-#endif--getEnvDefault :: String -> String -> IO String-#ifndef mingw32_HOST_OS-getEnvDefault = PE.getEnvDefault-#else-getEnvDefault var fallback = fromMaybe fallback <$> getEnv var-#endif--getEnvironment :: IO [(String, String)]-#ifndef mingw32_HOST_OS-getEnvironment = PE.getEnvironment-#else-getEnvironment = E.getEnvironment-#endif--{- Returns True if it could successfully set the environment variable.- -- - There is, apparently, no way to do this in Windows. Instead,- - environment varuables must be provided when running a new process. -}-setEnv :: String -> String -> Bool -> IO Bool-#ifndef mingw32_HOST_OS-setEnv var val overwrite = do- PE.setEnv var val overwrite- return True-#else-setEnv _ _ _ = return False-#endif--{- Returns True if it could successfully unset the environment variable. -}-unsetEnv :: String -> IO Bool-#ifndef mingw32_HOST_OS-unsetEnv var = do- PE.unsetEnv var- return True-#else-unsetEnv _ = return False-#endif--{- Adds the environment variable to the input environment. If already- - present in the list, removes the old value.- -- - This does not really belong here, but Data.AssocList is for some reason- - buried inside hxt.- -}-addEntry :: Eq k => k -> v -> [(k, v)] -> [(k, v)]-addEntry k v l = ( (k,v) : ) $! delEntry k l--addEntries :: Eq k => [(k, v)] -> [(k, v)] -> [(k, v)]-addEntries = foldr (.) id . map (uncurry addEntry) . reverse--delEntry :: Eq k => k -> [(k, v)] -> [(k, v)]-delEntry _ [] = []-delEntry k (x@(k1,_) : rest)- | k == k1 = rest- | otherwise = ( x : ) $! delEntry k rest
− Utility/Exception.hs
@@ -1,59 +0,0 @@-{- Simple IO exception handling (and some more)- -- - Copyright 2011-2012 Joey Hess <joey@kitenet.net>- -- - Licensed under the GNU GPL version 3 or higher.- -}--{-# LANGUAGE ScopedTypeVariables #-}--module Utility.Exception where--import Control.Exception-import qualified Control.Exception as E-import Control.Applicative-import Control.Monad-import System.IO.Error (isDoesNotExistError)-import Utility.Data--{- Catches IO errors and returns a Bool -}-catchBoolIO :: IO Bool -> IO Bool-catchBoolIO a = catchDefaultIO False a--{- Catches IO errors and returns a Maybe -}-catchMaybeIO :: IO a -> IO (Maybe a)-catchMaybeIO a = catchDefaultIO Nothing $ Just <$> a--{- Catches IO errors and returns a default value. -}-catchDefaultIO :: a -> IO a -> IO a-catchDefaultIO def a = catchIO a (const $ return def)--{- Catches IO errors and returns the error message. -}-catchMsgIO :: IO a -> IO (Either String a)-catchMsgIO a = either (Left . show) Right <$> tryIO a--{- catch specialized for IO errors only -}-catchIO :: IO a -> (IOException -> IO a) -> IO a-catchIO = E.catch--{- try specialized for IO errors only -}-tryIO :: IO a -> IO (Either IOException a)-tryIO = try--{- Catches all exceptions except for async exceptions.- - This is often better to use than catching them all, so that- - ThreadKilled and UserInterrupt get through.- -}-catchNonAsync :: IO a -> (SomeException -> IO a) -> IO a-catchNonAsync a onerr = a `catches`- [ Handler (\ (e :: AsyncException) -> throw e)- , Handler (\ (e :: SomeException) -> onerr e)- ]--tryNonAsync :: IO a -> IO (Either SomeException a)-tryNonAsync a = (Right <$> a) `catchNonAsync` (return . Left)--{- Catches only DoesNotExist exceptions, and lets all others through. -}-tryWhenExists :: IO a -> IO (Maybe a)-tryWhenExists a = eitherToMaybe <$>- tryJust (guard . isDoesNotExistError) a
− Utility/FileMode.hs
@@ -1,157 +0,0 @@-{- File mode utilities.- -- - Copyright 2010-2012 Joey Hess <joey@kitenet.net>- -- - Licensed under the GNU GPL version 3 or higher.- -}--{-# LANGUAGE CPP #-}--module Utility.FileMode where--import System.IO-import Control.Monad-import Control.Exception (bracket)-import System.PosixCompat.Types-#ifndef mingw32_HOST_OS-import System.Posix.Files-#endif-import Foreign (complement)--import Utility.Exception--{- Applies a conversion function to a file's mode. -}-modifyFileMode :: FilePath -> (FileMode -> FileMode) -> IO ()-modifyFileMode f convert = void $ modifyFileMode' f convert-modifyFileMode' :: FilePath -> (FileMode -> FileMode) -> IO FileMode-modifyFileMode' f convert = do- s <- getFileStatus f- let old = fileMode s- let new = convert old- when (new /= old) $- setFileMode f new- return old--{- Adds the specified FileModes to the input mode, leaving the rest- - unchanged. -}-addModes :: [FileMode] -> FileMode -> FileMode-addModes ms m = combineModes (m:ms)--{- Removes the specified FileModes from the input mode. -}-removeModes :: [FileMode] -> FileMode -> FileMode-removeModes ms m = m `intersectFileModes` complement (combineModes ms)--{- Runs an action after changing a file's mode, then restores the old mode. -}-withModifiedFileMode :: FilePath -> (FileMode -> FileMode) -> IO a -> IO a-withModifiedFileMode file convert a = bracket setup cleanup go- where- setup = modifyFileMode' file convert- cleanup oldmode = modifyFileMode file (const oldmode)- go _ = a--writeModes :: [FileMode]-writeModes = [ownerWriteMode, groupWriteMode, otherWriteMode]--readModes :: [FileMode]-readModes = [ownerReadMode, groupReadMode, otherReadMode]--executeModes :: [FileMode]-executeModes = [ownerExecuteMode, groupExecuteMode, otherExecuteMode]--otherGroupModes :: [FileMode]-otherGroupModes = - [ groupReadMode, otherReadMode- , groupWriteMode, otherWriteMode- ]--{- Removes the write bits from a file. -}-preventWrite :: FilePath -> IO ()-preventWrite f = modifyFileMode f $ removeModes writeModes--{- Turns a file's owner write bit back on. -}-allowWrite :: FilePath -> IO ()-allowWrite f = modifyFileMode f $ addModes [ownerWriteMode]--{- Turns a file's owner read bit back on. -}-allowRead :: FilePath -> IO ()-allowRead f = modifyFileMode f $ addModes [ownerReadMode]--{- Allows owner and group to read and write to a file. -}-groupSharedModes :: [FileMode]-groupSharedModes =- [ ownerWriteMode, groupWriteMode- , ownerReadMode, groupReadMode- ]--groupWriteRead :: FilePath -> IO ()-groupWriteRead f = modifyFileMode f $ addModes groupSharedModes--checkMode :: FileMode -> FileMode -> Bool-checkMode checkfor mode = checkfor `intersectFileModes` mode == checkfor--{- Checks if a file mode indicates it's a symlink. -}-isSymLink :: FileMode -> Bool-#ifdef mingw32_HOST_OS-isSymLink _ = False-#else-isSymLink = checkMode symbolicLinkMode-#endif--{- Checks if a file has any executable bits set. -}-isExecutable :: FileMode -> Bool-isExecutable mode = combineModes executeModes `intersectFileModes` mode /= 0--{- Runs an action without that pesky umask influencing it, unless the- - passed FileMode is the standard one. -}-noUmask :: FileMode -> IO a -> IO a-#ifndef mingw32_HOST_OS-noUmask mode a- | mode == stdFileMode = a- | otherwise = withUmask nullFileMode a-#else-noUmask _ a = a-#endif--withUmask :: FileMode -> IO a -> IO a-#ifndef mingw32_HOST_OS-withUmask umask a = bracket setup cleanup go- where- setup = setFileCreationMask umask- cleanup = setFileCreationMask- go _ = a-#else-withUmask _ a = a-#endif--combineModes :: [FileMode] -> FileMode-combineModes [] = undefined-combineModes [m] = m-combineModes (m:ms) = foldl unionFileModes m ms--isSticky :: FileMode -> Bool-#ifdef mingw32_HOST_OS-isSticky _ = False-#else-isSticky = checkMode stickyMode--stickyMode :: FileMode-stickyMode = 512--setSticky :: FilePath -> IO ()-setSticky f = modifyFileMode f $ addModes [stickyMode]-#endif--{- Writes a file, ensuring that its modes do not allow it to be read- - or written by anyone other than the current user,- - before any content is written.- -- - When possible, this is done using the umask.- -- - On a filesystem that does not support file permissions, this is the same- - as writeFile.- -}-writeFileProtected :: FilePath -> String -> IO ()-writeFileProtected file content = withUmask 0o0077 $- withFile file WriteMode $ \h -> do- void $ tryIO $ modifyFileMode file $ removeModes otherGroupModes- hPutStr h content
− Utility/FileSystemEncoding.hs
@@ -1,132 +0,0 @@-{- GHC File system encoding handling.- -- - Copyright 2012-2014 Joey Hess <joey@kitenet.net>- -- - Licensed under the GNU GPL version 3 or higher.- -}--{-# LANGUAGE CPP #-}--module Utility.FileSystemEncoding (- fileEncoding,- withFilePath,- md5FilePath,- decodeBS,- decodeW8,- encodeW8,- truncateFilePath,-) where--import qualified GHC.Foreign as GHC-import qualified GHC.IO.Encoding as Encoding-import Foreign.C-import System.IO-import System.IO.Unsafe-import qualified Data.Hash.MD5 as MD5-import Data.Word-import Data.Bits.Utils-import qualified Data.ByteString.Lazy as L-#ifdef mingw32_HOST_OS-import qualified Data.ByteString.Lazy.UTF8 as L8-#endif--{- Sets a Handle to use the filesystem encoding. This causes data- - written or read from it to be encoded/decoded the same- - as ghc 7.4 does to filenames etc. This special encoding- - allows "arbitrary undecodable bytes to be round-tripped through it".- -}-fileEncoding :: Handle -> IO ()-#ifndef mingw32_HOST_OS-fileEncoding h = hSetEncoding h =<< Encoding.getFileSystemEncoding-#else-{- The file system encoding does not work well on Windows,- - and Windows only has utf FilePaths anyway. -}-fileEncoding h = hSetEncoding h Encoding.utf8-#endif--{- Marshal a Haskell FilePath into a NUL terminated C string using temporary- - storage. The FilePath is encoded using the filesystem encoding,- - reversing the decoding that should have been done when the FilePath- - was obtained. -}-withFilePath :: FilePath -> (CString -> IO a) -> IO a-withFilePath fp f = Encoding.getFileSystemEncoding- >>= \enc -> GHC.withCString enc fp f--{- Encodes a FilePath into a String, applying the filesystem encoding.- -- - There are very few things it makes sense to do with such an encoded- - string. It's not a legal filename; it should not be displayed.- - So this function is not exported, but instead used by the few functions- - that can usefully consume it.- -- - This use of unsafePerformIO is belived to be safe; GHC's interface- - only allows doing this conversion with CStrings, and the CString buffer- - is allocated, used, and deallocated within the call, with no side- - effects.- -}-{-# NOINLINE _encodeFilePath #-}-_encodeFilePath :: FilePath -> String-_encodeFilePath fp = unsafePerformIO $ do- enc <- Encoding.getFileSystemEncoding- GHC.withCString enc fp $ GHC.peekCString Encoding.char8--{- Encodes a FilePath into a Md5.Str, applying the filesystem encoding. -}-md5FilePath :: FilePath -> MD5.Str-md5FilePath = MD5.Str . _encodeFilePath--{- Decodes a ByteString into a FilePath, applying the filesystem encoding. -}-decodeBS :: L.ByteString -> FilePath-#ifndef mingw32_HOST_OS-decodeBS = encodeW8 . L.unpack-#else-{- On Windows, we assume that the ByteString is utf-8, since Windows- - only uses unicode for filenames. -}-decodeBS = L8.toString-#endif--{- Converts a [Word8] to a FilePath, encoding using the filesystem encoding.- -- - w82c produces a String, which may contain Chars that are invalid- - unicode. From there, this is really a simple matter of applying the- - file system encoding, only complicated by GHC's interface to doing so.- -}-{-# NOINLINE encodeW8 #-}-encodeW8 :: [Word8] -> FilePath-encodeW8 w8 = unsafePerformIO $ do- enc <- Encoding.getFileSystemEncoding- GHC.withCString Encoding.char8 (w82s w8) $ GHC.peekCString enc--{- Useful when you want the actual number of bytes that will be used to- - represent the FilePath on disk. -}-decodeW8 :: FilePath -> [Word8]-decodeW8 = s2w8 . _encodeFilePath--{- Truncates a FilePath to the given number of bytes (or less),- - as represented on disk.- -- - Avoids returning an invalid part of a unicode byte sequence, at the- - cost of efficiency when running on a large FilePath.- -}-truncateFilePath :: Int -> FilePath -> FilePath-#ifndef mingw32_HOST_OS-truncateFilePath n = go . reverse- where- go f =- let bytes = decodeW8 f- in if length bytes <= n- then reverse f- else go (drop 1 f)-#else-{- On Windows, count the number of bytes used by each utf8 character. -}-truncateFilePath n = reverse . go [] n . L8.fromString- where- go coll cnt bs- | cnt <= 0 = coll- | otherwise = case L8.decode bs of- Just (c, x) | c /= L8.replacement_char ->- let x' = fromIntegral x- in if cnt - x' < 0- then coll- else go (c:coll) (cnt - x') (L8.drop 1 bs)- _ -> coll-#endif
− Utility/LinuxMkLibs.hs
@@ -1,61 +0,0 @@-{- Linux library copier and binary shimmer- -- - Copyright 2013 Joey Hess <joey@kitenet.net>- -- - Licensed under the GNU GPL version 3 or higher.- -}--module Utility.LinuxMkLibs where--import Control.Applicative-import Data.Maybe-import System.Directory-import Data.List.Utils-import System.Posix.Files-import Data.Char-import Control.Monad.IfElse--import Utility.PartialPrelude-import Utility.Directory-import Utility.Process-import Utility.Monad-import Utility.Path--{- Installs a library. If the library is a symlink to another file,- - install the file it links to, and update the symlink to be relative. -}-installLib :: (FilePath -> FilePath -> IO ()) -> FilePath -> FilePath -> IO (Maybe FilePath)-installLib installfile top lib = ifM (doesFileExist lib)- ( do- installfile top lib- checksymlink lib- return $ Just $ parentDir lib- , return Nothing- )- where- checksymlink f = whenM (isSymbolicLink <$> getSymbolicLinkStatus (inTop top f)) $ do- l <- readSymbolicLink (inTop top f)- let absl = absPathFrom (parentDir f) l- let target = relPathDirToFile (parentDir f) absl- installfile top absl- nukeFile (top ++ f)- createSymbolicLink target (inTop top f)- checksymlink absl---- Note that f is not relative, so cannot use </>-inTop :: FilePath -> FilePath -> FilePath-inTop top f = top ++ f--{- Parse ldd output, getting all the libraries that the input files- - link to. Note that some of the libraries may not exist - - (eg, linux-vdso.so) -}-parseLdd :: String -> [FilePath]-parseLdd = catMaybes . map (getlib . dropWhile isSpace) . lines- where- getlib l = headMaybe . words =<< lastMaybe (split " => " l)--{- Get all glibc libs and other support files, including gconv files- -- - XXX Debian specific. -}-glibcLibs :: IO [FilePath]-glibcLibs = lines <$> readProcess "sh"- ["-c", "dpkg -L libc6:$(dpkg --print-architecture) libgcc1:$(dpkg --print-architecture) | egrep '\\.so|gconv'"]
− Utility/Misc.hs
@@ -1,148 +0,0 @@-{- misc utility functions- -- - Copyright 2010-2011 Joey Hess <joey@kitenet.net>- -- - Licensed under the GNU GPL version 3 or higher.- -}--{-# LANGUAGE CPP #-}--module Utility.Misc where--import System.IO-import Control.Monad-import Foreign-import Data.Char-import Data.List-import Control.Applicative-import System.Exit-#ifndef mingw32_HOST_OS-import System.Posix.Process (getAnyProcessStatus)-import Utility.Exception-#endif--import Utility.FileSystemEncoding-import Utility.Monad--{- A version of hgetContents that is not lazy. Ensures file is - - all read before it gets closed. -}-hGetContentsStrict :: Handle -> IO String-hGetContentsStrict = hGetContents >=> \s -> length s `seq` return s--{- A version of readFile that is not lazy. -}-readFileStrict :: FilePath -> IO String-readFileStrict = readFile >=> \s -> length s `seq` return s--{- Reads a file strictly, and using the FileSystemEncoding, so it will- - never crash on a badly encoded file. -}-readFileStrictAnyEncoding :: FilePath -> IO String-readFileStrictAnyEncoding f = withFile f ReadMode $ \h -> do- fileEncoding h- hClose h `after` hGetContentsStrict h--{- Writes a file, using the FileSystemEncoding so it will never crash- - on a badly encoded content string. -}-writeFileAnyEncoding :: FilePath -> String -> IO ()-writeFileAnyEncoding f content = withFile f WriteMode $ \h -> do- fileEncoding h- hPutStr h content--{- Like break, but the item matching the condition is not included- - in the second result list.- -- - separate (== ':') "foo:bar" = ("foo", "bar")- - separate (== ':') "foobar" = ("foobar", "")- -}-separate :: (a -> Bool) -> [a] -> ([a], [a])-separate c l = unbreak $ break c l- where- unbreak r@(a, b)- | null b = r- | otherwise = (a, tail b)--{- Breaks out the first line. -}-firstLine :: String -> String-firstLine = takeWhile (/= '\n')--{- Splits a list into segments that are delimited by items matching- - a predicate. (The delimiters are not included in the segments.)- - Segments may be empty. -}-segment :: (a -> Bool) -> [a] -> [[a]]-segment p l = map reverse $ go [] [] l- where- go c r [] = reverse $ c:r- go c r (i:is)- | p i = go [] (c:r) is- | otherwise = go (i:c) r is--prop_segment_regressionTest :: Bool-prop_segment_regressionTest = all id- -- Even an empty list is a segment.- [ segment (== "--") [] == [[]]- -- There are two segements in this list, even though the first is empty.- , segment (== "--") ["--", "foo", "bar"] == [[],["foo","bar"]]- ]--{- Includes the delimiters as segments of their own. -}-segmentDelim :: (a -> Bool) -> [a] -> [[a]]-segmentDelim p l = map reverse $ go [] [] l- where- go c r [] = reverse $ c:r- go c r (i:is)- | p i = go [] ([i]:c:r) is- | otherwise = go (i:c) r is--{- Replaces multiple values in a string.- -- - Takes care to skip over just-replaced values, so that they are not- - mangled. For example, massReplace [("foo", "new foo")] does not- - replace the "new foo" with "new new foo".- -}-massReplace :: [(String, String)] -> String -> String-massReplace vs = go [] vs- where-- go acc _ [] = concat $ reverse acc- go acc [] (c:cs) = go ([c]:acc) vs cs- go acc ((val, replacement):rest) s- | val `isPrefixOf` s =- go (replacement:acc) vs (drop (length val) s)- | otherwise = go acc rest s--{- Wrapper around hGetBufSome that returns a String.- -- - The null string is returned on eof, otherwise returns whatever- - data is currently available to read from the handle, or waits for- - data to be written to it if none is currently available.- - - - Note on encodings: The normal encoding of the Handle is ignored;- - each byte is converted to a Char. Not unicode clean!- -}-hGetSomeString :: Handle -> Int -> IO String-hGetSomeString h sz = do- fp <- mallocForeignPtrBytes sz- len <- withForeignPtr fp $ \buf -> hGetBufSome h buf sz- map (chr . fromIntegral) <$> withForeignPtr fp (peekbytes len)- where- peekbytes :: Int -> Ptr Word8 -> IO [Word8]- peekbytes len buf = mapM (peekElemOff buf) [0..pred len]--{- Reaps any zombie git processes. - -- - Warning: Not thread safe. Anything that was expecting to wait- - on a process and get back an exit status is going to be confused- - if this reap gets there first. -}-reapZombies :: IO ()-#ifndef mingw32_HOST_OS-reapZombies = do- -- throws an exception when there are no child processes- catchDefaultIO Nothing (getAnyProcessStatus False True)- >>= maybe (return ()) (const reapZombies)--#else-reapZombies = return ()-#endif--exitBool :: Bool -> IO a-exitBool False = exitFailure-exitBool True = exitSuccess
− Utility/Monad.hs
@@ -1,69 +0,0 @@-{- monadic stuff- -- - Copyright 2010-2012 Joey Hess <joey@kitenet.net>- -- - Licensed under the GNU GPL version 3 or higher.- -}--module Utility.Monad where--import Data.Maybe-import Control.Monad--{- Return the first value from a list, if any, satisfying the given- - predicate -}-firstM :: Monad m => (a -> m Bool) -> [a] -> m (Maybe a)-firstM _ [] = return Nothing-firstM p (x:xs) = ifM (p x) (return $ Just x , firstM p xs)--{- Runs the action on values from the list until it succeeds, returning- - its result. -}-getM :: Monad m => (a -> m (Maybe b)) -> [a] -> m (Maybe b)-getM _ [] = return Nothing-getM p (x:xs) = maybe (getM p xs) (return . Just) =<< p x--{- Returns true if any value in the list satisfies the predicate,- - stopping once one is found. -}-anyM :: Monad m => (a -> m Bool) -> [a] -> m Bool-anyM p = liftM isJust . firstM p--allM :: Monad m => (a -> m Bool) -> [a] -> m Bool-allM _ [] = return True-allM p (x:xs) = p x <&&> allM p xs--{- Runs an action on values from a list until it succeeds. -}-untilTrue :: Monad m => [a] -> (a -> m Bool) -> m Bool-untilTrue = flip anyM--{- if with a monadic conditional. -}-ifM :: Monad m => m Bool -> (m a, m a) -> m a-ifM cond (thenclause, elseclause) = do- c <- cond- if c then thenclause else elseclause--{- short-circuiting monadic || -}-(<||>) :: Monad m => m Bool -> m Bool -> m Bool-ma <||> mb = ifM ma ( return True , mb )--{- short-circuiting monadic && -}-(<&&>) :: Monad m => m Bool -> m Bool -> m Bool-ma <&&> mb = ifM ma ( mb , return False )--{- Same fixity as && and || -}-infixr 3 <&&>-infixr 2 <||>--{- Runs an action, passing its value to an observer before returning it. -}-observe :: Monad m => (a -> m b) -> m a -> m a-observe observer a = do- r <- a- _ <- observer r- return r--{- b `after` a runs first a, then b, and returns the value of a -}-after :: Monad m => m b -> m a -> m a-after = observe . const--{- do nothing -}-noop :: Monad m => m ()-noop = return ()
− Utility/PartialPrelude.hs
@@ -1,68 +0,0 @@-{- Parts of the Prelude are partial functions, which are a common source of- - bugs.- -- - This exports functions that conflict with the prelude, which avoids- - them being accidentially used.- -}--module Utility.PartialPrelude where--import qualified Data.Maybe--{- read should be avoided, as it throws an error- - Instead, use: readish -}-read :: Read a => String -> a-read = Prelude.read--{- head is a partial function; head [] is an error- - Instead, use: take 1 or headMaybe -}-head :: [a] -> a-head = Prelude.head--{- tail is also partial- - Instead, use: drop 1 -}-tail :: [a] -> [a]-tail = Prelude.tail--{- init too- - Instead, use: beginning -}-init :: [a] -> [a]-init = Prelude.init--{- last too- - Instead, use: end or lastMaybe -}-last :: [a] -> a-last = Prelude.last--{- Attempts to read a value from a String.- -- - Ignores leading/trailing whitespace, and throws away any trailing- - text after the part that can be read.- -- - readMaybe is available in Text.Read in new versions of GHC,- - but that one requires the entire string to be consumed.- -}-readish :: Read a => String -> Maybe a-readish s = case reads s of- ((x,_):_) -> Just x- _ -> Nothing--{- Like head but Nothing on empty list. -}-headMaybe :: [a] -> Maybe a-headMaybe = Data.Maybe.listToMaybe--{- Like last but Nothing on empty list. -}-lastMaybe :: [a] -> Maybe a-lastMaybe [] = Nothing-lastMaybe v = Just $ Prelude.last v--{- All but the last element of a list.- - (Like init, but no error on an empty list.) -}-beginning :: [a] -> [a]-beginning [] = []-beginning l = Prelude.init l--{- Like last, but no error on an empty list. -}-end :: [a] -> [a]-end [] = []-end l = [Prelude.last l]
− Utility/Path.hs
@@ -1,293 +0,0 @@-{- path manipulation- -- - Copyright 2010-2014 Joey Hess <joey@kitenet.net>- -- - Licensed under the GNU GPL version 3 or higher.- -}--{-# LANGUAGE PackageImports, CPP #-}--module Utility.Path where--import Data.String.Utils-import System.FilePath-import System.Directory-import Data.List-import Data.Maybe-import Data.Char-import Control.Applicative--#ifdef mingw32_HOST_OS-import qualified System.FilePath.Posix as Posix-#else-import System.Posix.Files-#endif--import qualified "MissingH" System.Path as MissingH-import Utility.Monad-import Utility.UserInfo--{- Simplifies a path, removing any ".." or ".", and removing the trailing- - path separator.- -- - On Windows, preserves whichever style of path separator might be used in- - the input FilePaths. This is done because some programs in Windows- - demand a particular path separator -- and which one actually varies!- -- - This does not guarantee that two paths that refer to the same location,- - and are both relative to the same location (or both absolute) will- - yeild the same result. Run both through normalise from System.FilePath- - to ensure that.- -}-simplifyPath :: FilePath -> FilePath-simplifyPath path = dropTrailingPathSeparator $ - joinDrive drive $ joinPath $ norm [] $ splitPath path'- where- (drive, path') = splitDrive path-- norm c [] = reverse c- norm c (p:ps)- | p' == ".." = norm (drop 1 c) ps- | p' == "." = norm c ps- | otherwise = norm (p:c) ps- where- p' = dropTrailingPathSeparator p--{- Makes a path absolute.- -- - The first parameter is a base directory (ie, the cwd) to use if the path- - is not already absolute.- -- - Does not attempt to deal with edge cases or ensure security with- - untrusted inputs.- -}-absPathFrom :: FilePath -> FilePath -> FilePath-absPathFrom dir path = simplifyPath (combine dir path)--{- On Windows, this converts the paths to unix-style, in order to run- - MissingH's absNormPath on them. Resulting path will use / separators. -}-absNormPathUnix :: FilePath -> FilePath -> Maybe FilePath-#ifndef mingw32_HOST_OS-absNormPathUnix dir path = MissingH.absNormPath dir path-#else-absNormPathUnix dir path = todos <$> MissingH.absNormPath (fromdos dir) (fromdos path)- where- fromdos = replace "\\" "/"- todos = replace "/" "\\"-#endif--{- Returns the parent directory of a path.- -- - To allow this to be easily used in loops, which terminate upon reaching the- - top, the parent of / is "" -}-parentDir :: FilePath -> FilePath-parentDir dir- | null dirs = ""- | otherwise = joinDrive drive (join s $ init dirs)- where- -- on Unix, the drive will be "/" when the dir is absolute, otherwise ""- (drive, path) = splitDrive dir- dirs = filter (not . null) $ split s path- s = [pathSeparator]--prop_parentDir_basics :: FilePath -> Bool-prop_parentDir_basics dir- | null dir = True- | dir == "/" = parentDir dir == ""- | otherwise = p /= dir- where- p = parentDir dir--{- Checks if the first FilePath is, or could be said to contain the second.- - For example, "foo/" contains "foo/bar". Also, "foo", "./foo", "foo/" etc- - are all equivilant.- -}-dirContains :: FilePath -> FilePath -> Bool-dirContains a b = a == b || a' == b' || (addTrailingPathSeparator a') `isPrefixOf` b'- where- a' = norm a- b' = norm b- norm = normalise . simplifyPath--{- Converts a filename into an absolute path.- -- - Unlike Directory.canonicalizePath, this does not require the path- - already exists. -}-absPath :: FilePath -> IO FilePath-absPath file = do- cwd <- getCurrentDirectory- return $ absPathFrom cwd file--{- Constructs a relative path from the CWD to a file.- -- - For example, assuming CWD is /tmp/foo/bar:- - relPathCwdToFile "/tmp/foo" == ".."- - relPathCwdToFile "/tmp/foo/bar" == "" - -}-relPathCwdToFile :: FilePath -> IO FilePath-relPathCwdToFile f = relPathDirToFile <$> getCurrentDirectory <*> absPath f--{- Constructs a relative path from a directory to a file.- -- - Both must be absolute, and cannot contain .. etc. (eg use absPath first).- -}-relPathDirToFile :: FilePath -> FilePath -> FilePath-relPathDirToFile from to = join s $ dotdots ++ uncommon- where- s = [pathSeparator]- pfrom = split s from- pto = split s to- common = map fst $ takeWhile same $ zip pfrom pto- same (c,d) = c == d- uncommon = drop numcommon pto- dotdots = replicate (length pfrom - numcommon) ".."- numcommon = length common--prop_relPathDirToFile_basics :: FilePath -> FilePath -> Bool-prop_relPathDirToFile_basics from to- | from == to = null r- | otherwise = not (null r)- where- r = relPathDirToFile from to --prop_relPathDirToFile_regressionTest :: Bool-prop_relPathDirToFile_regressionTest = same_dir_shortcurcuits_at_difference- where- {- Two paths have the same directory component at the same- - location, but it's not really the same directory.- - Code used to get this wrong. -}- same_dir_shortcurcuits_at_difference =- relPathDirToFile (joinPath [pathSeparator : "tmp", "r", "lll", "xxx", "yyy", "18"])- (joinPath [pathSeparator : "tmp", "r", ".git", "annex", "objects", "18", "gk", "SHA256-foo", "SHA256-foo"])- == joinPath ["..", "..", "..", "..", ".git", "annex", "objects", "18", "gk", "SHA256-foo", "SHA256-foo"]--{- Given an original list of paths, and an expanded list derived from it,- - generates a list of lists, where each sublist corresponds to one of the- - original paths. When the original path is a directory, any items- - in the expanded list that are contained in that directory will appear in- - its segment.- -}-segmentPaths :: [FilePath] -> [FilePath] -> [[FilePath]]-segmentPaths [] new = [new]-segmentPaths [_] new = [new] -- optimisation-segmentPaths (l:ls) new = [found] ++ segmentPaths ls rest- where- (found, rest)=partition (l `dirContains`) new--{- This assumes that it's cheaper to call segmentPaths on the result,- - than it would be to run the action separately with each path. In- - the case of git file list commands, that assumption tends to hold.- -}-runSegmentPaths :: ([FilePath] -> IO [FilePath]) -> [FilePath] -> IO [[FilePath]]-runSegmentPaths a paths = segmentPaths paths <$> a paths--{- Converts paths in the home directory to use ~/ -}-relHome :: FilePath -> IO String-relHome path = do- home <- myHomeDir- return $ if dirContains home path- then "~/" ++ relPathDirToFile home path- else path--{- Checks if a command is available in PATH.- -- - The command may be fully-qualified, in which case, this succeeds as- - long as it exists. -}-inPath :: String -> IO Bool-inPath command = isJust <$> searchPath command--{- Finds a command in PATH and returns the full path to it.- -- - The command may be fully qualified already, in which case it will- - be returned if it exists.- -}-searchPath :: String -> IO (Maybe FilePath)-searchPath command- | isAbsolute command = check command- | otherwise = getSearchPath >>= getM indir- where- indir d = check $ d </> command- check f = firstM doesFileExist-#ifdef mingw32_HOST_OS- [f, f ++ ".exe"]-#else- [f]-#endif--{- Checks if a filename is a unix dotfile. All files inside dotdirs- - count as dotfiles. -}-dotfile :: FilePath -> Bool-dotfile file- | f == "." = False- | f == ".." = False- | f == "" = False- | otherwise = "." `isPrefixOf` f || dotfile (takeDirectory file)- where- f = takeFileName file--{- Converts a DOS style path to a Cygwin style path. Only on Windows.- - Any trailing '\' is preserved as a trailing '/' -}-toCygPath :: FilePath -> FilePath-#ifndef mingw32_HOST_OS-toCygPath = id-#else-toCygPath p- | null drive = recombine parts- | otherwise = recombine $ "/cygdrive" : driveletter drive : parts- where- (drive, p') = splitDrive p- parts = splitDirectories p'- driveletter = map toLower . takeWhile (/= ':')- recombine = fixtrailing . Posix.joinPath- fixtrailing s- | hasTrailingPathSeparator p = Posix.addTrailingPathSeparator s- | otherwise = s-#endif--{- Maximum size to use for a file in a specified directory.- -- - Many systems have a 255 byte limit to the name of a file, - - so that's taken as the max if the system has a larger limit, or has no- - limit.- -}-fileNameLengthLimit :: FilePath -> IO Int-#ifdef mingw32_HOST_OS-fileNameLengthLimit _ = return 255-#else-fileNameLengthLimit dir = do- l <- fromIntegral <$> getPathVar dir FileNameLimit- if l <= 0- then return 255- else return $ minimum [l, 255]- where-#endif--{- Given a string that we'd like to use as the basis for FilePath, but that- - was provided by a third party and is not to be trusted, returns the closest- - sane FilePath.- -- - All spaces and punctuation and other wacky stuff are replaced- - with '_', except for '.' "../" will thus turn into ".._", which is safe.- -}-sanitizeFilePath :: String -> FilePath-sanitizeFilePath = map sanitize- where- sanitize c- | c == '.' = c- | isSpace c || isPunctuation c || isSymbol c || isControl c || c == '/' = '_'- | otherwise = c--{- Similar to splitExtensions, but knows that some things in FilePaths- - after a dot are too long to be extensions. -}-splitShortExtensions :: FilePath -> (FilePath, [String])-splitShortExtensions = splitShortExtensions' 5 -- enough for ".jpeg"-splitShortExtensions' :: Int -> FilePath -> (FilePath, [String])-splitShortExtensions' maxextension = go []- where- go c f- | len > 0 && len <= maxextension && not (null base) = - go (ext:c) base- | otherwise = (f, c)- where- (base, ext) = splitExtension f- len = length ext
− Utility/PosixFiles.hs
@@ -1,33 +0,0 @@-{- POSIX files (and compatablity wrappers).- -- - This is like System.PosixCompat.Files, except with a fixed rename.- -- - Copyright 2014 Joey Hess <joey@kitenet.net>- -- - Licensed under the GNU GPL version 3 or higher.- -}--{-# LANGUAGE CPP #-}--module Utility.PosixFiles (- module X,- rename-) where--import System.PosixCompat.Files as X hiding (rename)--#ifndef mingw32_HOST_OS-import System.Posix.Files (rename)-#else-import qualified System.Win32.File as Win32-#endif--{- System.PosixCompat.Files.rename on Windows calls renameFile,- - so cannot rename directories. - -- - Instead, use Win32 moveFile, which can. It needs to be told to overwrite- - any existing file. -}-#ifdef mingw32_HOST_OS-rename :: FilePath -> FilePath -> IO ()-rename src dest = Win32.moveFileEx src dest Win32.mOVEFILE_REPLACE_EXISTING-#endif
− Utility/Process.hs
@@ -1,360 +0,0 @@-{- System.Process enhancements, including additional ways of running- - processes, and logging.- -- - Copyright 2012 Joey Hess <joey@kitenet.net>- -- - Licensed under the GNU GPL version 3 or higher.- -}--{-# LANGUAGE CPP, Rank2Types #-}--module Utility.Process (- module X,- CreateProcess,- StdHandle(..),- readProcess,- readProcessEnv,- writeReadProcessEnv,- forceSuccessProcess,- checkSuccessProcess,- ignoreFailureProcess,- createProcessSuccess,- createProcessChecked,- createBackgroundProcess,- processTranscript,- processTranscript',- withHandle,- withBothHandles,- withQuietOutput,- createProcess,- startInteractiveProcess,- stdinHandle,- stdoutHandle,- stderrHandle,- devNull,-) where--import qualified System.Process-import System.Process as X hiding (CreateProcess(..), createProcess, runInteractiveProcess, readProcess, readProcessWithExitCode, system, rawSystem, runInteractiveCommand, runProcess)-import System.Process hiding (createProcess, readProcess)-import System.Exit-import System.IO-import System.Log.Logger-import Control.Concurrent-import qualified Control.Exception as E-import Control.Monad-#ifndef mingw32_HOST_OS-import System.Posix.IO-#else-import Control.Applicative-#endif-import Data.Maybe--import Utility.Misc-import Utility.Exception--type CreateProcessRunner = forall a. CreateProcess -> ((Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) -> IO a) -> IO a--data StdHandle = StdinHandle | StdoutHandle | StderrHandle- deriving (Eq)--{- Normally, when reading from a process, it does not need to be fed any- - standard input. -}-readProcess :: FilePath -> [String] -> IO String-readProcess cmd args = readProcessEnv cmd args Nothing--readProcessEnv :: FilePath -> [String] -> Maybe [(String, String)] -> IO String-readProcessEnv cmd args environ =- withHandle StdoutHandle createProcessSuccess p $ \h -> do- output <- hGetContentsStrict h- hClose h- return output- where- p = (proc cmd args)- { std_out = CreatePipe- , env = environ- }--{- Runs an action to write to a process on its stdin, - - returns its output, and also allows specifying the environment.- -}-writeReadProcessEnv- :: FilePath- -> [String]- -> Maybe [(String, String)]- -> (Maybe (Handle -> IO ()))- -> (Maybe (Handle -> IO ()))- -> IO String-writeReadProcessEnv cmd args environ writestdin adjusthandle = do- (Just inh, Just outh, _, pid) <- createProcess p-- maybe (return ()) (\a -> a inh) adjusthandle- maybe (return ()) (\a -> a outh) adjusthandle-- -- fork off a thread to start consuming the output- output <- hGetContents outh- outMVar <- newEmptyMVar- _ <- forkIO $ E.evaluate (length output) >> putMVar outMVar ()-- -- now write and flush any input- maybe (return ()) (\a -> a inh >> hFlush inh) writestdin- hClose inh -- done with stdin-- -- wait on the output- takeMVar outMVar- hClose outh-- -- wait on the process- forceSuccessProcess p pid-- return output-- where- p = (proc cmd args)- { std_in = CreatePipe- , std_out = CreatePipe- , std_err = Inherit- , env = environ- }--{- Waits for a ProcessHandle, and throws an IOError if the process- - did not exit successfully. -}-forceSuccessProcess :: CreateProcess -> ProcessHandle -> IO ()-forceSuccessProcess p pid = do- code <- waitForProcess pid- case code of- ExitSuccess -> return ()- ExitFailure n -> fail $ showCmd p ++ " exited " ++ show n--{- Waits for a ProcessHandle and returns True if it exited successfully.- - Note that using this with createProcessChecked will throw away- - the Bool, and is only useful to ignore the exit code of a process,- - while still waiting for it. -}-checkSuccessProcess :: ProcessHandle -> IO Bool-checkSuccessProcess pid = do- code <- waitForProcess pid- return $ code == ExitSuccess--ignoreFailureProcess :: ProcessHandle -> IO Bool-ignoreFailureProcess pid = do- void $ waitForProcess pid- return True--{- Runs createProcess, then an action on its handles, and then- - forceSuccessProcess. -}-createProcessSuccess :: CreateProcessRunner-createProcessSuccess p a = createProcessChecked (forceSuccessProcess p) p a--{- Runs createProcess, then an action on its handles, and then- - a checker action on its exit code, which must wait for the process. -}-createProcessChecked :: (ProcessHandle -> IO b) -> CreateProcessRunner-createProcessChecked checker p a = do- t@(_, _, _, pid) <- createProcess p- r <- tryNonAsync $ a t- _ <- checker pid- either E.throw return r--{- Leaves the process running, suitable for lazy streaming.- - Note: Zombies will result, and must be waited on. -}-createBackgroundProcess :: CreateProcessRunner-createBackgroundProcess p a = a =<< createProcess p--{- Runs a process, optionally feeding it some input, and- - returns a transcript combining its stdout and stderr, and- - whether it succeeded or failed. -}-processTranscript :: String -> [String] -> (Maybe String) -> IO (String, Bool)-processTranscript cmd opts input = processTranscript' cmd opts Nothing input--processTranscript' :: String -> [String] -> Maybe [(String, String)] -> (Maybe String) -> IO (String, Bool)-#ifndef mingw32_HOST_OS-{- This implementation interleves stdout and stderr in exactly the order- - the process writes them. -}-processTranscript' cmd opts environ input = do- (readf, writef) <- createPipe- readh <- fdToHandle readf- writeh <- fdToHandle writef- p@(_, _, _, pid) <- createProcess $- (proc cmd opts)- { std_in = if isJust input then CreatePipe else Inherit- , std_out = UseHandle writeh- , std_err = UseHandle writeh- , env = environ- }- hClose writeh-- get <- mkreader readh-- -- now write and flush any input- case input of- Just s -> do- let inh = stdinHandle p- unless (null s) $ do- hPutStr inh s- hFlush inh- hClose inh- Nothing -> return ()-- transcript <- get-- ok <- checkSuccessProcess pid- return (transcript, ok)-#else-{- This implementation for Windows puts stderr after stdout. -}-processTranscript' cmd opts environ input = do- p@(_, _, _, pid) <- createProcess $- (proc cmd opts)- { std_in = if isJust input then CreatePipe else Inherit- , std_out = CreatePipe- , std_err = CreatePipe- , env = environ- }-- getout <- mkreader (stdoutHandle p)- geterr <- mkreader (stderrHandle p)-- case input of- Just s -> do- let inh = stdinHandle p- unless (null s) $ do- hPutStr inh s- hFlush inh- hClose inh- Nothing -> return ()- - transcript <- (++) <$> getout <*> geterr- ok <- checkSuccessProcess pid- return (transcript, ok)-#endif- where- mkreader h = do- s <- hGetContents h- v <- newEmptyMVar- void $ forkIO $ do- void $ E.evaluate (length s)- putMVar v ()- return $ do- takeMVar v- return s--{- Runs a CreateProcessRunner, on a CreateProcess structure, that- - is adjusted to pipe only from/to a single StdHandle, and passes- - the resulting Handle to an action. -}-withHandle- :: StdHandle- -> CreateProcessRunner- -> CreateProcess- -> (Handle -> IO a)- -> IO a-withHandle h creator p a = creator p' $ a . select- where- base = p- { std_in = Inherit- , std_out = Inherit- , std_err = Inherit- }- (select, p')- | h == StdinHandle =- (stdinHandle, base { std_in = CreatePipe })- | h == StdoutHandle =- (stdoutHandle, base { std_out = CreatePipe })- | h == StderrHandle =- (stderrHandle, base { std_err = CreatePipe })--{- Like withHandle, but passes (stdin, stdout) handles to the action. -}-withBothHandles- :: CreateProcessRunner- -> CreateProcess- -> ((Handle, Handle) -> IO a)- -> IO a-withBothHandles creator p a = creator p' $ a . bothHandles- where- p' = p- { std_in = CreatePipe- , std_out = CreatePipe- , std_err = Inherit- }--{- Forces the CreateProcessRunner to run quietly;- - both stdout and stderr are discarded. -}-withQuietOutput- :: CreateProcessRunner- -> CreateProcess- -> IO ()-withQuietOutput creator p = withFile devNull WriteMode $ \nullh -> do- let p' = p- { std_out = UseHandle nullh- , std_err = UseHandle nullh- }- creator p' $ const $ return ()--devNull :: FilePath-#ifndef mingw32_HOST_OS-devNull = "/dev/null"-#else-devNull = "NUL"-#endif--{- Extract a desired handle from createProcess's tuple.- - These partial functions are safe as long as createProcess is run- - with appropriate parameters to set up the desired handle.- - Get it wrong and the runtime crash will always happen, so should be- - easily noticed. -}-type HandleExtractor = (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) -> Handle-stdinHandle :: HandleExtractor-stdinHandle (Just h, _, _, _) = h-stdinHandle _ = error "expected stdinHandle"-stdoutHandle :: HandleExtractor-stdoutHandle (_, Just h, _, _) = h-stdoutHandle _ = error "expected stdoutHandle"-stderrHandle :: HandleExtractor-stderrHandle (_, _, Just h, _) = h-stderrHandle _ = error "expected stderrHandle"-bothHandles :: (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) -> (Handle, Handle)-bothHandles (Just hin, Just hout, _, _) = (hin, hout)-bothHandles _ = error "expected bothHandles"--{- Debugging trace for a CreateProcess. -}-debugProcess :: CreateProcess -> IO ()-debugProcess p = do- debugM "Utility.Process" $ unwords- [ action ++ ":"- , showCmd p- ]- where- action- | piped (std_in p) && piped (std_out p) = "chat"- | piped (std_in p) = "feed"- | piped (std_out p) = "read"- | otherwise = "call"- piped Inherit = False- piped _ = True--{- Shows the command that a CreateProcess will run. -}-showCmd :: CreateProcess -> String-showCmd = go . cmdspec- where- go (ShellCommand s) = s- go (RawCommand c ps) = c ++ " " ++ show ps--{- Starts an interactive process. Unlike runInteractiveProcess in- - System.Process, stderr is inherited. -}-startInteractiveProcess- :: FilePath- -> [String]- -> Maybe [(String, String)]- -> IO (ProcessHandle, Handle, Handle)-startInteractiveProcess cmd args environ = do- let p = (proc cmd args)- { std_in = CreatePipe- , std_out = CreatePipe- , std_err = Inherit- , env = environ- }- (Just from, Just to, _, pid) <- createProcess p- return (pid, to, from)--{- Wrapper around System.Process function that does debug logging. -}-createProcess :: CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)-createProcess p = do- debugProcess p- System.Process.createProcess p
− Utility/QuickCheck.hs
@@ -1,52 +0,0 @@-{- QuickCheck with additional instances- -- - Copyright 2012-2014 Joey Hess <joey@kitenet.net>- -- - Licensed under the GNU GPL version 3 or higher.- -}--{-# OPTIONS_GHC -fno-warn-orphans #-}-{-# LANGUAGE TypeSynonymInstances #-}--module Utility.QuickCheck- ( module X- , module Utility.QuickCheck- ) where--import Test.QuickCheck as X-import Data.Time.Clock.POSIX-import System.Posix.Types-import qualified Data.Map as M-import qualified Data.Set as S-import Control.Applicative--instance (Arbitrary k, Arbitrary v, Eq k, Ord k) => Arbitrary (M.Map k v) where- arbitrary = M.fromList <$> arbitrary--instance (Arbitrary v, Eq v, Ord v) => Arbitrary (S.Set v) where- arbitrary = S.fromList <$> arbitrary--{- Times before the epoch are excluded. -}-instance Arbitrary POSIXTime where- arbitrary = fromInteger <$> nonNegative arbitrarySizedIntegral--instance Arbitrary EpochTime where- arbitrary = fromInteger <$> nonNegative arbitrarySizedIntegral--{- Pids are never negative, or 0. -}-instance Arbitrary ProcessID where- arbitrary = arbitrarySizedBoundedIntegral `suchThat` (> 0)--{- Inodes are never negative. -}-instance Arbitrary FileID where- arbitrary = nonNegative arbitrarySizedIntegral--{- File sizes are never negative. -}-instance Arbitrary FileOffset where- arbitrary = nonNegative arbitrarySizedIntegral--nonNegative :: (Num a, Ord a) => Gen a -> Gen a-nonNegative g = g `suchThat` (>= 0)--positive :: (Num a, Ord a) => Gen a -> Gen a-positive g = g `suchThat` (> 0)
− Utility/SafeCommand.hs
@@ -1,120 +0,0 @@-{- safely running shell commands- -- - Copyright 2010-2013 Joey Hess <joey@kitenet.net>- -- - Licensed under the GNU GPL version 3 or higher.- -}--module Utility.SafeCommand where--import System.Exit-import Utility.Process-import System.Process (env)-import Data.String.Utils-import Control.Applicative-import System.FilePath-import Data.Char--{- A type for parameters passed to a shell command. A command can- - be passed either some Params (multiple parameters can be included,- - whitespace-separated, or a single Param (for when parameters contain- - whitespace), or a File.- -}-data CommandParam = Params String | Param String | File FilePath- deriving (Eq, Show, Ord)--{- Used to pass a list of CommandParams to a function that runs- - a command and expects Strings. -}-toCommand :: [CommandParam] -> [String]-toCommand = concatMap unwrap- where- unwrap (Param s) = [s]- unwrap (Params s) = filter (not . null) (split " " s)- -- Files that start with a non-alphanumeric that is not a path- -- separator are modified to avoid the command interpreting them as- -- options or other special constructs.- unwrap (File s@(h:_))- | isAlphaNum h || h `elem` pathseps = [s]- | otherwise = ["./" ++ s]- unwrap (File s) = [s]- -- '/' is explicitly included because it's an alternative- -- path separator on Windows.- pathseps = pathSeparator:"./"--{- Run a system command, and returns True or False- - if it succeeded or failed.- -}-boolSystem :: FilePath -> [CommandParam] -> IO Bool-boolSystem command params = boolSystemEnv command params Nothing--boolSystemEnv :: FilePath -> [CommandParam] -> Maybe [(String, String)] -> IO Bool-boolSystemEnv command params environ = dispatch <$> safeSystemEnv command params environ- where- dispatch ExitSuccess = True- dispatch _ = False--{- Runs a system command, returning the exit status. -}-safeSystem :: FilePath -> [CommandParam] -> IO ExitCode-safeSystem command params = safeSystemEnv command params Nothing--safeSystemEnv :: FilePath -> [CommandParam] -> Maybe [(String, String)] -> IO ExitCode-safeSystemEnv command params environ = do- (_, _, _, pid) <- createProcess (proc command $ toCommand params)- { env = environ }- waitForProcess pid--{- Wraps a shell command line inside sh -c, allowing it to be run in a- - login shell that may not support POSIX shell, eg csh. -}-shellWrap :: String -> String-shellWrap cmdline = "sh -c " ++ shellEscape cmdline--{- Escapes a filename or other parameter to be safely able to be exposed to- - the shell.- -- - This method works for POSIX shells, as well as other shells like csh.- -}-shellEscape :: String -> String-shellEscape f = "'" ++ escaped ++ "'"- where- -- replace ' with '"'"'- escaped = join "'\"'\"'" $ split "'" f--{- Unescapes a set of shellEscaped words or filenames. -}-shellUnEscape :: String -> [String]-shellUnEscape [] = []-shellUnEscape s = word : shellUnEscape rest- where- (word, rest) = findword "" s- findword w [] = (w, "")- findword w (c:cs)- | c == ' ' = (w, cs)- | c == '\'' = inquote c w cs- | c == '"' = inquote c w cs- | otherwise = findword (w++[c]) cs- inquote _ w [] = (w, "")- inquote q w (c:cs)- | c == q = findword w cs- | otherwise = inquote q (w++[c]) cs--{- For quickcheck. -}-prop_idempotent_shellEscape :: String -> Bool-prop_idempotent_shellEscape s = [s] == (shellUnEscape . shellEscape) s-prop_idempotent_shellEscape_multiword :: [String] -> Bool-prop_idempotent_shellEscape_multiword s = s == (shellUnEscape . unwords . map shellEscape) s--{- Segements a list of filenames into groups that are all below the manximum- - command-line length limit. Does not preserve order. -}-segmentXargs :: [FilePath] -> [[FilePath]]-segmentXargs l = go l [] 0 []- where- go [] c _ r = c:r- go (f:fs) c accumlen r- | len < maxlen && newlen > maxlen = go (f:fs) [] 0 (c:r)- | otherwise = go fs (f:c) newlen r- where- len = length f- newlen = accumlen + len-- {- 10k of filenames per command, well under Linux's 20k limit;- - allows room for other parameters etc. -}- maxlen = 10240
− Utility/Scheduled.hs
@@ -1,396 +0,0 @@-{- scheduled activities- - - - Copyright 2013-2014 Joey Hess <joey@kitenet.net>- -- - Licensed under the GNU GPL version 3 or higher.- -}--module Utility.Scheduled (- Schedule(..),- Recurrance(..),- ScheduledTime(..),- NextTime(..),- WeekDay,- MonthDay,- YearDay,- nextTime,- calcNextTime,- startTime,- fromSchedule,- fromScheduledTime,- toScheduledTime,- fromRecurrance,- toRecurrance,- toSchedule,- parseSchedule,- prop_schedule_roundtrips,- prop_past_sane,-) where--import Utility.Data-import Utility.QuickCheck-import Utility.PartialPrelude-import Utility.Misc--import Control.Applicative-import Data.List-import Data.Time.Clock-import Data.Time.LocalTime-import Data.Time.Calendar-import Data.Time.Calendar.WeekDate-import Data.Time.Calendar.OrdinalDate-import Data.Tuple.Utils-import Data.Char--{- Some sort of scheduled event. -}-data Schedule = Schedule Recurrance ScheduledTime- deriving (Eq, Read, Show, Ord)--data Recurrance- = Daily- | Weekly (Maybe WeekDay)- | Monthly (Maybe MonthDay)- | Yearly (Maybe YearDay)- | Divisible Int Recurrance- -- ^ Days, Weeks, or Months of the year evenly divisible by a number.- -- (Divisible Year is years evenly divisible by a number.)- deriving (Eq, Read, Show, Ord)--type WeekDay = Int-type MonthDay = Int-type YearDay = Int--data ScheduledTime- = AnyTime- | SpecificTime Hour Minute- deriving (Eq, Read, Show, Ord)--type Hour = Int-type Minute = Int---- | Next time a Schedule should take effect. The NextTimeWindow is used--- when a Schedule is allowed to start at some point within the window.-data NextTime- = NextTimeExactly LocalTime- | NextTimeWindow LocalTime LocalTime- deriving (Eq, Read, Show)--startTime :: NextTime -> LocalTime-startTime (NextTimeExactly t) = t-startTime (NextTimeWindow t _) = t--nextTime :: Schedule -> Maybe LocalTime -> IO (Maybe NextTime)-nextTime schedule lasttime = do- now <- getCurrentTime- tz <- getTimeZone now- return $ calcNextTime schedule lasttime $ utcToLocalTime tz now---- | Calculate the next time that fits a Schedule, based on the--- last time it occurred, and the current time.-calcNextTime :: Schedule -> Maybe LocalTime -> LocalTime -> Maybe NextTime-calcNextTime schedule@(Schedule recurrance scheduledtime) lasttime currenttime- | scheduledtime == AnyTime = do- next <- findfromtoday True- return $ case next of- NextTimeWindow _ _ -> next- NextTimeExactly t -> window (localDay t) (localDay t)- | otherwise = NextTimeExactly . startTime <$> findfromtoday False- where- findfromtoday anytime = findfrom recurrance afterday today- where- today = localDay currenttime- afterday = sameaslastrun || toolatetoday- toolatetoday = not anytime && localTimeOfDay currenttime >= nexttime- sameaslastrun = lastrun == Just today- lastrun = localDay <$> lasttime- nexttime = case scheduledtime of- AnyTime -> TimeOfDay 0 0 0- SpecificTime h m -> TimeOfDay h m 0- exactly d = NextTimeExactly $ LocalTime d nexttime- window startd endd = NextTimeWindow- (LocalTime startd nexttime)- (LocalTime endd (TimeOfDay 23 59 0))- findfrom r afterday candidate- | ynum candidate > (ynum (localDay currenttime)) + 100 =- -- avoid possible infinite recusion- error $ "bug: calcNextTime did not find a time within 100 years to run " ++- show (schedule, lasttime, currenttime)- | otherwise = findfromChecked r afterday candidate- findfromChecked r afterday candidate = case r of- Daily- | afterday -> Just $ exactly $ addDays 1 candidate- | otherwise -> Just $ exactly candidate- Weekly Nothing- | afterday -> skip 1- | otherwise -> case (wday <$> lastrun, wday candidate) of- (Nothing, _) -> Just $ window candidate (addDays 6 candidate)- (Just old, curr)- | old == curr -> Just $ window candidate (addDays 6 candidate)- | otherwise -> skip 1- Monthly Nothing- | afterday -> skip 1- | maybe True (candidate `oneMonthPast`) lastrun ->- Just $ window candidate (endOfMonth candidate)- | otherwise -> skip 1- Yearly Nothing- | afterday -> skip 1- | maybe True (candidate `oneYearPast`) lastrun ->- Just $ window candidate (endOfYear candidate)- | otherwise -> skip 1- Weekly (Just w)- | w < 0 || w > maxwday -> Nothing- | w == wday candidate -> if afterday- then Just $ exactly $ addDays 7 candidate- else Just $ exactly candidate- | otherwise -> Just $ exactly $- addDays (fromIntegral $ (w - wday candidate) `mod` 7) candidate- Monthly (Just m)- | m < 0 || m > maxmday -> Nothing- -- TODO can be done more efficiently than recursing- | m == mday candidate -> if afterday- then skip 1- else Just $ exactly candidate- | otherwise -> skip 1- Yearly (Just y)- | y < 0 || y > maxyday -> Nothing- | y == yday candidate -> if afterday- then skip 365- else Just $ exactly candidate- | otherwise -> skip 1- Divisible n r'@Daily -> handlediv n r' yday (Just maxyday)- Divisible n r'@(Weekly _) -> handlediv n r' wnum (Just maxwnum)- Divisible n r'@(Monthly _) -> handlediv n r' mnum (Just maxmnum)- Divisible n r'@(Yearly _) -> handlediv n r' ynum Nothing- Divisible _ r'@(Divisible _ _) -> findfrom r' afterday candidate- where- skip n = findfrom r False (addDays n candidate)- handlediv n r' getval mmax- | n > 0 && maybe True (n <=) mmax =- findfromwhere r' (divisible n . getval) afterday candidate- | otherwise = Nothing- findfromwhere r p afterday candidate- | maybe True (p . getday) next = next- | otherwise = maybe Nothing (findfromwhere r p True . getday) next- where- next = findfrom r afterday candidate- getday = localDay . startTime- divisible n v = v `rem` n == 0---- Check if the new Day occurs one month or more past the old Day.-oneMonthPast :: Day -> Day -> Bool-new `oneMonthPast` old = fromGregorian y (m+1) d <= new- where- (y,m,d) = toGregorian old---- Check if the new Day occurs one year or more past the old Day.-oneYearPast :: Day -> Day -> Bool-new `oneYearPast` old = fromGregorian (y+1) m d <= new- where- (y,m,d) = toGregorian old--endOfMonth :: Day -> Day-endOfMonth day =- let (y,m,_d) = toGregorian day- in fromGregorian y m (gregorianMonthLength y m)--endOfYear :: Day -> Day-endOfYear day =- let (y,_m,_d) = toGregorian day- in endOfMonth (fromGregorian y maxmnum 1)---- extracting various quantities from a Day-wday :: Day -> Int-wday = thd3 . toWeekDate-wnum :: Day -> Int-wnum = snd3 . toWeekDate-mday :: Day -> Int-mday = thd3 . toGregorian-mnum :: Day -> Int-mnum = snd3 . toGregorian-yday :: Day -> Int-yday = snd . toOrdinalDate-ynum :: Day -> Int-ynum = fromIntegral . fst . toOrdinalDate---- Calendar max values.-maxyday :: Int-maxyday = 366 -- with leap days-maxwnum :: Int-maxwnum = 53 -- some years have more than 52-maxmday :: Int-maxmday = 31-maxmnum :: Int-maxmnum = 12-maxwday :: Int-maxwday = 7--fromRecurrance :: Recurrance -> String-fromRecurrance (Divisible n r) =- fromRecurrance' (++ "s divisible by " ++ show n) r-fromRecurrance r = fromRecurrance' ("every " ++) r--fromRecurrance' :: (String -> String) -> Recurrance -> String-fromRecurrance' a Daily = a "day"-fromRecurrance' a (Weekly n) = onday n (a "week")-fromRecurrance' a (Monthly n) = onday n (a "month")-fromRecurrance' a (Yearly n) = onday n (a "year")-fromRecurrance' a (Divisible _n r) = fromRecurrance' a r -- not used--onday :: Maybe Int -> String -> String-onday (Just n) s = "on day " ++ show n ++ " of " ++ s-onday Nothing s = s--toRecurrance :: String -> Maybe Recurrance-toRecurrance s = case words s of- ("every":"day":[]) -> Just Daily- ("on":"day":sd:"of":"every":something:[]) -> withday sd something- ("every":something:[]) -> noday something- ("days":"divisible":"by":sn:[]) -> - Divisible <$> getdivisor sn <*> pure Daily- ("on":"day":sd:"of":something:"divisible":"by":sn:[]) -> - Divisible- <$> getdivisor sn- <*> withday sd something- ("every":something:"divisible":"by":sn:[]) -> - Divisible- <$> getdivisor sn- <*> noday something- (something:"divisible":"by":sn:[]) -> - Divisible- <$> getdivisor sn- <*> noday something- _ -> Nothing- where- constructor "week" = Just Weekly- constructor "month" = Just Monthly- constructor "year" = Just Yearly- constructor u- | "s" `isSuffixOf` u = constructor $ reverse $ drop 1 $ reverse u- | otherwise = Nothing- withday sd u = do- c <- constructor u- d <- readish sd- Just $ c (Just d)- noday u = do- c <- constructor u- Just $ c Nothing- getdivisor sn = do- n <- readish sn- if n > 0- then Just n- else Nothing--fromScheduledTime :: ScheduledTime -> String-fromScheduledTime AnyTime = "any time"-fromScheduledTime (SpecificTime h m) = - show h' ++ (if m > 0 then ":" ++ pad 2 (show m) else "") ++ " " ++ ampm- where- pad n s = take (n - length s) (repeat '0') ++ s- (h', ampm)- | h == 0 = (12, "AM")- | h < 12 = (h, "AM")- | h == 12 = (h, "PM")- | otherwise = (h - 12, "PM")--toScheduledTime :: String -> Maybe ScheduledTime-toScheduledTime "any time" = Just AnyTime-toScheduledTime v = case words v of- (s:ampm:[])- | map toUpper ampm == "AM" ->- go s h0- | map toUpper ampm == "PM" ->- go s (\h -> (h0 h) + 12)- | otherwise -> Nothing- (s:[]) -> go s id- _ -> Nothing- where- h0 h- | h == 12 = 0- | otherwise = h- go :: String -> (Int -> Int) -> Maybe ScheduledTime- go s adjust =- let (h, m) = separate (== ':') s- in SpecificTime- <$> (adjust <$> readish h)- <*> if null m then Just 0 else readish m--fromSchedule :: Schedule -> String-fromSchedule (Schedule recurrance scheduledtime) = unwords- [ fromRecurrance recurrance- , "at"- , fromScheduledTime scheduledtime- ]--toSchedule :: String -> Maybe Schedule-toSchedule = eitherToMaybe . parseSchedule--parseSchedule :: String -> Either String Schedule-parseSchedule s = do- r <- maybe (Left $ "bad recurrance: " ++ recurrance) Right- (toRecurrance recurrance)- t <- maybe (Left $ "bad time of day: " ++ scheduledtime) Right- (toScheduledTime scheduledtime)- Right $ Schedule r t- where- (rws, tws) = separate (== "at") (words s)- recurrance = unwords rws- scheduledtime = unwords tws--instance Arbitrary Schedule where- arbitrary = Schedule <$> arbitrary <*> arbitrary--instance Arbitrary ScheduledTime where- arbitrary = oneof- [ pure AnyTime- , SpecificTime - <$> choose (0, 23)- <*> choose (1, 59)- ]--instance Arbitrary Recurrance where- arbitrary = oneof- [ pure Daily- , Weekly <$> arbday- , Monthly <$> arbday- , Yearly <$> arbday- , Divisible- <$> positive arbitrary- <*> oneof -- no nested Divisibles- [ pure Daily- , Weekly <$> arbday- , Monthly <$> arbday- , Yearly <$> arbday- ]- ]- where- arbday = oneof- [ Just <$> nonNegative arbitrary- , pure Nothing- ]--prop_schedule_roundtrips :: Schedule -> Bool-prop_schedule_roundtrips s = toSchedule (fromSchedule s) == Just s--prop_past_sane :: Bool-prop_past_sane = and- [ all (checksout oneMonthPast) (mplus1 ++ yplus1)- , all (not . (checksout oneMonthPast)) (map swap (mplus1 ++ yplus1))- , all (checksout oneYearPast) yplus1- , all (not . (checksout oneYearPast)) (map swap yplus1)- ]- where- mplus1 = -- new date old date, 1+ months before it- [ (fromGregorian 2014 01 15, fromGregorian 2013 12 15)- , (fromGregorian 2014 01 15, fromGregorian 2013 02 15)- , (fromGregorian 2014 02 15, fromGregorian 2013 01 15)- , (fromGregorian 2014 03 01, fromGregorian 2013 01 15)- , (fromGregorian 2014 03 01, fromGregorian 2013 12 15)- , (fromGregorian 2015 01 01, fromGregorian 2010 01 01)- ]- yplus1 = -- new date old date, 1+ years before it- [ (fromGregorian 2014 01 15, fromGregorian 2012 01 16)- , (fromGregorian 2014 01 15, fromGregorian 2013 01 14)- , (fromGregorian 2022 12 31, fromGregorian 2000 01 01)- ]- checksout cmp (new, old) = new `cmp` old- swap (a,b) = (b,a)
− Utility/ThreadScheduler.hs
@@ -1,73 +0,0 @@-{- thread scheduling- -- - Copyright 2012, 2013 Joey Hess <joey@kitenet.net>- - Copyright 2011 Bas van Dijk & Roel van Dijk- -- - Licensed under the GNU GPL version 3 or higher.- -}--{-# LANGUAGE CPP #-}--module Utility.ThreadScheduler where--import Control.Monad-import Control.Monad.IfElse-import System.Posix.IO-import Control.Concurrent-#ifndef mingw32_HOST_OS-import System.Posix.Signals-#ifndef __ANDROID__-import System.Posix.Terminal-#endif-#endif--newtype Seconds = Seconds { fromSeconds :: Int }- deriving (Eq, Ord, Show)--type Microseconds = Integer--{- Runs an action repeatedly forever, sleeping at least the specified number- - of seconds in between. -}-runEvery :: Seconds -> IO a -> IO a-runEvery n a = forever $ do- threadDelaySeconds n- a--threadDelaySeconds :: Seconds -> IO ()-threadDelaySeconds (Seconds n) = unboundDelay (fromIntegral n * oneSecond)--{- Like threadDelay, but not bounded by an Int.- -- - There is no guarantee that the thread will be rescheduled promptly when the- - delay has expired, but the thread will never continue to run earlier than- - specified.- - - - Taken from the unbounded-delay package to avoid a dependency for 4 lines- - of code.- -}-unboundDelay :: Microseconds -> IO ()-unboundDelay time = do- let maxWait = min time $ toInteger (maxBound :: Int)- threadDelay $ fromInteger maxWait- when (maxWait /= time) $ unboundDelay (time - maxWait)--{- Pauses the main thread, letting children run until program termination. -}-waitForTermination :: IO ()-waitForTermination = do-#ifdef mingw32_HOST_OS- runEvery (Seconds 600) $- void getLine-#else- lock <- newEmptyMVar- let check sig = void $- installHandler sig (CatchOnce $ putMVar lock ()) Nothing- check softwareTermination-#ifndef __ANDROID__- whenM (queryTerminal stdInput) $- check keyboardSignal-#endif- takeMVar lock-#endif--oneSecond :: Microseconds-oneSecond = 1000000
− Utility/Tmp.hs
@@ -1,100 +0,0 @@-{- Temporary files and directories.- -- - Copyright 2010-2013 Joey Hess <joey@kitenet.net>- -- - Licensed under the GNU GPL version 3 or higher.- -}--{-# LANGUAGE CPP #-}--module Utility.Tmp where--import Control.Exception (bracket)-import System.IO-import System.Directory-import Control.Monad.IfElse-import System.FilePath--import Utility.Exception-import Utility.FileSystemEncoding-import Utility.PosixFiles--type Template = String--{- Runs an action like writeFile, writing to a temp file first and- - then moving it into place. The temp file is stored in the same- - directory as the final file to avoid cross-device renames. -}-viaTmp :: (FilePath -> String -> IO ()) -> FilePath -> String -> IO ()-viaTmp a file content = do- let (dir, base) = splitFileName file- createDirectoryIfMissing True dir- (tmpfile, handle) <- openTempFile dir (base ++ ".tmp")- hClose handle- a tmpfile content- rename tmpfile file--{- Runs an action with a tmp file located in the system's tmp directory- - (or in "." if there is none) then removes the file. -}-withTmpFile :: Template -> (FilePath -> Handle -> IO a) -> IO a-withTmpFile template a = do- tmpdir <- catchDefaultIO "." getTemporaryDirectory- withTmpFileIn tmpdir template a--{- Runs an action with a tmp file located in the specified directory,- - then removes the file. -}-withTmpFileIn :: FilePath -> Template -> (FilePath -> Handle -> IO a) -> IO a-withTmpFileIn tmpdir template a = bracket create remove use- where- create = openTempFile tmpdir template- remove (name, handle) = do- hClose handle- catchBoolIO (removeFile name >> return True)- use (name, handle) = a name handle--{- Runs an action with a tmp directory located within the system's tmp- - directory (or within "." if there is none), then removes the tmp- - directory and all its contents. -}-withTmpDir :: Template -> (FilePath -> IO a) -> IO a-withTmpDir template a = do- tmpdir <- catchDefaultIO "." getTemporaryDirectory- withTmpDirIn tmpdir template a--{- Runs an action with a tmp directory located within a specified directory,- - then removes the tmp directory and all its contents. -}-withTmpDirIn :: FilePath -> Template -> (FilePath -> IO a) -> IO a-withTmpDirIn tmpdir template = bracket create remove- where- remove d = whenM (doesDirectoryExist d) $ do-#if mingw32_HOST_OS- -- Windows will often refuse to delete a file- -- after a process has just written to it and exited.- -- Because it's crap, presumably. So, ignore failure- -- to delete the temp directory.- _ <- tryIO $ removeDirectoryRecursive d- return ()-#else- removeDirectoryRecursive d-#endif- create = do- createDirectoryIfMissing True tmpdir- makenewdir (tmpdir </> template) (0 :: Int)- makenewdir t n = do- let dir = t ++ "." ++ show n- either (const $ makenewdir t $ n + 1) (const $ return dir)- =<< tryIO (createDirectory dir)--{- It's not safe to use a FilePath of an existing file as the template- - for openTempFile, because if the FilePath is really long, the tmpfile- - will be longer, and may exceed the maximum filename length.- -- - This generates a template that is never too long.- - (Well, it allocates 20 characters for use in making a unique temp file,- - anyway, which is enough for the current implementation and any- - likely implementation.)- -}-relatedTemplate :: FilePath -> FilePath-relatedTemplate f- | len > 20 = truncateFilePath (len - 20) f- | otherwise = f- where- len = length f
− Utility/UserInfo.hs
@@ -1,55 +0,0 @@-{- user info- -- - Copyright 2012 Joey Hess <joey@kitenet.net>- -- - Licensed under the GNU GPL version 3 or higher.- -}--{-# LANGUAGE CPP #-}--module Utility.UserInfo (- myHomeDir,- myUserName,- myUserGecos,-) where--import Control.Applicative-import System.PosixCompat--import Utility.Env--{- Current user's home directory.- -- - getpwent will fail on LDAP or NIS, so use HOME if set. -}-myHomeDir :: IO FilePath-myHomeDir = myVal env homeDirectory- where-#ifndef mingw32_HOST_OS- env = ["HOME"]-#else- env = ["USERPROFILE", "HOME"] -- HOME is used in Cygwin-#endif--{- Current user's user name. -}-myUserName :: IO String-myUserName = myVal env userName- where-#ifndef mingw32_HOST_OS- env = ["USER", "LOGNAME"]-#else- env = ["USERNAME", "USER", "LOGNAME"]-#endif--myUserGecos :: IO String-#ifdef __ANDROID__-myUserGecos = return "" -- userGecos crashes on Android-#else-myUserGecos = myVal [] userGecos-#endif--myVal :: [String] -> (UserEntry -> String) -> IO String-myVal envvars extract = maybe (extract <$> getpwent) return =<< check envvars- where- check [] = return Nothing- check (v:vs) = maybe (check vs) (return . Just) =<< getEnv v- getpwent = getUserEntryForID =<< getEffectiveUserID
config-joey.hs view
@@ -1,4 +1,5 @@ -- This is the live config file used by propellor's author.+module Main where import Propellor import Propellor.CmdLine@@ -19,32 +20,33 @@ import qualified Propellor.Property.Git as Git import qualified Propellor.Property.Apache as Apache import qualified Propellor.Property.Postfix as Postfix+import qualified Propellor.Property.Service as Service+import qualified Propellor.Property.HostingProvider.DigitalOcean as DigitalOcean+import qualified Propellor.Property.HostingProvider.CloudAtCost as CloudAtCost import qualified Propellor.Property.SiteSpecific.GitHome as GitHome import qualified Propellor.Property.SiteSpecific.GitAnnexBuilder as GitAnnexBuilder import qualified Propellor.Property.SiteSpecific.JoeySites as JoeySites -main :: IO ()-main = defaultMain hosts -- -- _ ______`| ,-.__ - {- Propellor -- / \___-=O`/|O`/__| (____.'- Deployed -} -- \ / | / ) _.-"-._- -- `/-==__ _/__|/__=-| ( \_-hosts :: [Host] -- * \ | | '--------'-hosts = -- (o) `+main :: IO () -- _ ______`| ,-.__ +main = defaultMain hosts -- / \___-=O`/|O`/__| (____.'+ {- Propellor -- \ / | / ) _.-"-._+ Deployed -} -- `/-==__ _/__|/__=-| ( \_+hosts :: [Host] -- * \ | | '--------'+hosts = -- (o) ` -- My laptop [ host "darkstar.kitenet.net" & ipv6 "2001:4830:1600:187::2" -- sixxs tunnel & Docker.configured & Apt.buildDep ["git-annex"] `period` Daily+ & Docker.docked hosts "android-git-annex" -- Nothing super-important lives here. , standardSystem "clam.kitenet.net" Unstable "amd64" & ipv4 "162.248.143.249" & ipv6 "2002:5044:5531::1" - & cleanCloudAtCost+ & CloudAtCost.decruft & Apt.unattendedUpgrades & Network.ipv6to4 & Tor.isBridge@@ -56,7 +58,7 @@ & alias "openid.kitenet.net" & Docker.docked hosts "openid-provider"- `requires` Apt.installed ["ntp"]+ `requires` Apt.serviceInstalledRunning "ntp" & alias "ancient.kitenet.net" & Docker.docked hosts "ancient-kitenet"@@ -70,7 +72,15 @@ & alias "ns9.kitenet.net" & myDnsSecondary+ + & alias "znc.kitenet.net"+ & JoeySites.ircBouncer + -- Nothing is using https on clam, so listen on that port+ -- for ssh, for traveling on bad networks.+ & "/etc/ssh/sshd_config" `File.containsLine` "Port 443"+ `onChange` Service.restarted "ssh"+ & Docker.garbageCollected `period` Daily & Apt.installed ["git-annex", "mtr", "screen"] @@ -84,8 +94,9 @@ & Docker.configured & Docker.docked hosts "amd64-git-annex-builder" & Docker.docked hosts "i386-git-annex-builder"- ! Docker.docked hosts "armel-git-annex-builder-companion"- ! Docker.docked hosts "armel-git-annex-builder"+ & Docker.docked hosts "armel-git-annex-builder-companion"+ & Docker.docked hosts "armel-git-annex-builder"+ & Docker.docked hosts "android-git-annex-builder" & Docker.garbageCollected `period` Daily & Apt.buildDep ["git-annex"] `period` Daily @@ -93,6 +104,7 @@ , standardSystem "diatom.kitenet.net" Stable "amd64" & ipv4 "107.170.31.195" + & DigitalOcean.distroKernel & Hostname.sane & Ssh.hostKey SshDsa & Ssh.hostKey SshRsa@@ -179,29 +191,18 @@ (Just "remotes/origin/old-kitenet.net") -- git-annex autobuilder containers- , gitAnnexBuilder "amd64" 15- , gitAnnexBuilder "i386" 45- -- armel builder has a companion container that run amd64 and- -- runs the build first to get TH splices. They share a home- -- directory, and need to have the same versions of all haskell- -- libraries installed.- , Docker.container "armel-git-annex-builder-companion"- (image $ System (Debian Unstable) "amd64")- & Docker.volume GitAnnexBuilder.homedir- & Apt.unattendedUpgrades- , Docker.container "armel-git-annex-builder"- (image $ System (Debian Unstable) "armel")- & Docker.link "armel-git-annex-builder-companion" "companion"- & Docker.volumes_from "armel-git-annex-builder-companion"--- & GitAnnexBuilder.builder "armel" "15 * * * *" True- & Apt.unattendedUpgrades- ] ++ monsters+ , GitAnnexBuilder.standardAutoBuilderContainer dockerImage "amd64" 15 "2h"+ , GitAnnexBuilder.standardAutoBuilderContainer dockerImage "i386" 45 "2h"+ , GitAnnexBuilder.armelCompanionContainer dockerImage+ , GitAnnexBuilder.armelAutoBuilderContainer dockerImage "1 3 * * *" "5h"+ , GitAnnexBuilder.androidAutoBuilderContainer dockerImage "1 1 * * *" "3h" -gitAnnexBuilder :: Architecture -> Int -> Host-gitAnnexBuilder arch buildminute = Docker.container (arch ++ "-git-annex-builder")- (image $ System (Debian Unstable) arch)- & GitAnnexBuilder.builder arch (show buildminute ++ " * * * *") True- & Apt.unattendedUpgrades+ -- for development of git-annex for android, using my git-annex+ -- work tree+ , let gitannexdir = GitAnnexBuilder.homedir </> "git-annex"+ in GitAnnexBuilder.androidContainer dockerImage "android-git-annex" doNothing gitannexdir+ & Docker.volume ("/home/joey/src/git-annex:" ++ gitannexdir)+ ] ++ monsters -- This is my standard system setup. standardSystem :: HostName -> DebianSuite -> Architecture -> Host@@ -229,7 +230,7 @@ -- This is my standard container setup, featuring automatic upgrades. standardContainer :: Docker.ContainerName -> DebianSuite -> Architecture -> Host-standardContainer name suite arch = Docker.container name (image system)+standardContainer name suite arch = Docker.container name (dockerImage system) & os (System (Debian suite) arch) & Apt.stdSourcesList suite & Apt.unattendedUpgrades@@ -237,26 +238,10 @@ system = System (Debian suite) arch -- Docker images I prefer to use.-image :: System -> Docker.Image-image (System (Debian Unstable) arch) = "joeyh/debian-unstable-" ++ arch-image (System (Debian Stable) arch) = "joeyh/debian-stable-" ++ arch-image _ = "debian-stable-official" -- does not currently exist!---- Clean up a system as installed by cloudatcost.com-cleanCloudAtCost :: Property-cleanCloudAtCost = propertyList "cloudatcost cleanup"- [ Hostname.sane- , Ssh.randomHostKeys- , "worked around grub/lvm boot bug #743126" ==>- "/etc/default/grub" `File.containsLine` "GRUB_DISABLE_LINUX_UUID=true"- `onChange` cmdProperty "update-grub" []- `onChange` cmdProperty "update-initramfs" ["-u"]- , combineProperties "nuked cloudatcost cruft"- [ File.notPresent "/etc/rc.local"- , File.notPresent "/etc/init.d/S97-setup.sh"- , User.nuked "user" User.YesReallyDeleteHome- ]- ]+dockerImage :: System -> Docker.Image+dockerImage (System (Debian Unstable) arch) = "joeyh/debian-unstable-" ++ arch+dockerImage (System (Debian Stable) arch) = "joeyh/debian-stable-" ++ arch+dockerImage _ = "debian-stable-official" -- does not currently exist! myDnsSecondary :: Property myDnsSecondary = propertyList "dns secondary for all my domains" $ map toProp@@ -321,8 +306,8 @@ & alias "wortroot.kitenet.net" & alias "www.wortroot.kitenet.net" & alias "joey.kitenet.net"- & alias "annex.kitenet.net" & alias "ipv6.kitenet.net"+ & alias "bitlbee.kitenet.net" , host "mouse.kitenet.net" & ipv6 "2001:4830:1600:492::2" , host "beaver.kitenet.net"
− config.hs
@@ -1,46 +0,0 @@--- This is the main configuration file for Propellor, and is used to build--- the propellor program.--import Propellor-import Propellor.CmdLine-import Propellor.Property.Scheduled-import qualified Propellor.Property.File as File-import qualified Propellor.Property.Apt as Apt-import qualified Propellor.Property.Network as Network---import qualified Propellor.Property.Ssh as Ssh-import qualified Propellor.Property.Cron as Cron---import qualified Propellor.Property.Sudo as Sudo-import qualified Propellor.Property.User as User---import qualified Propellor.Property.Hostname as Hostname---import qualified Propellor.Property.Reboot as Reboot---import qualified Propellor.Property.Tor as Tor-import qualified Propellor.Property.Docker as Docker--main :: IO ()-main = defaultMain hosts---- The hosts propellor knows about.--- Edit this to configure propellor!-hosts :: [Host]-hosts =- [ host "mybox.example.com"- & Apt.stdSourcesList Unstable- & Apt.unattendedUpgrades- & Apt.installed ["etckeeper"]- & Apt.installed ["ssh"]- & User.hasSomePassword "root"- & Network.ipv6to4- & File.dirExists "/var/www"- & Docker.docked hosts "webserver"- & Docker.garbageCollected `period` Daily- & Cron.runPropellor "30 * * * *"-- -- A generic webserver in a Docker container.- , Docker.container "webserver" "joeyh/debian-unstable"- & Docker.publish "80:80"- & Docker.volume "/var/www:/var/www"- & Apt.serviceInstalledRunning "apache2"-- -- add more hosts here...- --, host "foo.example.com" = ...- ]
debian/changelog view
@@ -1,3 +1,24 @@+propellor (0.5.3) unstable; urgency=medium++ * Fix unattended-upgrades config for !stable.+ * Ensure that kernel hostname is same as /etc/hostname when configuring+ hostname.+ * Added modules for some hosting providers (DigitalOcean, CloudAtCost).++ -- Joey Hess <joeyh@debian.org> Thu, 29 May 2014 14:29:53 -0400++propellor (0.5.2) unstable; urgency=medium++ * A bug that caused propellor to hang when updating a running docker+ container appears to have been fixed. Note that since it affects+ the propellor process that serves as "init" of docker containers,+ they have to be restarted for the fix to take effect.+ * Licence changed from GPL to BSD.+ * A few changes to allow building Propellor on OSX. One user reports+ successfully using it there.++ -- Joey Hess <joeyh@debian.org> Sat, 17 May 2014 16:42:55 -0400+ propellor (0.5.1) unstable; urgency=medium * Primary DNS servers now have allow-transfer automatically populated
debian/copyright view
@@ -3,9 +3,26 @@ Files: * Copyright: © 2010-2014 Joey Hess <joey@kitenet.net>-License: GPL-3++License: BSD-2-clause -License: GPL-3+- The full text of version 3 of the GPL is distributed as GPL in- this package's source, or in /usr/share/common-licenses/GPL-3 on- Debian systems.+License: BSD-2-clause+ Redistribution and use in source and binary forms, with or without+ modification, are permitted provided that the following conditions+ are met:+ 1. Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.+ 2. Redistributions in binary form must reproduce the above copyright+ notice, this list of conditions and the following disclaimer in the+ documentation and/or other materials provided with the distribution.+ .+ THIS SOFTWARE IS PROVIDED BY AUTHORS AND CONTRIBUTORS ``AS IS'' AND+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE+ ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF+ SUCH DAMAGE.
debian/lintian-overrides view
@@ -1,3 +1,2 @@ # These files are used in a git repository that propellor sets up. propellor: package-contains-vcs-control-file usr/src/propellor/.gitignore-propellor: extra-license-file usr/src/propellor/GPL
doc/README.mdwn view
@@ -6,7 +6,8 @@ Propellor is configured via a git repository, which typically lives in `~/.propellor/` on your development machine. Propellor clones the repository to each host it manages, in a -[secure](http://propellor.branchable.com/security/) way.+[secure](http://propellor.branchable.com/security/) way. The git repository+contains the full source code to Propellor, along with its config file. Properties are defined using Haskell. Edit `~/.propellor/config.hs` to get started. There is fairly complete @@ -40,11 +41,12 @@ `apt-get install propellor` 2. Run propellor for the first time. It will set up a `~/.propellor/` git repository for you.-3. `cd ~/.propellor/`; use git to push the repository to a central+3. If you don't have a gpg private key already, generate one: `gpg --gen-key`+4. Run: `propellor --add-key $KEYID`, which will make propellor trust+ your gpg key, and will sign your `~/.propellor` repository using it.+5. `cd ~/.propellor/`; use git to push the repository to a central server (github, or your own git server). Configure that central server as the origin remote of the repository.-4. If you don't have a gpg private key, generate one: `gpg --gen-key`-5. Run: `propellor --add-key $KEYID` 6. Edit `~/.propellor/config.hs`, and add a host you want to manage. You can start by not adding any properties, or only a few. 7. Pick a host and run: `propellor --spin $HOST`
propellor.cabal view
@@ -1,12 +1,12 @@ Name: propellor-Version: 0.5.1+Version: 0.5.3 Cabal-Version: >= 1.6-License: GPL+License: BSD3 Maintainer: Joey Hess <joey@kitenet.net> Author: Joey Hess Stability: Stable Copyright: 2014 Joey Hess-License-File: GPL+License-File: LICENSE Build-Type: Simple Homepage: https://propellor.branchable.com/ Category: Utility@@ -33,9 +33,10 @@ . It is configured using haskell. -Executable propellor- Main-Is: propellor.hs+Executable wrapper+ Main-Is: wrapper.hs GHC-Options: -Wall -threaded+ Hs-Source-Dirs: src Build-Depends: MissingH, directory, filepath, base >= 4.5, base < 5, IfElse, process, bytestring, hslogger, unix-compat, ansi-terminal, containers, network, async, time, QuickCheck, mtl,@@ -47,6 +48,7 @@ Executable config Main-Is: config.hs GHC-Options: -Wall -threaded+ Hs-Source-Dirs: src Build-Depends: MissingH, directory, filepath, base >= 4.5, base < 5, IfElse, process, bytestring, hslogger, unix-compat, ansi-terminal, containers, network, async, time, QuickCheck, mtl,@@ -57,6 +59,7 @@ Library GHC-Options: -Wall+ Hs-Source-Dirs: src Build-Depends: MissingH, directory, filepath, base >= 4.5, base < 5, IfElse, process, bytestring, hslogger, unix-compat, ansi-terminal, containers, network, async, time, QuickCheck, mtl,@@ -89,6 +92,8 @@ Propellor.Property.Sudo Propellor.Property.Tor Propellor.Property.User+ Propellor.Property.HostingProvider.DigitalOcean+ Propellor.Property.HostingProvider.CloudAtCost Propellor.Property.SiteSpecific.GitHome Propellor.Property.SiteSpecific.JoeySites Propellor.Property.SiteSpecific.GitAnnexBuilder
− propellor.hs
@@ -1,91 +0,0 @@--- | Wrapper program for propellor distribution.------ Distributions should install this program into PATH.--- (Cabal builds it as dict/build/propellor.------ This is not the propellor main program (that's config.hs)------ This installs propellor's source into ~/.propellor,--- uses it to build the real propellor program (if not already built),--- and runs it.--- --- The source is either copied from /usr/src/propellor, or is cloned from--- git over the network.--import Utility.UserInfo-import Utility.Monad-import Utility.Process-import Utility.SafeCommand-import Utility.Directory--import Control.Monad-import Control.Monad.IfElse-import System.Directory-import System.FilePath-import System.Environment (getArgs)-import System.Exit-import System.Posix.Directory--srcdir :: FilePath-srcdir = "/usr/src/propellor"---- Using the github mirror of the main propellor repo because--- it is accessible over https for better security.-srcrepo :: String-srcrepo = "https://github.com/joeyh/propellor.git"--main :: IO ()-main = do- args <- getArgs- home <- myHomeDir- let propellordir = home </> ".propellor"- let propellorbin = propellordir </> "propellor"- wrapper args propellordir propellorbin--wrapper :: [String] -> FilePath -> FilePath -> IO ()-wrapper args propellordir propellorbin = do- unlessM (doesDirectoryExist propellordir) $- makeRepo- buildruncfg- where- chain = do- (_, _, _, pid) <- createProcess (proc propellorbin args) - exitWith =<< waitForProcess pid- makeRepo = do- putStrLn $ "Setting up your propellor repo in " ++ propellordir- putStrLn ""- ifM (doesDirectoryExist srcdir)- ( do- void $ boolSystem "cp" [Param "-a", File srcdir, File propellordir]- changeWorkingDirectory propellordir- void $ boolSystem "git" [Param "init"]- void $ boolSystem "git" [Param "add", Param "."]- setuprepo True- , do- void $ boolSystem "git" [Param "clone", Param srcrepo, File propellordir] - void $ boolSystem "git" [Param "remote", Param "rm", Param "origin"]- setuprepo False- )- setuprepo fromsrcdir = do- changeWorkingDirectory propellordir- whenM (doesDirectoryExist "privdata") $- mapM_ nukeFile =<< dirContents "privdata"- void $ boolSystem "git" [Param "commit", Param "--allow-empty", Param "--quiet", Param "-m", Param "setting up propellor git repository"]- void $ boolSystem "git" [Param "remote", Param "add", Param "upstream", Param srcrepo]- -- Connect synthetic git repo with upstream history so- -- merging with upstream will work going forward.- -- Note -s ours is used to avoid getting any divergent- -- changes from upstream.- when fromsrcdir $ do- void $ boolSystem "git" [Param "fetch", Param "upstream"]- version <- readProcess "dpkg-query" ["--showformat", "${Version}", "--show", "propellor"]- void $ boolSystem "git" [Param "merge", Param "-s", Param "ours", Param version]- buildruncfg = do- changeWorkingDirectory propellordir- ifM (boolSystem "make" [Param "build"])- ( do- putStrLn ""- putStrLn ""- chain- , error "Propellor build failed."- )
+ src/Propellor.hs view
@@ -0,0 +1,77 @@+{-# LANGUAGE PackageImports #-}++-- | Pulls in lots of useful modules for building and using Properties.+-- +-- When propellor runs on a Host, it ensures that its list of Properties+-- is satisfied, taking action as necessary when a Property is not+-- currently satisfied.+--+-- A simple propellor program example:+--+-- > import Propellor+-- > import Propellor.CmdLine+-- > import qualified Propellor.Property.File as File+-- > import qualified Propellor.Property.Apt as Apt+-- > +-- > main :: IO ()+-- > main = defaultMain hosts+-- > +-- > hosts :: [Host]+-- > hosts =+-- > [ host "example.com"+-- > & Apt.installed ["mydaemon"]+-- > & "/etc/mydaemon.conf" `File.containsLine` "secure=1"+-- > `onChange` cmdProperty "service" ["mydaemon", "restart"]+-- > ! Apt.installed ["unwantedpackage"]+-- > ]+--+-- See config.hs for a more complete example, and clone Propellor's+-- git repository for a deployable system using Propellor:+-- git clone <git://git.kitenet.net/propellor>++module Propellor (+ module Propellor.Types+ , module Propellor.Property+ , module Propellor.Property.Cmd+ , module Propellor.Attr+ , module Propellor.PrivData+ , module Propellor.Engine+ , module Propellor.Exception+ , module Propellor.Message+ , localdir++ , module X+) where++import Propellor.Types+import Propellor.Property+import Propellor.Engine+import Propellor.Property.Cmd+import Propellor.PrivData+import Propellor.Message+import Propellor.Exception+import Propellor.Attr++import Utility.PartialPrelude as X+import Utility.Process as X+import Utility.Exception as X+import Utility.Env as X+import Utility.Directory as X+import Utility.Tmp as X+import Utility.Monad as X+import Utility.Misc as X++import System.Directory as X+import System.IO as X+import System.FilePath as X+import Data.Maybe as X+import Data.Either as X+import Control.Applicative as X+import Control.Monad as X+import Data.Monoid as X+import Control.Monad.IfElse as X+import "mtl" Control.Monad.Reader as X++-- | This is where propellor installs itself when deploying a host.+localdir :: FilePath+localdir = "/usr/local/propellor"
+ src/Propellor/Attr.hs view
@@ -0,0 +1,111 @@+{-# LANGUAGE PackageImports #-}++module Propellor.Attr where++import Propellor.Types+import Propellor.Types.Attr++import "mtl" Control.Monad.Reader+import qualified Data.Set as S+import qualified Data.Map as M+import Data.Maybe+import Control.Applicative++pureAttrProperty :: Desc -> SetAttr -> Property +pureAttrProperty desc = Property ("has " ++ desc) (return NoChange)++hostname :: HostName -> Property+hostname name = pureAttrProperty ("hostname " ++ name) $+ \d -> d { _hostname = name }++getHostName :: Propellor HostName+getHostName = asks _hostname++os :: System -> Property+os system = pureAttrProperty ("Operating " ++ show system) $+ \d -> d { _os = Just system }++getOS :: Propellor (Maybe System)+getOS = asks _os++-- | Indidate that a host has an A record in the DNS.+--+-- TODO check at run time if the host really has this address.+-- (Can't change the host's address, but as a sanity check.)+ipv4 :: String -> Property+ipv4 addr = pureAttrProperty ("ipv4 " ++ addr)+ (addDNS $ Address $ IPv4 addr)++-- | Indidate that a host has an AAAA record in the DNS.+ipv6 :: String -> Property+ipv6 addr = pureAttrProperty ("ipv6 " ++ addr)+ (addDNS $ Address $ IPv6 addr)++-- | Indicates another name for the host in the DNS.+alias :: Domain -> Property+alias domain = pureAttrProperty ("alias " ++ domain)+ (addDNS $ CNAME $ AbsDomain domain)++addDNS :: Record -> SetAttr+addDNS record d = d { _dns = S.insert record (_dns d) }++-- | Adds a DNS NamedConf stanza.+--+-- Note that adding a Master stanza for a domain always overrides an+-- existing Secondary stanza, while a Secondary stanza is only added+-- when there is no existing Master stanza.+addNamedConf :: NamedConf -> SetAttr+addNamedConf conf d = d { _namedconf = new }+ where+ m = _namedconf d+ domain = confDomain conf+ new = case (confDnsServerType conf, confDnsServerType <$> M.lookup domain m) of+ (Secondary, Just Master) -> m+ _ -> M.insert domain conf m++getNamedConf :: Propellor (M.Map Domain NamedConf)+getNamedConf = asks _namedconf++sshPubKey :: String -> Property+sshPubKey k = pureAttrProperty ("ssh pubkey known") $+ \d -> d { _sshPubKey = Just k }++getSshPubKey :: Propellor (Maybe String)+getSshPubKey = asks _sshPubKey++hostnameless :: Attr+hostnameless = newAttr (error "hostname Attr not specified")++hostAttr :: Host -> Attr+hostAttr (Host _ mkattrs) = mkattrs hostnameless++hostProperties :: Host -> [Property]+hostProperties (Host ps _) = ps++hostMap :: [Host] -> M.Map HostName Host+hostMap l = M.fromList $ zip (map (_hostname . hostAttr) l) l ++hostAttrMap :: [Host] -> M.Map HostName Attr+hostAttrMap l = M.fromList $ zip (map _hostname attrs) attrs+ where+ attrs = map hostAttr l++findHost :: [Host] -> HostName -> Maybe Host+findHost l hn = M.lookup hn (hostMap l)++getAddresses :: Attr -> [IPAddr]+getAddresses = mapMaybe getIPAddr . S.toList . _dns++hostAddresses :: HostName -> [Host] -> [IPAddr]+hostAddresses hn hosts = case hostAttr <$> findHost hosts hn of+ Nothing -> []+ Just attr -> mapMaybe getIPAddr $ S.toList $ _dns attr++-- | Lifts an action into a different host.+--+-- For example, `fromHost hosts "otherhost" getSshPubKey`+fromHost :: [Host] -> HostName -> Propellor a -> Propellor (Maybe a)+fromHost l hn getter = case findHost l hn of+ Nothing -> return Nothing+ Just h -> liftIO $ Just <$>+ runReaderT (runWithAttr getter) (hostAttr h)
+ src/Propellor/CmdLine.hs view
@@ -0,0 +1,392 @@+module Propellor.CmdLine where++import System.Environment (getArgs)+import Data.List+import System.Exit+import System.Log.Logger+import System.Log.Formatter+import System.Log.Handler (setFormatter, LogHandler)+import System.Log.Handler.Simple+import System.PosixCompat+import Control.Exception (bracket)+import System.Posix.IO+import Data.Time.Clock.POSIX++import Propellor+import qualified Propellor.Property.Docker as Docker+import qualified Propellor.Property.Docker.Shim as DockerShim+import Utility.FileMode+import Utility.SafeCommand+import Utility.UserInfo++usage :: IO a+usage = do+ putStrLn $ unlines + [ "Usage:"+ , " propellor"+ , " propellor hostname"+ , " propellor --spin hostname"+ , " propellor --set hostname field"+ , " propellor --add-key keyid"+ ]+ exitFailure++processCmdLine :: IO CmdLine+processCmdLine = go =<< getArgs+ where+ go ("--help":_) = usage+ go ("--spin":h:[]) = return $ Spin h+ go ("--boot":h:[]) = return $ Boot h+ go ("--add-key":k:[]) = return $ AddKey k+ go ("--set":h:f:[]) = case readish f of+ Just pf -> return $ Set h pf+ Nothing -> errorMessage $ "Unknown privdata field " ++ f+ go ("--continue":s:[]) = case readish s of+ Just cmdline -> return $ Continue cmdline+ Nothing -> errorMessage "--continue serialization failure"+ go ("--chain":h:[]) = return $ Chain h+ go ("--docker":h:[]) = return $ Docker h+ go (h:[])+ | "--" `isPrefixOf` h = usage+ | otherwise = return $ Run h+ go [] = do+ s <- takeWhile (/= '\n') <$> readProcess "hostname" ["-f"]+ if null s+ then errorMessage "Cannot determine hostname! Pass it on the command line."+ else return $ Run s+ go _ = usage++defaultMain :: [Host] -> IO ()+defaultMain hostlist = do+ DockerShim.cleanEnv+ checkDebugMode+ cmdline <- processCmdLine+ debug ["command line: ", show cmdline]+ go True cmdline+ where+ go _ (Continue cmdline) = go False cmdline+ go _ (Set hn field) = setPrivData hn field+ go _ (AddKey keyid) = addKey keyid+ go _ (Chain hn) = withprops hn $ \attr ps -> do+ r <- runPropellor attr $ ensureProperties ps+ putStrLn $ "\n" ++ show r+ go _ (Docker hn) = Docker.chain hn+ go True cmdline@(Spin _) = buildFirst cmdline $ go False cmdline+ go True cmdline = updateFirst cmdline $ go False cmdline+ go False (Spin hn) = withprops hn $ const . const $ spin hn+ go False (Run hn) = ifM ((==) 0 <$> getRealUserID)+ ( onlyProcess $ withprops hn mainProperties+ , go True (Spin hn)+ )+ go False (Boot hn) = onlyProcess $ withprops hn boot++ withprops :: HostName -> (Attr -> [Property] -> IO ()) -> IO ()+ withprops hn a = maybe+ (unknownhost hn)+ (\h -> a (hostAttr h) (hostProperties h))+ (findHost hostlist hn)++onlyProcess :: IO a -> IO a+onlyProcess a = bracket lock unlock (const a)+ where+ lock = do+ l <- createFile lockfile stdFileMode+ setLock l (WriteLock, AbsoluteSeek, 0, 0)+ `catchIO` const alreadyrunning+ return l+ unlock = closeFd+ alreadyrunning = error "Propellor is already running on this host!"+ lockfile = localdir </> ".lock"++unknownhost :: HostName -> IO a+unknownhost h = errorMessage $ unlines+ [ "Propellor does not know about host: " ++ h+ , "(Perhaps you should specify the real hostname on the command line?)"+ , "(Or, edit propellor's config.hs to configure this host)"+ ]++buildFirst :: CmdLine -> IO () -> IO ()+buildFirst cmdline next = do+ oldtime <- getmtime+ ifM (actionMessage "Propellor build" $ boolSystem "make" [Param "build"])+ ( do+ newtime <- getmtime+ if newtime == oldtime+ then next+ else void $ boolSystem "./propellor" [Param "--continue", Param (show cmdline)]+ , errorMessage "Propellor build failed!" + )+ where+ getmtime = catchMaybeIO $ getModificationTime "propellor"++getCurrentBranch :: IO String+getCurrentBranch = takeWhile (/= '\n') + <$> readProcess "git" ["symbolic-ref", "--short", "HEAD"]++updateFirst :: CmdLine -> IO () -> IO ()+updateFirst cmdline next = do+ branchref <- getCurrentBranch+ let originbranch = "origin" </> branchref++ void $ actionMessage "Git fetch" $ boolSystem "git" [Param "fetch"]+ + whenM (doesFileExist keyring) $ do+ {- To verify origin branch commit's signature, have to+ - convince gpg to use our keyring. While running git log.+ - Which has no way to pass options to gpg.+ - Argh! -}+ let gpgconf = privDataDir </> "gpg.conf"+ writeFile gpgconf $ unlines+ [ " keyring " ++ keyring+ , "no-auto-check-trustdb"+ ]+ -- gpg is picky about perms+ modifyFileMode privDataDir (removeModes otherGroupModes)+ s <- readProcessEnv "git" ["log", "-n", "1", "--format=%G?", originbranch]+ (Just [("GNUPGHOME", privDataDir)])+ nukeFile $ privDataDir </> "trustdb.gpg"+ nukeFile $ privDataDir </> "pubring.gpg"+ nukeFile $ privDataDir </> "gpg.conf"+ if s == "U\n" || s == "G\n"+ then do+ putStrLn $ "git branch " ++ originbranch ++ " gpg signature verified; merging"+ hFlush stdout+ else errorMessage $ "git branch " ++ originbranch ++ " is not signed with a trusted gpg key; refusing to deploy it!"+ + oldsha <- getCurrentGitSha1 branchref+ void $ boolSystem "git" [Param "merge", Param originbranch]+ newsha <- getCurrentGitSha1 branchref++ if oldsha == newsha+ then next+ else ifM (actionMessage "Propellor build" $ boolSystem "make" [Param "build"])+ ( void $ boolSystem "./propellor" [Param "--continue", Param (show cmdline)]+ , errorMessage "Propellor build failed!" + )++getCurrentGitSha1 :: String -> IO String+getCurrentGitSha1 branchref = readProcess "git" ["show-ref", "--hash", branchref]++spin :: HostName -> IO ()+spin hn = do+ url <- getUrl+ void $ gitCommit [Param "--allow-empty", Param "-a", Param "-m", Param "propellor spin"]+ void $ boolSystem "git" [Param "push"]+ cacheparams <- toCommand <$> sshCachingParams hn+ go cacheparams url =<< gpgDecrypt (privDataFile hn)+ where+ go cacheparams url privdata = withBothHandles createProcessSuccess (proc "ssh" $ cacheparams ++ [user, bootstrapcmd]) $ \(toh, fromh) -> do+ let finish = do+ senddata toh (privDataFile hn) privDataMarker privdata+ hClose toh+ + -- Display remaining output.+ void $ tryIO $ forever $+ showremote =<< hGetLine fromh+ hClose fromh+ status <- getstatus fromh `catchIO` (const $ errorMessage "protocol error (perhaps the remote propellor failed to run?)")+ case status of+ Ready -> finish+ NeedGitClone -> do+ hClose toh+ hClose fromh+ sendGitClone hn url+ go cacheparams url privdata+ + user = "root@"++hn++ bootstrapcmd = shellWrap $ intercalate " ; "+ [ "if [ ! -d " ++ localdir ++ " ]"+ , "then " ++ intercalate " && "+ [ "apt-get --no-install-recommends --no-upgrade -y install git make"+ , "echo " ++ toMarked statusMarker (show NeedGitClone)+ ]+ , "else " ++ intercalate " && "+ [ "cd " ++ localdir+ , "if ! test -x ./propellor; then make deps build; fi"+ , "./propellor --boot " ++ hn+ ]+ , "fi"+ ]++ getstatus :: Handle -> IO BootStrapStatus+ getstatus h = do+ l <- hGetLine h+ case readish =<< fromMarked statusMarker l of+ Nothing -> do+ showremote l+ getstatus h+ Just status -> return status+ + showremote s = putStrLn s+ senddata toh f marker s = void $+ actionMessage ("Sending " ++ f ++ " (" ++ show (length s) ++ " bytes) to " ++ hn) $ do+ sendMarked toh marker s+ return True++sendGitClone :: HostName -> String -> IO ()+sendGitClone hn url = void $ actionMessage ("Pushing git repository to " ++ hn) $ do+ branch <- getCurrentBranch+ cacheparams <- sshCachingParams hn+ withTmpFile "propellor.git" $ \tmp _ -> allM id+ [ boolSystem "git" [Param "bundle", Param "create", File tmp, Param "HEAD"]+ , boolSystem "scp" $ cacheparams ++ [File tmp, Param ("root@"++hn++":"++remotebundle)]+ , boolSystem "ssh" $ cacheparams ++ [Param ("root@"++hn), Param $ unpackcmd branch]+ ]+ where+ remotebundle = "/usr/local/propellor.git"+ unpackcmd branch = shellWrap $ intercalate " && "+ [ "git clone " ++ remotebundle ++ " " ++ localdir+ , "cd " ++ localdir+ , "git checkout -b " ++ branch+ , "git remote rm origin"+ , "rm -f " ++ remotebundle+ , "git remote add origin " ++ url+ -- same as --set-upstream-to, except origin branch+ -- has not been pulled yet+ , "git config branch."++branch++".remote origin"+ , "git config branch."++branch++".merge refs/heads/"++branch+ ]++data BootStrapStatus = Ready | NeedGitClone+ deriving (Read, Show, Eq)++type Marker = String+type Marked = String++statusMarker :: Marker+statusMarker = "STATUS"++privDataMarker :: String+privDataMarker = "PRIVDATA "++toMarked :: Marker -> String -> String+toMarked marker = intercalate "\n" . map (marker ++) . lines++sendMarked :: Handle -> Marker -> String -> IO ()+sendMarked h marker s = do+ -- Prefix string with newline because sometimes a+ -- incomplete line is output.+ hPutStrLn h ("\n" ++ toMarked marker s)+ hFlush h++fromMarked :: Marker -> Marked -> Maybe String+fromMarked marker s+ | null matches = Nothing+ | otherwise = Just $ intercalate "\n" $+ map (drop len) matches+ where+ len = length marker+ matches = filter (marker `isPrefixOf`) $ lines s++boot :: Attr -> [Property] -> IO ()+boot attr ps = do+ sendMarked stdout statusMarker $ show Ready+ reply <- hGetContentsStrict stdin++ makePrivDataDir+ maybe noop (writeFileProtected privDataLocal) $+ fromMarked privDataMarker reply+ mainProperties attr ps++addKey :: String -> IO ()+addKey keyid = exitBool =<< allM id [ gpg, gitadd, gitconfig, gitcommit ]+ where+ gpg = do+ createDirectoryIfMissing True privDataDir+ boolSystem "sh"+ [ Param "-c"+ , Param $ "gpg --export " ++ keyid ++ " | gpg " +++ unwords (gpgopts ++ ["--import"])+ ]+ gitadd = boolSystem "git"+ [ Param "add"+ , File keyring+ ]++ gitconfig = boolSystem "git"+ [ Param "config"+ , Param "user.signingkey"+ , Param keyid+ ]++ gitcommit = gitCommit+ [ File keyring+ , Param "-m"+ , Param "propellor addkey"+ ]++{- Automatically sign the commit if there'a a keyring. -}+gitCommit :: [CommandParam] -> IO Bool+gitCommit ps = do+ k <- doesFileExist keyring+ boolSystem "git" $ catMaybes $+ [ Just (Param "commit")+ , if k then Just (Param "--gpg-sign") else Nothing+ ] ++ map Just ps++keyring :: FilePath+keyring = privDataDir </> "keyring.gpg"++gpgopts :: [String]+gpgopts = ["--options", "/dev/null", "--no-default-keyring", "--keyring", keyring]++getUrl :: IO String+getUrl = maybe nourl return =<< getM get urls+ where+ urls = ["remote.deploy.url", "remote.origin.url"]+ nourl = errorMessage $ "Cannot find deploy url in " ++ show urls+ get u = do+ v <- catchMaybeIO $ + takeWhile (/= '\n') + <$> readProcess "git" ["config", u]+ return $ case v of+ Just url | not (null url) -> Just url+ _ -> Nothing++checkDebugMode :: IO ()+checkDebugMode = go =<< getEnv "PROPELLOR_DEBUG"+ where+ go (Just s)+ | s == "1" = do+ f <- setFormatter+ <$> streamHandler stderr DEBUG+ <*> pure (simpleLogFormatter "[$time] $msg")+ updateGlobalLogger rootLoggerName $ + setLevel DEBUG . setHandlers [f]+ go _ = noop++-- Parameters can be passed to both ssh and scp, to enable a ssh connection+-- caching socket.+--+-- If the socket already exists, check if its mtime is older than 10+-- minutes, and if so stop that ssh process, in order to not try to+-- use an old stale connection. (atime would be nicer, but there's+-- a good chance a laptop uses noatime)+sshCachingParams :: HostName -> IO [CommandParam]+sshCachingParams hn = do+ home <- myHomeDir+ let cachedir = home </> ".ssh" </> "propellor"+ createDirectoryIfMissing False cachedir+ let socketfile = cachedir </> hn ++ ".sock"+ let ps = + [ Param "-o", Param ("ControlPath=" ++ socketfile)+ , Params "-o ControlMaster=auto -o ControlPersist=yes"+ ]++ maybe noop (expireold ps socketfile)+ =<< catchMaybeIO (getFileStatus socketfile)+ + return ps+ + where+ expireold ps f s = do+ now <- truncate <$> getPOSIXTime :: IO Integer+ if modificationTime s > fromIntegral now - tenminutes+ then touchFile f+ else do+ void $ boolSystem "ssh" $+ [ Params "-O stop" ] ++ ps +++ [ Param "localhost" ]+ nukeFile f+ tenminutes = 600
+ src/Propellor/Engine.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE PackageImports #-}++module Propellor.Engine where++import System.Exit+import System.IO+import Data.Monoid+import System.Console.ANSI+import "mtl" Control.Monad.Reader++import Propellor.Types+import Propellor.Message+import Propellor.Exception++runPropellor :: Attr -> Propellor a -> IO a+runPropellor attr a = runReaderT (runWithAttr a) attr++mainProperties :: Attr -> [Property] -> IO ()+mainProperties attr ps = do+ r <- runPropellor attr $+ ensureProperties [Property "overall" (ensureProperties ps) id]+ setTitle "propellor: done"+ hFlush stdout+ case r of+ FailedChange -> exitWith (ExitFailure 1)+ _ -> exitWith ExitSuccess++ensureProperties :: [Property] -> Propellor Result+ensureProperties ps = ensure ps NoChange+ where+ ensure [] rs = return rs+ ensure (l:ls) rs = do+ r <- actionMessage (propertyDesc l) (ensureProperty l)+ ensure ls (r <> rs)++ensureProperty :: Property -> Propellor Result+ensureProperty = catchPropellor . propertySatisfy
+ src/Propellor/Exception.hs view
@@ -0,0 +1,18 @@+{-# LANGUAGE PackageImports #-}++module Propellor.Exception where++import qualified "MonadCatchIO-transformers" Control.Monad.CatchIO as M+import Control.Exception++import Propellor.Types+import Propellor.Message++-- | Catches IO exceptions and returns FailedChange.+catchPropellor :: Propellor Result -> Propellor Result+catchPropellor a = either err return =<< tryPropellor a+ where+ err e = warningMessage (show e) >> return FailedChange++tryPropellor :: Propellor a -> Propellor (Either IOException a)+tryPropellor = M.try
+ src/Propellor/Message.hs view
@@ -0,0 +1,51 @@+{-# LANGUAGE PackageImports #-}++module Propellor.Message where++import System.Console.ANSI+import System.IO+import System.Log.Logger+import "mtl" Control.Monad.Reader++import Propellor.Types++-- | Shows a message while performing an action, with a colored status+-- display.+actionMessage :: (MonadIO m, ActionResult r) => Desc -> m r -> m r+actionMessage desc a = do+ liftIO $ do+ setTitle $ "propellor: " ++ desc+ hFlush stdout++ r <- a++ liftIO $ do+ setTitle "propellor: running"+ let (msg, intensity, color) = getActionResult r+ putStr $ desc ++ " ... "+ colorLine intensity color msg+ hFlush stdout++ return r++warningMessage :: MonadIO m => String -> m ()+warningMessage s = liftIO $ colorLine Vivid Magenta $ "** warning: " ++ s++colorLine :: ColorIntensity -> Color -> String -> IO ()+colorLine intensity color msg = do+ setSGR [SetColor Foreground intensity color]+ putStr msg+ setSGR []+ -- Note this comes after the color is reset, so that+ -- the color set and reset happen in the same line.+ putStrLn ""+ hFlush stdout++errorMessage :: String -> IO a+errorMessage s = do+ liftIO $ colorLine Vivid Red $ "** error: " ++ s+ error "Cannot continue!"++-- | Causes a debug message to be displayed when PROPELLOR_DEBUG=1+debug :: [String] -> IO ()+debug = debugM "propellor" . unwords
+ src/Propellor/PrivData.hs view
@@ -0,0 +1,91 @@+{-# LANGUAGE PackageImports #-}++module Propellor.PrivData where++import qualified Data.Map as M+import Control.Applicative+import System.FilePath+import System.IO+import System.Directory+import Data.Maybe+import Data.List+import Control.Monad+import "mtl" Control.Monad.Reader++import Propellor.Types+import Propellor.Attr+import Propellor.Message+import Utility.Monad+import Utility.PartialPrelude+import Utility.Exception+import Utility.Process+import Utility.Tmp+import Utility.SafeCommand+import Utility.Misc++-- | When the specified PrivDataField is available on the host Propellor+-- is provisioning, it provies the data to the action. Otherwise, it prints+-- a message to help the user make the necessary private data available.+withPrivData :: PrivDataField -> (String -> Propellor Result) -> Propellor Result+withPrivData field a = maybe missing a =<< liftIO (getPrivData field)+ where+ missing = do+ host <- getHostName+ let host' = if ".docker" `isSuffixOf` host+ then "$parent_host"+ else host+ liftIO $ do+ warningMessage $ "Missing privdata " ++ show field+ putStrLn $ "Fix this by running: propellor --set "++host'++" '" ++ show field ++ "'"+ return FailedChange++getPrivData :: PrivDataField -> IO (Maybe String)+getPrivData field = do+ m <- catchDefaultIO Nothing $ readish <$> readFile privDataLocal+ return $ maybe Nothing (M.lookup field) m++setPrivData :: HostName -> PrivDataField -> IO ()+setPrivData host field = do+ putStrLn "Enter private data on stdin; ctrl-D when done:"+ value <- chomp <$> hGetContentsStrict stdin+ makePrivDataDir+ let f = privDataFile host+ m <- fromMaybe M.empty . readish <$> gpgDecrypt f+ let m' = M.insert field value m+ gpgEncrypt f (show m')+ putStrLn "Private data set."+ void $ boolSystem "git" [Param "add", File f]+ where+ chomp s+ | end s == "\n" = chomp (beginning s)+ | otherwise = s++makePrivDataDir :: IO ()+makePrivDataDir = createDirectoryIfMissing False privDataDir++privDataDir :: FilePath+privDataDir = "privdata"++privDataFile :: HostName -> FilePath+privDataFile host = privDataDir </> host ++ ".gpg"++privDataLocal :: FilePath+privDataLocal = privDataDir </> "local"++gpgDecrypt :: FilePath -> IO String+gpgDecrypt f = ifM (doesFileExist f)+ ( readProcess "gpg" ["--decrypt", f]+ , return ""+ )++gpgEncrypt :: FilePath -> String -> IO ()+gpgEncrypt f s = do+ encrypted <- writeReadProcessEnv "gpg"+ [ "--default-recipient-self"+ , "--armor"+ , "--encrypt"+ ]+ Nothing+ (Just $ flip hPutStr s)+ Nothing+ viaTmp writeFile f encrypted
+ src/Propellor/Property.hs view
@@ -0,0 +1,166 @@+{-# LANGUAGE PackageImports #-}++module Propellor.Property where++import System.Directory+import Control.Monad+import Data.Monoid+import Data.List+import Control.Monad.IfElse+import "mtl" Control.Monad.Reader++import Propellor.Types+import Propellor.Types.Attr+import Propellor.Attr+import Propellor.Engine+import Utility.Monad+import System.FilePath++-- Constructs a Property.+property :: Desc -> Propellor Result -> Property+property d s = Property d s id++-- | Combines a list of properties, resulting in a single property+-- that when run will run each property in the list in turn,+-- and print out the description of each as it's run. Does not stop+-- on failure; does propigate overall success/failure.+propertyList :: Desc -> [Property] -> Property+propertyList desc ps = Property desc (ensureProperties ps) (combineSetAttrs ps)++-- | Combines a list of properties, resulting in one property that+-- ensures each in turn, stopping on failure.+combineProperties :: Desc -> [Property] -> Property+combineProperties desc ps = Property desc (go ps NoChange) (combineSetAttrs ps)+ where+ go [] rs = return rs+ go (l:ls) rs = do+ r <- ensureProperty l+ case r of+ FailedChange -> return FailedChange+ _ -> go ls (r <> rs)++-- | Combines together two properties, resulting in one property+-- that ensures the first, and if the first succeeds, ensures the second.+-- The property uses the description of the first property.+before :: Property -> Property -> Property+p1 `before` p2 = p2 `requires` p1+ `describe` (propertyDesc p1)++-- | Makes a perhaps non-idempotent Property be idempotent by using a flag+-- file to indicate whether it has run before.+-- Use with caution.+flagFile :: Property -> FilePath -> Property+flagFile p = flagFile' p . return++flagFile' :: Property -> IO FilePath -> Property+flagFile' p getflagfile = adjustProperty p $ \satisfy -> do+ flagfile <- liftIO getflagfile+ go satisfy flagfile =<< liftIO (doesFileExist flagfile)+ where+ go _ _ True = return NoChange+ go satisfy flagfile False = do+ r <- satisfy+ when (r == MadeChange) $ liftIO $ + unlessM (doesFileExist flagfile) $ do+ createDirectoryIfMissing True (takeDirectory flagfile)+ writeFile flagfile ""+ return r++--- | Whenever a change has to be made for a Property, causes a hook+-- Property to also be run, but not otherwise.+onChange :: Property -> Property -> Property+p `onChange` hook = Property (propertyDesc p) satisfy (combineSetAttr p hook)+ where+ satisfy = do+ r <- ensureProperty p+ case r of+ MadeChange -> do+ r' <- ensureProperty hook+ return $ r <> r'+ _ -> return r++(==>) :: Desc -> Property -> Property+(==>) = flip describe+infixl 1 ==>++-- | Makes a Property only need to do anything when a test succeeds.+check :: IO Bool -> Property -> Property+check c p = adjustProperty p $ \satisfy -> ifM (liftIO c)+ ( satisfy+ , return NoChange+ )++-- | Marks a Property as trivial. It can only return FailedChange or+-- NoChange. +--+-- Useful when it's just as expensive to check if a change needs+-- to be made as it is to just idempotently assure the property is+-- satisfied. For example, chmodding a file.+trivial :: Property -> Property+trivial p = adjustProperty p $ \satisfy -> do+ r <- satisfy+ if r == MadeChange+ then return NoChange+ else return r++doNothing :: Property+doNothing = property "noop property" noChange++-- | Makes a property that is satisfied differently depending on the host's+-- operating system. +--+-- Note that the operating system may not be declared for some hosts.+withOS :: Desc -> (Maybe System -> Propellor Result) -> Property+withOS desc a = property desc $ a =<< getOS++boolProperty :: Desc -> IO Bool -> Property+boolProperty desc a = property desc $ ifM (liftIO a)+ ( return MadeChange+ , return FailedChange+ )++-- | Undoes the effect of a property.+revert :: RevertableProperty -> RevertableProperty+revert (RevertableProperty p1 p2) = RevertableProperty p2 p1++-- | Starts accumulating the properties of a Host.+--+-- > host "example.com"+-- > & someproperty+-- > ! oldproperty+-- > & otherproperty+host :: HostName -> Host+host hn = Host [] (\_ -> newAttr hn)++-- | Adds a property to a Host+--+-- Can add Properties and RevertableProperties+(&) :: IsProp p => Host -> p -> Host+(Host ps as) & p = Host (ps ++ [toProp p]) (setAttr p . as)++infixl 1 &++-- | Adds a property to the Host in reverted form.+(!) :: Host -> RevertableProperty -> Host+(Host ps as) ! p = Host (ps ++ [toProp q]) (setAttr q . as)+ where+ q = revert p++infixl 1 !++-- Changes the action that is performed to satisfy a property. +adjustProperty :: Property -> (Propellor Result -> Propellor Result) -> Property+adjustProperty p f = p { propertySatisfy = f (propertySatisfy p) }++-- Combines the Attr settings of two properties.+combineSetAttr :: (IsProp p, IsProp q) => p -> q -> SetAttr+combineSetAttr p q = setAttr p . setAttr q++combineSetAttrs :: IsProp p => [p] -> SetAttr+combineSetAttrs = foldl' (.) id . map setAttr++makeChange :: IO () -> Propellor Result+makeChange a = liftIO a >> return MadeChange++noChange :: Propellor Result+noChange = return NoChange
+ src/Propellor/Property/Apache.hs view
@@ -0,0 +1,62 @@+module Propellor.Property.Apache where++import Propellor+import qualified Propellor.Property.File as File+import qualified Propellor.Property.Apt as Apt+import qualified Propellor.Property.Service as Service++type ConfigFile = [String]++siteEnabled :: HostName -> ConfigFile -> RevertableProperty+siteEnabled hn cf = RevertableProperty enable disable+ where+ enable = trivial $ cmdProperty "a2ensite" ["--quiet", hn]+ `describe` ("apache site enabled " ++ hn)+ `requires` siteAvailable hn cf+ `requires` installed+ `onChange` reloaded+ disable = trivial $ File.notPresent (siteCfg hn)+ `describe` ("apache site disabled " ++ hn)+ `onChange` cmdProperty "a2dissite" ["--quiet", hn]+ `requires` installed+ `onChange` reloaded++siteAvailable :: HostName -> ConfigFile -> Property+siteAvailable hn cf = siteCfg hn `File.hasContent` (comment:cf)+ `describe` ("apache site available " ++ hn)+ where+ comment = "# deployed with propellor, do not modify"++modEnabled :: String -> RevertableProperty+modEnabled modname = RevertableProperty enable disable+ where+ enable = trivial $ cmdProperty "a2enmod" ["--quiet", modname]+ `describe` ("apache module enabled " ++ modname)+ `requires` installed+ `onChange` reloaded+ disable = trivial $ cmdProperty "a2dismod" ["--quiet", modname]+ `describe` ("apache module disabled " ++ modname)+ `requires` installed+ `onChange` reloaded++siteCfg :: HostName -> FilePath+siteCfg hn = "/etc/apache2/sites-available/" ++ hn++installed :: Property+installed = Apt.installed ["apache2"]++restarted :: Property+restarted = cmdProperty "service" ["apache2", "restart"]++reloaded :: Property+reloaded = Service.reloaded "apache2"++-- | Configure apache to use SNI to differentiate between+-- https hosts.+multiSSL :: Property+multiSSL = "/etc/apache2/conf.d/ssl" `File.hasContent`+ [ "NameVirtualHost *:443"+ , "SSLStrictSNIVHostCheck off"+ ]+ `describe` "apache SNI enabled"+ `onChange` reloaded
+ src/Propellor/Property/Apt.hs view
@@ -0,0 +1,256 @@+module Propellor.Property.Apt where++import Data.Maybe+import Control.Applicative+import Data.List+import System.IO+import Control.Monad++import Propellor+import qualified Propellor.Property.File as File+import qualified Propellor.Property.Service as Service+import Propellor.Property.File (Line)++sourcesList :: FilePath+sourcesList = "/etc/apt/sources.list"++type Url = String+type Section = String++type SourcesGenerator = DebianSuite -> [Line]++showSuite :: DebianSuite -> String+showSuite Stable = "stable"+showSuite Testing = "testing"+showSuite Unstable = "unstable"+showSuite Experimental = "experimental"+showSuite (DebianRelease r) = r++backportSuite :: String+backportSuite = showSuite stableRelease ++ "-backports"++debLine :: String -> Url -> [Section] -> Line+debLine suite mirror sections = unwords $+ ["deb", mirror, suite] ++ sections++srcLine :: Line -> Line+srcLine l = case words l of+ ("deb":rest) -> unwords $ "deb-src" : rest+ _ -> ""++stdSections :: [Section]+stdSections = ["main", "contrib", "non-free"]++binandsrc :: String -> SourcesGenerator+binandsrc url suite+ | isStable suite = [l, srcLine l, bl, srcLine bl]+ | otherwise = [l, srcLine l]+ where+ l = debLine (showSuite suite) url stdSections+ bl = debLine backportSuite url stdSections++debCdn :: SourcesGenerator+debCdn = binandsrc "http://cdn.debian.net/debian"++kernelOrg :: SourcesGenerator+kernelOrg = binandsrc "http://mirrors.kernel.org/debian"++-- | Only available for Stable and Testing+securityUpdates :: SourcesGenerator+securityUpdates suite+ | isStable suite || suite == Testing =+ let l = "deb http://security.debian.org/ " ++ showSuite suite ++ "/updates " ++ unwords stdSections+ in [l, srcLine l]+ | otherwise = []++-- | Makes sources.list have a standard content using the mirror CDN,+-- with a particular DebianSuite.+--+-- Since the CDN is sometimes unreliable, also adds backup lines using+-- kernel.org.+stdSourcesList :: DebianSuite -> Property+stdSourcesList suite = stdSourcesList' suite []++-- | Adds additional sources.list generators.+--+-- Note that if a Property needs to enable an apt source, it's better+-- to do so via a separate file in /etc/apt/sources.list.d/+stdSourcesList' :: DebianSuite -> [SourcesGenerator] -> Property+stdSourcesList' suite more = setSourcesList+ (concatMap (\gen -> gen suite) generators)+ `describe` ("standard sources.list for " ++ show suite)+ where+ generators = [debCdn, kernelOrg, securityUpdates] ++ more++setSourcesList :: [Line] -> Property+setSourcesList ls = sourcesList `File.hasContent` ls `onChange` update++setSourcesListD :: [Line] -> FilePath -> Property+setSourcesListD ls basename = f `File.hasContent` ls `onChange` update+ where+ f = "/etc/apt/sources.list.d/" ++ basename ++ ".list"++runApt :: [String] -> Property+runApt ps = cmdProperty' "apt-get" ps noninteractiveEnv++noninteractiveEnv :: [(String, String)]+noninteractiveEnv =+ [ ("DEBIAN_FRONTEND", "noninteractive")+ , ("APT_LISTCHANGES_FRONTEND", "none")+ ]++update :: Property+update = runApt ["update"]+ `describe` "apt update"++upgrade :: Property+upgrade = runApt ["-y", "dist-upgrade"]+ `describe` "apt dist-upgrade"++type Package = String++installed :: [Package] -> Property+installed = installed' ["-y"]++installed' :: [String] -> [Package] -> Property+installed' params ps = robustly $ check (isInstallable ps) go+ `describe` (unwords $ "apt installed":ps)+ where+ go = runApt $ params ++ ["install"] ++ ps++installedBackport :: [Package] -> Property+installedBackport ps = trivial $ withOS desc $ \o -> case o of+ Nothing -> error "cannot install backports; os not declared"+ (Just (System (Debian suite) _))+ | isStable suite -> + ensureProperty $ runApt $ + ["install", "-t", backportSuite, "-y"] ++ ps+ _ -> error $ "backports not supported on " ++ show o+ where+ desc = (unwords $ "apt installed backport":ps)++-- | Minimal install of package, without recommends.+installedMin :: [Package] -> Property+installedMin = installed' ["--no-install-recommends", "-y"]++removed :: [Package] -> Property+removed ps = check (or <$> isInstalled' ps) go+ `describe` (unwords $ "apt removed":ps)+ where+ go = runApt $ ["-y", "remove"] ++ ps++buildDep :: [Package] -> Property+buildDep ps = robustly go+ `describe` (unwords $ "apt build-dep":ps)+ where+ go = runApt $ ["-y", "build-dep"] ++ ps++-- | Installs the build deps for the source package unpacked+-- in the specifed directory, with a dummy package also+-- installed so that autoRemove won't remove them.+buildDepIn :: FilePath -> Property+buildDepIn dir = go `requires` installedMin ["devscripts", "equivs"]+ where+ go = cmdProperty' "sh" ["-c", "cd '" ++ dir ++ "' && mk-build-deps debian/control --install --tool 'apt-get -y --no-install-recommends' --remove"]+ noninteractiveEnv++-- | Package installation may fail becuse the archive has changed.+-- Run an update in that case and retry.+robustly :: Property -> Property+robustly p = adjustProperty p $ \satisfy -> do+ r <- satisfy+ if r == FailedChange+ then ensureProperty $ p `requires` update+ else return r++isInstallable :: [Package] -> IO Bool+isInstallable ps = do+ l <- isInstalled' ps+ return $ any (== False) l && not (null l)++isInstalled :: Package -> IO Bool+isInstalled p = (== [True]) <$> isInstalled' [p]++-- | Note that the order of the returned list will not always+-- correspond to the order of the input list. The number of items may+-- even vary. If apt does not know about a package at all, it will not+-- be included in the result list.+isInstalled' :: [Package] -> IO [Bool]+isInstalled' ps = catMaybes . map parse . lines+ <$> readProcess "apt-cache" ("policy":ps)+ where+ parse l+ | "Installed: (none)" `isInfixOf` l = Just False+ | "Installed: " `isInfixOf` l = Just True+ | otherwise = Nothing++autoRemove :: Property+autoRemove = runApt ["-y", "autoremove"]+ `describe` "apt autoremove"++-- | Enables unattended upgrades. Revert to disable.+unattendedUpgrades :: RevertableProperty+unattendedUpgrades = RevertableProperty enable disable+ where+ enable = setup True+ `before` Service.running "cron"+ `before` configure+ disable = setup False++ setup enabled = (if enabled then installed else removed) ["unattended-upgrades"]+ `onChange` reConfigure "unattended-upgrades"+ [("unattended-upgrades/enable_auto_updates" , "boolean", v)]+ `describe` ("unattended upgrades " ++ v)+ where+ v+ | enabled = "true"+ | otherwise = "false"+ + configure = withOS "unattended upgrades configured" $ \o ->+ case o of+ -- the package defaults to only upgrading stable+ (Just (System (Debian suite) _))+ | not (isStable suite) -> ensureProperty $+ "/etc/apt/apt.conf.d/50unattended-upgrades"+ `File.containsLine`+ ("Unattended-Upgrade::Origins-Pattern { \"o=Debian,a="++showSuite suite++"\"; };")+ _ -> noChange++-- | Preseeds debconf values and reconfigures the package so it takes+-- effect.+reConfigure :: Package -> [(String, String, String)] -> Property+reConfigure package vals = reconfigure `requires` setselections+ `describe` ("reconfigure " ++ package)+ where+ setselections = property "preseed" $ makeChange $+ withHandle StdinHandle createProcessSuccess+ (proc "debconf-set-selections" []) $ \h -> do+ forM_ vals $ \(tmpl, tmpltype, value) ->+ hPutStrLn h $ unwords [package, tmpl, tmpltype, value]+ hClose h+ reconfigure = cmdProperty' "dpkg-reconfigure" ["-fnone", package] noninteractiveEnv++-- | Ensures that a service is installed and running.+--+-- Assumes that there is a 1:1 mapping between service names and apt+-- package names.+serviceInstalledRunning :: Package -> Property+serviceInstalledRunning svc = Service.running svc `requires` installed [svc]++data AptKey = AptKey+ { keyname :: String+ , pubkey :: String+ }++trustsKey :: AptKey -> RevertableProperty+trustsKey k = RevertableProperty trust untrust+ where+ desc = "apt trusts key " ++ keyname k+ f = "/etc/apt/trusted.gpg.d" </> keyname k ++ ".gpg"+ untrust = File.notPresent f+ trust = check (not <$> doesFileExist f) $ property desc $ makeChange $ do+ withHandle StdinHandle createProcessSuccess+ (proc "gpg" ["--no-default-keyring", "--keyring", f, "--import", "-"]) $ \h -> do+ hPutStr h (pubkey k)+ hClose h+ nukeFile $ f ++ "~" -- gpg dropping
+ src/Propellor/Property/Cmd.hs view
@@ -0,0 +1,49 @@+{-# LANGUAGE PackageImports #-}++module Propellor.Property.Cmd (+ cmdProperty,+ cmdProperty',+ scriptProperty,+ userScriptProperty,+) where++import Control.Applicative+import Data.List+import "mtl" Control.Monad.Reader++import Propellor.Types+import Propellor.Property+import Utility.Monad+import Utility.SafeCommand+import Utility.Env++-- | A property that can be satisfied by running a command.+--+-- The command must exit 0 on success.+cmdProperty :: String -> [String] -> Property+cmdProperty cmd params = cmdProperty' cmd params []++-- | A property that can be satisfied by running a command,+-- with added environment.+cmdProperty' :: String -> [String] -> [(String, String)] -> Property+cmdProperty' cmd params env = property desc $ liftIO $ do+ env' <- addEntries env <$> getEnvironment+ ifM (boolSystemEnv cmd (map Param params) (Just env'))+ ( return MadeChange+ , return FailedChange+ )+ where+ desc = unwords $ cmd : params++-- | A property that can be satisfied by running a series of shell commands.+scriptProperty :: [String] -> Property+scriptProperty script = cmdProperty "sh" ["-c", shellcmd]+ where+ shellcmd = intercalate " ; " ("set -e" : script)++-- | A property that can satisfied by running a series of shell commands,+-- as user (cd'd to their home directory).+userScriptProperty :: UserName -> [String] -> Property+userScriptProperty user script = cmdProperty "su" ["-c", shellcmd, user]+ where+ shellcmd = intercalate " ; " ("set -e" : "cd" : script)
+ src/Propellor/Property/Cron.hs view
@@ -0,0 +1,49 @@+module Propellor.Property.Cron where++import Propellor+import qualified Propellor.Property.File as File+import qualified Propellor.Property.Apt as Apt+import Utility.SafeCommand++import Data.Char++type CronTimes = String++-- | Installs a cron job, run as a specified user, in a particular+-- directory. Note that the Desc must be unique, as it is used for the +-- cron.d/ filename.+-- +-- Only one instance of the cron job is allowed to run at a time, no matter+-- how long it runs. This is accomplished using flock locking of the cron+-- job file.+--+-- The cron job's output will only be emailed if it exits nonzero.+job :: Desc -> CronTimes -> UserName -> FilePath -> String -> Property+job desc times user cddir command = cronjobfile `File.hasContent`+ [ "# Generated by propellor"+ , ""+ , "SHELL=/bin/sh"+ , "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin"+ , ""+ , times ++ "\t" ++ user ++ "\t"+ ++ "chronic flock -n " ++ shellEscape cronjobfile+ ++ " sh -c " ++ shellEscape cmdline+ ]+ `requires` Apt.serviceInstalledRunning "cron"+ `requires` Apt.installed ["util-linux", "moreutils"]+ `describe` ("cronned " ++ desc)+ where+ cmdline = "cd " ++ cddir ++ " && ( " ++ command ++ " )"+ cronjobfile = "/etc/cron.d/" ++ map sanitize desc+ sanitize c+ | isAlphaNum c = c+ | otherwise = '_'++-- | Installs a cron job, and runs it niced and ioniced.+niceJob :: Desc -> CronTimes -> UserName -> FilePath -> String -> Property+niceJob desc times user cddir command = job desc times user cddir+ ("nice ionice -c 3 " ++ command)++-- | Installs a cron job to run propellor.+runPropellor :: CronTimes -> Property+runPropellor times = niceJob "propellor" times "root" localdir "make"
+ src/Propellor/Property/Dns.hs view
@@ -0,0 +1,405 @@+module Propellor.Property.Dns (+ module Propellor.Types.Dns,+ primary,+ secondary,+ secondaryFor,+ mkSOA,+ writeZoneFile,+ nextSerialNumber,+ adjustSerialNumber,+ serialNumberOffset,+ WarningMessage,+ genZone,+) where++import Propellor+import Propellor.Types.Dns+import Propellor.Property.File+import Propellor.Types.Attr+import qualified Propellor.Property.Apt as Apt+import qualified Propellor.Property.Service as Service+import Utility.Applicative++import qualified Data.Map as M+import qualified Data.Set as S+import Data.List++-- | Primary dns server for a domain.+--+-- Most of the content of the zone file is configured by setting properties+-- of hosts. For example,+--+-- > host "foo.example.com"+-- > & ipv4 "192.168.1.1"+-- > & alias "mail.exmaple.com"+--+-- Will cause that hostmame and its alias to appear in the zone file,+-- with the configured IP address.+--+-- The [(BindDomain, Record)] list can be used for additional records+-- that cannot be configured elsewhere. This often includes NS records,+-- TXT records and perhaps CNAMEs pointing at hosts that propellor does+-- not control.+--+-- The primary server is configured to only allow zone transfers to+-- secondary dns servers. These are determined in two ways:+--+-- 1. By looking at the properties of other hosts, to find hosts that+-- are configured as the secondary dns server.+--+-- 2. By looking for NS Records in the passed list of records.+--+-- In either case, the secondary dns server Host should have an ipv4 and/or+-- ipv6 property defined.+primary :: [Host] -> Domain -> SOA -> [(BindDomain, Record)] -> RevertableProperty+primary hosts domain soa rs = RevertableProperty setup cleanup+ where+ setup = withwarnings (check needupdate baseprop)+ `requires` servingZones+ `onChange` Service.reloaded "bind9"+ cleanup = check (doesFileExist zonefile) $+ property ("removed dns primary for " ++ domain)+ (makeChange $ removeZoneFile zonefile)+ `requires` namedConfWritten+ `onChange` Service.reloaded "bind9"++ (partialzone, zonewarnings) = genZone hosts domain soa+ zone = partialzone { zHosts = zHosts partialzone ++ rs }+ zonefile = "/etc/bind/propellor/db." ++ domain+ baseprop = Property ("dns primary for " ++ domain)+ (makeChange $ writeZoneFile zone zonefile)+ (addNamedConf conf)+ withwarnings p = adjustProperty p $ \satisfy -> do+ mapM_ warningMessage $ zonewarnings ++ secondarywarnings+ satisfy+ conf = NamedConf+ { confDomain = domain+ , confDnsServerType = Master+ , confFile = zonefile+ , confMasters = []+ , confAllowTransfer = nub $+ concatMap (\h -> hostAddresses h hosts) $+ secondaries ++ nssecondaries+ , confLines = []+ }+ secondaries = otherServers Secondary hosts domain+ secondarywarnings = map (\h -> "No IP address defined for DNS seconary " ++ h) $+ filter (\h -> null (hostAddresses h hosts)) secondaries+ nssecondaries = mapMaybe (domainHostName <=< getNS) rootRecords+ rootRecords = map snd $+ filter (\(d, _r) -> d == RootDomain || d == AbsDomain domain) rs+ needupdate = do+ v <- readZonePropellorFile zonefile+ return $ case v of+ Nothing -> True+ Just oldzone ->+ -- compare everything except serial+ let oldserial = sSerial (zSOA oldzone)+ z = zone { zSOA = (zSOA zone) { sSerial = oldserial } }+ in z /= oldzone || oldserial < sSerial (zSOA zone)++-- | Secondary dns server for a domain.+--+-- The primary server is determined by looking at the properties of other+-- hosts to find which one is configured as the primary.+--+-- Note that if a host is declared to be a primary and a secondary dns+-- server for the same domain, the primary server config always wins.+secondary :: [Host] -> Domain -> RevertableProperty+secondary hosts domain = secondaryFor (otherServers Master hosts domain) hosts domain++-- | This variant is useful if the primary server does not have its DNS+-- configured via propellor.+secondaryFor :: [HostName] -> [Host] -> Domain -> RevertableProperty+secondaryFor masters hosts domain = RevertableProperty setup cleanup+ where+ setup = pureAttrProperty desc (addNamedConf conf)+ `requires` servingZones+ cleanup = namedConfWritten++ desc = "dns secondary for " ++ domain+ conf = NamedConf+ { confDomain = domain+ , confDnsServerType = Secondary+ , confFile = "db." ++ domain+ , confMasters = concatMap (\m -> hostAddresses m hosts) masters+ , confAllowTransfer = []+ , confLines = []+ }++otherServers :: DnsServerType -> [Host] -> Domain -> [HostName]+otherServers wantedtype hosts domain =+ M.keys $ M.filter wanted $ hostAttrMap hosts+ where+ wanted attr = case M.lookup domain (_namedconf attr) of+ Nothing -> False+ Just conf -> confDnsServerType conf == wantedtype+ && confDomain conf == domain++-- | Rewrites the whole named.conf.local file to serve the zones+-- configured by `primary` and `secondary`, and ensures that bind9 is+-- running.+servingZones :: Property+servingZones = namedConfWritten+ `onChange` Service.reloaded "bind9"+ `requires` Apt.serviceInstalledRunning "bind9"++namedConfWritten :: Property+namedConfWritten = property "named.conf configured" $ do+ zs <- getNamedConf+ ensureProperty $+ hasContent namedConfFile $+ concatMap confStanza $ M.elems zs++confStanza :: NamedConf -> [Line]+confStanza c =+ [ "// automatically generated by propellor"+ , "zone \"" ++ confDomain c ++ "\" {"+ , cfgline "type" (if confDnsServerType c == Master then "master" else "slave")+ , cfgline "file" ("\"" ++ confFile c ++ "\"")+ ] +++ mastersblock +++ allowtransferblock +++ (map (\l -> "\t" ++ l ++ ";") (confLines c)) +++ [ "};"+ , ""+ ]+ where+ cfgline f v = "\t" ++ f ++ " " ++ v ++ ";"+ ipblock name l = + [ "\t" ++ name ++ " {" ] +++ (map (\ip -> "\t\t" ++ fromIPAddr ip ++ ";") l) +++ [ "\t};" ]+ mastersblock+ | null (confMasters c) = []+ | otherwise = ipblock "masters" (confMasters c)+ -- an empty block prohibits any transfers+ allowtransferblock = ipblock "allow-transfer" (confAllowTransfer c)++namedConfFile :: FilePath+namedConfFile = "/etc/bind/named.conf.local"++-- | Generates a SOA with some fairly sane numbers in it.+--+-- The Domain is the domain to use in the SOA record. Typically+-- something like ns1.example.com. So, not the domain that this is the SOA+-- record for.+--+-- The SerialNumber can be whatever serial number was used by the domain+-- before propellor started managing it. Or 0 if the domain has only ever+-- been managed by propellor.+--+-- You do not need to increment the SerialNumber when making changes!+-- Propellor will automatically add the number of commits in the git+-- repository to the SerialNumber.+mkSOA :: Domain -> SerialNumber -> SOA+mkSOA d sn = SOA+ { sDomain = AbsDomain d+ , sSerial = sn+ , sRefresh = hours 4+ , sRetry = hours 1+ , sExpire = 2419200 -- 4 weeks+ , sNegativeCacheTTL = hours 8+ }+ where+ hours n = n * 60 * 60++dValue :: BindDomain -> String+dValue (RelDomain d) = d+dValue (AbsDomain d) = d ++ "."+dValue (RootDomain) = "@"++rField :: Record -> String+rField (Address (IPv4 _)) = "A"+rField (Address (IPv6 _)) = "AAAA"+rField (CNAME _) = "CNAME"+rField (MX _ _) = "MX"+rField (NS _) = "NS"+rField (TXT _) = "TXT"+rField (SRV _ _ _ _) = "SRV"++rValue :: Record -> String+rValue (Address (IPv4 addr)) = addr+rValue (Address (IPv6 addr)) = addr+rValue (CNAME d) = dValue d+rValue (MX pri d) = show pri ++ " " ++ dValue d+rValue (NS d) = dValue d+rValue (SRV priority weight port target) = unwords+ [ show priority+ , show weight+ , show port+ , dValue target+ ]+rValue (TXT s) = [q] ++ filter (/= q) s ++ [q]+ where+ q = '"'++-- | Adjusts the serial number of the zone to always be larger+-- than the serial number in the Zone record,+-- and always be larger than the passed SerialNumber.+nextSerialNumber :: Zone -> SerialNumber -> Zone+nextSerialNumber z serial = adjustSerialNumber z $ \sn -> succ $ max sn serial++adjustSerialNumber :: Zone -> (SerialNumber -> SerialNumber) -> Zone+adjustSerialNumber (Zone d soa l) f = Zone d soa' l+ where+ soa' = soa { sSerial = f (sSerial soa) }++-- | Count the number of git commits made to the current branch.+serialNumberOffset :: IO SerialNumber+serialNumberOffset = fromIntegral . length . lines+ <$> readProcess "git" ["log", "--pretty=%H"]++-- | Write a Zone out to a to a file.+--+-- The serial number in the Zone automatically has the serialNumberOffset+-- added to it. Also, just in case, the old serial number used in the zone+-- file is checked, and if it is somehow larger, its succ is used.+writeZoneFile :: Zone -> FilePath -> IO ()+writeZoneFile z f = do+ oldserial <- oldZoneFileSerialNumber f+ offset <- serialNumberOffset+ let z' = nextSerialNumber+ (adjustSerialNumber z (+ offset))+ oldserial+ createDirectoryIfMissing True (takeDirectory f)+ writeFile f (genZoneFile z')+ writeZonePropellorFile f z'++removeZoneFile :: FilePath -> IO ()+removeZoneFile f = do+ nukeFile f+ nukeFile (zonePropellorFile f)++-- | Next to the zone file, is a ".propellor" file, which contains+-- the serialized Zone. This saves the bother of parsing+-- the horrible bind zone file format.+zonePropellorFile :: FilePath -> FilePath+zonePropellorFile f = f ++ ".propellor"++oldZoneFileSerialNumber :: FilePath -> IO SerialNumber+oldZoneFileSerialNumber = maybe 0 (sSerial . zSOA) <$$> readZonePropellorFile++writeZonePropellorFile :: FilePath -> Zone -> IO ()+writeZonePropellorFile f z = writeFile (zonePropellorFile f) (show z)++readZonePropellorFile :: FilePath -> IO (Maybe Zone)+readZonePropellorFile f = catchDefaultIO Nothing $+ readish <$> readFileStrict (zonePropellorFile f)++-- | Generating a zone file.+genZoneFile :: Zone -> String+genZoneFile (Zone zdomain soa rs) = unlines $+ header : genSOA soa ++ map (genRecord zdomain) rs+ where+ header = com $ "BIND zone file for " ++ zdomain ++ ". Generated by propellor, do not edit."++genRecord :: Domain -> (BindDomain, Record) -> String+genRecord zdomain (domain, record) = intercalate "\t"+ [ domainHost zdomain domain+ , "IN"+ , rField record+ , rValue record+ ]++genSOA :: SOA -> [String]+genSOA soa = + -- "@ IN SOA ns1.example.com. root ("+ [ intercalate "\t"+ [ dValue RootDomain + , "IN"+ , "SOA"+ , dValue (sDomain soa)+ , "root"+ , "("+ ]+ , headerline sSerial "Serial"+ , headerline sRefresh "Refresh"+ , headerline sRetry "Retry"+ , headerline sExpire "Expire"+ , headerline sNegativeCacheTTL "Negative Cache TTL"+ , inheader ")"+ ]+ where+ headerline r comment = inheader $ show (r soa) ++ "\t\t" ++ com comment+ inheader l = "\t\t\t" ++ l++-- | Comment line in a zone file.+com :: String -> String+com s = "; " ++ s++type WarningMessage = String++-- | Generates a Zone for a particular Domain from the DNS properies of all+-- hosts that propellor knows about that are in that Domain.+genZone :: [Host] -> Domain -> SOA -> (Zone, [WarningMessage])+genZone hosts zdomain soa =+ let (warnings, zhosts) = partitionEithers $ concat $ map concat+ [ map hostips inzdomain+ , map hostrecords inzdomain+ , map addcnames (M.elems m)+ ]+ in (Zone zdomain soa (nub zhosts), warnings)+ where+ m = hostAttrMap hosts+ -- Known hosts with hostname located in the zone's domain.+ inzdomain = M.elems $ M.filterWithKey (\hn _ -> inDomain zdomain $ AbsDomain $ hn) m+ + -- Each host with a hostname located in the zdomain+ -- should have 1 or more IPAddrs in its Attr.+ --+ -- If a host lacks any IPAddr, it's probably a misconfiguration,+ -- so warn.+ hostips :: Attr -> [Either WarningMessage (BindDomain, Record)]+ hostips attr+ | null l = [Left $ "no IP address defined for host " ++ _hostname attr]+ | otherwise = map Right l+ where+ l = zip (repeat $ AbsDomain $ _hostname attr)+ (map Address $ getAddresses attr)++ -- Any host, whether its hostname is in the zdomain or not,+ -- may have cnames which are in the zdomain. The cname may even be+ -- the same as the root of the zdomain, which is a nice way to+ -- specify IP addresses for a SOA record.+ --+ -- Add Records for those.. But not actually, usually, cnames!+ -- Why not? Well, using cnames doesn't allow doing some things,+ -- including MX and round robin DNS, and certianly CNAMES+ -- shouldn't be used in SOA records.+ --+ -- We typically know the host's IPAddrs anyway.+ -- So we can just use the IPAddrs.+ addcnames :: Attr -> [Either WarningMessage (BindDomain, Record)]+ addcnames attr = concatMap gen $ filter (inDomain zdomain) $+ mapMaybe getCNAME $ S.toList (_dns attr)+ where+ gen c = case getAddresses attr of+ [] -> [ret (CNAME c)]+ l -> map (ret . Address) l+ where+ ret record = Right (c, record)+ + -- Adds any other DNS records for a host located in the zdomain.+ hostrecords :: Attr -> [Either WarningMessage (BindDomain, Record)]+ hostrecords attr = map Right l+ where+ l = zip (repeat $ AbsDomain $ _hostname attr)+ (S.toList $ S.filter (\r -> isNothing (getIPAddr r) && isNothing (getCNAME r)) (_dns attr))++inDomain :: Domain -> BindDomain -> Bool+inDomain domain (AbsDomain d) = domain == d || ('.':domain) `isSuffixOf` d+inDomain _ _ = False -- can't tell, so assume not++-- | Gets the hostname of the second domain, relative to the first domain,+-- suitable for using in a zone file.+domainHost :: Domain -> BindDomain -> String+domainHost _ (RelDomain d) = d+domainHost _ RootDomain = "@"+domainHost base (AbsDomain d)+ | dotbase `isSuffixOf` d = take (length d - length dotbase) d+ | base == d = "@"+ | otherwise = d+ where+ dotbase = '.':base+
+ src/Propellor/Property/Docker.hs view
@@ -0,0 +1,460 @@+{-# LANGUAGE BangPatterns #-}++-- | Docker support for propellor+--+-- The existance of a docker container is just another Property of a system,+-- which propellor can set up. See config.hs for an example.++module Propellor.Property.Docker where++import Propellor+import Propellor.SimpleSh+import Propellor.Types.Attr+import qualified Propellor.Property.File as File+import qualified Propellor.Property.Apt as Apt+import qualified Propellor.Property.Docker.Shim as Shim+import Utility.SafeCommand+import Utility.Path++import Control.Concurrent.Async+import System.Posix.Directory+import System.Posix.Process+import Data.List+import Data.List.Utils++-- | Configures docker with an authentication file, so that images can be+-- pushed to index.docker.io.+configured :: Property+configured = property "docker configured" go `requires` installed+ where+ go = withPrivData DockerAuthentication $ \cfg -> ensureProperty $ + "/root/.dockercfg" `File.hasContent` (lines cfg)++installed :: Property+installed = Apt.installed ["docker.io"]++-- | A short descriptive name for a container.+-- Should not contain whitespace or other unusual characters,+-- only [a-zA-Z0-9_-] are allowed+type ContainerName = String++-- | Starts accumulating the properties of a Docker container.+--+-- > container "web-server" "debian"+-- > & publish "80:80"+-- > & Apt.installed {"apache2"]+-- > & ...+container :: ContainerName -> Image -> Host+container cn image = Host [] (\_ -> attr)+ where+ attr = (newAttr (cn2hn cn)) { _dockerImage = Just image }++cn2hn :: ContainerName -> HostName+cn2hn cn = cn ++ ".docker"++-- | Ensures that a docker container is set up and running. The container+-- has its own Properties which are handled by running propellor+-- inside the container.+--+-- Reverting this property ensures that the container is stopped and+-- removed.+docked+ :: [Host]+ -> ContainerName+ -> RevertableProperty+docked hosts cn = RevertableProperty (go "docked" setup) (go "undocked" teardown)+ where+ go desc a = property (desc ++ " " ++ cn) $ do+ hn <- getHostName+ let cid = ContainerId hn cn+ ensureProperties [findContainer hosts cid cn $ a cid]++ setup cid (Container image runparams) =+ provisionContainer cid+ `requires`+ runningContainer cid image runparams+ `requires`+ installed++ teardown cid (Container image _runparams) =+ combineProperties ("undocked " ++ fromContainerId cid)+ [ stoppedContainer cid+ , property ("cleaned up " ++ fromContainerId cid) $+ liftIO $ report <$> mapM id+ [ removeContainer cid+ , removeImage image+ ]+ ]++findContainer+ :: [Host]+ -> ContainerId+ -> ContainerName+ -> (Container -> Property)+ -> Property+findContainer hosts cid cn mk = case findHost hosts (cn2hn cn) of+ Nothing -> cantfind+ Just h -> maybe cantfind mk (mkContainer cid h)+ where+ cantfind = containerDesc cid $ property "" $ do+ liftIO $ warningMessage $+ "missing definition for docker container \"" ++ cn2hn cn+ return FailedChange++mkContainer :: ContainerId -> Host -> Maybe Container+mkContainer cid@(ContainerId hn _cn) h = Container+ <$> _dockerImage attr+ <*> pure (map (\a -> a hn) (_dockerRunParams attr))+ where+ attr = hostAttr h'+ h' = h+ -- expose propellor directory inside the container+ & volume (localdir++":"++localdir)+ -- name the container in a predictable way so we+ -- and the user can easily find it later+ & name (fromContainerId cid)++-- | Causes *any* docker images that are not in use by running containers to+-- be deleted. And deletes any containers that propellor has set up+-- before that are not currently running. Does not delete any containers+-- that were not set up using propellor.+--+-- Generally, should come after the properties for the desired containers.+garbageCollected :: Property+garbageCollected = propertyList "docker garbage collected"+ [ gccontainers+ , gcimages+ ]+ where+ gccontainers = property "docker containers garbage collected" $+ liftIO $ report <$> (mapM removeContainer =<< listContainers AllContainers)+ gcimages = property "docker images garbage collected" $ do+ liftIO $ report <$> (mapM removeImage =<< listImages)++data Container = Container Image [RunParam]++-- | Parameters to pass to `docker run` when creating a container.+type RunParam = String++-- | A docker image, that can be used to run a container.+type Image = String++-- | Set custom dns server for container.+dns :: String -> Property+dns = runProp "dns"++-- | Set container host name.+hostname :: String -> Property+hostname = runProp "hostname"++-- | Set name for container. (Normally done automatically.)+name :: String -> Property+name = runProp "name"++-- | Publish a container's port to the host+-- (format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort)+publish :: String -> Property+publish = runProp "publish"++-- | Expose a container's port without publishing it.+expose :: String -> Property+expose = runProp "expose"++-- | Username or UID for container.+user :: String -> Property+user = runProp "user"++-- | Mount a volume+-- Create a bind mount with: [host-dir]:[container-dir]:[rw|ro]+-- With just a directory, creates a volume in the container.+volume :: String -> Property+volume = runProp "volume"++-- | Mount a volume from the specified container into the current+-- container.+volumes_from :: ContainerName -> Property+volumes_from cn = genProp "volumes-from" $ \hn ->+ fromContainerId (ContainerId hn cn)++-- | Work dir inside the container. +workdir :: String -> Property+workdir = runProp "workdir"++-- | Memory limit for container.+--Format: <number><optional unit>, where unit = b, k, m or g+memory :: String -> Property+memory = runProp "memory"++-- | Link with another container on the same host.+link :: ContainerName -> ContainerAlias -> Property+link linkwith calias = genProp "link" $ \hn ->+ fromContainerId (ContainerId hn linkwith) ++ ":" ++ calias++-- | A short alias for a linked container.+-- Each container has its own alias namespace.+type ContainerAlias = String++-- | A container is identified by its name, and the host+-- on which it's deployed.+data ContainerId = ContainerId HostName ContainerName+ deriving (Eq, Read, Show)++-- | Two containers with the same ContainerIdent were started from+-- the same base image (possibly a different version though), and+-- with the same RunParams.+data ContainerIdent = ContainerIdent Image HostName ContainerName [RunParam]+ deriving (Read, Show, Eq)++ident2id :: ContainerIdent -> ContainerId+ident2id (ContainerIdent _ hn cn _) = ContainerId hn cn++toContainerId :: String -> Maybe ContainerId+toContainerId s+ | myContainerSuffix `isSuffixOf` s = case separate (== '.') (desuffix s) of+ (cn, hn)+ | null hn || null cn -> Nothing+ | otherwise -> Just $ ContainerId hn cn+ | otherwise = Nothing+ where+ desuffix = reverse . drop len . reverse+ len = length myContainerSuffix++fromContainerId :: ContainerId -> String+fromContainerId (ContainerId hn cn) = cn++"."++hn++myContainerSuffix++containerHostName :: ContainerId -> HostName+containerHostName (ContainerId _ cn) = cn2hn cn++myContainerSuffix :: String+myContainerSuffix = ".propellor"++containerDesc :: ContainerId -> Property -> Property+containerDesc cid p = p `describe` desc+ where+ desc = "[" ++ fromContainerId cid ++ "] " ++ propertyDesc p++runningContainer :: ContainerId -> Image -> [RunParam] -> Property+runningContainer cid@(ContainerId hn cn) image runps = containerDesc cid $ property "running" $ do+ l <- liftIO $ listContainers RunningContainers+ if cid `elem` l+ then do+ -- Check if the ident has changed; if so the+ -- parameters of the container differ and it must+ -- be restarted.+ runningident <- liftIO $ getrunningident+ if runningident == Just ident+ then noChange+ else do+ void $ liftIO $ stopContainer cid+ restartcontainer+ else ifM (liftIO $ elem cid <$> listContainers AllContainers)+ ( restartcontainer+ , go image+ )+ where+ ident = ContainerIdent image hn cn runps++ restartcontainer = do+ oldimage <- liftIO $ fromMaybe image <$> commitContainer cid+ void $ liftIO $ removeContainer cid+ go oldimage++ getrunningident :: IO (Maybe ContainerIdent)+ getrunningident = simpleShClient (namedPipe cid) "cat" [propellorIdent] $ \rs -> do+ let !v = extractident rs+ return v++ extractident :: [Resp] -> Maybe ContainerIdent+ extractident = headMaybe . catMaybes . map readish . catMaybes . map getStdout++ go img = do+ liftIO $ do+ clearProvisionedFlag cid+ createDirectoryIfMissing True (takeDirectory $ identFile cid)+ shim <- liftIO $ Shim.setup (localdir </> "propellor") (localdir </> shimdir cid)+ liftIO $ writeFile (identFile cid) (show ident)+ ensureProperty $ boolProperty "run" $ runContainer img+ (runps ++ ["-i", "-d", "-t"])+ [shim, "--docker", fromContainerId cid]++-- | Called when propellor is running inside a docker container.+-- The string should be the container's ContainerId.+--+-- This process is effectively init inside the container.+-- It even needs to wait on zombie processes!+--+-- Fork a thread to run the SimpleSh server in the background.+-- In the foreground, run an interactive bash (or sh) shell,+-- so that the user can interact with it when attached to the container.+--+-- When the system reboots, docker restarts the container, and this is run+-- again. So, to make the necessary services get started on boot, this needs+-- to provision the container then. However, if the container is already+-- being provisioned by the calling propellor, it would be redundant and+-- problimatic to also provisoon it here.+--+-- The solution is a flag file. If the flag file exists, then the container+-- was already provisioned. So, it must be a reboot, and time to provision+-- again. If the flag file doesn't exist, don't provision here.+chain :: String -> IO ()+chain s = case toContainerId s of+ Nothing -> error $ "Invalid ContainerId: " ++ s+ Just cid -> do+ changeWorkingDirectory localdir+ writeFile propellorIdent . show =<< readIdentFile cid+ -- Run boot provisioning before starting simpleSh,+ -- to avoid ever provisioning twice at the same time.+ whenM (checkProvisionedFlag cid) $ do+ let shim = Shim.file (localdir </> "propellor") (localdir </> shimdir cid)+ unlessM (boolSystem shim [Param "--continue", Param $ show $ Chain $ containerHostName cid]) $+ warningMessage "Boot provision failed!"+ void $ async $ job reapzombies+ void $ async $ job $ simpleSh $ namedPipe cid+ job $ do+ void $ tryIO $ ifM (inPath "bash")+ ( boolSystem "bash" [Param "-l"]+ , boolSystem "/bin/sh" []+ )+ putStrLn "Container is still running. Press ^P^Q to detach."+ where+ job = forever . void . tryIO+ reapzombies = void $ getAnyProcessStatus True False++-- | Once a container is running, propellor can be run inside+-- it to provision it.+--+-- Note that there is a race here, between the simplesh+-- server starting up in the container, and this property+-- being run. So, retry connections to the client for up to+-- 1 minute.+provisionContainer :: ContainerId -> Property+provisionContainer cid = containerDesc cid $ property "provision" $ liftIO $ do+ let shim = Shim.file (localdir </> "propellor") (localdir </> shimdir cid)+ r <- simpleShClientRetry 60 (namedPipe cid) shim params (go Nothing)+ when (r /= FailedChange) $+ setProvisionedFlag cid + return r+ where+ params = ["--continue", show $ Chain $ containerHostName cid]++ go lastline (v:rest) = case v of+ StdoutLine s -> do+ maybe noop putStrLn lastline+ hFlush stdout+ go (Just s) rest+ StderrLine s -> do+ maybe noop putStrLn lastline+ hFlush stdout+ hPutStrLn stderr s+ hFlush stderr+ go Nothing rest+ Done -> ret lastline+ go lastline [] = ret lastline++ ret lastline = pure $ fromMaybe FailedChange $ readish =<< lastline++stopContainer :: ContainerId -> IO Bool+stopContainer cid = boolSystem dockercmd [Param "stop", Param $ fromContainerId cid ]++stoppedContainer :: ContainerId -> Property+stoppedContainer cid = containerDesc cid $ property desc $ + ifM (liftIO $ elem cid <$> listContainers RunningContainers)+ ( liftIO cleanup `after` ensureProperty + (boolProperty desc $ stopContainer cid)+ , return NoChange+ )+ where+ desc = "stopped"+ cleanup = do+ nukeFile $ namedPipe cid+ nukeFile $ identFile cid+ removeDirectoryRecursive $ shimdir cid+ clearProvisionedFlag cid++removeContainer :: ContainerId -> IO Bool+removeContainer cid = catchBoolIO $+ snd <$> processTranscript dockercmd ["rm", fromContainerId cid ] Nothing++removeImage :: Image -> IO Bool+removeImage image = catchBoolIO $+ snd <$> processTranscript dockercmd ["rmi", image ] Nothing++runContainer :: Image -> [RunParam] -> [String] -> IO Bool+runContainer image ps cmd = boolSystem dockercmd $ map Param $+ "run" : (ps ++ image : cmd)++commitContainer :: ContainerId -> IO (Maybe Image)+commitContainer cid = catchMaybeIO $+ takeWhile (/= '\n') + <$> readProcess dockercmd ["commit", fromContainerId cid]++data ContainerFilter = RunningContainers | AllContainers+ deriving (Eq)++-- | Only lists propellor managed containers.+listContainers :: ContainerFilter -> IO [ContainerId]+listContainers status = + catMaybes . map toContainerId . concat . map (split ",")+ . catMaybes . map (lastMaybe . words) . lines+ <$> readProcess dockercmd ps+ where+ ps+ | status == AllContainers = baseps ++ ["--all"]+ | otherwise = baseps+ baseps = ["ps", "--no-trunc"]++listImages :: IO [Image]+listImages = lines <$> readProcess dockercmd ["images", "--all", "--quiet"]++runProp :: String -> RunParam -> Property+runProp field val = pureAttrProperty (param) $ \attr ->+ attr { _dockerRunParams = _dockerRunParams attr ++ [\_ -> "--"++param] }+ where+ param = field++"="++val++genProp :: String -> (HostName -> RunParam) -> Property+genProp field mkval = pureAttrProperty field $ \attr ->+ attr { _dockerRunParams = _dockerRunParams attr ++ [\hn -> "--"++field++"=" ++ mkval hn] }++-- | The ContainerIdent of a container is written to+-- /.propellor-ident inside it. This can be checked to see if+-- the container has the same ident later.+propellorIdent :: FilePath+propellorIdent = "/.propellor-ident"++-- | Named pipe used for communication with the container.+namedPipe :: ContainerId -> FilePath+namedPipe cid = "docker" </> fromContainerId cid++provisionedFlag :: ContainerId -> FilePath+provisionedFlag cid = "docker" </> fromContainerId cid ++ ".provisioned"++clearProvisionedFlag :: ContainerId -> IO ()+clearProvisionedFlag = nukeFile . provisionedFlag++setProvisionedFlag :: ContainerId -> IO ()+setProvisionedFlag cid = do+ createDirectoryIfMissing True (takeDirectory (provisionedFlag cid))+ writeFile (provisionedFlag cid) "1"++checkProvisionedFlag :: ContainerId -> IO Bool+checkProvisionedFlag = doesFileExist . provisionedFlag++shimdir :: ContainerId -> FilePath+shimdir cid = "docker" </> fromContainerId cid ++ ".shim"++identFile :: ContainerId -> FilePath+identFile cid = "docker" </> fromContainerId cid ++ ".ident"++readIdentFile :: ContainerId -> IO ContainerIdent+readIdentFile cid = fromMaybe (error "bad ident in identFile")+ . readish <$> readFile (identFile cid)++dockercmd :: String+dockercmd = "docker.io"++report :: [Bool] -> Result+report rmed+ | or rmed = MadeChange+ | otherwise = NoChange+
+ src/Propellor/Property/Docker/Shim.hs view
@@ -0,0 +1,61 @@+-- | Support for running propellor, as built outside a docker container,+-- inside the container.+--+-- Note: This is currently Debian specific, due to glibcLibs.++module Propellor.Property.Docker.Shim (setup, cleanEnv, file) where++import Propellor+import Utility.LinuxMkLibs+import Utility.SafeCommand+import Utility.Path+import Utility.FileMode++import Data.List+import System.Posix.Files++-- | Sets up a shimmed version of the program, in a directory, and+-- returns its path.+setup :: FilePath -> FilePath -> IO FilePath+setup propellorbin dest = do+ createDirectoryIfMissing True dest++ libs <- parseLdd <$> readProcess "ldd" [propellorbin]+ glibclibs <- glibcLibs+ let libs' = nub $ libs ++ glibclibs+ libdirs <- map (dest ++) . nub . catMaybes+ <$> mapM (installLib installFile dest) libs'+ + let linker = (dest ++) $ + fromMaybe (error "cannot find ld-linux linker") $+ headMaybe $ filter ("ld-linux" `isInfixOf`) libs'+ let gconvdir = (dest ++) $ parentDir $+ fromMaybe (error "cannot find gconv directory") $+ headMaybe $ filter ("/gconv/" `isInfixOf`) glibclibs+ let linkerparams = ["--library-path", intercalate ":" libdirs ]+ let shim = file propellorbin dest+ writeFile shim $ unlines+ [ "#!/bin/sh"+ , "GCONV_PATH=" ++ shellEscape gconvdir+ , "export GCONV_PATH"+ , "exec " ++ unwords (map shellEscape $ linker : linkerparams) ++ + " " ++ shellEscape propellorbin ++ " \"$@\""+ ]+ modifyFileMode shim (addModes executeModes)+ return shim++cleanEnv :: IO ()+cleanEnv = void $ unsetEnv "GCONV_PATH"++file :: FilePath -> FilePath -> FilePath+file propellorbin dest = dest </> takeFileName propellorbin++installFile :: FilePath -> FilePath -> IO ()+installFile top f = do+ createDirectoryIfMissing True destdir+ nukeFile dest+ createLink f dest `catchIO` (const copy)+ where+ copy = void $ boolSystem "cp" [Param "-a", Param f, Param dest]+ destdir = inTop top $ parentDir f+ dest = inTop top f
+ src/Propellor/Property/File.hs view
@@ -0,0 +1,94 @@+module Propellor.Property.File where++import Propellor+import Utility.FileMode++import System.Posix.Files+import System.PosixCompat.Types++type Line = String++-- | Replaces all the content of a file.+hasContent :: FilePath -> [Line] -> Property+f `hasContent` newcontent = fileProperty ("replace " ++ f)+ (\_oldcontent -> newcontent) f++-- | Ensures a file has contents that comes from PrivData.+--+-- The file's permissions are preserved if the file already existed.+-- Otherwise, they're set to 600.+hasPrivContent :: FilePath -> Property+hasPrivContent f = property desc $ withPrivData (PrivFile f) $ \privcontent -> + ensureProperty $ fileProperty' writeFileProtected desc+ (\_oldcontent -> lines privcontent) f+ where+ desc = "privcontent " ++ f++-- | Leaves the file world-readable.+hasPrivContentExposed :: FilePath -> Property+hasPrivContentExposed f = hasPrivContent f `onChange`+ mode f (combineModes (ownerWriteMode:readModes))++-- | Ensures that a line is present in a file, adding it to the end if not.+containsLine :: FilePath -> Line -> Property+f `containsLine` l = f `containsLines` [l]++containsLines :: FilePath -> [Line] -> Property+f `containsLines` l = fileProperty (f ++ " contains:" ++ show l) go f+ where+ go ls+ | all (`elem` ls) l = ls+ | otherwise = ls++l++-- | Ensures that a line is not present in a file.+-- Note that the file is ensured to exist, so if it doesn't, an empty+-- file will be written.+lacksLine :: FilePath -> Line -> Property+f `lacksLine` l = fileProperty (f ++ " remove: " ++ l) (filter (/= l)) f++-- | Removes a file. Does not remove symlinks or non-plain-files.+notPresent :: FilePath -> Property+notPresent f = check (doesFileExist f) $ property (f ++ " not present") $ + makeChange $ nukeFile f++fileProperty :: Desc -> ([Line] -> [Line]) -> FilePath -> Property+fileProperty = fileProperty' writeFile+fileProperty' :: (FilePath -> String -> IO ()) -> Desc -> ([Line] -> [Line]) -> FilePath -> Property+fileProperty' writer desc a f = property desc $ go =<< liftIO (doesFileExist f)+ where+ go True = do+ ls <- liftIO $ lines <$> readFile f+ let ls' = a ls+ if ls' == ls+ then noChange+ else makeChange $ viaTmp updatefile f (unlines ls')+ go False = makeChange $ writer f (unlines $ a [])++ -- viaTmp makes the temp file mode 600.+ -- Replicate the original file's owner and mode.+ updatefile f' content = do+ writer f' content+ s <- getFileStatus f+ setFileMode f' (fileMode s)+ setOwnerAndGroup f' (fileOwner s) (fileGroup s)++-- | Ensures a directory exists.+dirExists :: FilePath -> Property+dirExists d = check (not <$> doesDirectoryExist d) $ property (d ++ " exists") $+ makeChange $ createDirectoryIfMissing True d++-- | Ensures that a file/dir has the specified owner and group.+ownerGroup :: FilePath -> UserName -> GroupName -> Property+ownerGroup f owner group = property (f ++ " owner " ++ og) $ do+ r <- ensureProperty $ cmdProperty "chown" [og, f]+ if r == FailedChange+ then return r+ else noChange+ where+ og = owner ++ ":" ++ group++-- | Ensures that a file/dir has the specfied mode.+mode :: FilePath -> FileMode -> Property+mode f v = property (f ++ " mode " ++ show v) $ do+ liftIO $ modifyFileMode f (\_old -> v)+ noChange
+ src/Propellor/Property/Git.hs view
@@ -0,0 +1,93 @@+module Propellor.Property.Git where++import Propellor+import Propellor.Property.File+import qualified Propellor.Property.Apt as Apt+import qualified Propellor.Property.Service as Service+import Utility.SafeCommand++import Data.List++-- | Exports all git repos in a directory (that user nobody can read)+-- using git-daemon, run from inetd.+--+-- Note that reverting this property does not remove or stop inetd.+daemonRunning :: FilePath -> RevertableProperty+daemonRunning exportdir = RevertableProperty setup unsetup+ where+ setup = containsLine conf (mkl "tcp4")+ `requires`+ containsLine conf (mkl "tcp6")+ `requires`+ dirExists exportdir+ `requires`+ Apt.serviceInstalledRunning "openbsd-inetd"+ `onChange`+ Service.running "openbsd-inetd"+ `describe` ("git-daemon exporting " ++ exportdir)+ unsetup = lacksLine conf (mkl "tcp4")+ `requires`+ lacksLine conf (mkl "tcp6")+ `onChange`+ Service.reloaded "openbsd-inetd"++ conf = "/etc/inetd.conf"++ mkl tcpv = intercalate "\t"+ [ "git"+ , "stream"+ , tcpv+ , "nowait"+ , "nobody"+ , "/usr/bin/git"+ , "git"+ , "daemon"+ , "--inetd"+ , "--export-all"+ , "--base-path=" ++ exportdir+ , exportdir+ ]++installed :: Property+installed = Apt.installed ["git"]++type RepoUrl = String++type Branch = String++-- | Specified git repository is cloned to the specified directory.+--+-- If the firectory exists with some other content, it will be recursively+-- deleted.+--+-- A branch can be specified, to check out.+cloned :: UserName -> RepoUrl -> FilePath -> Maybe Branch -> Property+cloned owner url dir mbranch = check originurl (property desc checkout)+ `requires` installed+ where+ desc = "git cloned " ++ url ++ " to " ++ dir+ gitconfig = dir </> ".git/config"+ originurl = ifM (doesFileExist gitconfig)+ ( do+ v <- catchDefaultIO Nothing $ headMaybe . lines <$>+ readProcess "git" ["config", "--file", gitconfig, "remote.origin.url"]+ return (v /= Just url)+ , return True+ )+ checkout = do+ liftIO $ do+ whenM (doesDirectoryExist dir) $+ removeDirectoryRecursive dir+ createDirectoryIfMissing True (takeDirectory dir)+ ensureProperty $ userScriptProperty owner $ catMaybes+ -- The </dev/null fixes an intermittent+ -- "fatal: read error: Bad file descriptor"+ -- when run across ssh with propellor --spin+ [ Just $ "git clone " ++ shellEscape url ++ " " ++ shellEscape dir ++ " < /dev/null"+ , Just $ "cd " ++ shellEscape dir+ , ("git checkout " ++) <$> mbranch+ -- In case this repo is exposted via the web,+ -- although the hook to do this ongoing is not+ -- installed here.+ , Just "git update-server-info"+ ]
+ src/Propellor/Property/Gpg.hs view
@@ -0,0 +1,41 @@+module Propellor.Property.Gpg where++import Propellor+import qualified Propellor.Property.Apt as Apt+import Utility.FileSystemEncoding++import System.PosixCompat++installed :: Property+installed = Apt.installed ["gnupg"]++-- | Sets up a user with a gpg key from the privdata.+--+-- Note that if a secret key is exported using gpg -a --export-secret-key,+-- the public key is also included. Or just a public key could be+-- exported, and this would set it up just as well.+--+-- Recommend only using this for low-value dedicated role keys.+-- No attempt has been made to scrub the key out of memory once it's used.+--+-- The GpgKeyId does not have to be a numeric id; it can just as easily+-- be a description of the key.+keyImported :: GpgKeyId -> UserName -> Property+keyImported keyid user = flagFile' (property desc go) genflag+ `requires` installed+ where+ desc = user ++ " has gpg key " ++ show keyid+ genflag = do+ d <- dotDir user+ return $ d </> ".propellor-imported-keyid-" ++ keyid+ go = withPrivData (GpgKey keyid) $ \key -> makeChange $+ withHandle StdinHandle createProcessSuccess+ (proc "su" ["-c", "gpg --import", user]) $ \h -> do+ fileEncoding h+ hPutStr h key+ hClose h++dotDir :: UserName -> IO FilePath+dotDir user = do+ home <- homeDirectory <$> getUserEntryForName user+ return $ home </> ".gnupg"
+ src/Propellor/Property/HostingProvider/CloudAtCost.hs view
@@ -0,0 +1,24 @@+module Propellor.Property.HostingProvider.CloudAtCost where++import Propellor+import qualified Propellor.Property.Hostname as Hostname+import qualified Propellor.Property.File as File+import qualified Propellor.Property.Ssh as Ssh+import qualified Propellor.Property.User as User++-- Clean up a system as installed by cloudatcost.com+decruft :: Property+decruft = propertyList "cloudatcost cleanup"+ [ Hostname.sane+ , Ssh.randomHostKeys+ , "worked around grub/lvm boot bug #743126" ==>+ "/etc/default/grub" `File.containsLine` "GRUB_DISABLE_LINUX_UUID=true"+ `onChange` cmdProperty "update-grub" []+ `onChange` cmdProperty "update-initramfs" ["-u"]+ , combineProperties "nuked cloudatcost cruft"+ [ File.notPresent "/etc/rc.local"+ , File.notPresent "/etc/init.d/S97-setup.sh"+ , User.nuked "user" User.YesReallyDeleteHome+ ]+ ]+
+ src/Propellor/Property/HostingProvider/DigitalOcean.hs view
@@ -0,0 +1,21 @@+module Propellor.Property.HostingProvider.DigitalOcean where++import Propellor+import qualified Propellor.Property.Apt as Apt+import qualified Propellor.Property.File as File++-- Digital Ocean does not provide any way to boot+-- the kernel provided by the distribution, except using kexec.+-- Without this, some old, and perhaps insecure kernel will be used.+--+-- Note that this only causes the new kernel to be loaded on reboot.+-- If the power is cycled, the old kernel still boots up.+-- TODO: detect this and reboot immediately?+distroKernel :: Property+distroKernel = propertyList "digital ocean distro kernel hack"+ [ Apt.installed ["grub-pc", "kexec-tools"]+ , "/etc/default/kexec" `File.containsLines`+ [ "LOAD_KEXEC=true"+ , "USE_GRUB_CONFIG=true"+ ] `describe` "kexec configured"+ ]
+ src/Propellor/Property/Hostname.hs view
@@ -0,0 +1,33 @@+module Propellor.Property.Hostname where++import Propellor+import qualified Propellor.Property.File as File++-- | Ensures that the hostname is set to the HostAttr value.+-- Configures /etc/hostname and the current hostname.+--+-- A FQDN also configures /etc/hosts, with an entry for 127.0.1.1, which is+-- standard at least on Debian to set the FDQN (127.0.0.1 is localhost).+sane :: Property+sane = property ("sane hostname") (ensureProperty . setTo =<< getHostName)++setTo :: HostName -> Property+setTo hn = combineProperties desc go+ where+ desc = "hostname " ++ hn+ (basehost, domain) = separate (== '.') hn++ go = catMaybes+ [ Just $ "/etc/hostname" `File.hasContent` [basehost]+ , if null domain+ then Nothing + else Just $ File.fileProperty desc+ addhostline "/etc/hosts"+ , Just $ trivial $ cmdProperty "hostname" [basehost]+ ]+ + hostip = "127.0.1.1"+ hostline = hostip ++ "\t" ++ hn ++ " " ++ basehost++ addhostline ls = hostline : filter (not . hashostip) ls+ hashostip l = headMaybe (words l) == Just hostip
+ src/Propellor/Property/Network.hs view
@@ -0,0 +1,30 @@+module Propellor.Property.Network where++import Propellor+import Propellor.Property.File++interfaces :: FilePath+interfaces = "/etc/network/interfaces"++-- | 6to4 ipv6 connection, should work anywhere+ipv6to4 :: Property+ipv6to4 = fileProperty "ipv6to4" go interfaces+ `onChange` ifUp "sit0"+ where+ go ls+ | all (`elem` ls) stanza = ls+ | otherwise = ls ++ stanza+ stanza =+ [ "# Automatically added by propeller"+ , "iface sit0 inet6 static"+ , "\taddress 2002:5044:5531::1"+ , "\tnetmask 64"+ , "\tgateway ::192.88.99.1"+ , "auto sit0"+ , "# End automatically added by propeller"+ ]++type Interface = String++ifUp :: Interface -> Property+ifUp iface = cmdProperty "ifup" [iface]
+ src/Propellor/Property/Obnam.hs view
@@ -0,0 +1,155 @@+module Propellor.Property.Obnam where++import Propellor+import qualified Propellor.Property.Apt as Apt+import qualified Propellor.Property.Cron as Cron+import Utility.SafeCommand++import Data.List++type ObnamParam = String++-- | An obnam repository can be used by multiple clients. Obnam uses+-- locking to allow only one client to write at a time. Since stale lock+-- files can prevent backups from happening, it's more robust, if you know+-- a repository has only one client, to force the lock before starting a+-- backup. Using OnlyClient allows propellor to do so when running obnam.+data NumClients = OnlyClient | MultipleClients+ deriving (Eq)++-- | Installs a cron job that causes a given directory to be backed+-- up, by running obnam with some parameters.+--+-- If the directory does not exist, or exists but is completely empty,+-- this Property will immediately restore it from an existing backup.+--+-- So, this property can be used to deploy a directory of content+-- to a host, while also ensuring any changes made to it get backed up.+-- And since Obnam encrypts, just make this property depend on a gpg+-- key, and tell obnam to use the key, and your data will be backed+-- up securely. For example: +--+-- > & Obnam.backup "/srv/git" "33 3 * * *"+-- > [ "--repository=sftp://2318@usw-s002.rsync.net/~/mygitrepos.obnam"+-- > , "--encrypt-with=1B169BE1"+-- > ] Obnam.OnlyClient+-- > `requires` Gpg.keyImported "1B169BE1" "root"+-- > `requires` Ssh.keyImported SshRsa "root"+--+-- How awesome is that?+backup :: FilePath -> Cron.CronTimes -> [ObnamParam] -> NumClients -> Property+backup dir crontimes params numclients = cronjob `describe` desc+ `requires` restored dir params+ where+ desc = dir ++ " backed up by obnam"+ cronjob = Cron.niceJob ("obnam_backup" ++ dir) crontimes "root" "/" $+ intercalate ";" $ catMaybes+ [ if numclients == OnlyClient+ then Just $ unwords $+ [ "obnam"+ , "force-lock"+ ] ++ map shellEscape params+ else Nothing+ , Just $ unwords $+ [ "obnam"+ , "backup"+ , shellEscape dir+ ] ++ map shellEscape params+ ]++-- | Restores a directory from an obnam backup.+--+-- Only does anything if the directory does not exist, or exists,+-- but is completely empty.+--+-- The restore is performed atomically; restoring to a temp directory+-- and then moving it to the directory.+restored :: FilePath -> [ObnamParam] -> Property+restored dir params = property (dir ++ " restored by obnam") go+ `requires` installed+ where+ go = ifM (liftIO needsRestore)+ ( do+ warningMessage $ dir ++ " is empty/missing; restoring from backup ..."+ liftIO restore+ , noChange+ )++ needsRestore = null <$> catchDefaultIO [] (dirContents dir)++ restore = withTmpDirIn (takeDirectory dir) "obnam-restore" $ \tmpdir -> do+ ok <- boolSystem "obnam" $+ [ Param "restore"+ , Param "--to"+ , Param tmpdir+ ] ++ map Param params+ let restoreddir = tmpdir ++ "/" ++ dir+ ifM (pure ok <&&> doesDirectoryExist restoreddir)+ ( do+ void $ tryIO $ removeDirectory dir+ renameDirectory restoreddir dir+ return MadeChange+ , return FailedChange+ )++installed :: Property+installed = Apt.installed ["obnam"]++-- | Ensures that a recent version of obnam gets installed.+--+-- Only does anything for Debian Stable.+latestVersion :: Property+latestVersion = withOS "obnam latest version" $ \o -> case o of+ (Just (System (Debian suite) _)) | isStable suite -> ensureProperty $+ Apt.setSourcesListD (sources suite) "obnam"+ `requires` toProp (Apt.trustsKey key)+ _ -> noChange+ where+ sources suite = + [ "deb http://code.liw.fi/debian " ++ Apt.showSuite suite ++ " main"+ ]+ -- gpg key used by the code.liw.fi repository.+ key = Apt.AptKey "obnam" $ unlines+ [ "-----BEGIN PGP PUBLIC KEY BLOCK-----"+ , "Version: GnuPG v1.4.9 (GNU/Linux)"+ , ""+ , "mQGiBEfzuTgRBACcVNG/H6QJqLx5qiQs2zmPe6D6BWOWHfgNgG4IWzNstm21YDxb"+ , "KqwFG0gxcnZJGHkXAhkSfqTokYd0lc5eBemcA1pkceNjzMEX8wwiZ810HzJD4eEH"+ , "sjoWR8+qKrZeixzZqReAfqztcXoBGKQ0u1R1vpg1txUa75OM4BUqaUbsmwCgmS4x"+ , "DjMxSaUSPuu6vQ7ZGZBXSP0D/RQw8DBHMfsv3DiaqFqk8tkuUkpMFPIekHidSHlO"+ , "EACbncqbbyHksyCpFNVNcQIDHrOLjOZK9BAXkSd8I3ww7U+nLdDcCblrW8CZnJtm"+ , "ZYrxfaXaHZ/It9/RCAsQ+c8xtmyUPjsf//4Vf8olxNQHzgBSe5/LJRi4Vd53he+K"+ , "YP4LA/9IZbjvVmm8+8Y0pQrTHlI6nTImtzdBXHc4+T3lLBj9XODHLozC2kSBOQky"+ , "q/EisTITHTXL8vYg4NsKm5RTbPAuBwdtxcny8CXfOqKtGOdrebmKotGllTozzdPv"+ , "9p53cuce6oJ2oMUodc074JOGTWwDSgLiJX4nViGcU1wy/vtQnrQkY29kZS5saXcu"+ , "ZmkgYXJjaGl2ZSBrZXkgPGxpd0BsaXcuZmk+iGAEExECACAFAkfzuTgCGwMGCwkI"+ , "BwMCBBUCCAMEFgIDAQIeAQIXgAAKCRBG53tJR95LscKrAJ0ZtKqa2x6Kplwa2mzx"+ , "ItImbIGMJACdETqofDYzUN91yLAFlOnxAyrE+UyIRgQQEQIABgUCSFd5GgAKCRAf"+ , "u5W/LZrMjqr8AJ4xPVHpW8ZNlgMwDSVb075RnA2DiACgg2SR69jAHFQOWV6xfLRr"+ , "vh0bLKGJAhwEEAEIAAYFAktEyIwACgkQ61zh116FEfm7Lg//Wiy3TjWAk8YHUddv"+ , "zOioYzCxQ985GsVhJGAVPqSGOc9vfTWBJZ8J3l0NnYTRpEGucmbF9G+mAt9iGXu6"+ , "7yZkxyFdvbo7EDsqMU1wLOM6PiU+Un63MKlbTNmFn7OKE8aXPRAFgcyUO/qjdqoD"+ , "sa9FgU5Z0f60m9qah6BPXH6IzMLHYoiP7t8rCBIwLgyl3w2w+Fjt1DFpbW9Kb7jz"+ , "i8jFvC8jPmxV8xh2OSgVZyNk4qg6hIV8GVQY7AJt8OurZSckgQd7ifHK9JTGohtF"+ , "tXCiqeDEvnMF4A9HI/TcXJBzonZ8ds1JCq42nSSKmL+8TyjtUSD/xHygazuc0CK0"+ , "hFnQWBub60IfyV6F0oTagJ8cmARv2sezHAeHDkzPHE8RdjgktazH1eJrA4LheEd6"+ , "KeSnVtYWpw8dgMv5PleFyQiAj/t3C/N50fd15tUyfnH15G7nFjMQV2Yx35uwSxOj"+ , "376OWnDN/YGTNk283XXULbyVJYR8Q2unso20XQ94yQ2A5EpHHPrHoLxrL/ydM08d"+ , "nvKstLZIZtal1seiMkymtlSiGz25A5oqsclwS6VZCKdWA8HO/wlElOMcaHyl6Y1y"+ , "gYP7y9O5yFYKFOrCH0nFjJbwmkRiBLsxuuWsYgJigVGq/atSrtawkHdshpCw0HCY"+ , "N/RFcWkJ864BdsO0C0sDzueNkQO5Ag0ER/O5RBAIAJiwPH9tyJTgXcC2Y4XWboOq"+ , "rx5CkOnr5b45oS9cK2eIJ8TKxE3XgKLxUr3mIH0QR2kZgDOwNl0WY+7/CXjn+Spn"+ , "BokPg54rafEUePodGpGdUXdgrHhAMHYjh8fXFJ1SlQcg46/zc1wDI7jBCkGrK3V8"+ , "5cXDqwTFTN5LcjoSRWeM4Voa6pEfDdL3rMlnOw9R9gDHRBBb6CDSjWXqM86pR889"+ , "5QrR0SDwiJNrMoyxSjMXFKGBQAsYHJ82myZrlbuZbroZjVp5Uh7eB1ZiPljNVtcr"+ , "sksACIWBCo1rvLzrPXsLYOeV3cDDtYAkSwGfuzC1Etbe+qgfIroFTOqdefMw4s8A"+ , "AwUH/0KLXm4MS54QQspg3evu4Q4U/E8Hem5/FqB0GhBCitQ4rUsucKyY8/ItpUn5"+ , "ismLE60bQqka+Mzd/Zw18TCTzImv0ozAaZ2sNtBado7f6jcC8EDfY5zzK1ukcsAr"+ , "Qc5hdLHYuTQW5KpA6fKaW969OUzIwPbdVaCOLOBpxKC6N6iBspQYd6uiQtLw6EUO"+ , "50oQqUiJABf0eOocvdw5e2KQQpuC3205+VMYtyl4w3pdJihK8NK0AikGXzDVsbQt"+ , "l8kmB5ZrN4WIKhMke1FxbqQC5Q3XATvYRzpzzisZb/HYGNti8W6du5EUwJ0D2NRh"+ , "cu+twocOzW0VKfmrDApfifJ9OsSISQQYEQIACQUCR/O5RAIbDAAKCRBG53tJR95L"+ , "seQOAJ95KUyzjRjdYgZkDC69Mgu25L86UACdGduINUaRly43ag4kwUXxpqswBBM="+ , "=i2c3"+ , "-----END PGP PUBLIC KEY BLOCK-----"+ ]
+ src/Propellor/Property/OpenId.hs view
@@ -0,0 +1,29 @@+module Propellor.Property.OpenId where++import Propellor+import qualified Propellor.Property.File as File+import qualified Propellor.Property.Apt as Apt+import qualified Propellor.Property.Service as Service++import Data.List++providerFor :: [UserName] -> String -> Property+providerFor users baseurl = propertyList desc $+ [ Apt.serviceInstalledRunning "apache2"+ , Apt.installed ["simpleid"]+ `onChange` Service.restarted "apache2"+ , File.fileProperty (desc ++ " configured")+ (map setbaseurl) "/etc/simpleid/config.inc"+ ] ++ map identfile users+ where+ url = "http://"++baseurl++"/simpleid"+ desc = "openid provider " ++ url+ setbaseurl l+ | "SIMPLEID_BASE_URL" `isInfixOf` l = + "define('SIMPLEID_BASE_URL', '"++url++"');"+ | otherwise = l+ + -- the identitites directory controls access, so open up+ -- file mode+ identfile u = File.hasPrivContentExposed $+ concat $ [ "/var/lib/simpleid/identities/", u, ".identity" ]
+ src/Propellor/Property/Postfix.hs view
@@ -0,0 +1,25 @@+module Propellor.Property.Postfix where++import Propellor+import qualified Propellor.Property.Apt as Apt++installed :: Property+installed = Apt.serviceInstalledRunning "postfix"++-- | Configures postfix as a satellite system, which +-- relats all mail through a relay host, which defaults to smtp.domain. +--+-- The smarthost may refuse to relay mail on to other domains, without+-- futher coniguration/keys. But this should be enough to get cron job+-- mail flowing to a place where it will be seen.+satellite :: Property+satellite = setup `requires` installed+ where+ setup = trivial $ property "postfix satellite system" $ do+ hn <- getHostName+ ensureProperty $ Apt.reConfigure "postfix"+ [ ("postfix/main_mailer_type", "select", "Satellite system")+ , ("postfix/root_address", "string", "root")+ , ("postfix/destinations", "string", " ")+ , ("postfix/mailname", "string", hn)+ ]
+ src/Propellor/Property/Reboot.hs view
@@ -0,0 +1,7 @@+module Propellor.Property.Reboot where++import Propellor++now :: Property+now = cmdProperty "reboot" []+ `describe` "reboot now"
+ src/Propellor/Property/Scheduled.hs view
@@ -0,0 +1,67 @@+module Propellor.Property.Scheduled+ ( period+ , periodParse+ , Recurrance(..)+ , WeekDay+ , MonthDay+ , YearDay+ ) where++import Propellor+import Utility.Scheduled++import Data.Time.Clock+import Data.Time.LocalTime+import qualified Data.Map as M++-- | Makes a Property only be checked every so often.+--+-- This uses the description of the Property to keep track of when it was+-- last run.+period :: Property -> Recurrance -> Property+period prop recurrance = flip describe desc $ adjustProperty prop $ \satisfy -> do+ lasttime <- liftIO $ getLastChecked (propertyDesc prop)+ nexttime <- liftIO $ fmap startTime <$> nextTime schedule lasttime+ t <- liftIO localNow+ if Just t >= nexttime+ then do+ r <- satisfy+ liftIO $ setLastChecked t (propertyDesc prop)+ return r+ else noChange+ where+ schedule = Schedule recurrance AnyTime+ desc = propertyDesc prop ++ " (period " ++ fromRecurrance recurrance ++ ")"++-- | Like period, but parse a human-friendly string.+periodParse :: Property -> String -> Property+periodParse prop s = case toRecurrance s of+ Just recurrance -> period prop recurrance+ Nothing -> property "periodParse" $ do+ liftIO $ warningMessage $ "failed periodParse: " ++ s+ noChange++lastCheckedFile :: FilePath+lastCheckedFile = localdir </> ".lastchecked"++getLastChecked :: Desc -> IO (Maybe LocalTime)+getLastChecked desc = M.lookup desc <$> readLastChecked++localNow :: IO LocalTime+localNow = do+ now <- getCurrentTime+ tz <- getTimeZone now+ return $ utcToLocalTime tz now++setLastChecked :: LocalTime -> Desc -> IO ()+setLastChecked time desc = do+ m <- readLastChecked+ writeLastChecked (M.insert desc time m)++readLastChecked :: IO (M.Map Desc LocalTime)+readLastChecked = fromMaybe M.empty <$> catchDefaultIO Nothing go+ where+ go = readish <$> readFileStrict lastCheckedFile++writeLastChecked :: M.Map Desc LocalTime -> IO ()+writeLastChecked = writeFile lastCheckedFile . show
+ src/Propellor/Property/Service.hs view
@@ -0,0 +1,31 @@+module Propellor.Property.Service where++import Propellor+import Utility.SafeCommand++type ServiceName = String++-- | Ensures that a service is running. Does not ensure that+-- any package providing that service is installed. See+-- Apt.serviceInstalledRunning+--+-- Note that due to the general poor state of init scripts, the best+-- we can do is try to start the service, and if it fails, assume+-- this means it's already running.+running :: ServiceName -> Property+running svc = property ("running " ++ svc) $ do+ void $ ensureProperty $+ scriptProperty ["service " ++ shellEscape svc ++ " start >/dev/null 2>&1 || true"]+ return NoChange++restarted :: ServiceName -> Property+restarted svc = property ("restarted " ++ svc) $ do+ void $ ensureProperty $+ scriptProperty ["service " ++ shellEscape svc ++ " restart >/dev/null 2>&1 || true"]+ return NoChange++reloaded :: ServiceName -> Property+reloaded svc = property ("reloaded " ++ svc) $ do+ void $ ensureProperty $+ scriptProperty ["service " ++ shellEscape svc ++ " reload >/dev/null 2>&1 || true"]+ return NoChange
+ src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs view
@@ -0,0 +1,176 @@+module Propellor.Property.SiteSpecific.GitAnnexBuilder where++import Propellor+import qualified Propellor.Property.Apt as Apt+import qualified Propellor.Property.User as User+import qualified Propellor.Property.Cron as Cron+import qualified Propellor.Property.Ssh as Ssh+import qualified Propellor.Property.File as File+import qualified Propellor.Property.Docker as Docker+import Propellor.Property.Cron (CronTimes)++builduser :: UserName+builduser = "builder"++homedir :: FilePath+homedir = "/home/builder"++gitbuilderdir :: FilePath+gitbuilderdir = homedir </> "gitbuilder"++builddir :: FilePath+builddir = gitbuilderdir </> "build"++type TimeOut = String -- eg, 5h++autobuilder :: CronTimes -> TimeOut -> Bool -> Property+autobuilder crontimes timeout rsyncupload = combineProperties "gitannexbuilder"+ [ Apt.serviceInstalledRunning "cron"+ , Cron.niceJob "gitannexbuilder" crontimes builduser gitbuilderdir $+ "git pull ; timeout " ++ timeout ++ " ./autobuild"+ -- The builduser account does not have a password set,+ -- instead use the password privdata to hold the rsync server+ -- password used to upload the built image.+ , property "rsync password" $ do+ let f = homedir </> "rsyncpassword"+ if rsyncupload + then withPrivData (Password builduser) $ \p -> do+ oldp <- liftIO $ catchDefaultIO "" $+ readFileStrict f+ if p /= oldp+ then makeChange $ writeFile f p+ else noChange+ else do+ ifM (liftIO $ doesFileExist f)+ ( noChange+ , makeChange $ writeFile f "no password configured"+ )+ ]++tree :: Architecture -> Property+tree buildarch = combineProperties "gitannexbuilder tree"+ [ Apt.installed ["git"]+ -- gitbuilderdir directory already exists when docker volume is used,+ -- but with wrong owner.+ , File.dirExists gitbuilderdir+ , File.ownerGroup gitbuilderdir builduser builduser+ , check (not <$> (doesDirectoryExist (gitbuilderdir </> ".git"))) $ + userScriptProperty builduser+ [ "git clone git://git.kitenet.net/gitannexbuilder " ++ gitbuilderdir+ , "cd " ++ gitbuilderdir+ , "git checkout " ++ buildarch+ ]+ `describe` "gitbuilder setup"+ , check (not <$> doesDirectoryExist builddir) $ userScriptProperty builduser+ [ "git clone git://git-annex.branchable.com/ " ++ builddir+ ]+ ]++buildDepsApt :: Property+buildDepsApt = combineProperties "gitannexbuilder build deps"+ [ Apt.buildDep ["git-annex"]+ , buildDepsFewHaskellLibs+ , "git-annex source build deps installed" ==> Apt.buildDepIn builddir+ ]++buildDepsFewHaskellLibs :: Property+buildDepsFewHaskellLibs = combineProperties "gitannexbuilder build deps"+ [ buildDepsNoHaskellLibs+ -- these haskell libs depend on C libs and don't use TH+ , Apt.installed ["libghc-dbus-dev", "libghc-fdo-notify-dev", "libghc-network-protocol-xmpp-dev"]+ ]++buildDepsNoHaskellLibs :: Property+buildDepsNoHaskellLibs = Apt.installed+ ["git", "rsync", "moreutils", "ca-certificates",+ "debhelper", "ghc", "curl", "openssh-client", "git-remote-gcrypt",+ "liblockfile-simple-perl", "cabal-install", "vim", "less",+ "alex", "happy", "c2hs"+ ]++-- Installs current versions of git-annex's deps from cabal, but only+-- does so once.+cabalDeps :: Property+cabalDeps = flagFile go cabalupdated+ where+ go = userScriptProperty builduser ["cabal update && cabal install git-annex --only-dependencies || true"]+ cabalupdated = homedir </> ".cabal" </> "packages" </> "hackage.haskell.org" </> "00-index.cache"++standardAutoBuilderContainer :: (System -> Docker.Image) -> Architecture -> Int -> TimeOut -> Host+standardAutoBuilderContainer dockerImage arch buildminute timeout = Docker.container (arch ++ "-git-annex-builder")+ (dockerImage $ System (Debian Unstable) arch)+ & Apt.stdSourcesList Unstable+ & Apt.unattendedUpgrades+ & buildDepsApt+ & autobuilder (show buildminute ++ " * * * *") timeout True+ `requires` tree arch++androidAutoBuilderContainer :: (System -> Docker.Image) -> Cron.CronTimes -> TimeOut -> Host+androidAutoBuilderContainer dockerImage crontimes timeout =+ androidContainer dockerImage "android-git-annex-builder" (tree "android") builddir+ & Apt.unattendedUpgrades+ & autobuilder crontimes timeout True++-- Android is cross-built in a Debian i386 container, using the Android NDK.+androidContainer :: (System -> Docker.Image) -> Docker.ContainerName -> Property -> FilePath -> Host+androidContainer dockerImage name setupgitannexdir gitannexdir = Docker.container name+ (dockerImage $ System (Debian Stable) "i386")+ & Apt.stdSourcesList Stable+ & User.accountFor builduser+ & File.dirExists gitbuilderdir+ & File.ownerGroup homedir builduser builduser+ & buildDepsNoHaskellLibs+ & flagFile chrootsetup ("/chrootsetup")+ `requires` setupgitannexdir+ -- TODO: automate installing haskell libs+ -- (Currently have to run+ -- git-annex/standalone/android/install-haskell-packages+ -- which is not fully automated.)+ where+ -- Use git-annex's android chroot setup script, which will install+ -- ghc-android and the NDK, all build deps, etc, in the home+ -- directory of the builder user.+ chrootsetup = scriptProperty+ [ "cd " ++ gitannexdir ++ " && ./standalone/android/buildchroot-inchroot"+ ]++-- armel builder has a companion container using amd64 that+-- runs the build first to get TH splices. They need+-- to have the same versions of all haskell libraries installed.+armelCompanionContainer :: (System -> Docker.Image) -> Host+armelCompanionContainer dockerImage = Docker.container "armel-git-annex-builder-companion"+ (dockerImage $ System (Debian Unstable) "amd64")+ & Apt.stdSourcesList Unstable+ & Apt.unattendedUpgrades+ -- This volume is shared with the armel builder.+ & Docker.volume gitbuilderdir+ -- Install current versions of build deps from cabal.+ & tree "armel"+ & buildDepsFewHaskellLibs+ & cabalDeps+ -- The armel builder can ssh to this companion.+ & Docker.expose "22"+ & Apt.serviceInstalledRunning "ssh"+ & Ssh.authorizedKeys builduser++armelAutoBuilderContainer :: (System -> Docker.Image) -> Cron.CronTimes -> TimeOut -> Host+armelAutoBuilderContainer dockerImage crontimes timeout = Docker.container "armel-git-annex-builder"+ (dockerImage $ System (Debian Unstable) "armel")+ & Apt.stdSourcesList Unstable+ & Apt.unattendedUpgrades+ & Apt.installed ["openssh-client"]+ & Docker.link "armel-git-annex-builder-companion" "companion"+ & Docker.volumes_from "armel-git-annex-builder-companion"+ -- TODO: automate installing haskell libs+ -- (Currently have to run+ -- git-annex/standalone/linux/install-haskell-packages+ -- which is not fully automated.)+ & buildDepsFewHaskellLibs+ & autobuilder crontimes timeout True+ `requires` tree "armel"+ & Ssh.keyImported SshRsa builduser+ & trivial writecompanionaddress+ where+ writecompanionaddress = scriptProperty+ [ "echo \"$COMPANION_PORT_22_TCP_ADDR\" > " ++ homedir </> "companion_address"+ ] `describe` "companion_address file"
+ src/Propellor/Property/SiteSpecific/GitHome.hs view
@@ -0,0 +1,34 @@+module Propellor.Property.SiteSpecific.GitHome where++import Propellor+import qualified Propellor.Property.Apt as Apt+import Propellor.Property.User+import Utility.SafeCommand++-- | Clones Joey Hess's git home directory, and runs its fixups script.+installedFor :: UserName -> Property+installedFor user = check (not <$> hasGitDir user) $ + property ("githome " ++ user) (go =<< liftIO (homedir user))+ `requires` Apt.installed ["git"]+ where+ go home = do+ let tmpdir = home </> "githome"+ ensureProperty $ combineProperties "githome setup"+ [ userScriptProperty user ["git clone " ++ url ++ " " ++ tmpdir]+ , property "moveout" $ makeChange $ void $+ moveout tmpdir home+ , property "rmdir" $ makeChange $ void $+ catchMaybeIO $ removeDirectory tmpdir+ , userScriptProperty user ["rm -rf .aptitude/ .bashrc .profile; bin/mr checkout; bin/fixups"]+ ]+ moveout tmpdir home = do+ fs <- dirContents tmpdir+ forM fs $ \f -> boolSystem "mv" [File f, File home]++url :: String+url = "git://git.kitenet.net/joey/home"++hasGitDir :: UserName -> IO Bool+hasGitDir user = go =<< homedir user+ where+ go home = doesDirectoryExist (home </> ".git")
+ src/Propellor/Property/SiteSpecific/JoeySites.hs view
@@ -0,0 +1,319 @@+-- | Specific configuation for Joey Hess's sites. Probably not useful to+-- others except as an example.++module Propellor.Property.SiteSpecific.JoeySites where++import Propellor+import qualified Propellor.Property.Apt as Apt+import qualified Propellor.Property.File as File+import qualified Propellor.Property.Gpg as Gpg+import qualified Propellor.Property.Ssh as Ssh+import qualified Propellor.Property.Git as Git+import qualified Propellor.Property.Cron as Cron+import qualified Propellor.Property.Service as Service+import qualified Propellor.Property.User as User+import qualified Propellor.Property.Obnam as Obnam+import qualified Propellor.Property.Apache as Apache+import Utility.SafeCommand+import Utility.FileMode++import Data.List+import System.Posix.Files++oldUseNetServer :: [Host] -> Property+oldUseNetServer hosts = propertyList ("olduse.net server")+ [ oldUseNetInstalled "oldusenet-server"+ , Obnam.latestVersion+ , Obnam.backup datadir "33 4 * * *"+ [ "--repository=sftp://2318@usw-s002.rsync.net/~/olduse.net"+ , "--client-name=spool"+ ] Obnam.OnlyClient+ `requires` Ssh.keyImported SshRsa "root"+ `requires` Ssh.knownHost hosts "usw-s002.rsync.net" "root"+ , check (not . isSymbolicLink <$> getSymbolicLinkStatus newsspool) $+ property "olduse.net spool in place" $ makeChange $ do+ removeDirectoryRecursive newsspool+ createSymbolicLink (datadir </> "news") newsspool+ , Apt.installed ["leafnode"]+ , "/etc/news/leafnode/config" `File.hasContent` + [ "# olduse.net configuration (deployed by propellor)"+ , "expire = 1000000" -- no expiry via texpire+ , "server = " -- no upstream server+ , "debugmode = 1"+ , "allowSTRANGERS = 42" -- lets anyone connect+ , "nopost = 1" -- no new posting (just gather them)+ ]+ , "/etc/hosts.deny" `File.lacksLine` "leafnode: ALL"+ , Apt.serviceInstalledRunning "openbsd-inetd"+ , File.notPresent "/etc/cron.daily/leafnode"+ , File.notPresent "/etc/cron.d/leafnode"+ , Cron.niceJob "oldusenet-expire" "11 1 * * *" "news" newsspool $ intercalate ";"+ [ "find \\( -path ./out.going -or -path ./interesting.groups -or -path './*/.overview' \\) -prune -or -type f -ctime +60 -print | xargs --no-run-if-empty rm"+ , "find -type d -empty | xargs --no-run-if-empty rmdir"+ ]+ , Cron.niceJob "oldusenet-uucp" "*/5 * * * *" "news" "/" $+ "/usr/bin/uucp " ++ datadir+ , toProp $ Apache.siteEnabled "nntp.olduse.net" $ apachecfg "nntp.olduse.net" False+ [ " DocumentRoot " ++ datadir ++ "/"+ , " <Directory " ++ datadir ++ "/>"+ , " Options Indexes FollowSymlinks"+ , " AllowOverride None"+ -- I had this in the file before.+ -- This may be needed by a newer version of apache?+ --, " Require all granted"+ , " </Directory>"+ ]+ ]+ where+ newsspool = "/var/spool/news"+ datadir = "/var/spool/oldusenet"++oldUseNetShellBox :: Property+oldUseNetShellBox = oldUseNetInstalled "oldusenet"++oldUseNetInstalled :: Apt.Package -> Property+oldUseNetInstalled pkg = check (not <$> Apt.isInstalled pkg) $+ propertyList ("olduse.net " ++ pkg)+ [ Apt.installed (words "build-essential devscripts debhelper git libncursesw5-dev libpcre3-dev pkg-config bison libicu-dev libidn11-dev libcanlock2-dev libuu-dev ghc libghc-strptime-dev libghc-hamlet-dev libghc-ifelse-dev libghc-hxt-dev libghc-utf8-string-dev libghc-missingh-dev libghc-sha-dev")+ `describe` "olduse.net build deps"+ , scriptProperty+ [ "rm -rf /root/tmp/oldusenet" -- idenpotency+ , "git clone git://olduse.net/ /root/tmp/oldusenet/source"+ , "cd /root/tmp/oldusenet/source/"+ , "dpkg-buildpackage -us -uc"+ , "dpkg -i ../" ++ pkg ++ "_*.deb || true"+ , "apt-get -fy install" -- dependencies+ , "rm -rf /root/tmp/oldusenet"+ ] `describe` "olduse.net built"+ ]+++kgbServer :: Property+kgbServer = withOS desc $ \o -> case o of+ (Just (System (Debian Unstable) _)) ->+ ensureProperty $ propertyList desc+ [ Apt.serviceInstalledRunning "kgb-bot"+ , File.hasPrivContent "/etc/kgb-bot/kgb.conf"+ `onChange` Service.restarted "kgb-bot"+ , "/etc/default/kgb-bot" `File.containsLine` "BOT_ENABLED=1"+ `describe` "kgb bot enabled"+ `onChange` Service.running "kgb-bot"+ ]+ _ -> error "kgb server needs Debian unstable (for kgb-bot 1.31+)"+ where+ desc = "kgb.kitenet.net setup"++mumbleServer :: [Host] -> Property+mumbleServer hosts = combineProperties "mumble.debian.net" + [ Apt.serviceInstalledRunning "mumble-server"+ , Obnam.latestVersion+ , Obnam.backup "/var/lib/mumble-server" "55 5 * * *"+ [ "--repository=sftp://joey@turtle.kitenet.net/~/lib/backup/mumble.debian.net.obnam"+ , "--client-name=mumble"+ ] Obnam.OnlyClient+ `requires` Ssh.keyImported SshRsa "root"+ `requires` Ssh.knownHost hosts "turtle.kitenet.net" "root"+ , trivial $ cmdProperty "chown" ["-R", "mumble-server:mumble-server", "/var/lib/mumble-server"]+ ]++obnamLowMem :: Property+obnamLowMem = combineProperties "obnam tuned for low memory use"+ [ Obnam.latestVersion+ , "/etc/obnam.conf" `File.containsLines`+ [ "[config]"+ , "# Suggested by liw to keep Obnam memory consumption down (at some speed cost)."+ , "upload-queue-size = 128"+ , "lru-size = 128"+ ]+ ]++-- git.kitenet.net and git.joeyh.name+gitServer :: [Host] -> Property+gitServer hosts = propertyList "git.kitenet.net setup"+ [ Obnam.latestVersion+ , Obnam.backup "/srv/git" "33 3 * * *"+ [ "--repository=sftp://2318@usw-s002.rsync.net/~/git.kitenet.net"+ , "--encrypt-with=1B169BE1"+ , "--client-name=wren"+ ] Obnam.OnlyClient+ `requires` Gpg.keyImported "1B169BE1" "root"+ `requires` Ssh.keyImported SshRsa "root"+ `requires` Ssh.knownHost hosts "usw-s002.rsync.net" "root"+ `requires` Ssh.authorizedKeys "family"+ `requires` User.accountFor "family"+ , Apt.installed ["git", "rsync", "gitweb"]+ -- backport avoids channel flooding on branch merge+ , Apt.installedBackport ["kgb-client"]+ -- backport supports ssh event notification+ , Apt.installedBackport ["git-annex"]+ , File.hasPrivContentExposed "/etc/kgb-bot/kgb-client.conf"+ , toProp $ Git.daemonRunning "/srv/git"+ , "/etc/gitweb.conf" `File.containsLines`+ [ "$projectroot = '/srv/git';"+ , "@git_base_url_list = ('git://git.kitenet.net', 'http://git.kitenet.net/git', 'https://git.kitenet.net/git', 'ssh://git.kitenet.net/srv/git');"+ , "# disable snapshot download; overloads server"+ , "$feature{'snapshot'}{'default'} = [];"+ ]+ `describe` "gitweb configured"+ -- Repos push on to github.+ , Ssh.knownHost hosts "github.com" "joey"+ -- I keep the website used for gitweb checked into git..+ , Git.cloned "root" "/srv/git/joey/git.kitenet.net.git" "/srv/web/git.kitenet.net" Nothing+ , website "git.kitenet.net"+ , website "git.joeyh.name"+ , toProp $ Apache.modEnabled "cgi"+ ]+ where+ website hn = toProp $ Apache.siteEnabled hn $ apachecfg hn True+ [ " DocumentRoot /srv/web/git.kitenet.net/"+ , " <Directory /srv/web/git.kitenet.net/>"+ , " Options Indexes ExecCGI FollowSymlinks"+ , " AllowOverride None"+ , " AddHandler cgi-script .cgi"+ , " DirectoryIndex index.cgi"+ , " </Directory>"+ , ""+ , " ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/"+ , " <Directory /usr/lib/cgi-bin>"+ , " SetHandler cgi-script"+ , " Options ExecCGI"+ , " </Directory>"+ ]++type AnnexUUID = String++-- | A website, with files coming from a git-annex repository.+annexWebSite :: [Host] -> Git.RepoUrl -> HostName -> AnnexUUID -> [(String, Git.RepoUrl)] -> Property+annexWebSite hosts origin hn uuid remotes = propertyList (hn ++" website using git-annex")+ [ Git.cloned "joey" origin dir Nothing+ `onChange` setup+ , postupdatehook `File.hasContent`+ [ "#!/bin/sh"+ , "exec git update-server-info"+ ] `onChange`+ (postupdatehook `File.mode` (combineModes (ownerWriteMode:readModes ++ executeModes)))+ , setupapache+ ]+ where+ dir = "/srv/web/" ++ hn+ postupdatehook = dir </> ".git/hooks/post-update"+ setup = userScriptProperty "joey" setupscript+ `requires` Ssh.keyImported SshRsa "joey"+ `requires` Ssh.knownHost hosts "turtle.kitenet.net" "joey"+ setupscript = + [ "cd " ++ shellEscape dir+ , "git config annex.uuid " ++ shellEscape uuid+ ] ++ map addremote remotes +++ [ "git annex get"+ ]+ addremote (name, url) = "git remote add " ++ shellEscape name ++ " " ++ shellEscape url+ setupapache = toProp $ Apache.siteEnabled hn $ apachecfg hn True $ + [ " ServerAlias www."++hn+ , ""+ , " DocumentRoot /srv/web/"++hn+ , " <Directory /srv/web/"++hn++">"+ , " Options FollowSymLinks"+ , " AllowOverride None"+ , " </Directory>"+ , " <Directory /srv/web/"++hn++">"+ , " Options Indexes FollowSymLinks ExecCGI"+ , " AllowOverride None"+ , " AddHandler cgi-script .cgi"+ , " DirectoryIndex index.html index.cgi"+ , " Order allow,deny"+ , " allow from all"+ , " </Directory>"+ ]++apachecfg :: HostName -> Bool -> Apache.ConfigFile -> Apache.ConfigFile+apachecfg hn withssl middle+ | withssl = vhost False ++ vhost True+ | otherwise = vhost False+ where+ vhost ssl = + [ "<VirtualHost *:"++show port++">"+ , " ServerAdmin grue@joeyh.name"+ , " ServerName "++hn++":"++show port+ ]+ ++ mainhttpscert ssl+ ++ middle +++ [ ""+ , " ErrorLog /var/log/apache2/error.log"+ , " LogLevel warn"+ , " CustomLog /var/log/apache2/access.log combined"+ , " ServerSignature On"+ , " "+ , " <Directory \"/usr/share/apache2/icons\">"+ , " Options Indexes MultiViews"+ , " AllowOverride None"+ , " Order allow,deny"+ , " Allow from all"+ , " </Directory>"+ , "</VirtualHost>"+ ]+ where+ port = if ssl then 443 else 80 :: Int++mainhttpscert :: Bool -> Apache.ConfigFile+mainhttpscert False = []+mainhttpscert True = + [ " SSLEngine on"+ , " SSLCertificateFile /etc/ssl/certs/web.pem"+ , " SSLCertificateKeyFile /etc/ssl/private/web.pem"+ , " SSLCertificateChainFile /etc/ssl/certs/startssl.pem"+ ]+ +gitAnnexDistributor :: Property+gitAnnexDistributor = combineProperties "git-annex distributor, including rsync server and signer"+ [ Apt.installed ["rsync"]+ , File.hasPrivContent "/etc/rsyncd.conf"+ `onChange` Service.restarted "rsync"+ , File.hasPrivContent "/etc/rsyncd.secrets"+ `onChange` Service.restarted "rsync"+ , "/etc/default/rsync" `File.containsLine` "RSYNC_ENABLE=true"+ `onChange` Service.running "rsync"+ , endpoint "/srv/web/downloads.kitenet.net/git-annex/autobuild"+ , endpoint "/srv/web/downloads.kitenet.net/git-annex/autobuild/x86_64-apple-mavericks"+ -- git-annex distribution signing key+ , Gpg.keyImported "89C809CB" "joey"+ ]+ where+ endpoint d = combineProperties ("endpoint " ++ d)+ [ File.dirExists d+ , File.ownerGroup d "joey" "joey"+ ]++-- Twitter, you kill us.+twitRss :: Property+twitRss = combineProperties "twitter rss"+ [ Git.cloned "joey" "git://git.kitenet.net/twitrss.git" dir Nothing+ , check (not <$> doesFileExist (dir </> "twitRss")) $+ userScriptProperty "joey"+ [ "cd " ++ dir+ , "ghc --make twitRss" + ]+ `requires` Apt.installed+ [ "libghc-xml-dev"+ , "libghc-feed-dev"+ , "libghc-tagsoup-dev"+ ]+ , feed "http://twitter.com/search/realtime?q=git-annex" "git-annex-twitter"+ , feed "http://twitter.com/search/realtime?q=olduse+OR+git-annex+OR+debhelper+OR+etckeeper+OR+ikiwiki+-ashley_ikiwiki" "twittergrep"+ ]+ where+ dir = "/srv/web/tmp.kitenet.net/twitrss"+ crontime = "15 * * * *"+ feed url desc = Cron.job desc crontime "joey" dir $+ "./twitRss " ++ shellEscape url ++ " > " ++ shellEscape ("../" ++ desc ++ ".rss")++ircBouncer :: Property+ircBouncer = propertyList "IRC bouncer"+ [ Apt.installed ["znc"]+ , User.accountFor "znc"+ , File.hasPrivContent conf+ , File.ownerGroup conf "znc" "znc"+ , Cron.job "znconboot" "@reboot" "znc" "~" "znc"+ , Cron.job "zncrunning" "@hourly" "znc" "~" "znc || true"+ ]+ where+ conf = "/home/znc/.znc/configs/znc.conf"
+ src/Propellor/Property/Ssh.hs view
@@ -0,0 +1,157 @@+module Propellor.Property.Ssh (+ setSshdConfig,+ permitRootLogin,+ passwordAuthentication,+ hasAuthorizedKeys,+ restartSshd,+ randomHostKeys,+ hostKey,+ keyImported,+ knownHost,+ authorizedKeys+) where++import Propellor+import qualified Propellor.Property.File as File+import Propellor.Property.User+import Utility.SafeCommand+import Utility.FileMode++import System.PosixCompat++sshBool :: Bool -> String+sshBool True = "yes"+sshBool False = "no"++sshdConfig :: FilePath+sshdConfig = "/etc/ssh/sshd_config"++setSshdConfig :: String -> Bool -> Property+setSshdConfig setting allowed = combineProperties "sshd config"+ [ sshdConfig `File.lacksLine` (sshline $ not allowed)+ , sshdConfig `File.containsLine` (sshline allowed)+ ]+ `onChange` restartSshd+ `describe` unwords [ "ssh config:", setting, sshBool allowed ]+ where+ sshline v = setting ++ " " ++ sshBool v++permitRootLogin :: Bool -> Property+permitRootLogin = setSshdConfig "PermitRootLogin"++passwordAuthentication :: Bool -> Property+passwordAuthentication = setSshdConfig "PasswordAuthentication"++dotDir :: UserName -> IO FilePath+dotDir user = do+ h <- homedir user+ return $ h </> ".ssh"++dotFile :: FilePath -> UserName -> IO FilePath+dotFile f user = do+ d <- dotDir user+ return $ d </> f++hasAuthorizedKeys :: UserName -> IO Bool+hasAuthorizedKeys = go <=< dotFile "authorized_keys"+ where+ go f = not . null <$> catchDefaultIO "" (readFile f)++restartSshd :: Property+restartSshd = cmdProperty "service" ["ssh", "restart"]++-- | Blows away existing host keys and make new ones.+-- Useful for systems installed from an image that might reuse host keys.+-- A flag file is used to only ever do this once.+randomHostKeys :: Property+randomHostKeys = flagFile prop "/etc/ssh/.unique_host_keys"+ `onChange` restartSshd+ where+ prop = property "ssh random host keys" $ do+ void $ liftIO $ boolSystem "sh"+ [ Param "-c"+ , Param "rm -f /etc/ssh/ssh_host_*"+ ]+ ensureProperty $+ cmdProperty "/var/lib/dpkg/info/openssh-server.postinst"+ ["configure"]++-- | Sets ssh host keys from the site's PrivData.+-- +-- (Uses a null username for host keys.)+hostKey :: SshKeyType -> Property+hostKey keytype = combineProperties desc+ [ property desc (install writeFile (SshPubKey keytype "") ".pub")+ , property desc (install writeFileProtected (SshPrivKey keytype "") "")+ ]+ `onChange` restartSshd+ where+ desc = "known ssh host key (" ++ fromKeyType keytype ++ ")"+ install writer p ext = withPrivData p $ \key -> do+ let f = "/etc/ssh/ssh_host_" ++ fromKeyType keytype ++ "_key" ++ ext+ s <- liftIO $ readFileStrict f+ if s == key+ then noChange+ else makeChange $ writer f key++-- | Sets up a user with a ssh private key and public key pair+-- from the site's PrivData.+keyImported :: SshKeyType -> UserName -> Property+keyImported keytype user = combineProperties desc+ [ property desc (install writeFile (SshPubKey keytype user) ".pub")+ , property desc (install writeFileProtected (SshPrivKey keytype user) "")+ ]+ where+ desc = user ++ " has ssh key (" ++ fromKeyType keytype ++ ")"+ install writer p ext = do+ f <- liftIO $ keyfile ext+ ifM (liftIO $ doesFileExist f)+ ( noChange+ , ensureProperties+ [ property desc $ + withPrivData p $ \key -> makeChange $ do+ createDirectoryIfMissing True (takeDirectory f)+ writer f key+ , File.ownerGroup f user user+ , File.ownerGroup (takeDirectory f) user user+ ]+ )+ keyfile ext = do+ home <- homeDirectory <$> getUserEntryForName user+ return $ home </> ".ssh" </> "id_" ++ fromKeyType keytype ++ ext++fromKeyType :: SshKeyType -> String+fromKeyType SshRsa = "rsa"+fromKeyType SshDsa = "dsa"+fromKeyType SshEcdsa = "ecdsa"+fromKeyType SshEd25519 = "ed25519"++-- | Puts some host's ssh public key into the known_hosts file for a user.+knownHost :: [Host] -> HostName -> UserName -> Property+knownHost hosts hn user = property desc $+ go =<< fromHost hosts hn getSshPubKey+ where+ desc = user ++ " knows ssh key for " ++ hn+ go (Just (Just k)) = do+ f <- liftIO $ dotFile "known_hosts" user+ ensureProperty $ combineProperties desc+ [ File.dirExists (takeDirectory f)+ , f `File.containsLine` (hn ++ " " ++ k)+ , File.ownerGroup f user user+ ]+ go _ = do+ warningMessage $ "no configred sshPubKey for " ++ hn+ return FailedChange++-- | Makes a user have authorized_keys from the PrivData+authorizedKeys :: UserName -> Property+authorizedKeys user = property (user ++ " has authorized_keys") $+ withPrivData (SshAuthorizedKeys user) $ \v -> do+ f <- liftIO $ dotFile "authorized_keys" user+ liftIO $ do+ createDirectoryIfMissing True (takeDirectory f)+ writeFileProtected f v+ ensureProperties + [ File.ownerGroup f user user+ , File.ownerGroup (takeDirectory f) user user+ ]
+ src/Propellor/Property/Sudo.hs view
@@ -0,0 +1,32 @@+module Propellor.Property.Sudo where++import Data.List++import Propellor+import Propellor.Property.File+import qualified Propellor.Property.Apt as Apt+import Propellor.Property.User++-- | Allows a user to sudo. If the user has a password, sudo is configured+-- to require it. If not, NOPASSWORD is enabled for the user.+enabledFor :: UserName -> Property+enabledFor user = property desc go `requires` Apt.installed ["sudo"]+ where+ go = do+ locked <- liftIO $ isLockedPassword user+ ensureProperty $+ fileProperty desc+ (modify locked . filter (wanted locked))+ "/etc/sudoers"+ desc = user ++ " is sudoer"+ sudobaseline = user ++ " ALL=(ALL:ALL)"+ sudoline True = sudobaseline ++ " NOPASSWD:ALL"+ sudoline False = sudobaseline ++ " ALL"+ wanted locked l+ -- TOOD: Full sudoers file format parse.. + | not (sudobaseline `isPrefixOf` l) = True+ | "NOPASSWD" `isInfixOf` l = locked+ | otherwise = True+ modify locked ls+ | sudoline locked `elem` ls = ls+ | otherwise = ls ++ [sudoline locked]
+ src/Propellor/Property/Tor.hs view
@@ -0,0 +1,19 @@+module Propellor.Property.Tor where++import Propellor+import qualified Propellor.Property.File as File+import qualified Propellor.Property.Apt as Apt++isBridge :: Property+isBridge = setup `requires` Apt.installed ["tor"]+ `describe` "tor bridge"+ where+ setup = "/etc/tor/torrc" `File.hasContent`+ [ "SocksPort 0"+ , "ORPort 443"+ , "BridgeRelay 1"+ , "Exitpolicy reject *:*"+ ] `onChange` restartTor++restartTor :: Property+restartTor = cmdProperty "service" ["tor", "restart"]
+ src/Propellor/Property/User.hs view
@@ -0,0 +1,61 @@+module Propellor.Property.User where++import System.Posix++import Propellor++data Eep = YesReallyDeleteHome++accountFor :: UserName -> Property+accountFor user = check (isNothing <$> catchMaybeIO (homedir user)) $ cmdProperty "adduser"+ [ "--disabled-password"+ , "--gecos", ""+ , user+ ]+ `describe` ("account for " ++ user)++-- | Removes user home directory!! Use with caution.+nuked :: UserName -> Eep -> Property+nuked user _ = check (isJust <$> catchMaybeIO (homedir user)) $ cmdProperty "userdel"+ [ "-r"+ , user+ ]+ `describe` ("nuked user " ++ user)++-- | Only ensures that the user has some password set. It may or may+-- not be the password from the PrivData.+hasSomePassword :: UserName -> Property+hasSomePassword user = check ((/= HasPassword) <$> getPasswordStatus user) $+ hasPassword user++hasPassword :: UserName -> Property+hasPassword user = property (user ++ " has password") $+ withPrivData (Password user) $ \password -> makeChange $+ withHandle StdinHandle createProcessSuccess+ (proc "chpasswd" []) $ \h -> do+ hPutStrLn h $ user ++ ":" ++ password+ hClose h++lockedPassword :: UserName -> Property+lockedPassword user = check (not <$> isLockedPassword user) $ cmdProperty "passwd"+ [ "--lock"+ , user+ ]+ `describe` ("locked " ++ user ++ " password")++data PasswordStatus = NoPassword | LockedPassword | HasPassword+ deriving (Eq)++getPasswordStatus :: UserName -> IO PasswordStatus+getPasswordStatus user = parse . words <$> readProcess "passwd" ["-S", user]+ where+ parse (_:"L":_) = LockedPassword+ parse (_:"NP":_) = NoPassword+ parse (_:"P":_) = HasPassword+ parse _ = NoPassword++isLockedPassword :: UserName -> IO Bool+isLockedPassword user = (== LockedPassword) <$> getPasswordStatus user++homedir :: UserName -> IO FilePath+homedir user = homeDirectory <$> getUserEntryForName user
+ src/Propellor/SimpleSh.hs view
@@ -0,0 +1,101 @@+-- | Simple server, using a named pipe. Client connects, sends a command,+-- and gets back all the output from the command, in a stream.+--+-- This is useful for eg, docker.++module Propellor.SimpleSh where++import Network.Socket+import Control.Concurrent+import Control.Concurrent.Async+import System.Process (std_in, std_out, std_err)++import Propellor+import Utility.FileMode+import Utility.ThreadScheduler++data Cmd = Cmd String [String]+ deriving (Read, Show)++data Resp = StdoutLine String | StderrLine String | Done+ deriving (Read, Show)++simpleSh :: FilePath -> IO ()+simpleSh namedpipe = do+ nukeFile namedpipe+ let dir = takeDirectory namedpipe+ createDirectoryIfMissing True dir+ modifyFileMode dir (removeModes otherGroupModes)+ s <- socket AF_UNIX Stream defaultProtocol+ bindSocket s (SockAddrUnix namedpipe)+ listen s 2+ forever $ do+ (client, _addr) <- accept s+ forkIO $ do+ h <- socketToHandle client ReadWriteMode+ maybe noop (run h) . readish =<< hGetLine h+ where+ run h (Cmd cmd params) = do+ chan <- newChan+ let runwriter = do+ v <- readChan chan+ hPutStrLn h (show v)+ hFlush h+ case v of+ Done -> noop+ _ -> runwriter+ writer <- async runwriter++ flip catchIO (\_e -> writeChan chan Done) $ do+ let p = (proc cmd params)+ { std_in = Inherit+ , std_out = CreatePipe+ , std_err = CreatePipe+ }+ (Nothing, Just outh, Just errh, pid) <- createProcess p++ let mkreader t from = maybe noop (const $ mkreader t from) + =<< catchMaybeIO (writeChan chan . t =<< hGetLine from)+ void $ concurrently+ (mkreader StdoutLine outh)+ (mkreader StderrLine errh)+ + void $ tryIO $ waitForProcess pid++ writeChan chan Done++ hClose outh+ hClose errh++ wait writer+ hClose h++simpleShClient :: FilePath -> String -> [String] -> ([Resp] -> IO a) -> IO a+simpleShClient namedpipe cmd params handler = do+ s <- socket AF_UNIX Stream defaultProtocol+ connect s (SockAddrUnix namedpipe)+ h <- socketToHandle s ReadWriteMode+ hPutStrLn h $ show $ Cmd cmd params+ hFlush h+ resps <- catMaybes . map readish . lines <$> hGetContents h+ v <- hClose h `after` handler resps+ return v++simpleShClientRetry :: Int -> FilePath -> String -> [String] -> ([Resp] -> IO a) -> IO a+simpleShClientRetry retries namedpipe cmd params handler = go retries+ where+ run = simpleShClient namedpipe cmd params handler+ go n+ | n < 1 = run+ | otherwise = do+ v <- tryIO run+ case v of+ Right r -> return r+ Left e -> do+ debug ["simplesh connection retry", show e]+ threadDelaySeconds (Seconds 1)+ go (n - 1)++getStdout :: Resp -> Maybe String+getStdout (StdoutLine s) = Just s+getStdout _ = Nothing
+ src/Propellor/Types.hs view
@@ -0,0 +1,154 @@+{-# LANGUAGE PackageImports #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++module Propellor.Types+ ( Host(..)+ , Attr+ , SetAttr+ , Propellor(..)+ , Property(..)+ , RevertableProperty(..)+ , IsProp+ , describe+ , toProp+ , setAttr+ , requires+ , Desc+ , Result(..)+ , ActionResult(..)+ , CmdLine(..)+ , PrivDataField(..)+ , GpgKeyId+ , SshKeyType(..)+ , module Propellor.Types.OS+ , module Propellor.Types.Dns+ ) where++import Data.Monoid+import Control.Applicative+import System.Console.ANSI+import "mtl" Control.Monad.Reader+import "MonadCatchIO-transformers" Control.Monad.CatchIO++import Propellor.Types.Attr+import Propellor.Types.OS+import Propellor.Types.Dns++-- | Everything Propellor knows about a system: Its properties and+-- attributes.+data Host = Host [Property] SetAttr++-- | Propellor's monad provides read-only access to attributes of the+-- system.+newtype Propellor p = Propellor { runWithAttr :: ReaderT Attr IO p }+ deriving+ ( Monad+ , Functor+ , Applicative+ , MonadReader Attr + , MonadIO+ , MonadCatchIO+ )++-- | The core data type of Propellor, this represents a property+-- that the system should have, and an action to ensure it has the+-- property.+data Property = Property+ { propertyDesc :: Desc+ , propertySatisfy :: Propellor Result+ -- ^ must be idempotent; may run repeatedly+ , propertyAttr :: SetAttr+ -- ^ a property can set an Attr on the host that has the property.+ }++-- | A property that can be reverted.+data RevertableProperty = RevertableProperty Property Property++class IsProp p where+ -- | Sets description.+ describe :: p -> Desc -> p+ toProp :: p -> Property+ -- | Indicates that the first property can only be satisfied+ -- once the second one is.+ requires :: p -> Property -> p+ setAttr :: p -> SetAttr++instance IsProp Property where+ describe p d = p { propertyDesc = d }+ toProp p = p+ setAttr = propertyAttr+ x `requires` y = Property (propertyDesc x) satisfy attr+ where+ attr = propertyAttr x . propertyAttr y+ satisfy = do+ r <- propertySatisfy y+ case r of+ FailedChange -> return FailedChange+ _ -> propertySatisfy x+ ++instance IsProp RevertableProperty where+ -- | Sets the description of both sides.+ describe (RevertableProperty p1 p2) d = + RevertableProperty (describe p1 d) (describe p2 ("not " ++ d))+ toProp (RevertableProperty p1 _) = p1+ (RevertableProperty p1 p2) `requires` y =+ RevertableProperty (p1 `requires` y) p2+ -- | Return the SetAttr of the currently active side.+ setAttr (RevertableProperty p1 _p2) = setAttr p1++type Desc = String++data Result = NoChange | MadeChange | FailedChange+ deriving (Read, Show, Eq)++instance Monoid Result where+ mempty = NoChange++ mappend FailedChange _ = FailedChange+ mappend _ FailedChange = FailedChange+ mappend MadeChange _ = MadeChange+ mappend _ MadeChange = MadeChange+ mappend NoChange NoChange = NoChange++-- | Results of actions, with color.+class ActionResult a where+ getActionResult :: a -> (String, ColorIntensity, Color)++instance ActionResult Bool where+ getActionResult False = ("failed", Vivid, Red)+ getActionResult True = ("done", Dull, Green)++instance ActionResult Result where+ getActionResult NoChange = ("ok", Dull, Green)+ getActionResult MadeChange = ("done", Vivid, Green)+ getActionResult FailedChange = ("failed", Vivid, Red)++data CmdLine+ = Run HostName+ | Spin HostName+ | Boot HostName+ | Set HostName PrivDataField+ | AddKey String+ | Continue CmdLine+ | Chain HostName+ | Docker HostName+ deriving (Read, Show, Eq)++-- | Note that removing or changing field names will break the+-- serialized privdata files, so don't do that!+-- It's fine to add new fields.+data PrivDataField+ = DockerAuthentication+ | SshPubKey SshKeyType UserName+ | SshPrivKey SshKeyType UserName+ | SshAuthorizedKeys UserName+ | Password UserName+ | PrivFile FilePath+ | GpgKey GpgKeyId+ deriving (Read, Show, Ord, Eq)++type GpgKeyId = String++data SshKeyType = SshRsa | SshDsa | SshEcdsa | SshEd25519+ deriving (Read, Show, Ord, Eq)
+ src/Propellor/Types/Attr.hs view
@@ -0,0 +1,48 @@+module Propellor.Types.Attr where++import Propellor.Types.OS+import qualified Propellor.Types.Dns as Dns++import qualified Data.Set as S+import qualified Data.Map as M++-- | The attributes of a host. For example, its hostname.+data Attr = Attr+ { _hostname :: HostName+ , _os :: Maybe System+ , _sshPubKey :: Maybe String+ , _dns :: S.Set Dns.Record+ , _namedconf :: M.Map Dns.Domain Dns.NamedConf++ , _dockerImage :: Maybe String+ , _dockerRunParams :: [HostName -> String]+ }++instance Eq Attr where+ x == y = and+ [ _hostname x == _hostname y+ , _os x == _os y+ , _dns x == _dns y+ , _namedconf x == _namedconf y+ , _sshPubKey x == _sshPubKey y++ , _dockerImage x == _dockerImage y+ , let simpl v = map (\a -> a "") (_dockerRunParams v)+ in simpl x == simpl y+ ]++instance Show Attr where+ show a = unlines+ [ "hostname " ++ _hostname a+ , "OS " ++ show (_os a)+ , "sshPubKey " ++ show (_sshPubKey a)+ , "dns " ++ show (_dns a)+ , "namedconf " ++ show (_namedconf a)+ , "docker image " ++ show (_dockerImage a)+ , "docker run params " ++ show (map (\mk -> mk "") (_dockerRunParams a))+ ]++newAttr :: HostName -> Attr+newAttr hn = Attr hn Nothing Nothing S.empty M.empty Nothing []++type SetAttr = Attr -> Attr
+ src/Propellor/Types/Dns.hs view
@@ -0,0 +1,92 @@+module Propellor.Types.Dns where++import Propellor.Types.OS (HostName)++import Data.Word++type Domain = String++data IPAddr = IPv4 String | IPv6 String+ deriving (Read, Show, Eq, Ord)++fromIPAddr :: IPAddr -> String+fromIPAddr (IPv4 addr) = addr+fromIPAddr (IPv6 addr) = addr++-- | Represents a bind 9 named.conf file.+data NamedConf = NamedConf+ { confDomain :: Domain+ , confDnsServerType :: DnsServerType+ , confFile :: FilePath+ , confMasters :: [IPAddr]+ , confAllowTransfer :: [IPAddr]+ , confLines :: [String]+ }+ deriving (Show, Eq, Ord)++data DnsServerType = Master | Secondary+ deriving (Show, Eq, Ord)++-- | Represents a bind 9 zone file.+data Zone = Zone+ { zDomain :: Domain+ , zSOA :: SOA+ , zHosts :: [(BindDomain, Record)]+ }+ deriving (Read, Show, Eq)++-- | Every domain has a SOA record, which is big and complicated.+data SOA = SOA+ { sDomain :: BindDomain+ -- ^ Typically ns1.your.domain+ , sSerial :: SerialNumber+ -- ^ The most important parameter is the serial number,+ -- which must increase after each change.+ , sRefresh :: Integer+ , sRetry :: Integer+ , sExpire :: Integer+ , sNegativeCacheTTL :: Integer+ }+ deriving (Read, Show, Eq)++-- | Types of DNS records.+--+-- This is not a complete list, more can be added.+data Record+ = Address IPAddr+ | CNAME BindDomain+ | MX Int BindDomain+ | NS BindDomain+ | TXT String+ | SRV Word16 Word16 Word16 BindDomain+ deriving (Read, Show, Eq, Ord)++getIPAddr :: Record -> Maybe IPAddr+getIPAddr (Address addr) = Just addr+getIPAddr _ = Nothing++getCNAME :: Record -> Maybe BindDomain+getCNAME (CNAME d) = Just d+getCNAME _ = Nothing++getNS :: Record -> Maybe BindDomain+getNS (NS d) = Just d+getNS _ = Nothing++-- | Bind serial numbers are unsigned, 32 bit integers.+type SerialNumber = Word32++-- | Domains in the zone file must end with a period if they are absolute.+--+-- Let's use a type to keep absolute domains straight from relative+-- domains.+--+-- The RootDomain refers to the top level of the domain, so can be used+-- to add nameservers, MX's, etc to a domain.+data BindDomain = RelDomain Domain | AbsDomain Domain | RootDomain+ deriving (Read, Show, Eq, Ord)++domainHostName :: BindDomain -> Maybe HostName+domainHostName (RelDomain d) = Just d+domainHostName (AbsDomain d) = Just d+domainHostName RootDomain = Nothing
+ src/Propellor/Types/OS.hs view
@@ -0,0 +1,27 @@+module Propellor.Types.OS where++type HostName = String+type UserName = String+type GroupName = String++-- | High level descritption of a operating system.+data System = System Distribution Architecture+ deriving (Show, Eq)++data Distribution+ = Debian DebianSuite+ | Ubuntu Release+ deriving (Show, Eq)++data DebianSuite = Experimental | Unstable | Testing | Stable | DebianRelease Release+ deriving (Show, Eq)++-- | The release that currently corresponds to stable.+stableRelease :: DebianSuite+stableRelease = DebianRelease "wheezy"++isStable :: DebianSuite -> Bool+isStable s = s == Stable || s == stableRelease++type Release = String+type Architecture = String
+ src/Utility/Applicative.hs view
@@ -0,0 +1,16 @@+{- applicative stuff+ -+ - Copyright 2012 Joey Hess <joey@kitenet.net>+ -+ - License: BSD-2-clause+ -}++module Utility.Applicative where++{- Like <$> , but supports one level of currying.+ - + - foo v = bar <$> action v == foo = bar <$$> action+ -}+(<$$>) :: Functor f => (a -> b) -> (c -> f a) -> c -> f b+f <$$> v = fmap f . v+infixr 4 <$$>
+ src/Utility/Data.hs view
@@ -0,0 +1,17 @@+{- utilities for simple data types+ -+ - Copyright 2013 Joey Hess <joey@kitenet.net>+ -+ - License: BSD-2-clause+ -}++module Utility.Data where++{- First item in the list that is not Nothing. -}+firstJust :: Eq a => [Maybe a] -> Maybe a+firstJust ms = case dropWhile (== Nothing) ms of+ [] -> Nothing+ (md:_) -> md++eitherToMaybe :: Either a b -> Maybe b+eitherToMaybe = either (const Nothing) Just
+ src/Utility/Directory.hs view
@@ -0,0 +1,135 @@+{- directory manipulation+ -+ - Copyright 2011-2014 Joey Hess <joey@kitenet.net>+ -+ - License: BSD-2-clause+ -}++{-# LANGUAGE CPP #-}++module Utility.Directory where++import System.IO.Error+import System.Directory+import Control.Exception (throw)+import Control.Monad+import Control.Monad.IfElse+import System.FilePath+import Control.Applicative+import System.IO.Unsafe (unsafeInterleaveIO)++import Utility.PosixFiles+import Utility.SafeCommand+import Utility.Tmp+import Utility.Exception+import Utility.Monad+import Utility.Applicative++dirCruft :: FilePath -> Bool+dirCruft "." = True+dirCruft ".." = True+dirCruft _ = False++{- Lists the contents of a directory.+ - Unlike getDirectoryContents, paths are not relative to the directory. -}+dirContents :: FilePath -> IO [FilePath]+dirContents d = map (d </>) . filter (not . dirCruft) <$> getDirectoryContents d++{- Gets files in a directory, and then its subdirectories, recursively,+ - and lazily.+ -+ - Does not follow symlinks to other subdirectories.+ -+ - When the directory does not exist, no exception is thrown,+ - instead, [] is returned. -}+dirContentsRecursive :: FilePath -> IO [FilePath]+dirContentsRecursive = dirContentsRecursiveSkipping (const False) True++{- Skips directories whose basenames match the skipdir. -}+dirContentsRecursiveSkipping :: (FilePath -> Bool) -> Bool -> FilePath -> IO [FilePath]+dirContentsRecursiveSkipping skipdir followsubdirsymlinks topdir = go [topdir]+ where+ go [] = return []+ go (dir:dirs)+ | skipdir (takeFileName dir) = go dirs+ | otherwise = unsafeInterleaveIO $ do+ (files, dirs') <- collect [] []+ =<< catchDefaultIO [] (dirContents dir)+ files' <- go (dirs' ++ dirs)+ return (files ++ files')+ collect files dirs' [] = return (reverse files, reverse dirs')+ collect files dirs' (entry:entries)+ | dirCruft entry = collect files dirs' entries+ | otherwise = do+ let skip = collect (entry:files) dirs' entries+ let recurse = collect files (entry:dirs') entries+ ms <- catchMaybeIO $ getSymbolicLinkStatus entry+ case ms of+ (Just s) + | isDirectory s -> recurse+ | isSymbolicLink s && followsubdirsymlinks ->+ ifM (doesDirectoryExist entry)+ ( recurse+ , skip+ )+ _ -> skip++{- Gets the directory tree from a point, recursively and lazily,+ - with leaf directories **first**, skipping any whose basenames+ - match the skipdir. Does not follow symlinks. -}+dirTreeRecursiveSkipping :: (FilePath -> Bool) -> FilePath -> IO [FilePath]+dirTreeRecursiveSkipping skipdir topdir = go [] [topdir]+ where+ go c [] = return c+ go c (dir:dirs)+ | skipdir (takeFileName dir) = go c dirs+ | otherwise = unsafeInterleaveIO $ do+ subdirs <- go c+ =<< filterM (isDirectory <$$> getSymbolicLinkStatus)+ =<< catchDefaultIO [] (dirContents dir)+ go (subdirs++[dir]) dirs++{- Moves one filename to another.+ - First tries a rename, but falls back to moving across devices if needed. -}+moveFile :: FilePath -> FilePath -> IO ()+moveFile src dest = tryIO (rename src dest) >>= onrename+ where+ onrename (Right _) = noop+ onrename (Left e)+ | isPermissionError e = rethrow+ | isDoesNotExistError e = rethrow+ | otherwise = do+ -- copyFile is likely not as optimised as+ -- the mv command, so we'll use the latter.+ -- But, mv will move into a directory if+ -- dest is one, which is not desired.+ whenM (isdir dest) rethrow+ viaTmp mv dest undefined+ where+ rethrow = throw e+ mv tmp _ = do+ ok <- boolSystem "mv" [Param "-f", Param src, Param tmp]+ unless ok $ do+ -- delete any partial+ _ <- tryIO $ removeFile tmp+ rethrow++ isdir f = do+ r <- tryIO $ getFileStatus f+ case r of+ (Left _) -> return False+ (Right s) -> return $ isDirectory s++{- Removes a file, which may or may not exist, and does not have to+ - be a regular file.+ -+ - Note that an exception is thrown if the file exists but+ - cannot be removed. -}+nukeFile :: FilePath -> IO ()+nukeFile file = void $ tryWhenExists go+ where+#ifndef mingw32_HOST_OS+ go = removeLink file+#else+ go = removeFile file+#endif
+ src/Utility/Env.hs view
@@ -0,0 +1,81 @@+{- portable environment variables+ -+ - Copyright 2013 Joey Hess <joey@kitenet.net>+ -+ - License: BSD-2-clause+ -}++{-# LANGUAGE CPP #-}++module Utility.Env where++#ifdef mingw32_HOST_OS+import Utility.Exception+import Control.Applicative+import Data.Maybe+import qualified System.Environment as E+#else+import qualified System.Posix.Env as PE+#endif++getEnv :: String -> IO (Maybe String)+#ifndef mingw32_HOST_OS+getEnv = PE.getEnv+#else+getEnv = catchMaybeIO . E.getEnv+#endif++getEnvDefault :: String -> String -> IO String+#ifndef mingw32_HOST_OS+getEnvDefault = PE.getEnvDefault+#else+getEnvDefault var fallback = fromMaybe fallback <$> getEnv var+#endif++getEnvironment :: IO [(String, String)]+#ifndef mingw32_HOST_OS+getEnvironment = PE.getEnvironment+#else+getEnvironment = E.getEnvironment+#endif++{- Returns True if it could successfully set the environment variable.+ -+ - There is, apparently, no way to do this in Windows. Instead,+ - environment varuables must be provided when running a new process. -}+setEnv :: String -> String -> Bool -> IO Bool+#ifndef mingw32_HOST_OS+setEnv var val overwrite = do+ PE.setEnv var val overwrite+ return True+#else+setEnv _ _ _ = return False+#endif++{- Returns True if it could successfully unset the environment variable. -}+unsetEnv :: String -> IO Bool+#ifndef mingw32_HOST_OS+unsetEnv var = do+ PE.unsetEnv var+ return True+#else+unsetEnv _ = return False+#endif++{- Adds the environment variable to the input environment. If already+ - present in the list, removes the old value.+ -+ - This does not really belong here, but Data.AssocList is for some reason+ - buried inside hxt.+ -}+addEntry :: Eq k => k -> v -> [(k, v)] -> [(k, v)]+addEntry k v l = ( (k,v) : ) $! delEntry k l++addEntries :: Eq k => [(k, v)] -> [(k, v)] -> [(k, v)]+addEntries = foldr (.) id . map (uncurry addEntry) . reverse++delEntry :: Eq k => k -> [(k, v)] -> [(k, v)]+delEntry _ [] = []+delEntry k (x@(k1,_) : rest)+ | k == k1 = rest+ | otherwise = ( x : ) $! delEntry k rest
+ src/Utility/Exception.hs view
@@ -0,0 +1,59 @@+{- Simple IO exception handling (and some more)+ -+ - Copyright 2011-2012 Joey Hess <joey@kitenet.net>+ -+ - License: BSD-2-clause+ -}++{-# LANGUAGE ScopedTypeVariables #-}++module Utility.Exception where++import Control.Exception+import qualified Control.Exception as E+import Control.Applicative+import Control.Monad+import System.IO.Error (isDoesNotExistError)+import Utility.Data++{- Catches IO errors and returns a Bool -}+catchBoolIO :: IO Bool -> IO Bool+catchBoolIO = catchDefaultIO False++{- Catches IO errors and returns a Maybe -}+catchMaybeIO :: IO a -> IO (Maybe a)+catchMaybeIO a = catchDefaultIO Nothing $ Just <$> a++{- Catches IO errors and returns a default value. -}+catchDefaultIO :: a -> IO a -> IO a+catchDefaultIO def a = catchIO a (const $ return def)++{- Catches IO errors and returns the error message. -}+catchMsgIO :: IO a -> IO (Either String a)+catchMsgIO a = either (Left . show) Right <$> tryIO a++{- catch specialized for IO errors only -}+catchIO :: IO a -> (IOException -> IO a) -> IO a+catchIO = E.catch++{- try specialized for IO errors only -}+tryIO :: IO a -> IO (Either IOException a)+tryIO = try++{- Catches all exceptions except for async exceptions.+ - This is often better to use than catching them all, so that+ - ThreadKilled and UserInterrupt get through.+ -}+catchNonAsync :: IO a -> (SomeException -> IO a) -> IO a+catchNonAsync a onerr = a `catches`+ [ Handler (\ (e :: AsyncException) -> throw e)+ , Handler (\ (e :: SomeException) -> onerr e)+ ]++tryNonAsync :: IO a -> IO (Either SomeException a)+tryNonAsync a = (Right <$> a) `catchNonAsync` (return . Left)++{- Catches only DoesNotExist exceptions, and lets all others through. -}+tryWhenExists :: IO a -> IO (Maybe a)+tryWhenExists a = eitherToMaybe <$>+ tryJust (guard . isDoesNotExistError) a
+ src/Utility/FileMode.hs view
@@ -0,0 +1,158 @@+{- File mode utilities.+ -+ - Copyright 2010-2012 Joey Hess <joey@kitenet.net>+ -+ - License: BSD-2-clause+ -}++{-# LANGUAGE CPP #-}++module Utility.FileMode where++import System.IO+import Control.Monad+import Control.Exception (bracket)+import System.PosixCompat.Types+import Utility.PosixFiles+#ifndef mingw32_HOST_OS+import System.Posix.Files+#endif+import Foreign (complement)++import Utility.Exception++{- Applies a conversion function to a file's mode. -}+modifyFileMode :: FilePath -> (FileMode -> FileMode) -> IO ()+modifyFileMode f convert = void $ modifyFileMode' f convert+modifyFileMode' :: FilePath -> (FileMode -> FileMode) -> IO FileMode+modifyFileMode' f convert = do+ s <- getFileStatus f+ let old = fileMode s+ let new = convert old+ when (new /= old) $+ setFileMode f new+ return old++{- Adds the specified FileModes to the input mode, leaving the rest+ - unchanged. -}+addModes :: [FileMode] -> FileMode -> FileMode+addModes ms m = combineModes (m:ms)++{- Removes the specified FileModes from the input mode. -}+removeModes :: [FileMode] -> FileMode -> FileMode+removeModes ms m = m `intersectFileModes` complement (combineModes ms)++{- Runs an action after changing a file's mode, then restores the old mode. -}+withModifiedFileMode :: FilePath -> (FileMode -> FileMode) -> IO a -> IO a+withModifiedFileMode file convert a = bracket setup cleanup go+ where+ setup = modifyFileMode' file convert+ cleanup oldmode = modifyFileMode file (const oldmode)+ go _ = a++writeModes :: [FileMode]+writeModes = [ownerWriteMode, groupWriteMode, otherWriteMode]++readModes :: [FileMode]+readModes = [ownerReadMode, groupReadMode, otherReadMode]++executeModes :: [FileMode]+executeModes = [ownerExecuteMode, groupExecuteMode, otherExecuteMode]++otherGroupModes :: [FileMode]+otherGroupModes = + [ groupReadMode, otherReadMode+ , groupWriteMode, otherWriteMode+ ]++{- Removes the write bits from a file. -}+preventWrite :: FilePath -> IO ()+preventWrite f = modifyFileMode f $ removeModes writeModes++{- Turns a file's owner write bit back on. -}+allowWrite :: FilePath -> IO ()+allowWrite f = modifyFileMode f $ addModes [ownerWriteMode]++{- Turns a file's owner read bit back on. -}+allowRead :: FilePath -> IO ()+allowRead f = modifyFileMode f $ addModes [ownerReadMode]++{- Allows owner and group to read and write to a file. -}+groupSharedModes :: [FileMode]+groupSharedModes =+ [ ownerWriteMode, groupWriteMode+ , ownerReadMode, groupReadMode+ ]++groupWriteRead :: FilePath -> IO ()+groupWriteRead f = modifyFileMode f $ addModes groupSharedModes++checkMode :: FileMode -> FileMode -> Bool+checkMode checkfor mode = checkfor `intersectFileModes` mode == checkfor++{- Checks if a file mode indicates it's a symlink. -}+isSymLink :: FileMode -> Bool+#ifdef mingw32_HOST_OS+isSymLink _ = False+#else+isSymLink = checkMode symbolicLinkMode+#endif++{- Checks if a file has any executable bits set. -}+isExecutable :: FileMode -> Bool+isExecutable mode = combineModes executeModes `intersectFileModes` mode /= 0++{- Runs an action without that pesky umask influencing it, unless the+ - passed FileMode is the standard one. -}+noUmask :: FileMode -> IO a -> IO a+#ifndef mingw32_HOST_OS+noUmask mode a+ | mode == stdFileMode = a+ | otherwise = withUmask nullFileMode a+#else+noUmask _ a = a+#endif++withUmask :: FileMode -> IO a -> IO a+#ifndef mingw32_HOST_OS+withUmask umask a = bracket setup cleanup go+ where+ setup = setFileCreationMask umask+ cleanup = setFileCreationMask+ go _ = a+#else+withUmask _ a = a+#endif++combineModes :: [FileMode] -> FileMode+combineModes [] = undefined+combineModes [m] = m+combineModes (m:ms) = foldl unionFileModes m ms++isSticky :: FileMode -> Bool+#ifdef mingw32_HOST_OS+isSticky _ = False+#else+isSticky = checkMode stickyMode++stickyMode :: FileMode+stickyMode = 512++setSticky :: FilePath -> IO ()+setSticky f = modifyFileMode f $ addModes [stickyMode]+#endif++{- Writes a file, ensuring that its modes do not allow it to be read+ - or written by anyone other than the current user,+ - before any content is written.+ -+ - When possible, this is done using the umask.+ -+ - On a filesystem that does not support file permissions, this is the same+ - as writeFile.+ -}+writeFileProtected :: FilePath -> String -> IO ()+writeFileProtected file content = withUmask 0o0077 $+ withFile file WriteMode $ \h -> do+ void $ tryIO $ modifyFileMode file $ removeModes otherGroupModes+ hPutStr h content
+ src/Utility/FileSystemEncoding.hs view
@@ -0,0 +1,132 @@+{- GHC File system encoding handling.+ -+ - Copyright 2012-2014 Joey Hess <joey@kitenet.net>+ -+ - License: BSD-2-clause+ -}++{-# LANGUAGE CPP #-}++module Utility.FileSystemEncoding (+ fileEncoding,+ withFilePath,+ md5FilePath,+ decodeBS,+ decodeW8,+ encodeW8,+ truncateFilePath,+) where++import qualified GHC.Foreign as GHC+import qualified GHC.IO.Encoding as Encoding+import Foreign.C+import System.IO+import System.IO.Unsafe+import qualified Data.Hash.MD5 as MD5+import Data.Word+import Data.Bits.Utils+import qualified Data.ByteString.Lazy as L+#ifdef mingw32_HOST_OS+import qualified Data.ByteString.Lazy.UTF8 as L8+#endif++{- Sets a Handle to use the filesystem encoding. This causes data+ - written or read from it to be encoded/decoded the same+ - as ghc 7.4 does to filenames etc. This special encoding+ - allows "arbitrary undecodable bytes to be round-tripped through it".+ -}+fileEncoding :: Handle -> IO ()+#ifndef mingw32_HOST_OS+fileEncoding h = hSetEncoding h =<< Encoding.getFileSystemEncoding+#else+{- The file system encoding does not work well on Windows,+ - and Windows only has utf FilePaths anyway. -}+fileEncoding h = hSetEncoding h Encoding.utf8+#endif++{- Marshal a Haskell FilePath into a NUL terminated C string using temporary+ - storage. The FilePath is encoded using the filesystem encoding,+ - reversing the decoding that should have been done when the FilePath+ - was obtained. -}+withFilePath :: FilePath -> (CString -> IO a) -> IO a+withFilePath fp f = Encoding.getFileSystemEncoding+ >>= \enc -> GHC.withCString enc fp f++{- Encodes a FilePath into a String, applying the filesystem encoding.+ -+ - There are very few things it makes sense to do with such an encoded+ - string. It's not a legal filename; it should not be displayed.+ - So this function is not exported, but instead used by the few functions+ - that can usefully consume it.+ -+ - This use of unsafePerformIO is belived to be safe; GHC's interface+ - only allows doing this conversion with CStrings, and the CString buffer+ - is allocated, used, and deallocated within the call, with no side+ - effects.+ -}+{-# NOINLINE _encodeFilePath #-}+_encodeFilePath :: FilePath -> String+_encodeFilePath fp = unsafePerformIO $ do+ enc <- Encoding.getFileSystemEncoding+ GHC.withCString enc fp $ GHC.peekCString Encoding.char8++{- Encodes a FilePath into a Md5.Str, applying the filesystem encoding. -}+md5FilePath :: FilePath -> MD5.Str+md5FilePath = MD5.Str . _encodeFilePath++{- Decodes a ByteString into a FilePath, applying the filesystem encoding. -}+decodeBS :: L.ByteString -> FilePath+#ifndef mingw32_HOST_OS+decodeBS = encodeW8 . L.unpack+#else+{- On Windows, we assume that the ByteString is utf-8, since Windows+ - only uses unicode for filenames. -}+decodeBS = L8.toString+#endif++{- Converts a [Word8] to a FilePath, encoding using the filesystem encoding.+ -+ - w82c produces a String, which may contain Chars that are invalid+ - unicode. From there, this is really a simple matter of applying the+ - file system encoding, only complicated by GHC's interface to doing so.+ -}+{-# NOINLINE encodeW8 #-}+encodeW8 :: [Word8] -> FilePath+encodeW8 w8 = unsafePerformIO $ do+ enc <- Encoding.getFileSystemEncoding+ GHC.withCString Encoding.char8 (w82s w8) $ GHC.peekCString enc++{- Useful when you want the actual number of bytes that will be used to+ - represent the FilePath on disk. -}+decodeW8 :: FilePath -> [Word8]+decodeW8 = s2w8 . _encodeFilePath++{- Truncates a FilePath to the given number of bytes (or less),+ - as represented on disk.+ -+ - Avoids returning an invalid part of a unicode byte sequence, at the+ - cost of efficiency when running on a large FilePath.+ -}+truncateFilePath :: Int -> FilePath -> FilePath+#ifndef mingw32_HOST_OS+truncateFilePath n = go . reverse+ where+ go f =+ let bytes = decodeW8 f+ in if length bytes <= n+ then reverse f+ else go (drop 1 f)+#else+{- On Windows, count the number of bytes used by each utf8 character. -}+truncateFilePath n = reverse . go [] n . L8.fromString+ where+ go coll cnt bs+ | cnt <= 0 = coll+ | otherwise = case L8.decode bs of+ Just (c, x) | c /= L8.replacement_char ->+ let x' = fromIntegral x+ in if cnt - x' < 0+ then coll+ else go (c:coll) (cnt - x') (L8.drop 1 bs)+ _ -> coll+#endif
+ src/Utility/LinuxMkLibs.hs view
@@ -0,0 +1,61 @@+{- Linux library copier and binary shimmer+ -+ - Copyright 2013 Joey Hess <joey@kitenet.net>+ -+ - License: BSD-2-clause+ -}++module Utility.LinuxMkLibs where++import Control.Applicative+import Data.Maybe+import System.Directory+import Data.List.Utils+import System.Posix.Files+import Data.Char+import Control.Monad.IfElse++import Utility.PartialPrelude+import Utility.Directory+import Utility.Process+import Utility.Monad+import Utility.Path++{- Installs a library. If the library is a symlink to another file,+ - install the file it links to, and update the symlink to be relative. -}+installLib :: (FilePath -> FilePath -> IO ()) -> FilePath -> FilePath -> IO (Maybe FilePath)+installLib installfile top lib = ifM (doesFileExist lib)+ ( do+ installfile top lib+ checksymlink lib+ return $ Just $ parentDir lib+ , return Nothing+ )+ where+ checksymlink f = whenM (isSymbolicLink <$> getSymbolicLinkStatus (inTop top f)) $ do+ l <- readSymbolicLink (inTop top f)+ let absl = absPathFrom (parentDir f) l+ let target = relPathDirToFile (parentDir f) absl+ installfile top absl+ nukeFile (top ++ f)+ createSymbolicLink target (inTop top f)+ checksymlink absl++-- Note that f is not relative, so cannot use </>+inTop :: FilePath -> FilePath -> FilePath+inTop top f = top ++ f++{- Parse ldd output, getting all the libraries that the input files+ - link to. Note that some of the libraries may not exist + - (eg, linux-vdso.so) -}+parseLdd :: String -> [FilePath]+parseLdd = mapMaybe (getlib . dropWhile isSpace) . lines+ where+ getlib l = headMaybe . words =<< lastMaybe (split " => " l)++{- Get all glibc libs and other support files, including gconv files+ -+ - XXX Debian specific. -}+glibcLibs :: IO [FilePath]+glibcLibs = lines <$> readProcess "sh"+ ["-c", "dpkg -L libc6:$(dpkg --print-architecture) libgcc1:$(dpkg --print-architecture) | egrep '\\.so|gconv'"]
+ src/Utility/Misc.hs view
@@ -0,0 +1,148 @@+{- misc utility functions+ -+ - Copyright 2010-2011 Joey Hess <joey@kitenet.net>+ -+ - License: BSD-2-clause+ -}++{-# LANGUAGE CPP #-}++module Utility.Misc where++import System.IO+import Control.Monad+import Foreign+import Data.Char+import Data.List+import Control.Applicative+import System.Exit+#ifndef mingw32_HOST_OS+import System.Posix.Process (getAnyProcessStatus)+import Utility.Exception+#endif++import Utility.FileSystemEncoding+import Utility.Monad++{- A version of hgetContents that is not lazy. Ensures file is + - all read before it gets closed. -}+hGetContentsStrict :: Handle -> IO String+hGetContentsStrict = hGetContents >=> \s -> length s `seq` return s++{- A version of readFile that is not lazy. -}+readFileStrict :: FilePath -> IO String+readFileStrict = readFile >=> \s -> length s `seq` return s++{- Reads a file strictly, and using the FileSystemEncoding, so it will+ - never crash on a badly encoded file. -}+readFileStrictAnyEncoding :: FilePath -> IO String+readFileStrictAnyEncoding f = withFile f ReadMode $ \h -> do+ fileEncoding h+ hClose h `after` hGetContentsStrict h++{- Writes a file, using the FileSystemEncoding so it will never crash+ - on a badly encoded content string. -}+writeFileAnyEncoding :: FilePath -> String -> IO ()+writeFileAnyEncoding f content = withFile f WriteMode $ \h -> do+ fileEncoding h+ hPutStr h content++{- Like break, but the item matching the condition is not included+ - in the second result list.+ -+ - separate (== ':') "foo:bar" = ("foo", "bar")+ - separate (== ':') "foobar" = ("foobar", "")+ -}+separate :: (a -> Bool) -> [a] -> ([a], [a])+separate c l = unbreak $ break c l+ where+ unbreak r@(a, b)+ | null b = r+ | otherwise = (a, tail b)++{- Breaks out the first line. -}+firstLine :: String -> String+firstLine = takeWhile (/= '\n')++{- Splits a list into segments that are delimited by items matching+ - a predicate. (The delimiters are not included in the segments.)+ - Segments may be empty. -}+segment :: (a -> Bool) -> [a] -> [[a]]+segment p l = map reverse $ go [] [] l+ where+ go c r [] = reverse $ c:r+ go c r (i:is)+ | p i = go [] (c:r) is+ | otherwise = go (i:c) r is++prop_segment_regressionTest :: Bool+prop_segment_regressionTest = all id+ -- Even an empty list is a segment.+ [ segment (== "--") [] == [[]]+ -- There are two segements in this list, even though the first is empty.+ , segment (== "--") ["--", "foo", "bar"] == [[],["foo","bar"]]+ ]++{- Includes the delimiters as segments of their own. -}+segmentDelim :: (a -> Bool) -> [a] -> [[a]]+segmentDelim p l = map reverse $ go [] [] l+ where+ go c r [] = reverse $ c:r+ go c r (i:is)+ | p i = go [] ([i]:c:r) is+ | otherwise = go (i:c) r is++{- Replaces multiple values in a string.+ -+ - Takes care to skip over just-replaced values, so that they are not+ - mangled. For example, massReplace [("foo", "new foo")] does not+ - replace the "new foo" with "new new foo".+ -}+massReplace :: [(String, String)] -> String -> String+massReplace vs = go [] vs+ where++ go acc _ [] = concat $ reverse acc+ go acc [] (c:cs) = go ([c]:acc) vs cs+ go acc ((val, replacement):rest) s+ | val `isPrefixOf` s =+ go (replacement:acc) vs (drop (length val) s)+ | otherwise = go acc rest s++{- Wrapper around hGetBufSome that returns a String.+ -+ - The null string is returned on eof, otherwise returns whatever+ - data is currently available to read from the handle, or waits for+ - data to be written to it if none is currently available.+ - + - Note on encodings: The normal encoding of the Handle is ignored;+ - each byte is converted to a Char. Not unicode clean!+ -}+hGetSomeString :: Handle -> Int -> IO String+hGetSomeString h sz = do+ fp <- mallocForeignPtrBytes sz+ len <- withForeignPtr fp $ \buf -> hGetBufSome h buf sz+ map (chr . fromIntegral) <$> withForeignPtr fp (peekbytes len)+ where+ peekbytes :: Int -> Ptr Word8 -> IO [Word8]+ peekbytes len buf = mapM (peekElemOff buf) [0..pred len]++{- Reaps any zombie git processes. + -+ - Warning: Not thread safe. Anything that was expecting to wait+ - on a process and get back an exit status is going to be confused+ - if this reap gets there first. -}+reapZombies :: IO ()+#ifndef mingw32_HOST_OS+reapZombies = do+ -- throws an exception when there are no child processes+ catchDefaultIO Nothing (getAnyProcessStatus False True)+ >>= maybe (return ()) (const reapZombies)++#else+reapZombies = return ()+#endif++exitBool :: Bool -> IO a+exitBool False = exitFailure+exitBool True = exitSuccess
+ src/Utility/Monad.hs view
@@ -0,0 +1,69 @@+{- monadic stuff+ -+ - Copyright 2010-2012 Joey Hess <joey@kitenet.net>+ -+ - License: BSD-2-clause+ -}++module Utility.Monad where++import Data.Maybe+import Control.Monad++{- Return the first value from a list, if any, satisfying the given+ - predicate -}+firstM :: Monad m => (a -> m Bool) -> [a] -> m (Maybe a)+firstM _ [] = return Nothing+firstM p (x:xs) = ifM (p x) (return $ Just x , firstM p xs)++{- Runs the action on values from the list until it succeeds, returning+ - its result. -}+getM :: Monad m => (a -> m (Maybe b)) -> [a] -> m (Maybe b)+getM _ [] = return Nothing+getM p (x:xs) = maybe (getM p xs) (return . Just) =<< p x++{- Returns true if any value in the list satisfies the predicate,+ - stopping once one is found. -}+anyM :: Monad m => (a -> m Bool) -> [a] -> m Bool+anyM p = liftM isJust . firstM p++allM :: Monad m => (a -> m Bool) -> [a] -> m Bool+allM _ [] = return True+allM p (x:xs) = p x <&&> allM p xs++{- Runs an action on values from a list until it succeeds. -}+untilTrue :: Monad m => [a] -> (a -> m Bool) -> m Bool+untilTrue = flip anyM++{- if with a monadic conditional. -}+ifM :: Monad m => m Bool -> (m a, m a) -> m a+ifM cond (thenclause, elseclause) = do+ c <- cond+ if c then thenclause else elseclause++{- short-circuiting monadic || -}+(<||>) :: Monad m => m Bool -> m Bool -> m Bool+ma <||> mb = ifM ma ( return True , mb )++{- short-circuiting monadic && -}+(<&&>) :: Monad m => m Bool -> m Bool -> m Bool+ma <&&> mb = ifM ma ( mb , return False )++{- Same fixity as && and || -}+infixr 3 <&&>+infixr 2 <||>++{- Runs an action, passing its value to an observer before returning it. -}+observe :: Monad m => (a -> m b) -> m a -> m a+observe observer a = do+ r <- a+ _ <- observer r+ return r++{- b `after` a runs first a, then b, and returns the value of a -}+after :: Monad m => m b -> m a -> m a+after = observe . const++{- do nothing -}+noop :: Monad m => m ()+noop = return ()
+ src/Utility/PartialPrelude.hs view
@@ -0,0 +1,68 @@+{- Parts of the Prelude are partial functions, which are a common source of+ - bugs.+ -+ - This exports functions that conflict with the prelude, which avoids+ - them being accidentially used.+ -}++module Utility.PartialPrelude where++import qualified Data.Maybe++{- read should be avoided, as it throws an error+ - Instead, use: readish -}+read :: Read a => String -> a+read = Prelude.read++{- head is a partial function; head [] is an error+ - Instead, use: take 1 or headMaybe -}+head :: [a] -> a+head = Prelude.head++{- tail is also partial+ - Instead, use: drop 1 -}+tail :: [a] -> [a]+tail = Prelude.tail++{- init too+ - Instead, use: beginning -}+init :: [a] -> [a]+init = Prelude.init++{- last too+ - Instead, use: end or lastMaybe -}+last :: [a] -> a+last = Prelude.last++{- Attempts to read a value from a String.+ -+ - Ignores leading/trailing whitespace, and throws away any trailing+ - text after the part that can be read.+ -+ - readMaybe is available in Text.Read in new versions of GHC,+ - but that one requires the entire string to be consumed.+ -}+readish :: Read a => String -> Maybe a+readish s = case reads s of+ ((x,_):_) -> Just x+ _ -> Nothing++{- Like head but Nothing on empty list. -}+headMaybe :: [a] -> Maybe a+headMaybe = Data.Maybe.listToMaybe++{- Like last but Nothing on empty list. -}+lastMaybe :: [a] -> Maybe a+lastMaybe [] = Nothing+lastMaybe v = Just $ Prelude.last v++{- All but the last element of a list.+ - (Like init, but no error on an empty list.) -}+beginning :: [a] -> [a]+beginning [] = []+beginning l = Prelude.init l++{- Like last, but no error on an empty list. -}+end :: [a] -> [a]+end [] = []+end l = [Prelude.last l]
+ src/Utility/Path.hs view
@@ -0,0 +1,293 @@+{- path manipulation+ -+ - Copyright 2010-2014 Joey Hess <joey@kitenet.net>+ -+ - License: BSD-2-clause+ -}++{-# LANGUAGE PackageImports, CPP #-}++module Utility.Path where++import Data.String.Utils+import System.FilePath+import System.Directory+import Data.List+import Data.Maybe+import Data.Char+import Control.Applicative++#ifdef mingw32_HOST_OS+import qualified System.FilePath.Posix as Posix+#else+import System.Posix.Files+#endif++import qualified "MissingH" System.Path as MissingH+import Utility.Monad+import Utility.UserInfo++{- Simplifies a path, removing any ".." or ".", and removing the trailing+ - path separator.+ -+ - On Windows, preserves whichever style of path separator might be used in+ - the input FilePaths. This is done because some programs in Windows+ - demand a particular path separator -- and which one actually varies!+ -+ - This does not guarantee that two paths that refer to the same location,+ - and are both relative to the same location (or both absolute) will+ - yeild the same result. Run both through normalise from System.FilePath+ - to ensure that.+ -}+simplifyPath :: FilePath -> FilePath+simplifyPath path = dropTrailingPathSeparator $ + joinDrive drive $ joinPath $ norm [] $ splitPath path'+ where+ (drive, path') = splitDrive path++ norm c [] = reverse c+ norm c (p:ps)+ | p' == ".." = norm (drop 1 c) ps+ | p' == "." = norm c ps+ | otherwise = norm (p:c) ps+ where+ p' = dropTrailingPathSeparator p++{- Makes a path absolute.+ -+ - The first parameter is a base directory (ie, the cwd) to use if the path+ - is not already absolute.+ -+ - Does not attempt to deal with edge cases or ensure security with+ - untrusted inputs.+ -}+absPathFrom :: FilePath -> FilePath -> FilePath+absPathFrom dir path = simplifyPath (combine dir path)++{- On Windows, this converts the paths to unix-style, in order to run+ - MissingH's absNormPath on them. Resulting path will use / separators. -}+absNormPathUnix :: FilePath -> FilePath -> Maybe FilePath+#ifndef mingw32_HOST_OS+absNormPathUnix dir path = MissingH.absNormPath dir path+#else+absNormPathUnix dir path = todos <$> MissingH.absNormPath (fromdos dir) (fromdos path)+ where+ fromdos = replace "\\" "/"+ todos = replace "/" "\\"+#endif++{- Returns the parent directory of a path.+ -+ - To allow this to be easily used in loops, which terminate upon reaching the+ - top, the parent of / is "" -}+parentDir :: FilePath -> FilePath+parentDir dir+ | null dirs = ""+ | otherwise = joinDrive drive (join s $ init dirs)+ where+ -- on Unix, the drive will be "/" when the dir is absolute, otherwise ""+ (drive, path) = splitDrive dir+ dirs = filter (not . null) $ split s path+ s = [pathSeparator]++prop_parentDir_basics :: FilePath -> Bool+prop_parentDir_basics dir+ | null dir = True+ | dir == "/" = parentDir dir == ""+ | otherwise = p /= dir+ where+ p = parentDir dir++{- Checks if the first FilePath is, or could be said to contain the second.+ - For example, "foo/" contains "foo/bar". Also, "foo", "./foo", "foo/" etc+ - are all equivilant.+ -}+dirContains :: FilePath -> FilePath -> Bool+dirContains a b = a == b || a' == b' || (addTrailingPathSeparator a') `isPrefixOf` b'+ where+ a' = norm a+ b' = norm b+ norm = normalise . simplifyPath++{- Converts a filename into an absolute path.+ -+ - Unlike Directory.canonicalizePath, this does not require the path+ - already exists. -}+absPath :: FilePath -> IO FilePath+absPath file = do+ cwd <- getCurrentDirectory+ return $ absPathFrom cwd file++{- Constructs a relative path from the CWD to a file.+ -+ - For example, assuming CWD is /tmp/foo/bar:+ - relPathCwdToFile "/tmp/foo" == ".."+ - relPathCwdToFile "/tmp/foo/bar" == "" + -}+relPathCwdToFile :: FilePath -> IO FilePath+relPathCwdToFile f = relPathDirToFile <$> getCurrentDirectory <*> absPath f++{- Constructs a relative path from a directory to a file.+ -+ - Both must be absolute, and cannot contain .. etc. (eg use absPath first).+ -}+relPathDirToFile :: FilePath -> FilePath -> FilePath+relPathDirToFile from to = join s $ dotdots ++ uncommon+ where+ s = [pathSeparator]+ pfrom = split s from+ pto = split s to+ common = map fst $ takeWhile same $ zip pfrom pto+ same (c,d) = c == d+ uncommon = drop numcommon pto+ dotdots = replicate (length pfrom - numcommon) ".."+ numcommon = length common++prop_relPathDirToFile_basics :: FilePath -> FilePath -> Bool+prop_relPathDirToFile_basics from to+ | from == to = null r+ | otherwise = not (null r)+ where+ r = relPathDirToFile from to ++prop_relPathDirToFile_regressionTest :: Bool+prop_relPathDirToFile_regressionTest = same_dir_shortcurcuits_at_difference+ where+ {- Two paths have the same directory component at the same+ - location, but it's not really the same directory.+ - Code used to get this wrong. -}+ same_dir_shortcurcuits_at_difference =+ relPathDirToFile (joinPath [pathSeparator : "tmp", "r", "lll", "xxx", "yyy", "18"])+ (joinPath [pathSeparator : "tmp", "r", ".git", "annex", "objects", "18", "gk", "SHA256-foo", "SHA256-foo"])+ == joinPath ["..", "..", "..", "..", ".git", "annex", "objects", "18", "gk", "SHA256-foo", "SHA256-foo"]++{- Given an original list of paths, and an expanded list derived from it,+ - generates a list of lists, where each sublist corresponds to one of the+ - original paths. When the original path is a directory, any items+ - in the expanded list that are contained in that directory will appear in+ - its segment.+ -}+segmentPaths :: [FilePath] -> [FilePath] -> [[FilePath]]+segmentPaths [] new = [new]+segmentPaths [_] new = [new] -- optimisation+segmentPaths (l:ls) new = [found] ++ segmentPaths ls rest+ where+ (found, rest)=partition (l `dirContains`) new++{- This assumes that it's cheaper to call segmentPaths on the result,+ - than it would be to run the action separately with each path. In+ - the case of git file list commands, that assumption tends to hold.+ -}+runSegmentPaths :: ([FilePath] -> IO [FilePath]) -> [FilePath] -> IO [[FilePath]]+runSegmentPaths a paths = segmentPaths paths <$> a paths++{- Converts paths in the home directory to use ~/ -}+relHome :: FilePath -> IO String+relHome path = do+ home <- myHomeDir+ return $ if dirContains home path+ then "~/" ++ relPathDirToFile home path+ else path++{- Checks if a command is available in PATH.+ -+ - The command may be fully-qualified, in which case, this succeeds as+ - long as it exists. -}+inPath :: String -> IO Bool+inPath command = isJust <$> searchPath command++{- Finds a command in PATH and returns the full path to it.+ -+ - The command may be fully qualified already, in which case it will+ - be returned if it exists.+ -}+searchPath :: String -> IO (Maybe FilePath)+searchPath command+ | isAbsolute command = check command+ | otherwise = getSearchPath >>= getM indir+ where+ indir d = check $ d </> command+ check f = firstM doesFileExist+#ifdef mingw32_HOST_OS+ [f, f ++ ".exe"]+#else+ [f]+#endif++{- Checks if a filename is a unix dotfile. All files inside dotdirs+ - count as dotfiles. -}+dotfile :: FilePath -> Bool+dotfile file+ | f == "." = False+ | f == ".." = False+ | f == "" = False+ | otherwise = "." `isPrefixOf` f || dotfile (takeDirectory file)+ where+ f = takeFileName file++{- Converts a DOS style path to a Cygwin style path. Only on Windows.+ - Any trailing '\' is preserved as a trailing '/' -}+toCygPath :: FilePath -> FilePath+#ifndef mingw32_HOST_OS+toCygPath = id+#else+toCygPath p+ | null drive = recombine parts+ | otherwise = recombine $ "/cygdrive" : driveletter drive : parts+ where+ (drive, p') = splitDrive p+ parts = splitDirectories p'+ driveletter = map toLower . takeWhile (/= ':')+ recombine = fixtrailing . Posix.joinPath+ fixtrailing s+ | hasTrailingPathSeparator p = Posix.addTrailingPathSeparator s+ | otherwise = s+#endif++{- Maximum size to use for a file in a specified directory.+ -+ - Many systems have a 255 byte limit to the name of a file, + - so that's taken as the max if the system has a larger limit, or has no+ - limit.+ -}+fileNameLengthLimit :: FilePath -> IO Int+#ifdef mingw32_HOST_OS+fileNameLengthLimit _ = return 255+#else+fileNameLengthLimit dir = do+ l <- fromIntegral <$> getPathVar dir FileNameLimit+ if l <= 0+ then return 255+ else return $ minimum [l, 255]+ where+#endif++{- Given a string that we'd like to use as the basis for FilePath, but that+ - was provided by a third party and is not to be trusted, returns the closest+ - sane FilePath.+ -+ - All spaces and punctuation and other wacky stuff are replaced+ - with '_', except for '.' "../" will thus turn into ".._", which is safe.+ -}+sanitizeFilePath :: String -> FilePath+sanitizeFilePath = map sanitize+ where+ sanitize c+ | c == '.' = c+ | isSpace c || isPunctuation c || isSymbol c || isControl c || c == '/' = '_'+ | otherwise = c++{- Similar to splitExtensions, but knows that some things in FilePaths+ - after a dot are too long to be extensions. -}+splitShortExtensions :: FilePath -> (FilePath, [String])+splitShortExtensions = splitShortExtensions' 5 -- enough for ".jpeg"+splitShortExtensions' :: Int -> FilePath -> (FilePath, [String])+splitShortExtensions' maxextension = go []+ where+ go c f+ | len > 0 && len <= maxextension && not (null base) = + go (ext:c) base+ | otherwise = (f, c)+ where+ (base, ext) = splitExtension f+ len = length ext
+ src/Utility/PosixFiles.hs view
@@ -0,0 +1,33 @@+{- POSIX files (and compatablity wrappers).+ -+ - This is like System.PosixCompat.Files, except with a fixed rename.+ -+ - Copyright 2014 Joey Hess <joey@kitenet.net>+ -+ - License: BSD-2-clause+ -}++{-# LANGUAGE CPP #-}++module Utility.PosixFiles (+ module X,+ rename+) where++import System.PosixCompat.Files as X hiding (rename)++#ifndef mingw32_HOST_OS+import System.Posix.Files (rename)+#else+import qualified System.Win32.File as Win32+#endif++{- System.PosixCompat.Files.rename on Windows calls renameFile,+ - so cannot rename directories. + -+ - Instead, use Win32 moveFile, which can. It needs to be told to overwrite+ - any existing file. -}+#ifdef mingw32_HOST_OS+rename :: FilePath -> FilePath -> IO ()+rename src dest = Win32.moveFileEx src dest Win32.mOVEFILE_REPLACE_EXISTING+#endif
+ src/Utility/Process.hs view
@@ -0,0 +1,353 @@+{- System.Process enhancements, including additional ways of running+ - processes, and logging.+ -+ - Copyright 2012 Joey Hess <joey@kitenet.net>+ -+ - License: BSD-2-clause+ -}++{-# LANGUAGE CPP, Rank2Types #-}++module Utility.Process (+ module X,+ CreateProcess,+ StdHandle(..),+ readProcess,+ readProcessEnv,+ writeReadProcessEnv,+ forceSuccessProcess,+ checkSuccessProcess,+ ignoreFailureProcess,+ createProcessSuccess,+ createProcessChecked,+ createBackgroundProcess,+ processTranscript,+ processTranscript',+ withHandle,+ withBothHandles,+ withQuietOutput,+ createProcess,+ startInteractiveProcess,+ stdinHandle,+ stdoutHandle,+ stderrHandle,+ processHandle,+ devNull,+) where++import qualified System.Process+import System.Process as X hiding (CreateProcess(..), createProcess, runInteractiveProcess, readProcess, readProcessWithExitCode, system, rawSystem, runInteractiveCommand, runProcess)+import System.Process hiding (createProcess, readProcess)+import System.Exit+import System.IO+import System.Log.Logger+import Control.Concurrent+import qualified Control.Exception as E+import Control.Monad+#ifndef mingw32_HOST_OS+import System.Posix.IO+#else+import Control.Applicative+#endif+import Data.Maybe++import Utility.Misc+import Utility.Exception++type CreateProcessRunner = forall a. CreateProcess -> ((Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) -> IO a) -> IO a++data StdHandle = StdinHandle | StdoutHandle | StderrHandle+ deriving (Eq)++{- Normally, when reading from a process, it does not need to be fed any+ - standard input. -}+readProcess :: FilePath -> [String] -> IO String+readProcess cmd args = readProcessEnv cmd args Nothing++readProcessEnv :: FilePath -> [String] -> Maybe [(String, String)] -> IO String+readProcessEnv cmd args environ =+ withHandle StdoutHandle createProcessSuccess p $ \h -> do+ output <- hGetContentsStrict h+ hClose h+ return output+ where+ p = (proc cmd args)+ { std_out = CreatePipe+ , env = environ+ }++{- Runs an action to write to a process on its stdin, + - returns its output, and also allows specifying the environment.+ -}+writeReadProcessEnv+ :: FilePath+ -> [String]+ -> Maybe [(String, String)]+ -> (Maybe (Handle -> IO ()))+ -> (Maybe (Handle -> IO ()))+ -> IO String+writeReadProcessEnv cmd args environ writestdin adjusthandle = do+ (Just inh, Just outh, _, pid) <- createProcess p++ maybe (return ()) (\a -> a inh) adjusthandle+ maybe (return ()) (\a -> a outh) adjusthandle++ -- fork off a thread to start consuming the output+ output <- hGetContents outh+ outMVar <- newEmptyMVar+ _ <- forkIO $ E.evaluate (length output) >> putMVar outMVar ()++ -- now write and flush any input+ maybe (return ()) (\a -> a inh >> hFlush inh) writestdin+ hClose inh -- done with stdin++ -- wait on the output+ takeMVar outMVar+ hClose outh++ -- wait on the process+ forceSuccessProcess p pid++ return output++ where+ p = (proc cmd args)+ { std_in = CreatePipe+ , std_out = CreatePipe+ , std_err = Inherit+ , env = environ+ }++{- Waits for a ProcessHandle, and throws an IOError if the process+ - did not exit successfully. -}+forceSuccessProcess :: CreateProcess -> ProcessHandle -> IO ()+forceSuccessProcess p pid = do+ code <- waitForProcess pid+ case code of+ ExitSuccess -> return ()+ ExitFailure n -> fail $ showCmd p ++ " exited " ++ show n++{- Waits for a ProcessHandle and returns True if it exited successfully.+ - Note that using this with createProcessChecked will throw away+ - the Bool, and is only useful to ignore the exit code of a process,+ - while still waiting for it. -}+checkSuccessProcess :: ProcessHandle -> IO Bool+checkSuccessProcess pid = do+ code <- waitForProcess pid+ return $ code == ExitSuccess++ignoreFailureProcess :: ProcessHandle -> IO Bool+ignoreFailureProcess pid = do+ void $ waitForProcess pid+ return True++{- Runs createProcess, then an action on its handles, and then+ - forceSuccessProcess. -}+createProcessSuccess :: CreateProcessRunner+createProcessSuccess p a = createProcessChecked (forceSuccessProcess p) p a++{- Runs createProcess, then an action on its handles, and then+ - a checker action on its exit code, which must wait for the process. -}+createProcessChecked :: (ProcessHandle -> IO b) -> CreateProcessRunner+createProcessChecked checker p a = do+ t@(_, _, _, pid) <- createProcess p+ r <- tryNonAsync $ a t+ _ <- checker pid+ either E.throw return r++{- Leaves the process running, suitable for lazy streaming.+ - Note: Zombies will result, and must be waited on. -}+createBackgroundProcess :: CreateProcessRunner+createBackgroundProcess p a = a =<< createProcess p++{- Runs a process, optionally feeding it some input, and+ - returns a transcript combining its stdout and stderr, and+ - whether it succeeded or failed. -}+processTranscript :: String -> [String] -> (Maybe String) -> IO (String, Bool)+processTranscript cmd opts input = processTranscript' cmd opts Nothing input++processTranscript' :: String -> [String] -> Maybe [(String, String)] -> (Maybe String) -> IO (String, Bool)+processTranscript' cmd opts environ input = do+#ifndef mingw32_HOST_OS+{- This implementation interleves stdout and stderr in exactly the order+ - the process writes them. -}+ (readf, writef) <- createPipe+ readh <- fdToHandle readf+ writeh <- fdToHandle writef+ p@(_, _, _, pid) <- createProcess $+ (proc cmd opts)+ { std_in = if isJust input then CreatePipe else Inherit+ , std_out = UseHandle writeh+ , std_err = UseHandle writeh+ , env = environ+ }+ hClose writeh++ get <- mkreader readh+ writeinput input p+ transcript <- get++ ok <- checkSuccessProcess pid+ return (transcript, ok)+#else+{- This implementation for Windows puts stderr after stdout. -}+ p@(_, _, _, pid) <- createProcess $+ (proc cmd opts)+ { std_in = if isJust input then CreatePipe else Inherit+ , std_out = CreatePipe+ , std_err = CreatePipe+ , env = environ+ }++ getout <- mkreader (stdoutHandle p)+ geterr <- mkreader (stderrHandle p)+ writeinput input p+ transcript <- (++) <$> getout <*> geterr++ ok <- checkSuccessProcess pid+ return (transcript, ok)+#endif+ where+ mkreader h = do+ s <- hGetContents h+ v <- newEmptyMVar+ void $ forkIO $ do+ void $ E.evaluate (length s)+ putMVar v ()+ return $ do+ takeMVar v+ return s++ writeinput (Just s) p = do+ let inh = stdinHandle p+ unless (null s) $ do+ hPutStr inh s+ hFlush inh+ hClose inh+ writeinput Nothing _ = return ()++{- Runs a CreateProcessRunner, on a CreateProcess structure, that+ - is adjusted to pipe only from/to a single StdHandle, and passes+ - the resulting Handle to an action. -}+withHandle+ :: StdHandle+ -> CreateProcessRunner+ -> CreateProcess+ -> (Handle -> IO a)+ -> IO a+withHandle h creator p a = creator p' $ a . select+ where+ base = p+ { std_in = Inherit+ , std_out = Inherit+ , std_err = Inherit+ }+ (select, p')+ | h == StdinHandle =+ (stdinHandle, base { std_in = CreatePipe })+ | h == StdoutHandle =+ (stdoutHandle, base { std_out = CreatePipe })+ | h == StderrHandle =+ (stderrHandle, base { std_err = CreatePipe })++{- Like withHandle, but passes (stdin, stdout) handles to the action. -}+withBothHandles+ :: CreateProcessRunner+ -> CreateProcess+ -> ((Handle, Handle) -> IO a)+ -> IO a+withBothHandles creator p a = creator p' $ a . bothHandles+ where+ p' = p+ { std_in = CreatePipe+ , std_out = CreatePipe+ , std_err = Inherit+ }++{- Forces the CreateProcessRunner to run quietly;+ - both stdout and stderr are discarded. -}+withQuietOutput+ :: CreateProcessRunner+ -> CreateProcess+ -> IO ()+withQuietOutput creator p = withFile devNull WriteMode $ \nullh -> do+ let p' = p+ { std_out = UseHandle nullh+ , std_err = UseHandle nullh+ }+ creator p' $ const $ return ()++devNull :: FilePath+#ifndef mingw32_HOST_OS+devNull = "/dev/null"+#else+devNull = "NUL"+#endif++{- Extract a desired handle from createProcess's tuple.+ - These partial functions are safe as long as createProcess is run+ - with appropriate parameters to set up the desired handle.+ - Get it wrong and the runtime crash will always happen, so should be+ - easily noticed. -}+type HandleExtractor = (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) -> Handle+stdinHandle :: HandleExtractor+stdinHandle (Just h, _, _, _) = h+stdinHandle _ = error "expected stdinHandle"+stdoutHandle :: HandleExtractor+stdoutHandle (_, Just h, _, _) = h+stdoutHandle _ = error "expected stdoutHandle"+stderrHandle :: HandleExtractor+stderrHandle (_, _, Just h, _) = h+stderrHandle _ = error "expected stderrHandle"+bothHandles :: (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) -> (Handle, Handle)+bothHandles (Just hin, Just hout, _, _) = (hin, hout)+bothHandles _ = error "expected bothHandles"++processHandle :: (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) -> ProcessHandle+processHandle (_, _, _, pid) = pid++{- Debugging trace for a CreateProcess. -}+debugProcess :: CreateProcess -> IO ()+debugProcess p = do+ debugM "Utility.Process" $ unwords+ [ action ++ ":"+ , showCmd p+ ]+ where+ action+ | piped (std_in p) && piped (std_out p) = "chat"+ | piped (std_in p) = "feed"+ | piped (std_out p) = "read"+ | otherwise = "call"+ piped Inherit = False+ piped _ = True++{- Shows the command that a CreateProcess will run. -}+showCmd :: CreateProcess -> String+showCmd = go . cmdspec+ where+ go (ShellCommand s) = s+ go (RawCommand c ps) = c ++ " " ++ show ps++{- Starts an interactive process. Unlike runInteractiveProcess in+ - System.Process, stderr is inherited. -}+startInteractiveProcess+ :: FilePath+ -> [String]+ -> Maybe [(String, String)]+ -> IO (ProcessHandle, Handle, Handle)+startInteractiveProcess cmd args environ = do+ let p = (proc cmd args)+ { std_in = CreatePipe+ , std_out = CreatePipe+ , std_err = Inherit+ , env = environ+ }+ (Just from, Just to, _, pid) <- createProcess p+ return (pid, to, from)++{- Wrapper around System.Process function that does debug logging. -}+createProcess :: CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)+createProcess p = do+ debugProcess p+ System.Process.createProcess p
+ src/Utility/QuickCheck.hs view
@@ -0,0 +1,52 @@+{- QuickCheck with additional instances+ -+ - Copyright 2012-2014 Joey Hess <joey@kitenet.net>+ -+ - License: BSD-2-clause+ -}++{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# LANGUAGE TypeSynonymInstances #-}++module Utility.QuickCheck+ ( module X+ , module Utility.QuickCheck+ ) where++import Test.QuickCheck as X+import Data.Time.Clock.POSIX+import System.Posix.Types+import qualified Data.Map as M+import qualified Data.Set as S+import Control.Applicative++instance (Arbitrary k, Arbitrary v, Eq k, Ord k) => Arbitrary (M.Map k v) where+ arbitrary = M.fromList <$> arbitrary++instance (Arbitrary v, Eq v, Ord v) => Arbitrary (S.Set v) where+ arbitrary = S.fromList <$> arbitrary++{- Times before the epoch are excluded. -}+instance Arbitrary POSIXTime where+ arbitrary = fromInteger <$> nonNegative arbitrarySizedIntegral++instance Arbitrary EpochTime where+ arbitrary = fromInteger <$> nonNegative arbitrarySizedIntegral++{- Pids are never negative, or 0. -}+instance Arbitrary ProcessID where+ arbitrary = arbitrarySizedBoundedIntegral `suchThat` (> 0)++{- Inodes are never negative. -}+instance Arbitrary FileID where+ arbitrary = nonNegative arbitrarySizedIntegral++{- File sizes are never negative. -}+instance Arbitrary FileOffset where+ arbitrary = nonNegative arbitrarySizedIntegral++nonNegative :: (Num a, Ord a) => Gen a -> Gen a+nonNegative g = g `suchThat` (>= 0)++positive :: (Num a, Ord a) => Gen a -> Gen a+positive g = g `suchThat` (> 0)
+ src/Utility/SafeCommand.hs view
@@ -0,0 +1,120 @@+{- safely running shell commands+ -+ - Copyright 2010-2013 Joey Hess <joey@kitenet.net>+ -+ - License: BSD-2-clause+ -}++module Utility.SafeCommand where++import System.Exit+import Utility.Process+import System.Process (env)+import Data.String.Utils+import Control.Applicative+import System.FilePath+import Data.Char++{- A type for parameters passed to a shell command. A command can+ - be passed either some Params (multiple parameters can be included,+ - whitespace-separated, or a single Param (for when parameters contain+ - whitespace), or a File.+ -}+data CommandParam = Params String | Param String | File FilePath+ deriving (Eq, Show, Ord)++{- Used to pass a list of CommandParams to a function that runs+ - a command and expects Strings. -}+toCommand :: [CommandParam] -> [String]+toCommand = concatMap unwrap+ where+ unwrap (Param s) = [s]+ unwrap (Params s) = filter (not . null) (split " " s)+ -- Files that start with a non-alphanumeric that is not a path+ -- separator are modified to avoid the command interpreting them as+ -- options or other special constructs.+ unwrap (File s@(h:_))+ | isAlphaNum h || h `elem` pathseps = [s]+ | otherwise = ["./" ++ s]+ unwrap (File s) = [s]+ -- '/' is explicitly included because it's an alternative+ -- path separator on Windows.+ pathseps = pathSeparator:"./"++{- Run a system command, and returns True or False+ - if it succeeded or failed.+ -}+boolSystem :: FilePath -> [CommandParam] -> IO Bool+boolSystem command params = boolSystemEnv command params Nothing++boolSystemEnv :: FilePath -> [CommandParam] -> Maybe [(String, String)] -> IO Bool+boolSystemEnv command params environ = dispatch <$> safeSystemEnv command params environ+ where+ dispatch ExitSuccess = True+ dispatch _ = False++{- Runs a system command, returning the exit status. -}+safeSystem :: FilePath -> [CommandParam] -> IO ExitCode+safeSystem command params = safeSystemEnv command params Nothing++safeSystemEnv :: FilePath -> [CommandParam] -> Maybe [(String, String)] -> IO ExitCode+safeSystemEnv command params environ = do+ (_, _, _, pid) <- createProcess (proc command $ toCommand params)+ { env = environ }+ waitForProcess pid++{- Wraps a shell command line inside sh -c, allowing it to be run in a+ - login shell that may not support POSIX shell, eg csh. -}+shellWrap :: String -> String+shellWrap cmdline = "sh -c " ++ shellEscape cmdline++{- Escapes a filename or other parameter to be safely able to be exposed to+ - the shell.+ -+ - This method works for POSIX shells, as well as other shells like csh.+ -}+shellEscape :: String -> String+shellEscape f = "'" ++ escaped ++ "'"+ where+ -- replace ' with '"'"'+ escaped = join "'\"'\"'" $ split "'" f++{- Unescapes a set of shellEscaped words or filenames. -}+shellUnEscape :: String -> [String]+shellUnEscape [] = []+shellUnEscape s = word : shellUnEscape rest+ where+ (word, rest) = findword "" s+ findword w [] = (w, "")+ findword w (c:cs)+ | c == ' ' = (w, cs)+ | c == '\'' = inquote c w cs+ | c == '"' = inquote c w cs+ | otherwise = findword (w++[c]) cs+ inquote _ w [] = (w, "")+ inquote q w (c:cs)+ | c == q = findword w cs+ | otherwise = inquote q (w++[c]) cs++{- For quickcheck. -}+prop_idempotent_shellEscape :: String -> Bool+prop_idempotent_shellEscape s = [s] == (shellUnEscape . shellEscape) s+prop_idempotent_shellEscape_multiword :: [String] -> Bool+prop_idempotent_shellEscape_multiword s = s == (shellUnEscape . unwords . map shellEscape) s++{- Segements a list of filenames into groups that are all below the manximum+ - command-line length limit. Does not preserve order. -}+segmentXargs :: [FilePath] -> [[FilePath]]+segmentXargs l = go l [] 0 []+ where+ go [] c _ r = c:r+ go (f:fs) c accumlen r+ | len < maxlen && newlen > maxlen = go (f:fs) [] 0 (c:r)+ | otherwise = go fs (f:c) newlen r+ where+ len = length f+ newlen = accumlen + len++ {- 10k of filenames per command, well under Linux's 20k limit;+ - allows room for other parameters etc. -}+ maxlen = 10240
+ src/Utility/Scheduled.hs view
@@ -0,0 +1,396 @@+{- scheduled activities+ - + - Copyright 2013-2014 Joey Hess <joey@kitenet.net>+ -+ - License: BSD-2-clause+ -}++module Utility.Scheduled (+ Schedule(..),+ Recurrance(..),+ ScheduledTime(..),+ NextTime(..),+ WeekDay,+ MonthDay,+ YearDay,+ nextTime,+ calcNextTime,+ startTime,+ fromSchedule,+ fromScheduledTime,+ toScheduledTime,+ fromRecurrance,+ toRecurrance,+ toSchedule,+ parseSchedule,+ prop_schedule_roundtrips,+ prop_past_sane,+) where++import Utility.Data+import Utility.QuickCheck+import Utility.PartialPrelude+import Utility.Misc++import Control.Applicative+import Data.List+import Data.Time.Clock+import Data.Time.LocalTime+import Data.Time.Calendar+import Data.Time.Calendar.WeekDate+import Data.Time.Calendar.OrdinalDate+import Data.Tuple.Utils+import Data.Char++{- Some sort of scheduled event. -}+data Schedule = Schedule Recurrance ScheduledTime+ deriving (Eq, Read, Show, Ord)++data Recurrance+ = Daily+ | Weekly (Maybe WeekDay)+ | Monthly (Maybe MonthDay)+ | Yearly (Maybe YearDay)+ | Divisible Int Recurrance+ -- ^ Days, Weeks, or Months of the year evenly divisible by a number.+ -- (Divisible Year is years evenly divisible by a number.)+ deriving (Eq, Read, Show, Ord)++type WeekDay = Int+type MonthDay = Int+type YearDay = Int++data ScheduledTime+ = AnyTime+ | SpecificTime Hour Minute+ deriving (Eq, Read, Show, Ord)++type Hour = Int+type Minute = Int++-- | Next time a Schedule should take effect. The NextTimeWindow is used+-- when a Schedule is allowed to start at some point within the window.+data NextTime+ = NextTimeExactly LocalTime+ | NextTimeWindow LocalTime LocalTime+ deriving (Eq, Read, Show)++startTime :: NextTime -> LocalTime+startTime (NextTimeExactly t) = t+startTime (NextTimeWindow t _) = t++nextTime :: Schedule -> Maybe LocalTime -> IO (Maybe NextTime)+nextTime schedule lasttime = do+ now <- getCurrentTime+ tz <- getTimeZone now+ return $ calcNextTime schedule lasttime $ utcToLocalTime tz now++-- | Calculate the next time that fits a Schedule, based on the+-- last time it occurred, and the current time.+calcNextTime :: Schedule -> Maybe LocalTime -> LocalTime -> Maybe NextTime+calcNextTime schedule@(Schedule recurrance scheduledtime) lasttime currenttime+ | scheduledtime == AnyTime = do+ next <- findfromtoday True+ return $ case next of+ NextTimeWindow _ _ -> next+ NextTimeExactly t -> window (localDay t) (localDay t)+ | otherwise = NextTimeExactly . startTime <$> findfromtoday False+ where+ findfromtoday anytime = findfrom recurrance afterday today+ where+ today = localDay currenttime+ afterday = sameaslastrun || toolatetoday+ toolatetoday = not anytime && localTimeOfDay currenttime >= nexttime+ sameaslastrun = lastrun == Just today+ lastrun = localDay <$> lasttime+ nexttime = case scheduledtime of+ AnyTime -> TimeOfDay 0 0 0+ SpecificTime h m -> TimeOfDay h m 0+ exactly d = NextTimeExactly $ LocalTime d nexttime+ window startd endd = NextTimeWindow+ (LocalTime startd nexttime)+ (LocalTime endd (TimeOfDay 23 59 0))+ findfrom r afterday candidate+ | ynum candidate > (ynum (localDay currenttime)) + 100 =+ -- avoid possible infinite recusion+ error $ "bug: calcNextTime did not find a time within 100 years to run " +++ show (schedule, lasttime, currenttime)+ | otherwise = findfromChecked r afterday candidate+ findfromChecked r afterday candidate = case r of+ Daily+ | afterday -> Just $ exactly $ addDays 1 candidate+ | otherwise -> Just $ exactly candidate+ Weekly Nothing+ | afterday -> skip 1+ | otherwise -> case (wday <$> lastrun, wday candidate) of+ (Nothing, _) -> Just $ window candidate (addDays 6 candidate)+ (Just old, curr)+ | old == curr -> Just $ window candidate (addDays 6 candidate)+ | otherwise -> skip 1+ Monthly Nothing+ | afterday -> skip 1+ | maybe True (candidate `oneMonthPast`) lastrun ->+ Just $ window candidate (endOfMonth candidate)+ | otherwise -> skip 1+ Yearly Nothing+ | afterday -> skip 1+ | maybe True (candidate `oneYearPast`) lastrun ->+ Just $ window candidate (endOfYear candidate)+ | otherwise -> skip 1+ Weekly (Just w)+ | w < 0 || w > maxwday -> Nothing+ | w == wday candidate -> if afterday+ then Just $ exactly $ addDays 7 candidate+ else Just $ exactly candidate+ | otherwise -> Just $ exactly $+ addDays (fromIntegral $ (w - wday candidate) `mod` 7) candidate+ Monthly (Just m)+ | m < 0 || m > maxmday -> Nothing+ -- TODO can be done more efficiently than recursing+ | m == mday candidate -> if afterday+ then skip 1+ else Just $ exactly candidate+ | otherwise -> skip 1+ Yearly (Just y)+ | y < 0 || y > maxyday -> Nothing+ | y == yday candidate -> if afterday+ then skip 365+ else Just $ exactly candidate+ | otherwise -> skip 1+ Divisible n r'@Daily -> handlediv n r' yday (Just maxyday)+ Divisible n r'@(Weekly _) -> handlediv n r' wnum (Just maxwnum)+ Divisible n r'@(Monthly _) -> handlediv n r' mnum (Just maxmnum)+ Divisible n r'@(Yearly _) -> handlediv n r' ynum Nothing+ Divisible _ r'@(Divisible _ _) -> findfrom r' afterday candidate+ where+ skip n = findfrom r False (addDays n candidate)+ handlediv n r' getval mmax+ | n > 0 && maybe True (n <=) mmax =+ findfromwhere r' (divisible n . getval) afterday candidate+ | otherwise = Nothing+ findfromwhere r p afterday candidate+ | maybe True (p . getday) next = next+ | otherwise = maybe Nothing (findfromwhere r p True . getday) next+ where+ next = findfrom r afterday candidate+ getday = localDay . startTime+ divisible n v = v `rem` n == 0++-- Check if the new Day occurs one month or more past the old Day.+oneMonthPast :: Day -> Day -> Bool+new `oneMonthPast` old = fromGregorian y (m+1) d <= new+ where+ (y,m,d) = toGregorian old++-- Check if the new Day occurs one year or more past the old Day.+oneYearPast :: Day -> Day -> Bool+new `oneYearPast` old = fromGregorian (y+1) m d <= new+ where+ (y,m,d) = toGregorian old++endOfMonth :: Day -> Day+endOfMonth day =+ let (y,m,_d) = toGregorian day+ in fromGregorian y m (gregorianMonthLength y m)++endOfYear :: Day -> Day+endOfYear day =+ let (y,_m,_d) = toGregorian day+ in endOfMonth (fromGregorian y maxmnum 1)++-- extracting various quantities from a Day+wday :: Day -> Int+wday = thd3 . toWeekDate+wnum :: Day -> Int+wnum = snd3 . toWeekDate+mday :: Day -> Int+mday = thd3 . toGregorian+mnum :: Day -> Int+mnum = snd3 . toGregorian+yday :: Day -> Int+yday = snd . toOrdinalDate+ynum :: Day -> Int+ynum = fromIntegral . fst . toOrdinalDate++-- Calendar max values.+maxyday :: Int+maxyday = 366 -- with leap days+maxwnum :: Int+maxwnum = 53 -- some years have more than 52+maxmday :: Int+maxmday = 31+maxmnum :: Int+maxmnum = 12+maxwday :: Int+maxwday = 7++fromRecurrance :: Recurrance -> String+fromRecurrance (Divisible n r) =+ fromRecurrance' (++ "s divisible by " ++ show n) r+fromRecurrance r = fromRecurrance' ("every " ++) r++fromRecurrance' :: (String -> String) -> Recurrance -> String+fromRecurrance' a Daily = a "day"+fromRecurrance' a (Weekly n) = onday n (a "week")+fromRecurrance' a (Monthly n) = onday n (a "month")+fromRecurrance' a (Yearly n) = onday n (a "year")+fromRecurrance' a (Divisible _n r) = fromRecurrance' a r -- not used++onday :: Maybe Int -> String -> String+onday (Just n) s = "on day " ++ show n ++ " of " ++ s+onday Nothing s = s++toRecurrance :: String -> Maybe Recurrance+toRecurrance s = case words s of+ ("every":"day":[]) -> Just Daily+ ("on":"day":sd:"of":"every":something:[]) -> withday sd something+ ("every":something:[]) -> noday something+ ("days":"divisible":"by":sn:[]) -> + Divisible <$> getdivisor sn <*> pure Daily+ ("on":"day":sd:"of":something:"divisible":"by":sn:[]) -> + Divisible+ <$> getdivisor sn+ <*> withday sd something+ ("every":something:"divisible":"by":sn:[]) -> + Divisible+ <$> getdivisor sn+ <*> noday something+ (something:"divisible":"by":sn:[]) -> + Divisible+ <$> getdivisor sn+ <*> noday something+ _ -> Nothing+ where+ constructor "week" = Just Weekly+ constructor "month" = Just Monthly+ constructor "year" = Just Yearly+ constructor u+ | "s" `isSuffixOf` u = constructor $ reverse $ drop 1 $ reverse u+ | otherwise = Nothing+ withday sd u = do+ c <- constructor u+ d <- readish sd+ Just $ c (Just d)+ noday u = do+ c <- constructor u+ Just $ c Nothing+ getdivisor sn = do+ n <- readish sn+ if n > 0+ then Just n+ else Nothing++fromScheduledTime :: ScheduledTime -> String+fromScheduledTime AnyTime = "any time"+fromScheduledTime (SpecificTime h m) = + show h' ++ (if m > 0 then ":" ++ pad 2 (show m) else "") ++ " " ++ ampm+ where+ pad n s = take (n - length s) (repeat '0') ++ s+ (h', ampm)+ | h == 0 = (12, "AM")+ | h < 12 = (h, "AM")+ | h == 12 = (h, "PM")+ | otherwise = (h - 12, "PM")++toScheduledTime :: String -> Maybe ScheduledTime+toScheduledTime "any time" = Just AnyTime+toScheduledTime v = case words v of+ (s:ampm:[])+ | map toUpper ampm == "AM" ->+ go s h0+ | map toUpper ampm == "PM" ->+ go s (\h -> (h0 h) + 12)+ | otherwise -> Nothing+ (s:[]) -> go s id+ _ -> Nothing+ where+ h0 h+ | h == 12 = 0+ | otherwise = h+ go :: String -> (Int -> Int) -> Maybe ScheduledTime+ go s adjust =+ let (h, m) = separate (== ':') s+ in SpecificTime+ <$> (adjust <$> readish h)+ <*> if null m then Just 0 else readish m++fromSchedule :: Schedule -> String+fromSchedule (Schedule recurrance scheduledtime) = unwords+ [ fromRecurrance recurrance+ , "at"+ , fromScheduledTime scheduledtime+ ]++toSchedule :: String -> Maybe Schedule+toSchedule = eitherToMaybe . parseSchedule++parseSchedule :: String -> Either String Schedule+parseSchedule s = do+ r <- maybe (Left $ "bad recurrance: " ++ recurrance) Right+ (toRecurrance recurrance)+ t <- maybe (Left $ "bad time of day: " ++ scheduledtime) Right+ (toScheduledTime scheduledtime)+ Right $ Schedule r t+ where+ (rws, tws) = separate (== "at") (words s)+ recurrance = unwords rws+ scheduledtime = unwords tws++instance Arbitrary Schedule where+ arbitrary = Schedule <$> arbitrary <*> arbitrary++instance Arbitrary ScheduledTime where+ arbitrary = oneof+ [ pure AnyTime+ , SpecificTime + <$> choose (0, 23)+ <*> choose (1, 59)+ ]++instance Arbitrary Recurrance where+ arbitrary = oneof+ [ pure Daily+ , Weekly <$> arbday+ , Monthly <$> arbday+ , Yearly <$> arbday+ , Divisible+ <$> positive arbitrary+ <*> oneof -- no nested Divisibles+ [ pure Daily+ , Weekly <$> arbday+ , Monthly <$> arbday+ , Yearly <$> arbday+ ]+ ]+ where+ arbday = oneof+ [ Just <$> nonNegative arbitrary+ , pure Nothing+ ]++prop_schedule_roundtrips :: Schedule -> Bool+prop_schedule_roundtrips s = toSchedule (fromSchedule s) == Just s++prop_past_sane :: Bool+prop_past_sane = and+ [ all (checksout oneMonthPast) (mplus1 ++ yplus1)+ , all (not . (checksout oneMonthPast)) (map swap (mplus1 ++ yplus1))+ , all (checksout oneYearPast) yplus1+ , all (not . (checksout oneYearPast)) (map swap yplus1)+ ]+ where+ mplus1 = -- new date old date, 1+ months before it+ [ (fromGregorian 2014 01 15, fromGregorian 2013 12 15)+ , (fromGregorian 2014 01 15, fromGregorian 2013 02 15)+ , (fromGregorian 2014 02 15, fromGregorian 2013 01 15)+ , (fromGregorian 2014 03 01, fromGregorian 2013 01 15)+ , (fromGregorian 2014 03 01, fromGregorian 2013 12 15)+ , (fromGregorian 2015 01 01, fromGregorian 2010 01 01)+ ]+ yplus1 = -- new date old date, 1+ years before it+ [ (fromGregorian 2014 01 15, fromGregorian 2012 01 16)+ , (fromGregorian 2014 01 15, fromGregorian 2013 01 14)+ , (fromGregorian 2022 12 31, fromGregorian 2000 01 01)+ ]+ checksout cmp (new, old) = new `cmp` old+ swap (a,b) = (b,a)
+ src/Utility/ThreadScheduler.hs view
@@ -0,0 +1,75 @@+{- thread scheduling+ -+ - Copyright 2012, 2013 Joey Hess <joey@kitenet.net>+ - Copyright 2011 Bas van Dijk & Roel van Dijk+ -+ - License: BSD-2-clause+ -}++{-# LANGUAGE CPP #-}++module Utility.ThreadScheduler where++import Control.Monad+import Control.Concurrent+#ifndef mingw32_HOST_OS+import Control.Monad.IfElse+import System.Posix.IO+#endif+#ifndef mingw32_HOST_OS+import System.Posix.Signals+#ifndef __ANDROID__+import System.Posix.Terminal+#endif+#endif++newtype Seconds = Seconds { fromSeconds :: Int }+ deriving (Eq, Ord, Show)++type Microseconds = Integer++{- Runs an action repeatedly forever, sleeping at least the specified number+ - of seconds in between. -}+runEvery :: Seconds -> IO a -> IO a+runEvery n a = forever $ do+ threadDelaySeconds n+ a++threadDelaySeconds :: Seconds -> IO ()+threadDelaySeconds (Seconds n) = unboundDelay (fromIntegral n * oneSecond)++{- Like threadDelay, but not bounded by an Int.+ -+ - There is no guarantee that the thread will be rescheduled promptly when the+ - delay has expired, but the thread will never continue to run earlier than+ - specified.+ - + - Taken from the unbounded-delay package to avoid a dependency for 4 lines+ - of code.+ -}+unboundDelay :: Microseconds -> IO ()+unboundDelay time = do+ let maxWait = min time $ toInteger (maxBound :: Int)+ threadDelay $ fromInteger maxWait+ when (maxWait /= time) $ unboundDelay (time - maxWait)++{- Pauses the main thread, letting children run until program termination. -}+waitForTermination :: IO ()+waitForTermination = do+#ifdef mingw32_HOST_OS+ runEvery (Seconds 600) $+ void getLine+#else+ lock <- newEmptyMVar+ let check sig = void $+ installHandler sig (CatchOnce $ putMVar lock ()) Nothing+ check softwareTermination+#ifndef __ANDROID__+ whenM (queryTerminal stdInput) $+ check keyboardSignal+#endif+ takeMVar lock+#endif++oneSecond :: Microseconds+oneSecond = 1000000
+ src/Utility/Tmp.hs view
@@ -0,0 +1,100 @@+{- Temporary files and directories.+ -+ - Copyright 2010-2013 Joey Hess <joey@kitenet.net>+ -+ - License: BSD-2-clause+ -}++{-# LANGUAGE CPP #-}++module Utility.Tmp where++import Control.Exception (bracket)+import System.IO+import System.Directory+import Control.Monad.IfElse+import System.FilePath++import Utility.Exception+import Utility.FileSystemEncoding+import Utility.PosixFiles++type Template = String++{- Runs an action like writeFile, writing to a temp file first and+ - then moving it into place. The temp file is stored in the same+ - directory as the final file to avoid cross-device renames. -}+viaTmp :: (FilePath -> String -> IO ()) -> FilePath -> String -> IO ()+viaTmp a file content = do+ let (dir, base) = splitFileName file+ createDirectoryIfMissing True dir+ (tmpfile, handle) <- openTempFile dir (base ++ ".tmp")+ hClose handle+ a tmpfile content+ rename tmpfile file++{- Runs an action with a tmp file located in the system's tmp directory+ - (or in "." if there is none) then removes the file. -}+withTmpFile :: Template -> (FilePath -> Handle -> IO a) -> IO a+withTmpFile template a = do+ tmpdir <- catchDefaultIO "." getTemporaryDirectory+ withTmpFileIn tmpdir template a++{- Runs an action with a tmp file located in the specified directory,+ - then removes the file. -}+withTmpFileIn :: FilePath -> Template -> (FilePath -> Handle -> IO a) -> IO a+withTmpFileIn tmpdir template a = bracket create remove use+ where+ create = openTempFile tmpdir template+ remove (name, handle) = do+ hClose handle+ catchBoolIO (removeFile name >> return True)+ use (name, handle) = a name handle++{- Runs an action with a tmp directory located within the system's tmp+ - directory (or within "." if there is none), then removes the tmp+ - directory and all its contents. -}+withTmpDir :: Template -> (FilePath -> IO a) -> IO a+withTmpDir template a = do+ tmpdir <- catchDefaultIO "." getTemporaryDirectory+ withTmpDirIn tmpdir template a++{- Runs an action with a tmp directory located within a specified directory,+ - then removes the tmp directory and all its contents. -}+withTmpDirIn :: FilePath -> Template -> (FilePath -> IO a) -> IO a+withTmpDirIn tmpdir template = bracket create remove+ where+ remove d = whenM (doesDirectoryExist d) $ do+#if mingw32_HOST_OS+ -- Windows will often refuse to delete a file+ -- after a process has just written to it and exited.+ -- Because it's crap, presumably. So, ignore failure+ -- to delete the temp directory.+ _ <- tryIO $ removeDirectoryRecursive d+ return ()+#else+ removeDirectoryRecursive d+#endif+ create = do+ createDirectoryIfMissing True tmpdir+ makenewdir (tmpdir </> template) (0 :: Int)+ makenewdir t n = do+ let dir = t ++ "." ++ show n+ either (const $ makenewdir t $ n + 1) (const $ return dir)+ =<< tryIO (createDirectory dir)++{- It's not safe to use a FilePath of an existing file as the template+ - for openTempFile, because if the FilePath is really long, the tmpfile+ - will be longer, and may exceed the maximum filename length.+ -+ - This generates a template that is never too long.+ - (Well, it allocates 20 characters for use in making a unique temp file,+ - anyway, which is enough for the current implementation and any+ - likely implementation.)+ -}+relatedTemplate :: FilePath -> FilePath+relatedTemplate f+ | len > 20 = truncateFilePath (len - 20) f+ | otherwise = f+ where+ len = length f
+ src/Utility/UserInfo.hs view
@@ -0,0 +1,55 @@+{- user info+ -+ - Copyright 2012 Joey Hess <joey@kitenet.net>+ -+ - License: BSD-2-clause+ -}++{-# LANGUAGE CPP #-}++module Utility.UserInfo (+ myHomeDir,+ myUserName,+ myUserGecos,+) where++import Control.Applicative+import System.PosixCompat++import Utility.Env++{- Current user's home directory.+ -+ - getpwent will fail on LDAP or NIS, so use HOME if set. -}+myHomeDir :: IO FilePath+myHomeDir = myVal env homeDirectory+ where+#ifndef mingw32_HOST_OS+ env = ["HOME"]+#else+ env = ["USERPROFILE", "HOME"] -- HOME is used in Cygwin+#endif++{- Current user's user name. -}+myUserName :: IO String+myUserName = myVal env userName+ where+#ifndef mingw32_HOST_OS+ env = ["USER", "LOGNAME"]+#else+ env = ["USERNAME", "USER", "LOGNAME"]+#endif++myUserGecos :: IO String+#ifdef __ANDROID__+myUserGecos = return "" -- userGecos crashes on Android+#else+myUserGecos = myVal [] userGecos+#endif++myVal :: [String] -> (UserEntry -> String) -> IO String+myVal envvars extract = maybe (extract <$> getpwent) return =<< check envvars+ where+ check [] = return Nothing+ check (v:vs) = maybe (check vs) (return . Just) =<< getEnv v+ getpwent = getUserEntryForID =<< getEffectiveUserID
+ src/config.hs view
@@ -0,0 +1,46 @@+-- This is the main configuration file for Propellor, and is used to build+-- the propellor program.++import Propellor+import Propellor.CmdLine+import Propellor.Property.Scheduled+import qualified Propellor.Property.File as File+import qualified Propellor.Property.Apt as Apt+import qualified Propellor.Property.Network as Network+--import qualified Propellor.Property.Ssh as Ssh+import qualified Propellor.Property.Cron as Cron+--import qualified Propellor.Property.Sudo as Sudo+import qualified Propellor.Property.User as User+--import qualified Propellor.Property.Hostname as Hostname+--import qualified Propellor.Property.Reboot as Reboot+--import qualified Propellor.Property.Tor as Tor+import qualified Propellor.Property.Docker as Docker++main :: IO ()+main = defaultMain hosts++-- The hosts propellor knows about.+-- Edit this to configure propellor!+hosts :: [Host]+hosts =+ [ host "mybox.example.com"+ & Apt.stdSourcesList Unstable+ & Apt.unattendedUpgrades+ & Apt.installed ["etckeeper"]+ & Apt.installed ["ssh"]+ & User.hasSomePassword "root"+ & Network.ipv6to4+ & File.dirExists "/var/www"+ & Docker.docked hosts "webserver"+ & Docker.garbageCollected `period` Daily+ & Cron.runPropellor "30 * * * *"++ -- A generic webserver in a Docker container.+ , Docker.container "webserver" "joeyh/debian-unstable"+ & Docker.publish "80:80"+ & Docker.volume "/var/www:/var/www"+ & Apt.serviceInstalledRunning "apache2"++ -- add more hosts here...+ --, host "foo.example.com" = ...+ ]
+ src/wrapper.hs view
@@ -0,0 +1,93 @@+-- | Wrapper program for propellor distribution.+--+-- Distributions should install this program into PATH.+-- (Cabal builds it as dist/build/propellor.+--+-- This is not the propellor main program (that's config.hs)+--+-- This installs propellor's source into ~/.propellor,+-- uses it to build the real propellor program (if not already built),+-- and runs it.+-- +-- The source is either copied from /usr/src/propellor, or is cloned from+-- git over the network.++module Main where++import Utility.UserInfo+import Utility.Monad+import Utility.Process+import Utility.SafeCommand+import Utility.Directory++import Control.Monad+import Control.Monad.IfElse+import System.Directory+import System.FilePath+import System.Environment (getArgs)+import System.Exit+import System.Posix.Directory++srcdir :: FilePath+srcdir = "/usr/src/propellor"++-- Using the github mirror of the main propellor repo because+-- it is accessible over https for better security.+srcrepo :: String+srcrepo = "https://github.com/joeyh/propellor.git"++main :: IO ()+main = do+ args <- getArgs+ home <- myHomeDir+ let propellordir = home </> ".propellor"+ let propellorbin = propellordir </> "propellor"+ wrapper args propellordir propellorbin++wrapper :: [String] -> FilePath -> FilePath -> IO ()+wrapper args propellordir propellorbin = do+ unlessM (doesDirectoryExist propellordir) $+ makeRepo+ buildruncfg+ where+ chain = do+ (_, _, _, pid) <- createProcess (proc propellorbin args) + exitWith =<< waitForProcess pid+ makeRepo = do+ putStrLn $ "Setting up your propellor repo in " ++ propellordir+ putStrLn ""+ ifM (doesDirectoryExist srcdir)+ ( do+ void $ boolSystem "cp" [Param "-a", File srcdir, File propellordir]+ changeWorkingDirectory propellordir+ void $ boolSystem "git" [Param "init"]+ void $ boolSystem "git" [Param "add", Param "."]+ setuprepo True+ , do+ void $ boolSystem "git" [Param "clone", Param srcrepo, File propellordir] + void $ boolSystem "git" [Param "remote", Param "rm", Param "origin"]+ setuprepo False+ )+ setuprepo fromsrcdir = do+ changeWorkingDirectory propellordir+ whenM (doesDirectoryExist "privdata") $+ mapM_ nukeFile =<< dirContents "privdata"+ void $ boolSystem "git" [Param "commit", Param "--allow-empty", Param "--quiet", Param "-m", Param "setting up propellor git repository"]+ void $ boolSystem "git" [Param "remote", Param "add", Param "upstream", Param srcrepo]+ -- Connect synthetic git repo with upstream history so+ -- merging with upstream will work going forward.+ -- Note -s ours is used to avoid getting any divergent+ -- changes from upstream.+ when fromsrcdir $ do+ void $ boolSystem "git" [Param "fetch", Param "upstream"]+ version <- readProcess "dpkg-query" ["--showformat", "${Version}", "--show", "propellor"]+ void $ boolSystem "git" [Param "merge", Param "-s", Param "ours", Param version]+ buildruncfg = do+ changeWorkingDirectory propellordir+ ifM (boolSystem "make" [Param "build"])+ ( do+ putStrLn ""+ putStrLn ""+ chain+ , error "Propellor build failed."+ )