summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile5
-rw-r--r--config-freebsd.hs2
-rw-r--r--debian/changelog430
-rw-r--r--debian/control7
-rw-r--r--doc/Linux.mdwn3
-rw-r--r--doc/README.mdwn6
-rw-r--r--doc/download.mdwn5
-rw-r--r--doc/forum/Compatibility_between_different_software_versions.mdwn1
-rw-r--r--doc/forum/Compatibility_between_different_software_versions/comment_1_1bc12b78e09c7060f4b5c434004b4b7f._comment12
-rw-r--r--doc/forum/DiskImage_creation_does_not_work_on_my_system.mdwn36
-rw-r--r--doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_10_7982113b64a7884ce95ff38a6d876e2e._comment7
-rw-r--r--doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_11_b1ad266b5c34b600d2d724bf5ffc40de._comment8
-rw-r--r--doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_12_4baf7efcc6f9c50e3aebd663b7792279._comment23
-rw-r--r--doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_13_2f8c7bb7f8ffb734a99ac3d7b28e2d62._comment15
-rw-r--r--doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_1_2daa4574bce2179bfd7e9e505de3f7b0._comment8
-rw-r--r--doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_2_98fb34d4e76bab6ef7a981c87533f395._comment14
-rw-r--r--doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_3_047bca6e0676f0d93338d4eff20825bf._comment18
-rw-r--r--doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_4_fc50b46606eacf59e5db227760ce38ab._comment24
-rw-r--r--doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_5_df27f39bfb7104b4440c972b71f586e4._comment17
-rw-r--r--doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_6_1410b386c0f3e1ff41adb068dd611f10._comment12
-rw-r--r--doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_7_a3de897d9d056fcb6821f3b03485ede5._comment13
-rw-r--r--doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_8_ca5d1f161c037c09fe853c56281f88bc._comment18
-rw-r--r--doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_9_eebdf852c9d73c7b11b184b7654aa78c._comment16
-rw-r--r--doc/forum/Docker.hs_will_Break_in_Stretch.mdwn16
-rw-r--r--doc/forum/Docker.hs_will_Break_in_Stretch/comment_1_8a4f16ae6d04b9d4bedb437ef333562b._comment11
-rw-r--r--doc/forum/Error_building_on_remote_host.mdwn31
-rw-r--r--doc/forum/Error_building_on_remote_host/comment_1_f0f6f241e971d048486ae159585a4ab2._comment21
-rw-r--r--doc/forum/Error_building_on_remote_host/comment_2_9029575e378c3ed67ea7b7d9fd0a11b5._comment13
-rw-r--r--doc/forum/Error_building_on_remote_host/comment_3_3090e63b93e00d6eca95ca8fe523f5b8._comment8
-rw-r--r--doc/forum/Error_building_on_remote_host/comment_4_8a3eac770c1bee9295272c46f022a03c._comment8
-rw-r--r--doc/forum/Error_building_on_remote_host/comment_4_c2e07d9bfba84fbdcf408a09965d6cb6._comment10
-rw-r--r--doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap.mdwn3
-rw-r--r--doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap/comment_1_8ab6b313c80486f8f87a5e13e830bfa9._comment20
-rw-r--r--doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap/comment_2_773fc1441dd06e9dd41508bd800298eb._comment13
-rw-r--r--doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap/comment_3_f48a6191c56bed41eda55436f0aa3e9c._comment15
-rw-r--r--doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap/comment_4_b1769231a633ad2b978ee4c9fa90591c._comment9
-rw-r--r--doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap/comment_5_6dc24952c8efa31a401191a8cf2d0b39._comment14
-rw-r--r--doc/forum/Git.cloned_deletes_harmless_empty_directory.mdwn3
-rw-r--r--doc/forum/Git.cloned_deletes_harmless_empty_directory/comment_1_7cd0521c6d071b25852f8355f4f61f94._comment20
-rw-r--r--doc/forum/Git.cloned_deletes_harmless_empty_directory/comment_2_289f157f129511242d93beae76fd03a3._comment11
-rw-r--r--doc/forum/How_to_create_a_property_with_info.mdwn65
-rw-r--r--doc/forum/How_to_create_a_property_with_info/comment_1_819902ee6b8e571f735dd2c9c93c49a9._comment29
-rw-r--r--doc/forum/How_to_create_a_property_with_info/comment_2_1c2b3cb54f27fb6b6bb5de9d159dd34f._comment15
-rw-r--r--doc/forum/How_to_create_a_property_with_info/comment_3_6cf0360b4922a131bca33d33acf078be._comment11
-rw-r--r--doc/forum/Inherited_Variables....mdwn26
-rw-r--r--doc/forum/Inherited_Variables.../comment_1_082e5d5b8e25335bc90577abcfef1d21._comment15
-rw-r--r--doc/forum/Inherited_Variables.../comment_2_988319ed6de46eff2eac0d5ef36382f9._comment15
-rw-r--r--doc/forum/Inherited_Variables.../comment_3_acf78fa9f732f070bf73c2ab601464ee._comment8
-rw-r--r--doc/forum/Inherited_Variables.../comment_4_5bf7b1f69b48b4d9c516d424e4438208._comment21
-rw-r--r--doc/forum/Inherited_Variables.../comment_5_6fbd29f568ec8b97be47874e2aac57a3._comment20
-rw-r--r--doc/forum/Manage_multiple_different_projects_with_propellor.mdwn7
-rw-r--r--doc/forum/Manage_multiple_different_projects_with_propellor/comment_1_dbad48163b2efd6434ea7c37a72dfd30._comment14
-rw-r--r--doc/forum/Modules_with_Multiple_cmdProperty_causing_build_failures/comment_2_5afe0f200d7139499ef4b01ea6445206._comment11
-rw-r--r--doc/forum/Sbuild_chroot_are_not_compatible_with_schroot.mdwn29
-rw-r--r--doc/forum/Sbuild_chroot_are_not_compatible_with_schroot/comment_1_59ac4661a896a514ce953a0069341869._comment24
-rw-r--r--doc/forum/Sbuild_chroot_are_not_compatible_with_schroot/comment_2_579894632e567a08d83e306be5e355b2._comment84
-rw-r--r--doc/forum/Sbuild_chroot_are_not_compatible_with_schroot/comment_3_6aeee8ba74b363d26a49d6773c5d5014._comment12
-rw-r--r--doc/forum/Supported_OS/comment_3_f2924708a819b962ba7ed690019601ed._comment7
-rw-r--r--doc/forum/Using_propellor_for_continers_only.mdwn5
-rw-r--r--doc/forum/Using_propellor_for_continers_only/comment_1_95e8b7103f248d93570fecb6b8999996._comment20
-rw-r--r--doc/forum/Using_propellor_for_continers_only/comment_2_42b45a126cfdf1dfc370b166c8042690._comment8
-rw-r--r--doc/forum/Using_propellor_for_continers_only/comment_3_cd4b9b9e160469e9f0b105f6c40a4ef8._comment54
-rw-r--r--doc/forum/Using_propellor_for_continers_only/comment_4_9dc985b26c29b9ce21e6c75ec03f6262._comment21
-rw-r--r--doc/forum/Using_propellor_for_continers_only/comment_5_8552ce821f5a3b386cb9e6ad417670ec._comment8
-rw-r--r--doc/forum/Why_downloading_package_list_from_hackage.haskell.org__63__/comment_5_61d7ef8a61ac7b922c810825d794da5f._comment8
-rw-r--r--doc/forum/Why_downloading_package_list_from_hackage.haskell.org__63__/comment_6_ceddc6d118b7ea71ec8f498960a5fe97._comment8
-rw-r--r--doc/forum/Work_on_OS_X.mdwn5
-rw-r--r--doc/forum/Work_on_OS_X/comment_1_6d7d5b89f1de9604718f7973e4b3eeb1._comment20
-rw-r--r--doc/forum/Work_on_OS_X/comment_2_00b20c240fc13bed6dc54e5b985b41e2._comment17
-rw-r--r--doc/forum/Work_on_OS_X/comment_3_294f4783522a8e4887793aac921ee546._comment14
-rw-r--r--doc/forum/Work_on_OS_X/comment_4_74b579d4d590432b6bd236ccb929cc11._comment16
-rw-r--r--doc/forum/creating_Bind9_configuration.mdwn9
-rw-r--r--doc/forum/creating_Bind9_configuration/comment_1_0798f44e1f5a91fbc91c0b472ad92bfa._comment29
-rw-r--r--doc/forum/creating_Bind9_configuration/comment_2_f1bffbdd7c2ebab2dd9518ee024e7a92._comment18
-rw-r--r--doc/forum/creating_Bind9_configuration/comment_3_6b4d73b17d87d00845fda26431ded422._comment10
-rw-r--r--doc/forum/host_to_deal_with_dpkg::options.mdwn41
-rw-r--r--doc/forum/host_to_deal_with_dpkg::options/comment_1_641dcb7be62151bdc97fd5e574f334d0._comment12
-rw-r--r--doc/forum/host_to_deal_with_dpkg::options/comment_2_bac8129b570ce216ef9f6aa6c0e12c1e._comment9
-rw-r--r--doc/forum/host_to_deal_with_dpkg::options/comment_3_62d671fb3c787aafcd4d058975208f75._comment10
-rw-r--r--doc/forum/propellor_4.7.6_does_not_compile_on_jessie.mdwn32
-rw-r--r--doc/forum/propellor_4.7.6_does_not_compile_on_jessie/comment_1_c35f458b4c958f6397fe726f5676b700._comment7
-rw-r--r--doc/forum/propellor_and_gpg2.mdwn14
-rw-r--r--doc/forum/propellor_and_gpg2/comment_1_4b732110f59f78f73fdfb745bdd9c0dd._comment13
-rw-r--r--doc/forum/propellor_failed_to_sign_the_commit.mdwn30
-rw-r--r--doc/forum/propellor_failed_to_sign_the_commit/comment_1_c1dab7554841bd88d2109e9d46b31102._comment30
-rw-r--r--doc/forum/propellor_failed_to_sign_the_commit/comment_2_21ff16e0871e7069749cd6c47a6fc8fe._comment9
-rw-r--r--doc/forum/propellor_failed_to_sign_the_commit/comment_3_f0e087ed1a80f42d11d34fb215183205._comment11
-rw-r--r--doc/haskell_newbie.mdwn6
-rw-r--r--doc/index.mdwn2
-rw-r--r--doc/install.mdwn4
-rw-r--r--doc/news/version_3.1.1.mdwn4
-rw-r--r--doc/news/version_3.1.2.mdwn22
-rw-r--r--doc/news/version_3.2.0.mdwn17
-rw-r--r--doc/news/version_3.2.1.mdwn5
-rw-r--r--doc/news/version_3.2.2.mdwn5
-rw-r--r--doc/news/version_4.7.3.mdwn3
-rw-r--r--doc/news/version_4.7.4.mdwn7
-rw-r--r--doc/news/version_4.7.5.mdwn3
-rw-r--r--doc/news/version_4.7.6.mdwn6
-rw-r--r--doc/news/version_4.7.7.mdwn11
-rw-r--r--doc/todo/Add_MonadBaseControl_instance_to_Propellor.mdwn3
-rw-r--r--doc/todo/Add_MonadBaseControl_instance_to_Propellor/comment_1_4b0cd7acc6442210a80c547981b5ae45._comment18
-rw-r--r--doc/todo/Add_MonadBaseControl_instance_to_Propellor/comment_2_60d6e06ebada37648df77442733e325f._comment24
-rw-r--r--doc/todo/Add_MonadBaseControl_instance_to_Propellor/comment_3_45413e6e811c34edc38a6ff70ca7c208._comment50
-rw-r--r--doc/todo/Arch_Linux_Port.mdwn16
-rw-r--r--doc/todo/Arch_Linux_Port/comment_1_8e39dc177e21e9e20c1b74b59b9926d2._comment28
-rw-r--r--doc/todo/Arch_Linux_Port/comment_2_cc4623c156a0d12c88461bc5deec07cd._comment18
-rw-r--r--doc/todo/Arch_Linux_Port/comment_3_d917de766dfe7fded7317d7614d1467f._comment25
-rw-r--r--doc/todo/Arch_Linux_Port/comment_4_924c73c0ab6fb39c9b25ae51facf6bb6._comment8
-rw-r--r--doc/todo/Are_--check_and_--build_on_the_way_in_or_on_the_way_out__63__.mdwn3
-rw-r--r--doc/todo/Are_--check_and_--build_on_the_way_in_or_on_the_way_out__63__/comment_1_7c2b2447254ad44ee1316b47eac130df._comment12
-rw-r--r--doc/todo/Are_--check_and_--build_on_the_way_in_or_on_the_way_out__63__/comment_2_b4910f50225a8b763566126861faea11._comment8
-rw-r--r--doc/todo/Info_property_to_select_host__39__s_preferred_Apt_mirror.mdwn5
-rw-r--r--doc/todo/Info_property_to_select_host__39__s_preferred_Apt_mirror/comment_1_ac66a33d71092261a745378c82959e69._comment7
-rw-r--r--doc/todo/Info_property_to_select_host__39__s_preferred_Apt_mirror/comment_2_2c2c4817a4259acbc1a63bac2e3fb2e3._comment8
-rw-r--r--doc/todo/LVM_logical_volume_creation__44___resize__44___format___38___removal.mdwn7
-rw-r--r--doc/todo/LVM_logical_volume_creation__44___resize__44___format___38___removal/comment_1_74c6576b25f74c6e620eb015af8b0f6a._comment26
-rw-r--r--doc/todo/LVM_logical_volume_creation__44___resize__44___format___38___removal/comment_2_d63d84b56ece233f795d1075aaba887a._comment18
-rw-r--r--doc/todo/LVM_logical_volume_creation__44___resize__44___format___38___removal/comment_3_1405e20c8f5dc6e9cca3732e3e368d03._comment25
-rw-r--r--doc/todo/LVM_logical_volume_creation__44___resize__44___format___38___removal/comment_4_20c6734d67fefeb1a8c07730d537e06b._comment8
-rw-r--r--doc/todo/Merging_from___47__usr__47__src__47__propellor_broken_now_CHANGELOG_not_a_symlink.mdwn36
-rw-r--r--doc/todo/Merging_from___47__usr__47__src__47__propellor_broken_now_CHANGELOG_not_a_symlink/comment_1_62b47d7c0530c2988b7e6e998878b920._comment11
-rw-r--r--doc/todo/Merging_from___47__usr__47__src__47__propellor_broken_now_CHANGELOG_not_a_symlink/comment_2_61463030200038542d293149754d36ed._comment8
-rw-r--r--doc/todo/PROPELLOR_TRACE_propigation.mdwn6
-rw-r--r--doc/todo/Propellor.Property.Versioned_support_asymmetric_RevertableProperty_types.mdwn7
-rw-r--r--doc/todo/bug_in_diskimage_finalization.mdwn13
-rw-r--r--doc/todo/differential_update_via_RevertableProperty.mdwn146
-rw-r--r--doc/todo/hostChroot.mdwn9
-rw-r--r--doc/todo/initial_spin_compile_failure_recovery.mdwn5
-rw-r--r--doc/todo/merge_request:_Timezone.hs.mdwn9
-rw-r--r--doc/todo/merge_request:_Timezone.hs/comment_1_9cfb5e48940e58f2064cbb5edf462c06._comment15
-rw-r--r--doc/todo/modify_Apt.pinnedTo_to_pin_a_package_to_multiple_suites_with_different_priorities.mdwn7
-rw-r--r--doc/todo/new_apt_pinning_properties.mdwn10
-rw-r--r--doc/todo/new_apt_pinning_properties/comment_1_fd9e6775868eaa8d6aee49d06944ef0c._comment38
-rw-r--r--doc/todo/new_apt_pinning_properties/comment_2_c82f7e83f3fcc7648222d9dbf90e5ddd._comment66
-rw-r--r--doc/todo/new_apt_pinning_properties/comment_3_58d323602f293471ce3d2d9b4d271130._comment23
-rw-r--r--doc/todo/new_apt_pinning_properties/comment_4_add83ed58963e944ccd705a50e8b5a47._comment20
-rw-r--r--doc/todo/property_to_install_propellor.mdwn16
-rw-r--r--doc/todo/property_to_install_propellor/comment_1_b05e9a44e5c7130d9cc928223cd82d78._comment16
-rw-r--r--doc/todo/property_to_install_propellor/comment_2_9fea601af57777e1cb49952483f4da63._comment7
-rw-r--r--doc/todo/sbuild_setup_should_use_apt-cacher-ng.mdwn22
-rw-r--r--doc/todo/spin_failure_HEAD.mdwn130
-rw-r--r--doc/todo/unpropelling_a_host.mdwn9
-rw-r--r--doc/todo/usage__47__help_text_improvements.mdwn3
-rw-r--r--doc/todo/usage__47__help_text_improvements/comment_1_66878945cdb57d06849337262d939701._comment13
-rw-r--r--doc/todo/usage__47__help_text_improvements/comment_2_d531a45851cdef87a8f7b8182b3d04ce._comment12
-rw-r--r--doc/todo/use_stack_for_remote_building_propellor.mdwn13
-rw-r--r--doc/usage.mdwn12
-rw-r--r--doc/user/craige.mdwn1
-rw-r--r--doc/user/spwhitton.mdwn1
-rw-r--r--joeyconfig.hs206
-rw-r--r--privdata/.joeyconfig/keyring.gpgbin113014 -> 157269 bytes
-rw-r--r--privdata/.joeyconfig/privdata.gpg2900
-rw-r--r--propellor.cabal43
-rw-r--r--src/Propellor/Bootstrap.hs203
-rw-r--r--src/Propellor/CmdLine.hs53
-rw-r--r--src/Propellor/Container.hs21
-rw-r--r--src/Propellor/DotDir.hs6
-rw-r--r--src/Propellor/Engine.hs61
-rw-r--r--src/Propellor/EnsureProperty.hs4
-rw-r--r--src/Propellor/Gpg.hs34
-rw-r--r--src/Propellor/Info.hs9
-rw-r--r--src/Propellor/Message.hs58
-rw-r--r--src/Propellor/PrivData.hs10
-rw-r--r--src/Propellor/Property.hs60
-rw-r--r--src/Propellor/Property/Apache.hs36
-rw-r--r--src/Propellor/Property/Apt.hs193
-rw-r--r--src/Propellor/Property/Apt/PPA.hs30
-rw-r--r--src/Propellor/Property/Attic.hs18
-rw-r--r--src/Propellor/Property/Bootstrap.hs144
-rw-r--r--src/Propellor/Property/Borg.hs16
-rw-r--r--src/Propellor/Property/Ccache.hs2
-rw-r--r--src/Propellor/Property/Chroot.hs122
-rw-r--r--src/Propellor/Property/Cmd.hs1
-rw-r--r--src/Propellor/Property/Concurrent.hs11
-rw-r--r--src/Propellor/Property/Conductor.hs6
-rw-r--r--src/Propellor/Property/ConfFile.hs25
-rw-r--r--src/Propellor/Property/Cron.hs5
-rw-r--r--src/Propellor/Property/DebianMirror.hs2
-rw-r--r--src/Propellor/Property/Debootstrap.hs3
-rw-r--r--src/Propellor/Property/DiskImage.hs290
-rw-r--r--src/Propellor/Property/DiskImage/PartSpec.hs80
-rw-r--r--src/Propellor/Property/Dns.hs14
-rw-r--r--src/Propellor/Property/Docker.hs28
-rw-r--r--src/Propellor/Property/File.hs119
-rw-r--r--src/Propellor/Property/Firejail.hs2
-rw-r--r--src/Propellor/Property/Firewall.hs77
-rw-r--r--src/Propellor/Property/FreeBSD/Pkg.hs5
-rw-r--r--src/Propellor/Property/FreeBSD/Poudriere.hs14
-rw-r--r--src/Propellor/Property/FreeDesktop.hs29
-rw-r--r--src/Propellor/Property/Fstab.hs29
-rw-r--r--src/Propellor/Property/Gpg.hs2
-rw-r--r--src/Propellor/Property/Grub.hs72
-rw-r--r--src/Propellor/Property/HostingProvider/Linode.hs5
-rw-r--r--src/Propellor/Property/Hostname.hs2
-rw-r--r--src/Propellor/Property/LightDM.hs13
-rw-r--r--src/Propellor/Property/List.hs9
-rw-r--r--src/Propellor/Property/Locale.hs6
-rw-r--r--src/Propellor/Property/Logcheck.hs12
-rw-r--r--src/Propellor/Property/Mount.hs23
-rw-r--r--src/Propellor/Property/Munin.hs4
-rw-r--r--src/Propellor/Property/Network.hs70
-rw-r--r--src/Propellor/Property/OS.hs4
-rw-r--r--src/Propellor/Property/Obnam.hs7
-rw-r--r--src/Propellor/Property/OpenId.hs2
-rw-r--r--src/Propellor/Property/Pacman.hs68
-rw-r--r--src/Propellor/Property/Parted.hs219
-rw-r--r--src/Propellor/Property/Parted/Types.hs119
-rw-r--r--src/Propellor/Property/Partition.hs24
-rw-r--r--src/Propellor/Property/Reboot.hs19
-rw-r--r--src/Propellor/Property/Restic.hs202
-rw-r--r--src/Propellor/Property/Rsync.hs16
-rw-r--r--src/Propellor/Property/Sbuild.hs227
-rw-r--r--src/Propellor/Property/SiteSpecific/Branchable.hs30
-rw-r--r--src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs15
-rw-r--r--src/Propellor/Property/SiteSpecific/JoeySites.hs231
-rw-r--r--src/Propellor/Property/Ssh.hs12
-rw-r--r--src/Propellor/Property/Sudo.hs24
-rw-r--r--src/Propellor/Property/Systemd.hs74
-rw-r--r--src/Propellor/Property/Timezone.hs21
-rw-r--r--src/Propellor/Property/Tor.hs34
-rw-r--r--src/Propellor/Property/Unbound.hs4
-rw-r--r--src/Propellor/Property/User.hs35
-rw-r--r--src/Propellor/Property/Versioned.hs124
-rw-r--r--src/Propellor/Property/XFCE.hs41
-rw-r--r--src/Propellor/Property/ZFS/Process.hs3
-rw-r--r--src/Propellor/Shim.hs2
-rw-r--r--src/Propellor/Spin.hs119
-rw-r--r--src/Propellor/Ssh.hs18
-rw-r--r--src/Propellor/Types.hs58
-rw-r--r--src/Propellor/Types/Bootloader.hs12
-rw-r--r--src/Propellor/Types/Chroot.hs2
-rw-r--r--src/Propellor/Types/CmdLine.hs1
-rw-r--r--src/Propellor/Types/ConfigurableValue.hs44
-rw-r--r--src/Propellor/Types/Core.hs7
-rw-r--r--src/Propellor/Types/Dns.hs23
-rw-r--r--src/Propellor/Types/Docker.hs2
-rw-r--r--src/Propellor/Types/Info.hs23
-rw-r--r--src/Propellor/Types/MetaTypes.hs28
-rw-r--r--src/Propellor/Types/OS.hs29
-rw-r--r--src/Propellor/Types/PartSpec.hs66
-rw-r--r--src/Propellor/Types/Result.hs3
-rw-r--r--src/Propellor/Types/ZFS.hs79
-rw-r--r--src/Utility/DataUnits.hs8
-rw-r--r--src/Utility/Exception.hs26
-rw-r--r--src/Utility/FileMode.hs22
-rw-r--r--src/Utility/FileSystemEncoding.hs74
-rw-r--r--src/Utility/LinuxMkLibs.hs2
-rw-r--r--src/Utility/Misc.hs17
-rw-r--r--src/Utility/PartialPrelude.hs2
-rw-r--r--src/Utility/Path.hs32
-rw-r--r--src/Utility/Process.hs28
-rw-r--r--src/Utility/SafeCommand.hs4
-rw-r--r--src/Utility/Scheduled.hs2
-rw-r--r--src/Utility/Split.hs30
-rw-r--r--src/Utility/SystemDirectory.hs2
-rw-r--r--src/Utility/Tuple.hs17
-rw-r--r--src/Utility/UserInfo.hs16
l---------src/propellor-config.hs (renamed from src/config.hs)0
-rw-r--r--src/wrapper.hs5
-rw-r--r--stack.yaml2
261 files changed, 7648 insertions, 2822 deletions
diff --git a/Makefile b/Makefile
index 4ae11991..84a92f0e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,11 +1,6 @@
CABAL?=cabal
DATE := $(shell dpkg-parsechangelog 2>/dev/null | grep Date | cut -d " " -f2-)
-# this target is provided (and is first) to keep old versions of the
-# propellor cron job working, and will eventually be removed
-run: build
- ./propellor
-
build: tags propellor.1 dist/setup-config
$(CABAL) build
ln -sf dist/build/propellor-config/propellor-config propellor
diff --git a/config-freebsd.hs b/config-freebsd.hs
index 80abb89d..34880113 100644
--- a/config-freebsd.hs
+++ b/config-freebsd.hs
@@ -59,7 +59,7 @@ linuxbox = host "linuxbox.example.com" $ props
-- A generic webserver in a Docker container.
webserverContainer :: Docker.Container
webserverContainer = Docker.container "webserver" (Docker.latestImage "debian") $ props
- & osDebian' KFreeBSD (Stable "jessie") X86_64
+ & osDebian' KFreeBSD (Stable "stretch") X86_64
& Apt.stdSourcesList
& Docker.publish "80:80"
& Docker.volume "/var/www:/var/www"
diff --git a/debian/changelog b/debian/changelog
index cb313e2f..f254b5a6 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,430 @@
+propellor (4.8.0) UNRELEASED; urgency=medium
+
+ * DiskImage: Made a DiskImage type class, so that different disk image
+ formats can be implemented. The properties in this module can generate
+ any type that is a member of DiskImage. (API change)
+ (To convert existing configs, convert the filename of the disk image
+ to RawDiskImage filename.)
+ * Removed DiskImage.vmdkBuiltFor property. (API change)
+ Instead, use VirtualBoxPointer in the property that creates the disk
+ image.
+ * Borg: Fix broken shell escaping in borg cron job.
+ * Attic: Fix broken shell escaping in attic cron job.
+ * Make lock file descriptors close-on-exec.
+
+ -- Joey Hess <id@joeyh.name> Thu, 24 Aug 2017 11:00:19 -0400
+
+propellor (4.7.7) unstable; urgency=medium
+
+ * Locale: Display an error message when /etc/locale.gen does not contain
+ the requested locale.
+ * Attic module is deprecated and will warn when used.
+ Attic is no longer available in Debian and appears to have been
+ mostly supersceded by Borg.
+ * Obnam module is deprecated and will warn when used.
+ Obnam has been retired by its author.
+ * Add Typeable instance to Bootstrapper, fixing build with old versions
+ of ghc. (Previous attempt was incomplete.)
+
+ -- Joey Hess <id@joeyh.name> Wed, 23 Aug 2017 12:15:31 -0400
+
+propellor (4.7.6) unstable; urgency=medium
+
+ * Sbuild: Add Sbuild.userConfig property.
+ Thanks, Sean Whitton
+ * Locale: Make sure that the locales package is installed when enabling
+ locales.
+
+ -- Joey Hess <id@joeyh.name> Tue, 01 Aug 2017 17:59:07 -0400
+
+propellor (4.7.5) unstable; urgency=medium
+
+ * Avoid crashing when getTerminalName fails due to eg, being in a chroot.
+
+ -- Joey Hess <id@joeyh.name> Tue, 01 Aug 2017 15:28:58 -0400
+
+propellor (4.7.4) unstable; urgency=medium
+
+ * Set GPG_TTY when run at a terminal, so that gpg can do password
+ prompting despite being connected by pipes to propellor (or git).
+ * Rsync: Make rsync display less verbose.
+ * Improve PROPELLOR_TRACE output so serialized trace values always
+ come on their own line, not mixed with title setting.
+
+ -- Joey Hess <id@joeyh.name> Tue, 01 Aug 2017 13:30:54 -0400
+
+propellor (4.7.3) unstable; urgency=medium
+
+ * Expand the Trace data type.
+
+ -- Joey Hess <id@joeyh.name> Sat, 29 Jul 2017 17:26:32 -0400
+
+propellor (4.7.2) unstable; urgency=medium
+
+ * Added PROPELLOR_TRACE environment variable, which can be set to 1 to
+ make propellor output serialized Propellor.Message.Trace values,
+ for consumption by another program.
+ * Rsync: Make rsync display its progress, in a minimal format to avoid
+ scrolling each file down the screen.
+
+ -- Joey Hess <id@joeyh.name> Sat, 29 Jul 2017 15:49:00 -0400
+
+propellor (4.7.1) unstable; urgency=medium
+
+ * Added Mount.isMounted.
+ * Grub.bootsMounted: Bugfix.
+
+ -- Joey Hess <id@joeyh.name> Fri, 28 Jul 2017 22:22:40 -0400
+
+propellor (4.7.0) unstable; urgency=medium
+
+ * Add Apt.proxy property to set a host's apt proxy.
+ Thanks, Sean Whitton.
+ * Add Apt.useLocalCacher property to set up apt-cacher-ng.
+ Thanks, Sean Whitton.
+ * Rework Sbuild properties to use apt proxies/cachers instead of
+ bind-mounting the host's apt cache. This makes it possible to run more
+ than one build at a time, and lets sbuild run even if apt's cache is
+ locked by the host's apt.
+ Thanks, Sean Whitton.
+ * Sbuild: When Apt.proxy is set, it is assumed that the proxy does some
+ sort of caching, and sbuild chroots are set up to use the same proxy.
+ * Sbuild: When Apt.proxy is not set, install apt-cacher-ng, and point
+ sbuild chroots at the local apt cacher.
+ * Sbuild: Droped Sbuild.piupartsConfFor, Sbuild.piupartsConf,
+ Sbuild.shareAptCache
+ (API change)
+ No longer needed now that we are using apt proxies/cachers.
+ * Sbuild: Updated sample config in haddock for Propellor.Property.Sbuild.
+ If you use this module, please compare both your config.hs and
+ your ~/.sbuildrc with the haddock documentation.
+ * Grub.bootsMounted: Avoid failing when proc sys etc are already mounted
+ within the chroot.
+
+ -- Joey Hess <id@joeyh.name> Fri, 28 Jul 2017 20:42:35 -0400
+
+propellor (4.6.2) unstable; urgency=medium
+
+ * Systemd.nspawned: Recent systemd versions such as 234 ignore
+ non-symlinks in /etc/systemd/system/multi-user.target.wants,
+ which was used to configure systemd-nspawn parameters. Instead,
+ use a service.d/local.conf file to configure that.
+ * Grub: Added bootsMounted property, a generalization of
+ DiskImage.grubBooted
+
+ -- Joey Hess <id@joeyh.name> Fri, 28 Jul 2017 15:48:32 -0400
+
+propellor (4.6.1) unstable; urgency=medium
+
+ * Added Network.dhcp' and Network.static', which allow specifying
+ additional options for interfaces files.
+ * Fix build failure on ghc-8.2.1
+ Thanks, Sergei Trofimovich.
+ * DiskImage: Fix strictness bug in .parttable read/write sequence.
+
+ -- Joey Hess <id@joeyh.name> Thu, 27 Jul 2017 09:17:32 -0400
+
+propellor (4.6.0) unstable; urgency=medium
+
+ * Add Typeable instance to Bootstrapper, fixing build with old versions
+ of ghc.
+ * Network.static changed to take address and gateway parameters.
+ If you used the old Network.static property, it has been renamed to
+ Network.preserveStatic.
+ (Minor API change)
+
+ -- Joey Hess <id@joeyh.name> Wed, 26 Jul 2017 20:02:50 -0400
+
+propellor (4.5.2) unstable; urgency=medium
+
+ * Added Rsync.installed property.
+ * Added DiskImage.vmdkBuiltFor property which is useful for booting
+ a disk image in VirtualBox.
+
+ -- Joey Hess <id@joeyh.name> Tue, 25 Jul 2017 17:58:46 -0400
+
+propellor (4.5.1) unstable; urgency=medium
+
+ * Reboot.toKernelNewerThan: If running kernel is new enough, avoid
+ looking at what kernels are installed.
+ Thanks, Sean Whitton.
+ * DiskImage: Avoid re-partitioning disk image unncessarily, for a large
+ speedup.
+
+ -- Joey Hess <id@joeyh.name> Tue, 25 Jul 2017 15:51:33 -0400
+
+propellor (4.5.0) unstable; urgency=medium
+
+ * Generalized the PartSpec DSL, so it can be used for both
+ disk image partitioning, and disk device partitioning, with
+ different partition sizing methods as appropriate for the different
+ uses. (minor API change)
+ * Propellor.Property.Parted: Added calcPartTable function which uses
+ PartSpec DiskPart, and a useDiskSpace combinator.
+ * Generate a better description for versioned properties.
+
+ -- Joey Hess <id@joeyh.name> Fri, 21 Jul 2017 16:40:13 -0400
+
+propellor (4.4.0) unstable; urgency=medium
+
+ * Propellor.Property.Timezone: New module, contributed by Sean Whitton.
+ * Propellor.Property.Sudo.enabledFor: Made revertable.
+ (minor API change)
+ * Propellor.Property.LightDM.autoLogin: Made revertable.
+ (minor API change)
+ * Propellor.Property.Conffile: Added lacksIniSetting.
+
+ -- Joey Hess <id@joeyh.name> Mon, 17 Jul 2017 12:55:02 -0400
+
+propellor (4.3.4) unstable; urgency=medium
+
+ * Propellor.Property.Versioned: New module which allows different
+ versions of a property or host to be written down in a propellor config
+ file. Has many applications, including staged upgrades and rollbacks.
+ * LightDM.autoLogin: Use [Seat:*] rather than the old [SeatDefaults].
+ The new name has been supported since lightdm 1.15.
+
+ -- Joey Hess <id@joeyh.name> Sat, 15 Jul 2017 17:22:53 -0400
+
+propellor (4.3.3) unstable; urgency=medium
+
+ * Hosts can be configured to build propellor using stack, by adding
+ a property:
+ & bootstrapWith (Robustly Stack)
+ * Hosts can be configured to build propellor using cabal, but using
+ only packages installed from the operating system. This
+ will work on eg Debian:
+ & bootstrapWith OSOnly
+ * Iproved fix for bug that sometimes made --spin fail with
+ "fatal: Couldn't find remote ref HEAD". The previous fix didn't work
+ reliably.
+ * User: add systemGroup and use it for systemAccountFor'
+ Thanks, Félix Sipma.
+ * Export a Restic.backup' property.
+ Thanks, Félix Sipma.
+ * Updated stack config to lts-8.22.
+
+ -- Joey Hess <id@joeyh.name> Thu, 13 Jul 2017 12:34:09 -0400
+
+propellor (4.3.2) unstable; urgency=medium
+
+ * Really include Propellor.Property.FreeDesktop.
+
+ -- Joey Hess <id@joeyh.name> Thu, 06 Jul 2017 17:28:53 -0400
+
+propellor (4.3.1) unstable; urgency=medium
+
+ * Added Propellor.Property.FreeDesktop module.
+ * Added reservedSpacePercentage to the PartSpec EDSL.
+
+ -- Joey Hess <id@joeyh.name> Thu, 06 Jul 2017 17:03:15 -0400
+
+propellor (4.3.0) unstable; urgency=medium
+
+ * DiskImage: Removed grubBooted; properties that used to need it as a
+ parameter now look at Info about the bootloader that is installed in
+ the chroot that the disk image is created from.
+ (API change)
+
+ -- Joey Hess <id@joeyh.name> Wed, 05 Jul 2017 21:04:04 -0400
+
+propellor (4.2.0) unstable; urgency=medium
+
+ * DiskImage.grubBooted no longer takes a BIOS parameter,
+ and no longer implicitly adds Grub.installed to the properties of
+ the disk image. If you used DiskImage.grubBooted, you'll need to update
+ your propellor configuration, removing the BIOS parameter from
+ grubBooted and adding a Grub.installed property to the disk image, eg:
+ & Grub.installed PC
+ (API change)
+ * Grub.installed: Avoid running update-grub when used in a chroot, since
+ it will get confused.
+ * DiskImage.Finalization: Simplified this type since it does not need to
+ be used to install packages anymore. (API change)
+
+ -- Joey Hess <id@joeyh.name> Wed, 05 Jul 2017 18:10:49 -0400
+
+propellor (4.1.0) unstable; urgency=medium
+
+ * User.hasInsecurePassword makes sure shadow passwords are enabled,
+ so if the insecure password is later changed, the new password won't be
+ exposed.
+ * Bugfix: Apache.httpsVirtualHost' must create ssl/hn/ dir earlier
+ Thanks, Sean Whitton.
+ * Bootstrap.clonedFrom: Fix bug that broke copying .git/config into
+ chroot.
+ * Diskimage.imageExists: Align disk image size to multiple of 4096
+ sector size, since some programs (such as VBoxManage convertdd)
+ refuse to operate on disk images not aligned to a sector size.
+ * Bootstrap.bootstrappedFrom: Fix bug that caused propellor to only
+ be built from the bootstrapped config the first time.
+ * Bootstrap.bootstrappedFrom: Avoid doing anything when not run in a
+ chroot.
+ * When provisioning a container, output was buffered until the whole
+ process was done; now output will be displayed immediately.
+ * LightDM.autoLogin: Make it require LightDM.installed.
+ (minor API change as the type changed)
+ * Propellor.Property.XFCE added with some useful properties for the
+ desktop environment.
+ * Added File.applyPath property.
+ * Added File.checkOverwrite.
+ * File.isCopyOf: Fix bug that prevented this property from working
+ when the destination file did not yet exist.
+
+ -- Joey Hess <id@joeyh.name> Wed, 05 Jul 2017 17:30:00 -0400
+
+propellor (4.0.6) unstable; urgency=medium
+
+ * Fix bug that sometimes made --spin fail with
+ "fatal: Couldn't find remote ref HEAD"
+ * Display error and warning messages to stderr, not stdout.
+
+ -- Joey Hess <id@joeyh.name> Sun, 18 Jun 2017 19:30:50 -0400
+
+propellor (4.0.5) unstable; urgency=medium
+
+ * Switch cabal file from Extensions to Default-Extensions to fix
+ new picky hackage rejection.
+
+ -- Joey Hess <id@joeyh.name> Sat, 03 Jun 2017 15:07:36 -0400
+
+propellor (4.0.4) unstable; urgency=medium
+
+ * Propellor.Property.Restic added for yet another backup program.
+ Thanks, Félix Sipma.
+ * Removed dependency on MissingH, instead depends on split and hashable.
+
+ -- Joey Hess <id@joeyh.name> Sat, 03 Jun 2017 14:56:44 -0400
+
+propellor (4.0.3) unstable; urgency=medium
+
+ * Added Fstab.listed, Fstab.swap, and Mount.swapOn properties.
+ Thanks, Daniel Brooks.
+ * Added Propellor.Property.Bootstrap, which can be used to make
+ disk images contain their own installation of propellor.
+
+ -- Joey Hess <id@joeyh.name> Thu, 20 Apr 2017 00:54:32 -0400
+
+propellor (4.0.2) unstable; urgency=medium
+
+ * Apt.mirror can be used to set the preferred apt mirror of a host,
+ overriding the default CDN. This info is used by
+ Apt.stdSourcesList and Sbuild.builtFor.
+ Thanks, Sean Whitton.
+ * Property.Partition: Update kpartx output parser, as its output format
+ changed around version 0.6. Both output formats are supported now.
+ * Fix bug when using setContainerProps with a chroot that prevented
+ properties added to a chroot that way from being seen when propellor
+ was running inside the chroot. This affected disk image creation, and
+ possibly other things that use chroots.
+
+ -- Joey Hess <id@joeyh.name> Fri, 24 Mar 2017 14:04:50 -0400
+
+propellor (4.0.1) unstable; urgency=medium
+
+ * Fix build with pre-AMP ghc.
+ * Tor: Restart daemon after installing private key.
+ * Tor.named, Tor.torPrivKey: Include the new ed25519 public/private key
+ pair in addition to the old secret_id_key.
+
+ -- Joey Hess <id@joeyh.name> Sun, 19 Mar 2017 16:18:11 -0400
+
+propellor (4.0.0) unstable; urgency=medium
+
+ * Added Monoid instances for Property and RevertableProperty.
+ * Removed applyToList. Instead, use mconcat. (API change)
+ If you had: applyToList accountFor [User "joey", User "root"]
+ use instead: mconcat (map accountFor [User "joey", User "root"])
+ * Makefile: Removed "run" target which was default target.
+ "make" now only builds propellor, does not run it.
+ Note that propellor 1.0.0 and earlier relied on this target for
+ the Cron.runPropellor property's cronjob to work, so upgrading
+ directly from 1.0.0 to 4.0.0 would break that cron job.
+ * Remove make from propellor's dependency list; it's not used by
+ propellor any longer.
+ * Implemented hostChroot, as originally seen in my slides at
+ Linux.Conf.Au 2017 in January. Now that it's not vaporware, it allows
+ one Host to build a disk image that has all the properties of another
+ Host.
+ * DiskImage building properties used to propagate DNS info out from
+ the chroot used to build the disk image to the Host. That is no longer
+ done, since that chroot only exists as a side effect of the disk image
+ creation and servers will not be running in it.
+ * The IsInfo types class's propagateInfo function changed to use a
+ PropagateInfo data type. (API change)
+ * The action used to satisfy a property changed to Maybe (Propellor Result).
+ When it is Nothing, propellor knows it can skip displaying the
+ description of that property. This is mostly useful in the
+ implementation of mempty. (API change)
+ * The doNothing property is now simply mempty. The name was retained
+ because it can be clearer than mempty in some contexts.
+ * Added Apache.confEnabled.
+
+ -- Joey Hess <id@joeyh.name> Wed, 15 Mar 2017 15:46:42 -0400
+
+propellor (3.4.1) unstable; urgency=medium
+
+ * Fixed https url to propellor git repository.
+
+ -- Joey Hess <id@joeyh.name> Wed, 01 Mar 2017 16:50:05 -0400
+
+propellor (3.4.0) unstable; urgency=medium
+
+ * Added ConfigurableValue type class, for values that can be used in a
+ config file, or to otherwise configure a program.
+ * The val function converts such values to String.
+ * Removed fromPort and fromIPAddr (use val instead). (API change)
+ * Removed several Show instances that were only used for generating
+ configuration, replacing with ConfigurableValue instances. (API change)
+ * The github mirror of propellor's git repository has been removed,
+ since github's terms of service has started imposing unwanted licensing
+ requirements.
+ * propellor --init: The option to clone propellor's git repository
+ used to use the github mirror, and has been changed to use a different
+ mirror.
+
+ -- Joey Hess <id@joeyh.name> Wed, 01 Mar 2017 16:44:20 -0400
+
+propellor (3.3.1) unstable; urgency=medium
+
+ * Apt: Removed the mirrors.kernel.org line from stdSourcesList etc.
+ The mirror CDN has a new implementation that should avoid the problems
+ with httpredir that made an extra mirror sometimes be needed.
+ * Switch Debian CDN address to deb.debian.org.
+ * Tor.hiddenService: Fix bug in torrc's HiddenServicePort configuration.
+ Thanks, Félix Sipma
+
+ -- Joey Hess <id@joeyh.name> Mon, 20 Feb 2017 13:49:26 -0400
+
+propellor (3.3.0) unstable; urgency=medium
+
+ * Arch Linux is now supported by Propellor!
+ Thanks to Zihao Wang for this port.
+ * Added Propellor.Property.Pacman for Arch's package manager.
+ Maintained by Zihao Wang.
+ * The types of some properties changed; eg from Property DebianLike
+ to Property (DebianLike + ArchLinux). Also, DebianLike and Linux
+ are no longer type synonyms; propellor now knows that Linux includes
+ ArchLinux. This could require updates to code, so is a minor API change.
+ * GHC's fileSystemEncoding is used for all String IO, to avoid
+ encoding-related crashes in eg, Propellor.Property.File.
+ * Add --build option to simply build config.hs.
+ * More informative usage message. Thanks, Daniel Brooks
+ * Tor.hiddenService' added to support multiple ports.
+ Thanks, Félix Sipma.
+ * Apt.noPDiffs added.
+ Thanks, Sean Whitton.
+ * stack.yaml: Compile with GHC 8.0.1 against lts-7.16.
+ Thanks, Andrew Cowie.
+ * Added Propellor.Property.File.configFileName and related functions
+ to generate good filenames for config directories.
+ * Added Apt.suiteAvailablePinned, Apt.pinnedTo.
+ Thanks, Sean Whitton.
+ * Added File.containsBlock
+ Thanks, Sean Whitton.
+
+ -- Joey Hess <id@joeyh.name> Tue, 07 Feb 2017 12:09:24 -0400
+
propellor (3.2.3) unstable; urgency=medium
* Improve extraction of gpg secret key id list, to work with gpg 2.1.
@@ -260,7 +687,8 @@ propellor (3.0.0) unstable; urgency=medium
a clone of propellor's git repository, or a minimal config, and will
configure propellor to use a gpg key.
* Stack support. "git config propellor.buildsystem stack" will make
- propellor build its config using stack.
+ propellor build its config using stack. (This does not affect
+ how propellor is bootstrapped on a host by "propellor --spin host".)
* When propellor is installed using stack, propellor --init will
automatically set propellor.buildsystem=stack.
diff --git a/debian/control b/debian/control
index 9194b6c2..e6819060 100644
--- a/debian/control
+++ b/debian/control
@@ -7,7 +7,7 @@ Build-Depends:
ghc (>= 7.6),
cabal-install,
libghc-async-dev,
- libghc-missingh-dev,
+ libghc-split-dev,
libghc-hslogger-dev,
libghc-unix-compat-dev,
libghc-ansi-terminal-dev,
@@ -18,6 +18,7 @@ Build-Depends:
libghc-exceptions-dev (>= 0.6),
libghc-stm-dev,
libghc-text-dev,
+ libghc-hashable-dev,
libghc-concurrent-output-dev,
Maintainer: Joey Hess <id@joeyh.name>
Standards-Version: 3.9.8
@@ -31,7 +32,7 @@ Depends: ${misc:Depends}, ${shlibs:Depends},
ghc (>= 7.4),
cabal-install,
libghc-async-dev,
- libghc-missingh-dev,
+ libghc-split-dev,
libghc-hslogger-dev,
libghc-unix-compat-dev,
libghc-ansi-terminal-dev,
@@ -42,9 +43,9 @@ Depends: ${misc:Depends}, ${shlibs:Depends},
libghc-exceptions-dev (>= 0.6),
libghc-stm-dev,
libghc-text-dev,
+ libghc-hashable-dev,
libghc-concurrent-output-dev,
git,
- make,
Description: property-based host configuration management in haskell
Propellor ensures that the system it's run in satisfies a list of
properties, taking action as necessary when a property is not yet met.
diff --git a/doc/Linux.mdwn b/doc/Linux.mdwn
index 00276f69..ca0cfd65 100644
--- a/doc/Linux.mdwn
+++ b/doc/Linux.mdwn
@@ -1,5 +1,6 @@
Propellor was written to manage Linux systems.
-It supports Debian and Debian-derived distributions.
+It supports Debian and Debian-derived distributions,
+as well as Arch Linux.
Support for other distributions should not be too hard to add.
Indeed, Propellor has been ported to [[FreeBSD]] now!
diff --git a/doc/README.mdwn b/doc/README.mdwn
index 31d222c1..a4a38c5f 100644
--- a/doc/README.mdwn
+++ b/doc/README.mdwn
@@ -12,8 +12,8 @@ repository to each host it manages, in a
[components](http://propellor.branchable.com/components/)
for details.
-Properties are defined using Haskell. Edit `~/.propellor/config.hs`
-to get started. There is fairly complete
+Properties are defined using Haskell in the file `~/.propellor/config.hs`.
+There is fairly complete
[API documentation](http://hackage.haskell.org/package/propellor/),
which includes many built-in Properties for dealing with
[Apt](http://hackage.haskell.org/package/propellor/docs/Propellor-Property-Apt.html)
@@ -41,6 +41,8 @@ see [configuration for the Haskell newbie](https://propellor.branchable.com/hask
1. Get propellor installed on your development machine (ie, laptop).
`cabal install propellor`
or
+ `stack install propellor`
+ or
`apt-get install propellor`
2. Run `propellor --init` ; this will set up a `~/.propellor/` git
repository for you.
diff --git a/doc/download.mdwn b/doc/download.mdwn
new file mode 100644
index 00000000..6fe1ca33
--- /dev/null
+++ b/doc/download.mdwn
@@ -0,0 +1,5 @@
+Propellor's source code and some example configs are in a git repository:
+
+`git clone git://propellor.branchable.com/propellor`
+
+See the [[README]] for details on installing and configuring propellor.
diff --git a/doc/forum/Compatibility_between_different_software_versions.mdwn b/doc/forum/Compatibility_between_different_software_versions.mdwn
new file mode 100644
index 00000000..b2de3439
--- /dev/null
+++ b/doc/forum/Compatibility_between_different_software_versions.mdwn
@@ -0,0 +1 @@
+I'm just asking myself how (or if) we can guarantee compatibility between different versions of an application. Let's take "prosody" as an example. Even if we use the "DebianLike" property, there might be different versions of "prosody" in Debian Stable and Debian Unstable and therefore different configurations options available. Is there a way to catch those cases? Another example would be a "generic" property (which works for DebianLike and ArchLinux) for a specific software, but inside these distributions are different versions of the application. Even a "Prosody.installed" might be problematic, if the package has been renamed in a newer Debian release.
diff --git a/doc/forum/Compatibility_between_different_software_versions/comment_1_1bc12b78e09c7060f4b5c434004b4b7f._comment b/doc/forum/Compatibility_between_different_software_versions/comment_1_1bc12b78e09c7060f4b5c434004b4b7f._comment
new file mode 100644
index 00000000..97ab02e8
--- /dev/null
+++ b/doc/forum/Compatibility_between_different_software_versions/comment_1_1bc12b78e09c7060f4b5c434004b4b7f._comment
@@ -0,0 +1,12 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2017-08-31T22:26:42Z"
+ content="""
+`withOS` or `getOS` is often used to deal with such differences,
+varying behavior depending on the Host's defined OS. For example,
+Propellor.Property.Borg.installed does one thing on Debian jessie
+and another thing on other versions of Debian. And
+Propellor.Property.Apt.getMirror generates different urls for Debian and
+Ubuntu.
+"""]]
diff --git a/doc/forum/DiskImage_creation_does_not_work_on_my_system.mdwn b/doc/forum/DiskImage_creation_does_not_work_on_my_system.mdwn
new file mode 100644
index 00000000..f7f56889
--- /dev/null
+++ b/doc/forum/DiskImage_creation_does_not_work_on_my_system.mdwn
@@ -0,0 +1,36 @@
+Hello, I am trying to create a virtualbox image from my stretch system.
+
+But I hve two problems :)
+
+I took your example from the DiskImage property, but in the end, I got this
+
+/srv/diskimages/soleil.img.chroot no services started ... ok
+/srv/diskimages/soleil.img.chroot has Operating System (Debian Linux Unstable) X86_32 ... ok
+/srv/diskimages/soleil.img.chroot apt installed linux-image-i686 ... ok
+/srv/diskimages/soleil.img.chroot grub package installed ... ok
+/srv/diskimages/soleil.img.chroot root has insecure password ... done
+/srv/diskimages/soleil.img.chroot account for soleil ... ok
+/srv/diskimages/soleil.img.chroot soleil has insecure password ... done
+/srv/diskimages/soleil.img.chroot user soleil in group audio ... ok
+/srv/diskimages/soleil.img.chroot user soleil in group cdrom ... ok
+/srv/diskimages/soleil.img.chroot user soleil in group dip ... ok
+/srv/diskimages/soleil.img.chroot user soleil in group floppy ... ok
+/srv/diskimages/soleil.img.chroot user soleil in group video ... ok
+/srv/diskimages/soleil.img.chroot user soleil in group plugdev ... ok
+/srv/diskimages/soleil.img.chroot user soleil in group netdev ... ok
+/srv/diskimages/soleil.img.chroot user soleil is in standard desktop groups ... ok
+/srv/diskimages/soleil.img.chroot cache cleaned ... ok
+ 0 0% 0.00kB/s 0:00:00 (xfr#0, to-chk=0/3)
+ 930 0% 1.77kB/s 0:00:00 (xfr#3, to-chk=0/11069)
+chroot: impossible d'exécuter la commande « update-initramfs »: No such file or directory
+loop deleted : /dev/loop0
+
+I will try to add the pacakge which contain update-initramfs and report back
+
+
+the second problem is thaht virtualbox is no more part of stretch.
+So it is not possible to create a virtualbox image.
+
+Cheers
+
+Frederic
diff --git a/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_10_7982113b64a7884ce95ff38a6d876e2e._comment b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_10_7982113b64a7884ce95ff38a6d876e2e._comment
new file mode 100644
index 00000000..3ccfc4db
--- /dev/null
+++ b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_10_7982113b64a7884ce95ff38a6d876e2e._comment
@@ -0,0 +1,7 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 10"""
+ date="2017-08-24T15:35:22Z"
+ content="""
+I've implemented the DiskImage type class.
+"""]]
diff --git a/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_11_b1ad266b5c34b600d2d724bf5ffc40de._comment b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_11_b1ad266b5c34b600d2d724bf5ffc40de._comment
new file mode 100644
index 00000000..79debc75
--- /dev/null
+++ b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_11_b1ad266b5c34b600d2d724bf5ffc40de._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="picca"
+ avatar="http://cdn.libravatar.org/avatar/7e61c80d28018b10d31f6db7dddb864c"
+ subject="comment 11"
+ date="2017-08-24T18:36:12Z"
+ content="""
+Thanks a lot joey.
+"""]]
diff --git a/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_12_4baf7efcc6f9c50e3aebd663b7792279._comment b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_12_4baf7efcc6f9c50e3aebd663b7792279._comment
new file mode 100644
index 00000000..b6de7d0a
--- /dev/null
+++ b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_12_4baf7efcc6f9c50e3aebd663b7792279._comment
@@ -0,0 +1,23 @@
+[[!comment format=mdwn
+ username="picca"
+ avatar="http://cdn.libravatar.org/avatar/7e61c80d28018b10d31f6db7dddb864c"
+ subject="comment 12"
+ date="2017-08-24T19:11:24Z"
+ content="""
+If I understand correctly, the new typeclass need to provide a method which return the
+(RawDiskImage filename). In the process we have at least 2 cache level
+One for the chroot, and one for the RawImage.
+
+I was wondering if these cache (side effect) could not be regrouped
+under /var/cache/propellor instead of putting this randomly everywhere on the disk.
+
+This way It should be possible to \"reset\" propellor by removing the cache in order to force
+a cache rebuild.
+
+I think about this because I am not aware as a user of all these \"side effects\".
+
+propellor --purge-cache ;)
+
+cheers and thanks again
+
+"""]]
diff --git a/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_13_2f8c7bb7f8ffb734a99ac3d7b28e2d62._comment b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_13_2f8c7bb7f8ffb734a99ac3d7b28e2d62._comment
new file mode 100644
index 00000000..74dc528e
--- /dev/null
+++ b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_13_2f8c7bb7f8ffb734a99ac3d7b28e2d62._comment
@@ -0,0 +1,15 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 13"""
+ date="2017-08-24T21:11:07Z"
+ content="""
+Yes, there are two levels of caches. This does make updating the images a
+whole lot faster!
+
+Some systems don't have a very large /var partition and so I think it's
+better to let the user pick where they go. The documentation could
+certainly (always) be improved.
+
+Note that reverting any of the properties in DiskImage will clean up
+all the cache files as well as the final disk image.
+"""]]
diff --git a/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_1_2daa4574bce2179bfd7e9e505de3f7b0._comment b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_1_2daa4574bce2179bfd7e9e505de3f7b0._comment
new file mode 100644
index 00000000..90283031
--- /dev/null
+++ b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_1_2daa4574bce2179bfd7e9e505de3f7b0._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="picca"
+ avatar="http://cdn.libravatar.org/avatar/7e61c80d28018b10d31f6db7dddb864c"
+ subject="comment 1"
+ date="2017-08-22T07:02:51Z"
+ content="""
+Haaaaaaa the format of the post is ugly. Is it possible to change this ?
+"""]]
diff --git a/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_2_98fb34d4e76bab6ef7a981c87533f395._comment b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_2_98fb34d4e76bab6ef7a981c87533f395._comment
new file mode 100644
index 00000000..e8898a96
--- /dev/null
+++ b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_2_98fb34d4e76bab6ef7a981c87533f395._comment
@@ -0,0 +1,14 @@
+[[!comment format=mdwn
+ username="picca"
+ avatar="http://cdn.libravatar.org/avatar/7e61c80d28018b10d31f6db7dddb864c"
+ subject="comment 2"
+ date="2017-08-22T07:12:13Z"
+ content="""
+OK, I tryed to install the wrong kernel so the initramfs was not installed.
+
+So now the only real problem is the virtualbox one ;)
+
+Cheers
+
+Frederic
+"""]]
diff --git a/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_3_047bca6e0676f0d93338d4eff20825bf._comment b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_3_047bca6e0676f0d93338d4eff20825bf._comment
new file mode 100644
index 00000000..aeeaf724
--- /dev/null
+++ b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_3_047bca6e0676f0d93338d4eff20825bf._comment
@@ -0,0 +1,18 @@
+[[!comment format=mdwn
+ username="picca"
+ avatar="http://cdn.libravatar.org/avatar/7e61c80d28018b10d31f6db7dddb864c"
+ subject="comment 3"
+ date="2017-08-22T07:36:06Z"
+ content="""
+It seems that we do not need virtualbox in order to generate a vmdk image
+
+I installed *qemu-utils* and then
+
+ # qemu-img convert -O vmdk soleil.img soleil.vmdk
+ # file soleil.vmdk
+ soleil.vmdk: VMware4 disk image
+
+what about using this solution instead of the virtualbox one ?
+
+Cheers
+"""]]
diff --git a/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_4_fc50b46606eacf59e5db227760ce38ab._comment b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_4_fc50b46606eacf59e5db227760ce38ab._comment
new file mode 100644
index 00000000..27b70a57
--- /dev/null
+++ b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_4_fc50b46606eacf59e5db227760ce38ab._comment
@@ -0,0 +1,24 @@
+[[!comment format=mdwn
+ username="picca"
+ avatar="http://cdn.libravatar.org/avatar/7e61c80d28018b10d31f6db7dddb864c"
+ subject="comment 4"
+ date="2017-08-22T08:42:35Z"
+ content="""
+ vmdkBuiltFor :: FilePath -> RevertableProperty DebianLike UnixLike
+ vmdkBuiltFor diskimage = (setup <!> cleanup)
+ `describe` (vmdkfile ++ \" built\")
+ where
+ vmdkfile = diskimage ++ \".vmdk\"
+ setup = cmdProperty \"qemu-img\"
+ [ \"convert\"
+ , \"-O\", \"vmdk\"
+ , diskimage, vmdkfile
+ ]
+ `changesFile` vmdkfile
+ `onChange` File.mode vmdkfile (combineModes (ownerWriteMode : readModes))
+ `requires` Apt.installed [\"qemu-utils\"]
+ `requires` File.notPresent vmdkfile
+ cleanup = File.notPresent vmdkfile
+
+seems to work :))
+"""]]
diff --git a/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_5_df27f39bfb7104b4440c972b71f586e4._comment b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_5_df27f39bfb7104b4440c972b71f586e4._comment
new file mode 100644
index 00000000..374de327
--- /dev/null
+++ b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_5_df27f39bfb7104b4440c972b71f586e4._comment
@@ -0,0 +1,17 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 5"""
+ date="2017-08-23T15:49:27Z"
+ content="""
+The `vmdkBuiltFor` property is provided to make a disk image
+usable with virtualbox. If your distribution chooses not to include
+virtualbox and so you don't have virtualbox installed, what good would
+such an image be to you?
+
+To use `vmdkBuiltFor` you must already have a disk image file, which qemu
+etc can already use.
+
+"qemu-img convert" writes a whole disk image file. This is a much more
+expensive operation than what `vmdkBuiltFor` currently does, which creates
+a tiny text file that makes virtualbox use the existing disk image.
+"""]]
diff --git a/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_6_1410b386c0f3e1ff41adb068dd611f10._comment b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_6_1410b386c0f3e1ff41adb068dd611f10._comment
new file mode 100644
index 00000000..5bd1ab6d
--- /dev/null
+++ b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_6_1410b386c0f3e1ff41adb068dd611f10._comment
@@ -0,0 +1,12 @@
+[[!comment format=mdwn
+ username="picca"
+ avatar="http://cdn.libravatar.org/avatar/7e61c80d28018b10d31f6db7dddb864c"
+ subject="comment 6"
+ date="2017-08-23T19:42:31Z"
+ content="""
+this is good for me because I prepare a virtualbox image not for me but for our windows / MacOSX users.
+
+This is why I need to build these images.
+
+thanks for your help
+"""]]
diff --git a/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_7_a3de897d9d056fcb6821f3b03485ede5._comment b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_7_a3de897d9d056fcb6821f3b03485ede5._comment
new file mode 100644
index 00000000..7c0995ff
--- /dev/null
+++ b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_7_a3de897d9d056fcb6821f3b03485ede5._comment
@@ -0,0 +1,13 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 7"""
+ date="2017-08-23T21:07:41Z"
+ content="""
+The vmdk text file is so small that I did think about just having propellor
+generate it by itself. I don't know how stable/documented it is however.
+
+I suppose that if you're distributing a vmdk image to others, you would not
+want to use the text file format, since that hard-codes the path to the
+.img file. So, perhaps there should be separate properties for vmdk text
+files that point at disk images and self-contained vmdk images.
+"""]]
diff --git a/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_8_ca5d1f161c037c09fe853c56281f88bc._comment b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_8_ca5d1f161c037c09fe853c56281f88bc._comment
new file mode 100644
index 00000000..9891845e
--- /dev/null
+++ b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_8_ca5d1f161c037c09fe853c56281f88bc._comment
@@ -0,0 +1,18 @@
+[[!comment format=mdwn
+ username="picca"
+ avatar="http://cdn.libravatar.org/avatar/7e61c80d28018b10d31f6db7dddb864c"
+ subject="comment 8"
+ date="2017-08-24T07:04:07Z"
+ content="""
+It is true that my uszer prefer the embeded virtual image :).
+
+Maybe we could have a DiskImage export property which could take an output format type
+I do not know how many format are out there for these kind of virtual machines.
+Maybe this could be also a way to prepare images for the cloud. (I do not use this mayself but why not).
+What is the difference between Diskimage and containers ?
+
+Cheers
+
+Frederic
+
+"""]]
diff --git a/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_9_eebdf852c9d73c7b11b184b7654aa78c._comment b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_9_eebdf852c9d73c7b11b184b7654aa78c._comment
new file mode 100644
index 00000000..1b1f1e64
--- /dev/null
+++ b/doc/forum/DiskImage_creation_does_not_work_on_my_system/comment_9_eebdf852c9d73c7b11b184b7654aa78c._comment
@@ -0,0 +1,16 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 9"""
+ date="2017-08-24T14:39:05Z"
+ content="""
+The `DiskImage` data type could be expanded to support different output
+formats.
+
+Or, a type class could be used, so eg:
+
+ imageBuilt :: DiskImage d => d -> (FilePath -> Chroot) -> TableType -> [PartSpec ()] -> RevertableProperty (HasInfo + DebianLike) Linux
+
+The type class would just need a function to convert from the raw disk
+image to the desired file format. Then anyone could add whatever disk image
+formats they want (which can probably shade into containers in some cases).
+"""]]
diff --git a/doc/forum/Docker.hs_will_Break_in_Stretch.mdwn b/doc/forum/Docker.hs_will_Break_in_Stretch.mdwn
new file mode 100644
index 00000000..c89c995c
--- /dev/null
+++ b/doc/forum/Docker.hs_will_Break_in_Stretch.mdwn
@@ -0,0 +1,16 @@
+G'day Joey!
+
+I'm in the process of deploying Docker infrastructure via Propellor on both Jessie and Stretch and I've come to discover that Docker.io did not make it into Stretch:
+
+* [docker.io REMOVED from testing](https://packages.qa.debian.org/d/docker.io/news/20161012T163916Z.html)
+* [docker.io - Linux container runtime](https://tracker.debian.org/pkg/docker.io)
+* [Excuse for docker.io](https://qa.debian.org/excuses.php?package=docker.io)
+
+So the below from Docker.hs will fail beyond Jessie:
+
+ installed :: Property DebianLike
+ installed = Apt.installed ["docker.io"]
+
+Before I embarked on my own path to re-implement the above (probably based on [How to install Docker engine on Debian 9 Stretch Linux](https://linuxconfig.org/how-to-install-docker-engine-on-debian-9-stretch-linux)), I thought I'd see what you thought might be the way to resolve this, so that my work could be contributed upstream (if suitable).
+
+Thanks!
diff --git a/doc/forum/Docker.hs_will_Break_in_Stretch/comment_1_8a4f16ae6d04b9d4bedb437ef333562b._comment b/doc/forum/Docker.hs_will_Break_in_Stretch/comment_1_8a4f16ae6d04b9d4bedb437ef333562b._comment
new file mode 100644
index 00000000..949f8d0c
--- /dev/null
+++ b/doc/forum/Docker.hs_will_Break_in_Stretch/comment_1_8a4f16ae6d04b9d4bedb437ef333562b._comment
@@ -0,0 +1,11 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2017-02-02T17:28:49Z"
+ content="""
+Apparently the Debian way to install docker will be from backports.
+<https://bugs.debian.org/cgi-bin/bugreport.cgi?att=3;bug=781554;msg=9>
+
+Note that I'm no longer using any docker Properties myself, so
+propellor users who are will need to send patches..
+"""]]
diff --git a/doc/forum/Error_building_on_remote_host.mdwn b/doc/forum/Error_building_on_remote_host.mdwn
new file mode 100644
index 00000000..240db464
--- /dev/null
+++ b/doc/forum/Error_building_on_remote_host.mdwn
@@ -0,0 +1,31 @@
+I recently updated to the latest Propellor and now I'm getting an error building on a remote host:
+
+ [86 of 94] Compiling Propellor.Bootstrap ( src/Propellor/Bootstrap.hs, dist/build/propellor-config/propellor-config-tmp/Propellor/Bootstrap.o )
+
+ src/Propellor/Bootstrap.hs:237:22:
+ No instance for (Typeable Bootstrapper)
+ arising from a use of `fromInfo'
+ Possible fix:
+ add an instance declaration for (Typeable Bootstrapper)
+ In the expression: fromInfo (maybe mempty hostInfo mh)
+ In a stmt of a 'do' block:
+ case fromInfo (maybe mempty hostInfo mh) of {
+ NoInfoVal
+ -> do { bs <- getGitConfigValue "propellor.buildsystem";
+ case bs of {
+ Just "stack" -> ...
+ _ -> ... } }
+ InfoVal bs
+ -> case getBuilder bs of {
+ Cabal -> cabalBuild msys
+ Stack -> stackBuild msys } }
+ In the second argument of `($)', namely
+ `do { case fromInfo (maybe mempty hostInfo mh) of {
+ NoInfoVal -> do { ... }
+ InfoVal bs
+ -> case getBuilder bs of {
+ Cabal -> ...
+ Stack -> ... } } }'
+ propellor: cabal build failed
+
+I guess I'm missing something, but not sure what?
diff --git a/doc/forum/Error_building_on_remote_host/comment_1_f0f6f241e971d048486ae159585a4ab2._comment b/doc/forum/Error_building_on_remote_host/comment_1_f0f6f241e971d048486ae159585a4ab2._comment
new file mode 100644
index 00000000..eca6c8c6
--- /dev/null
+++ b/doc/forum/Error_building_on_remote_host/comment_1_f0f6f241e971d048486ae159585a4ab2._comment
@@ -0,0 +1,21 @@
+[[!comment format=mdwn
+ username="mithrandi"
+ avatar="http://cdn.libravatar.org/avatar/869963bdf99b541c9f0bbfb04b0320f1"
+ subject="comment 1"
+ date="2017-07-25T22:22:49Z"
+ content="""
+I tried adding:
+
+ & bootstrapWith (Robustly Stack)
+
+But that fails trying to install stack:
+
+ Fetched 413 kB in 7s (54.3 kB/s)
+ Reading package lists...
+ E: Unable to locate package haskell-stack
+ sh: 1: stack: not found
+ sh: 1: stack: not found
+
+That's not really too surprising, of course, since this package isn't in jessie (or stretch, for that matter).
+
+"""]]
diff --git a/doc/forum/Error_building_on_remote_host/comment_2_9029575e378c3ed67ea7b7d9fd0a11b5._comment b/doc/forum/Error_building_on_remote_host/comment_2_9029575e378c3ed67ea7b7d9fd0a11b5._comment
new file mode 100644
index 00000000..34750553
--- /dev/null
+++ b/doc/forum/Error_building_on_remote_host/comment_2_9029575e378c3ed67ea7b7d9fd0a11b5._comment
@@ -0,0 +1,13 @@
+[[!comment format=mdwn
+ username="mithrandi"
+ avatar="http://cdn.libravatar.org/avatar/869963bdf99b541c9f0bbfb04b0320f1"
+ subject="comment 2"
+ date="2017-07-25T22:50:42Z"
+ content="""
+Okay, got it to work:
+
+1. Installed haskell-stack by hand from unstable (the package works fine even on jessie).
+2. Removed the \"dist\" directory in the remote /usr/local/propellor.
+
+After that the build was successful; I think that points to the original failure being due to the ancient GHC in jessie, but I'm not 100% sure.
+"""]]
diff --git a/doc/forum/Error_building_on_remote_host/comment_3_3090e63b93e00d6eca95ca8fe523f5b8._comment b/doc/forum/Error_building_on_remote_host/comment_3_3090e63b93e00d6eca95ca8fe523f5b8._comment
new file mode 100644
index 00000000..1790ac78
--- /dev/null
+++ b/doc/forum/Error_building_on_remote_host/comment_3_3090e63b93e00d6eca95ca8fe523f5b8._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 3"""
+ date="2017-07-26T00:49:51Z"
+ content="""
+The haskell-stack package is available in Debian stretch
+<https://packages.debian.org/search?keywords=haskell-stack>
+"""]]
diff --git a/doc/forum/Error_building_on_remote_host/comment_4_8a3eac770c1bee9295272c46f022a03c._comment b/doc/forum/Error_building_on_remote_host/comment_4_8a3eac770c1bee9295272c46f022a03c._comment
new file mode 100644
index 00000000..5129fb5d
--- /dev/null
+++ b/doc/forum/Error_building_on_remote_host/comment_4_8a3eac770c1bee9295272c46f022a03c._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="mithrandi"
+ avatar="http://cdn.libravatar.org/avatar/869963bdf99b541c9f0bbfb04b0320f1"
+ subject="comment 4"
+ date="2017-07-26T01:03:02Z"
+ content="""
+Oh, so it is! Must have misread something earlier.
+"""]]
diff --git a/doc/forum/Error_building_on_remote_host/comment_4_c2e07d9bfba84fbdcf408a09965d6cb6._comment b/doc/forum/Error_building_on_remote_host/comment_4_c2e07d9bfba84fbdcf408a09965d6cb6._comment
new file mode 100644
index 00000000..7d8f26fe
--- /dev/null
+++ b/doc/forum/Error_building_on_remote_host/comment_4_c2e07d9bfba84fbdcf408a09965d6cb6._comment
@@ -0,0 +1,10 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 4"""
+ date="2017-07-26T00:52:20Z"
+ content="""
+Interesting that it works on newer ghc without an explict
+`Typeable Bootstrapper` instance.
+
+I've added the missing instance.
+"""]]
diff --git a/doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap.mdwn b/doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap.mdwn
new file mode 100644
index 00000000..61cd10cc
--- /dev/null
+++ b/doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap.mdwn
@@ -0,0 +1,3 @@
+The mount command won't work when activating a swap partition/file, so we should call swapon instead.
+
+https://github.com/ArchiveTeam/glowing-computing-machine/tree/fstab-swap
diff --git a/doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap/comment_1_8ab6b313c80486f8f87a5e13e830bfa9._comment b/doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap/comment_1_8ab6b313c80486f8f87a5e13e830bfa9._comment
new file mode 100644
index 00000000..4a144df5
--- /dev/null
+++ b/doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap/comment_1_8ab6b313c80486f8f87a5e13e830bfa9._comment
@@ -0,0 +1,20 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2017-04-05T02:17:00Z"
+ content="""
+This idea kind of makes sense, because swap partitions in /etc/fstab
+get swaponed at boot.
+
+But, the implementation doesn't take the types into account. The `mounted`
+property takes a FilePath for the mountpoint, but for swap that
+needs to be "none", which is not really a file-path. Also, the `fstabbed`
+property has a separate `SwapPartition` type, so making `mount` support
+swap partitions without using that type feels wrong.
+
+It might be simpler all round to treat swap partitions being able to
+be specified in /etc/fstab as a historical accident, which it kind of
+is (increasingly so, since eg systemd has other ways to accomplish
+that), and instead of shoehorning this into the `mounted` property,
+add a new `swaponed` property.
+"""]]
diff --git a/doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap/comment_2_773fc1441dd06e9dd41508bd800298eb._comment b/doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap/comment_2_773fc1441dd06e9dd41508bd800298eb._comment
new file mode 100644
index 00000000..62cabc0a
--- /dev/null
+++ b/doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap/comment_2_773fc1441dd06e9dd41508bd800298eb._comment
@@ -0,0 +1,13 @@
+[[!comment format=mdwn
+ username="db48x@80bd751a72d5a80737e2f875342cf845629c7202"
+ nickname="db48x"
+ avatar="http://cdn.libravatar.org/avatar/ad2688127feb555a92154b16d8eeb5d3"
+ subject="comment 2"
+ date="2017-04-05T02:48:08Z"
+ content="""
+Yes, perhaps if it took an Option FilePath (am I saying this correctly in Haskellese?) it would be nicer.
+
+I don't mind much how it's structured; this was just the smallest obvious change, since it was failing to mount it. Perhaps breaking it up into smaller, more primitive, pieces would help. Fstab.mounted could = Fstab.fstabbed `onChange` Fstab.mounted, for instance, and then I could write Fstab.fstabbed `onChange` Swap.swapEnabled (oh, but Fstab.fstabbed already exists; I'm not using it because it replaces the whole file, which seems like an odd thing to do. Maybe call it Fstab.listed instead?).
+
+Also, for maximum irony I was just perusing your most recent dozen commits or so, and saw you enable Apt.serviceInstalledRunning \"swapspace\" on one of your machines. That's amazing; I had no idea it existed! I am re-evaluating all of my life choices now.
+"""]]
diff --git a/doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap/comment_3_f48a6191c56bed41eda55436f0aa3e9c._comment b/doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap/comment_3_f48a6191c56bed41eda55436f0aa3e9c._comment
new file mode 100644
index 00000000..95c69551
--- /dev/null
+++ b/doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap/comment_3_f48a6191c56bed41eda55436f0aa3e9c._comment
@@ -0,0 +1,15 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 3"""
+ date="2017-04-05T03:08:30Z"
+ content="""
+I like the idea of composing smaller properties to build the current
+property, and add flexability.
+
+Renaming the existing `fstabbed` would probably be too much bother.
+(Also, I think I picked that name because it kind of hints that the
+existing fstab does not come out alive.)
+
+(The swapspace package is great if you can eat the now tiny overhead of a
+swap file compared to a swap partition.)
+"""]]
diff --git a/doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap/comment_4_b1769231a633ad2b978ee4c9fa90591c._comment b/doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap/comment_4_b1769231a633ad2b978ee4c9fa90591c._comment
new file mode 100644
index 00000000..ca04f945
--- /dev/null
+++ b/doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap/comment_4_b1769231a633ad2b978ee4c9fa90591c._comment
@@ -0,0 +1,9 @@
+[[!comment format=mdwn
+ username="db48x@80bd751a72d5a80737e2f875342cf845629c7202"
+ nickname="db48x"
+ avatar="http://cdn.libravatar.org/avatar/ad2688127feb555a92154b16d8eeb5d3"
+ subject="comment 4"
+ date="2017-04-05T06:39:49Z"
+ content="""
+I took a stab at implementing this. It compiles, but I've not tested it yet as I need to get some sleep; consider it a work in progress. Looks right to me though.
+"""]]
diff --git a/doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap/comment_5_6dc24952c8efa31a401191a8cf2d0b39._comment b/doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap/comment_5_6dc24952c8efa31a401191a8cf2d0b39._comment
new file mode 100644
index 00000000..f87500b2
--- /dev/null
+++ b/doc/forum/Fstab.mounted_could_call_swapon_when_activating_swap/comment_5_6dc24952c8efa31a401191a8cf2d0b39._comment
@@ -0,0 +1,14 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 5"""
+ date="2017-04-06T23:51:08Z"
+ content="""
+Merged. Have not tested it either.
+
+On my Debian system, the swapon command does not support the
+`--no-headings` that you used. It's `--noheadings` here. Is that a typo in
+your patch?
+
+I've simply removed that option for now, since it probably won't
+hurt if it treats the heading like another device that's swapped on.
+"""]]
diff --git a/doc/forum/Git.cloned_deletes_harmless_empty_directory.mdwn b/doc/forum/Git.cloned_deletes_harmless_empty_directory.mdwn
new file mode 100644
index 00000000..ce3c192c
--- /dev/null
+++ b/doc/forum/Git.cloned_deletes_harmless_empty_directory.mdwn
@@ -0,0 +1,3 @@
+In my case I have carefully set up the directory that I'm going to clone into with the correct group ownership and setgid permission, so that the cloned files will also have the correct ownership. This change just checks to see if the directory actually has anything in it before it deletes it.
+
+https://github.com/ArchiveTeam/glowing-computing-machine/tree/git-in-emtpy-directory
diff --git a/doc/forum/Git.cloned_deletes_harmless_empty_directory/comment_1_7cd0521c6d071b25852f8355f4f61f94._comment b/doc/forum/Git.cloned_deletes_harmless_empty_directory/comment_1_7cd0521c6d071b25852f8355f4f61f94._comment
new file mode 100644
index 00000000..91b403b0
--- /dev/null
+++ b/doc/forum/Git.cloned_deletes_harmless_empty_directory/comment_1_7cd0521c6d071b25852f8355f4f61f94._comment
@@ -0,0 +1,20 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2017-04-05T02:22:54Z"
+ content="""
+I am not entirely happy with this patch, because it seems that if
+Git.cloned took care to preserve permissions in this case, it could be
+argued that it should also preserve permissions when the directory already
+exists but has the wrong content. Or equally well argued that it should not
+preserve permissions, which might be a leftover from some past unwanted
+state.
+
+Is that really the best way to do it? You could instead say:
+
+ Git.cloned user repo dir Nothing
+ `onChange` recursiveSetGID user dir
+
+And then you just have to write a recursiveSetGID which would be a
+generally useful property.
+"""]]
diff --git a/doc/forum/Git.cloned_deletes_harmless_empty_directory/comment_2_289f157f129511242d93beae76fd03a3._comment b/doc/forum/Git.cloned_deletes_harmless_empty_directory/comment_2_289f157f129511242d93beae76fd03a3._comment
new file mode 100644
index 00000000..1a8c1447
--- /dev/null
+++ b/doc/forum/Git.cloned_deletes_harmless_empty_directory/comment_2_289f157f129511242d93beae76fd03a3._comment
@@ -0,0 +1,11 @@
+[[!comment format=mdwn
+ username="db48x@80bd751a72d5a80737e2f875342cf845629c7202"
+ nickname="db48x"
+ avatar="http://cdn.libravatar.org/avatar/ad2688127feb555a92154b16d8eeb5d3"
+ subject="comment 2"
+ date="2017-04-05T02:37:44Z"
+ content="""
+Yea, I guess that's a fair point about the other cases.
+
+It just seems inelegant to go back over all the files and fix up their permissions, when it could just have been set right to begin with.
+"""]]
diff --git a/doc/forum/How_to_create_a_property_with_info.mdwn b/doc/forum/How_to_create_a_property_with_info.mdwn
new file mode 100644
index 00000000..ea8babe5
--- /dev/null
+++ b/doc/forum/How_to_create_a_property_with_info.mdwn
@@ -0,0 +1,65 @@
+Hello Joey,
+
+I try to setup a debomatic service on one of my computer.
+So I created a data which will store on which host it was installed
+
+ data DebOMaticHostMirror = DebOMaticHostMirror Url
+ deriving (Eq, Show, Typeable)
+
+So now I try to create a property which get the hostname and set the info,
+BUT I did not find the right way to do this. Here an attempt
+
+ debomaticHostMirror :: Property (HasInfo + UnixLike)
+ debomaticHostMirror = property' desc $ \w -> do
+ hostname <- asks hostName
+ ensureProperty $ pureInfoProperty desc (InfoVal (DebOMaticHostMirror hostname))
+ where
+ desc = "setup the Deb-O-Matic host name for other properties"
+
+but I get this error message
+
+ src/propellor-config.hs:935:3: error:
+ • Couldn't match expected type ‘Propellor Result’
+ with actual type ‘Property
+ (Propellor.Types.MetaTypes.MetaTypes inner0)
+ -> Propellor Result’
+ • In a stmt of a 'do' block:
+ ensureProperty
+ $ pureInfoProperty desc (InfoVal (DebOMaticHostMirror hostname))
+ In the expression:
+ do { hostname <- asks hostName;
+ ensureProperty
+ $ pureInfoProperty desc (InfoVal (DebOMaticHostMirror hostname)) }
+ In the second argument of ‘($)’, namely
+ ‘\ w
+ -> do { hostname <- asks hostName;
+ ensureProperty
+ $ pureInfoProperty desc (InfoVal (DebOMaticHostMirror hostname)) }’
+
+ src/propellor-config.hs:935:20: error:
+ • Couldn't match expected type ‘OuterMetaTypesWitness outer0’
+ with actual type ‘Property (HasInfo + UnixLike)’
+ • In the second argument of ‘($)’, namely
+ ‘pureInfoProperty desc (InfoVal (DebOMaticHostMirror hostname))’
+ In a stmt of a 'do' block:
+ ensureProperty
+ $ pureInfoProperty desc (InfoVal (DebOMaticHostMirror hostname))
+ In the expression:
+ do { hostname <- asks hostName;
+ ensureProperty
+ $ pureInfoProperty desc (InfoVal (DebOMaticHostMirror hostname)) }
+
+the Idea after is to create a property which will take the DeboMatic Info and generate the
+/etc/apt/sourses.list.d/debomatic.list on a bunch of hosts.
+
+Maybe we could have a
+
+ typeclass Mirror a where
+ toSourceListDLines :: a -> [Line]
+
+ instance Mirror DebOMaticHostMirror where
+ toSourceListDLines (DebOMaticHostMirror hostname) = ...
+
+then the stdSourceListD property should be change to use toSourceListDLines
+
+but this is another story :)
diff --git a/doc/forum/How_to_create_a_property_with_info/comment_1_819902ee6b8e571f735dd2c9c93c49a9._comment b/doc/forum/How_to_create_a_property_with_info/comment_1_819902ee6b8e571f735dd2c9c93c49a9._comment
new file mode 100644
index 00000000..853e6e86
--- /dev/null
+++ b/doc/forum/How_to_create_a_property_with_info/comment_1_819902ee6b8e571f735dd2c9c93c49a9._comment
@@ -0,0 +1,29 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2017-08-25T23:07:12Z"
+ content="""
+It's not allowed for the content of Info to come from an IO action.
+Info has to be static. This allows one Host to introspect the Info of
+another Host. The Dns properties rely on that.
+
+So, the type checker is right in preventing this. It's also not allowed
+to use ensureProperty with a property that HasInfo, as the info would
+not propigate to the outer property. The type checker is also preventing
+you making that mistake.
+
+(You also forgot to pass the `w` parameter to `ensureProperty`,
+which made the type checker unhappy as well and probably confused the error
+messages.)
+
+To accomplish your goal, you could use:
+
+ data DebOMaticHostMirror = DebOMaticHostMirror
+
+If a Host has this in its Info, you know that Host is the one with
+debomatic installed. You can then get its hostname using the `hostName`
+field accessor on the Host.
+
+The property that does that will need to be passed a `[Host]` which will
+typically be the `hosts` list defined in config.hs.
+"""]]
diff --git a/doc/forum/How_to_create_a_property_with_info/comment_2_1c2b3cb54f27fb6b6bb5de9d159dd34f._comment b/doc/forum/How_to_create_a_property_with_info/comment_2_1c2b3cb54f27fb6b6bb5de9d159dd34f._comment
new file mode 100644
index 00000000..6034e6e5
--- /dev/null
+++ b/doc/forum/How_to_create_a_property_with_info/comment_2_1c2b3cb54f27fb6b6bb5de9d159dd34f._comment
@@ -0,0 +1,15 @@
+[[!comment format=mdwn
+ username="picca"
+ avatar="http://cdn.libravatar.org/avatar/7e61c80d28018b10d31f6db7dddb864c"
+ subject="comment 2"
+ date="2017-08-26T06:29:44Z"
+ content="""
+I could have multiple host with debomatic install on it.
+I need to create a property which take a list of hosts (all with the Debomatic info) in order to generate the sources.list files.
+This way it is possible for me to select per host the sources of packages.
+
+what should be done in order to type check this ?
+I would like the compiler to says. Hey you ask for a source list from this host but it dos not contain a Debian mirror.
+
+Cheers
+"""]]
diff --git a/doc/forum/How_to_create_a_property_with_info/comment_3_6cf0360b4922a131bca33d33acf078be._comment b/doc/forum/How_to_create_a_property_with_info/comment_3_6cf0360b4922a131bca33d33acf078be._comment
new file mode 100644
index 00000000..ac4ca94b
--- /dev/null
+++ b/doc/forum/How_to_create_a_property_with_info/comment_3_6cf0360b4922a131bca33d33acf078be._comment
@@ -0,0 +1,11 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 3"""
+ date="2017-08-28T22:38:55Z"
+ content="""
+Finding a way to type check that, I don't know. It would certianly be nice
+to be able to statically check such things. The way that Info is
+implemented as a monoid that contains many different types seems to
+preclude exposing enough information for the type checker to catch such a
+problem. So it would have to be changed somehow, I don't know how.
+"""]]
diff --git a/doc/forum/Inherited_Variables....mdwn b/doc/forum/Inherited_Variables....mdwn
new file mode 100644
index 00000000..1535ec77
--- /dev/null
+++ b/doc/forum/Inherited_Variables....mdwn
@@ -0,0 +1,26 @@
+I've got a server defined in config.hs as follows:
+
+ myserver :: Host
+ myserver = host "myserver.mydomain" $ props
+ & standardSystem (Stable "jessie") X86_64 [ "Welcome to myserver!" ]
+
+I'm writing a module (to deploy Matrix, FWIW) which has a section like this:
+
+ sources :: Property Debian
+ sources = File.hasContent "/etc/apt/sources.list.d/matrix.list"
+ [ "# Deployed by Propellor"
+ , ""
+ , "deb http://matrix.org/packages/debian/ jessie main"
+ ] `onChange` Apt.update
+
+What I would like to be able to do, for example, is pull "jessie" from the standardSystem line into the sources function.
+
+The host name is another I'd like to be able to pull in, so that I can abstract as much as possible and wind up with a line that looks not unlike this:
+
+ & Matrix.server
+
+Instead of
+
+ & Matrix.server hostname jessie
+
+Am I barking up the wrong tree and should I just embrace the latter?
diff --git a/doc/forum/Inherited_Variables.../comment_1_082e5d5b8e25335bc90577abcfef1d21._comment b/doc/forum/Inherited_Variables.../comment_1_082e5d5b8e25335bc90577abcfef1d21._comment
new file mode 100644
index 00000000..e4b32398
--- /dev/null
+++ b/doc/forum/Inherited_Variables.../comment_1_082e5d5b8e25335bc90577abcfef1d21._comment
@@ -0,0 +1,15 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2017-01-26T06:39:35Z"
+ content="""
+This is where propellor's `Info` system comes in. `Propellor.Info.getOS`
+can be run to get the OS info.
+
+It's also possible to add new properties that add new values with custom
+types to `Info`.
+
+The hostname is not currently stored in `Info`, but it probably should be;
+that would be a good simplification. Currently there's a
+separate way to get the hostname: `asks hostName` (run in the Propellor monad)
+"""]]
diff --git a/doc/forum/Inherited_Variables.../comment_2_988319ed6de46eff2eac0d5ef36382f9._comment b/doc/forum/Inherited_Variables.../comment_2_988319ed6de46eff2eac0d5ef36382f9._comment
new file mode 100644
index 00000000..676f41ac
--- /dev/null
+++ b/doc/forum/Inherited_Variables.../comment_2_988319ed6de46eff2eac0d5ef36382f9._comment
@@ -0,0 +1,15 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 2"""
+ date="2017-01-26T06:50:39Z"
+ content="""
+A worked example:
+
+ server :: Property Debian
+ server = property' "some description" $ \w -> do
+ os <- getOS
+ hostname <- asks hostName
+ ensureProperty w $
+ File.hasContent "/etc/apt/sources.list.d/matrix.list"
+ (genSourcesList os hostname)
+"""]]
diff --git a/doc/forum/Inherited_Variables.../comment_3_acf78fa9f732f070bf73c2ab601464ee._comment b/doc/forum/Inherited_Variables.../comment_3_acf78fa9f732f070bf73c2ab601464ee._comment
new file mode 100644
index 00000000..fcdf923b
--- /dev/null
+++ b/doc/forum/Inherited_Variables.../comment_3_acf78fa9f732f070bf73c2ab601464ee._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="craige"
+ avatar="http://cdn.libravatar.org/avatar/64ac5816ea3a51347d1f699022d1fdc1"
+ subject="Thanks!"
+ date="2017-01-27T21:54:45Z"
+ content="""
+Thanks Joey. I think that's exactly what I need. Very helpful :-)
+"""]]
diff --git a/doc/forum/Inherited_Variables.../comment_4_5bf7b1f69b48b4d9c516d424e4438208._comment b/doc/forum/Inherited_Variables.../comment_4_5bf7b1f69b48b4d9c516d424e4438208._comment
new file mode 100644
index 00000000..3b691b2a
--- /dev/null
+++ b/doc/forum/Inherited_Variables.../comment_4_5bf7b1f69b48b4d9c516d424e4438208._comment
@@ -0,0 +1,21 @@
+[[!comment format=mdwn
+ username="craige@a46118dff5bc0fad85259759970d8b4b9fc377d7"
+ nickname="craige"
+ avatar="http://cdn.libravatar.org/avatar/6d2207226de755da46aa2fdff9af70b2"
+ subject="comment 4"
+ date="2017-02-03T00:04:05Z"
+ content="""
+Ugh, sorry to ask again but I'm specifically stuck trying to extract the Debian suite only from this. Is this stored as a specific value I can draw on? I've been wading through the source and added in a swag of trial and error with no luck.
+
+I can see the suite listed in the output
+
+ Just (System (Debian Linux (Stable \"jessie\"))
+
+but I was wondering if there was a method to pull out just the suite code name (ie: \"jessie\") that did not involve a regex looking for it amongst that output.
+
+The goal is to query Info so that a suite name can be added to a sources list.
+
+If I have to regex, that's OK, I just didn't want to go down that path if there was a smarted way.
+
+Thanks Joey :-)
+"""]]
diff --git a/doc/forum/Inherited_Variables.../comment_5_6fbd29f568ec8b97be47874e2aac57a3._comment b/doc/forum/Inherited_Variables.../comment_5_6fbd29f568ec8b97be47874e2aac57a3._comment
new file mode 100644
index 00000000..16819bd6
--- /dev/null
+++ b/doc/forum/Inherited_Variables.../comment_5_6fbd29f568ec8b97be47874e2aac57a3._comment
@@ -0,0 +1,20 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 5"""
+ date="2017-02-03T19:32:58Z"
+ content="""
+What you're looking for is not a regexp, but Haskell's [pattern
+matching](https://www.haskell.org/tutorial/patterns.html).
+
+For example:
+
+ myproperty :: Property Debian
+ myproperty = withOS "some desc here" $ \w o -> case o of
+ -- Pattern match on the OS, to get the Debian stable release
+ (Just (System (Debian _kernel (Stable release)) _arch)) ->
+ ensureProperty w $ Apt.setSourcesListD (sourcesLines release) "mysources"
+ _ -> unsupportedOS
+
+ sourcesLines :: Release -> [Line]
+ sourcesLines release = undefined
+"""]]
diff --git a/doc/forum/Manage_multiple_different_projects_with_propellor.mdwn b/doc/forum/Manage_multiple_different_projects_with_propellor.mdwn
new file mode 100644
index 00000000..bcba383c
--- /dev/null
+++ b/doc/forum/Manage_multiple_different_projects_with_propellor.mdwn
@@ -0,0 +1,7 @@
+Hi there,
+
+I've been tasked with investigating propellor as an alternative to Ansible. I'm a little bit confused about how one might go about managing a *single* project's hosts with propellor, without infecting the global propellor config. It seems that everything is concerned with the ~/.propellor repository. However, I don't want project A's hosts to know about project B's and vice versa. I'm sure I'm overlooking something obvious!
+
+Thanks very much!
+
+Mitchell
diff --git a/doc/forum/Manage_multiple_different_projects_with_propellor/comment_1_dbad48163b2efd6434ea7c37a72dfd30._comment b/doc/forum/Manage_multiple_different_projects_with_propellor/comment_1_dbad48163b2efd6434ea7c37a72dfd30._comment
new file mode 100644
index 00000000..7513cc09
--- /dev/null
+++ b/doc/forum/Manage_multiple_different_projects_with_propellor/comment_1_dbad48163b2efd6434ea7c37a72dfd30._comment
@@ -0,0 +1,14 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2017-03-24T18:14:14Z"
+ content="""
+There did not used to be a good way to do that, but since propellor 3.2.3,
+when you run eg "propellor --spin host", it first checks to see if there is
+a `./config.hs` file, and if so, uses it instead of the user-global
+`~/.propellor/config.hs`.
+
+So, just make different git repos for the different projects with propellor
+`config.hs` files in them, and `cd` into the one you want to run before running
+propellor.
+"""]]
diff --git a/doc/forum/Modules_with_Multiple_cmdProperty_causing_build_failures/comment_2_5afe0f200d7139499ef4b01ea6445206._comment b/doc/forum/Modules_with_Multiple_cmdProperty_causing_build_failures/comment_2_5afe0f200d7139499ef4b01ea6445206._comment
new file mode 100644
index 00000000..00f77116
--- /dev/null
+++ b/doc/forum/Modules_with_Multiple_cmdProperty_causing_build_failures/comment_2_5afe0f200d7139499ef4b01ea6445206._comment
@@ -0,0 +1,11 @@
+[[!comment format=mdwn
+ username="craige@a46118dff5bc0fad85259759970d8b4b9fc377d7"
+ nickname="craige"
+ avatar="http://cdn.libravatar.org/avatar/6d2207226de755da46aa2fdff9af70b2"
+ subject="Fixed!"
+ date="2017-01-26T05:54:22Z"
+ content="""
+The original suggestions did fix my problems.
+
+Apologies for the late response.
+"""]]
diff --git a/doc/forum/Sbuild_chroot_are_not_compatible_with_schroot.mdwn b/doc/forum/Sbuild_chroot_are_not_compatible_with_schroot.mdwn
new file mode 100644
index 00000000..8887f438
--- /dev/null
+++ b/doc/forum/Sbuild_chroot_are_not_compatible_with_schroot.mdwn
@@ -0,0 +1,29 @@
+Hello, I am preparing a property in order to setup a debomatic machine
+but when I try to upload a package I get this error from debomatic
+
+ DEBUG: Command '['schroot', '-l']' returned non-zero exit status 1
+ Traceback (most recent call last):
+ File "/usr/share/debomatic/Debomatic/process.py", line 197, in _finish
+ raise e
+ File "/usr/lib/python3.5/concurrent/futures/thread.py", line 55, in run
+ result = self.fn(*self.args, **self.kwargs)
+ File "/usr/share/debomatic/Debomatic/build.py", line 525, in run
+ self._build()
+ File "/usr/share/debomatic/Debomatic/build.py", line 133, in _build
+ self._setup_chroot()
+ File "/usr/share/debomatic/Debomatic/build.py", line 395, in _setup_chroot
+ chroots = check_output(['schroot', '-l'], stderr=fd)
+ File "/usr/lib/python3.5/subprocess.py", line 316, in check_output
+ **kwargs).stdout
+ File "/usr/lib/python3.5/subprocess.py", line 398, in run
+ output=stdout, stderr=stderr)
+ subprocess.CalledProcessError: Command '['schroot', '-l']' returned non-zero exit status 1
+
+so tried on my own
+
+ :/etc/debomatic# schroot -l
+ E: /etc/schroot/chroot.d/stretch-amd64-sbuild-propellor: [stretch-amd64-sbuild]: Required key ‘directory’ is missing
+
+to my opinion the schroot config file generated by Sbuild property does something wrong.
+
+Cheers
diff --git a/doc/forum/Sbuild_chroot_are_not_compatible_with_schroot/comment_1_59ac4661a896a514ce953a0069341869._comment b/doc/forum/Sbuild_chroot_are_not_compatible_with_schroot/comment_1_59ac4661a896a514ce953a0069341869._comment
new file mode 100644
index 00000000..b4e411b7
--- /dev/null
+++ b/doc/forum/Sbuild_chroot_are_not_compatible_with_schroot/comment_1_59ac4661a896a514ce953a0069341869._comment
@@ -0,0 +1,24 @@
+[[!comment format=mdwn
+ username="picca"
+ avatar="http://cdn.libravatar.org/avatar/7e61c80d28018b10d31f6db7dddb864c"
+ subject="comment 1"
+ date="2017-08-23T13:00:13Z"
+ content="""
+this is strange because the stretch-amd64-sbuild file is wrong.
+
+here the content
+
+ [stretch-amd64-sbuild]
+ command-prefix=/var/cache/ccache-sbuild/sbuild-setup,eatmydata
+
+to compare with my previous jessie-amd64-sbuild
+
+ [jessie-amd64-sbuild]
+ type=directory
+ description=Debian jessie/amd64 autobuilder
+ directory=/srv/chroot/jessie-amd64
+ groups=root,sbuild
+ root-groups=root,sbuild
+ profile=sbuild
+ command-prefix=/var/cache/ccache-sbuild/sbuild-setup,eatmydata
+"""]]
diff --git a/doc/forum/Sbuild_chroot_are_not_compatible_with_schroot/comment_2_579894632e567a08d83e306be5e355b2._comment b/doc/forum/Sbuild_chroot_are_not_compatible_with_schroot/comment_2_579894632e567a08d83e306be5e355b2._comment
new file mode 100644
index 00000000..53595ad2
--- /dev/null
+++ b/doc/forum/Sbuild_chroot_are_not_compatible_with_schroot/comment_2_579894632e567a08d83e306be5e355b2._comment
@@ -0,0 +1,84 @@
+[[!comment format=mdwn
+ username="picca"
+ avatar="http://cdn.libravatar.org/avatar/7e61c80d28018b10d31f6db7dddb864c"
+ subject="comment 2"
+ date="2017-08-23T13:26:31Z"
+ content="""
+Hello, so I try to restart from scratch and ask for a stretch Sbuild
+
+everything went fine until the update
+
+
+ I: schroot chroot configuration written to /etc/schroot/chroot.d/stretch-amd64-propellor-VYWULd.
+ +------------------------------------------------------------------------
+ |[stretch-amd64-propellor]
+ |description=Debian stretch/amd64 autobuilder
+ |groups=root,sbuild
+ |root-groups=root,sbuild
+ |profile=sbuild
+ |type=directory
+ |directory=/srv/chroot/stretch-amd64
+ |union-type=overlay
+ +------------------------------------------------------------------------
+ I: Please rename and modify this file as required.
+ W: Not creating symlink /srv/chroot/stretch-amd64 to /etc/sbuild/chroot/stretch-amd64-propellor: file already exists
+ perl: warning: Setting locale failed.
+ perl: warning: Please check that your locale settings:
+ LANGUAGE = (unset),
+ LC_ALL = (unset),
+ LANG = \"en_GB.UTF-8\"
+ are supported and installed on your system.
+ perl: warning: Falling back to the standard locale (\"C\").
+ I: Setting reference package list.
+ I: Updating chroot.
+
+
+On my network, I need a proxy so I setup the host with
+
+ ...
+ & Apt.proxy myproxy
+ & Sbuild.builtFor stretch Sbuild.UseCcache
+
+If I understand correctly the Apt.proxy should propagate the Apt.proxy into the Sbuild
+but when I look inside the chroot, I can not find the
+
+ /etc/apt/apt.conf.d/20proxy
+
+file which is on the host
+
+And Indeed after a certain amount of time, the network gives a timeout
+
+ Err:1 http://deb.debian.org/debian stretch InRelease
+ Cannot initiate the connection to deb.debian.org:80 (2001:41c8:1000:21::21:4). - connect (101: Network is unreachable) [IP: 2001:41c8:1000:21::21:4 80]
+ Reading package lists...
+ W: Failed to fetch http://deb.debian.org/debian/dists/stretch/InRelease Cannot initiate the connection to deb.debian.org:80 (2001:41c8:1000:21::21:4). - connect (101: Network is unreachable) [IP: 2001:41c8:1000:21::21:4 80]
+ W: Some index files failed to download. They have been ignored, or old ones used instead.
+ Reading package lists...
+ Building dependency tree...
+ Calculating upgrade...
+ 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
+ I: Successfully set up stretch chroot.
+ I: Run \"sbuild-adduser\" to add new sbuild users.
+ sixs7.exp.synchrotron-soleil.fr sbuild schroot for System (Debian Linux (Stable \"stretch\")) X86_64 ... done
+
+the good news is that now the schroot file contain the right informations
+
+ [stretch-amd64-sbuild]
+ description=Debian stretch/amd64 autobuilder
+ groups=root,sbuild
+ root-groups=root,sbuild
+ profile=sbuild
+ type=directory
+ directory=/srv/chroot/stretch-amd64
+ union-type=overlay
+ command-prefix=/var/cache/ccache-sbuild/sbuild-setup,eatmydata
+
+
+So to summarize, I think that the Apt.proxy propagation does not work.
+
+This propagation should be optional because sometime we prepare images which are not meant to be used behind a proxy (where they were prepare)
+
+thanks for your attention :)
+
+
+"""]]
diff --git a/doc/forum/Sbuild_chroot_are_not_compatible_with_schroot/comment_3_6aeee8ba74b363d26a49d6773c5d5014._comment b/doc/forum/Sbuild_chroot_are_not_compatible_with_schroot/comment_3_6aeee8ba74b363d26a49d6773c5d5014._comment
new file mode 100644
index 00000000..12d59028
--- /dev/null
+++ b/doc/forum/Sbuild_chroot_are_not_compatible_with_schroot/comment_3_6aeee8ba74b363d26a49d6773c5d5014._comment
@@ -0,0 +1,12 @@
+[[!comment format=mdwn
+ username="spwhitton"
+ avatar="http://cdn.libravatar.org/avatar/9c3f08f80e67733fd506c353239569eb"
+ subject="comment 3"
+ date="2017-09-02T02:47:01Z"
+ content="""
+Thank you for the detailed report.
+
+I think the problem is the proxy propagation happens after the sbuild-createchroot command has run, but if the sbuild-createchroot command needs the proxy, it will fail in the way you describe.
+
+After speaking to Joey at DebConf I think I can rework the sbuild module to bypass sbuild-createchroot and run debootstrap itself, without thereby polluting the chroot that is created. That should make it much easier to fix this bug, so I'll do that first.
+"""]]
diff --git a/doc/forum/Supported_OS/comment_3_f2924708a819b962ba7ed690019601ed._comment b/doc/forum/Supported_OS/comment_3_f2924708a819b962ba7ed690019601ed._comment
new file mode 100644
index 00000000..c03f6cd9
--- /dev/null
+++ b/doc/forum/Supported_OS/comment_3_f2924708a819b962ba7ed690019601ed._comment
@@ -0,0 +1,7 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""Arch too!"""
+ date="2017-02-04T21:30:26Z"
+ content="""
+Propellor just got support for Arch Linux!
+"""]]
diff --git a/doc/forum/Using_propellor_for_continers_only.mdwn b/doc/forum/Using_propellor_for_continers_only.mdwn
new file mode 100644
index 00000000..faf07956
--- /dev/null
+++ b/doc/forum/Using_propellor_for_continers_only.mdwn
@@ -0,0 +1,5 @@
+Hi,
+
+I was wondering: Is it possible to use propellor to generate images only without actually managing any hosts per-se? I couldn't find any documentation on that.
+
+Ideally, I'd also be able to use it directly from a sandbox so that I wouldn't have to even "pollute" the GHC/Cabal "global" (user home dir) database on the development machine. I see that there's support for having the config.hs stored in a different directory than ~/.propellor, but I haven't managed to get it working when I use a sandbox in e.g. ~/foo with the config.hs stored in the same directory. Perhaps that's just a bug? If it's supposed to work I can provide detailed error messages, etc. **EDIT:** I'd also like to manage the git repository myself -- is that possible?
diff --git a/doc/forum/Using_propellor_for_continers_only/comment_1_95e8b7103f248d93570fecb6b8999996._comment b/doc/forum/Using_propellor_for_continers_only/comment_1_95e8b7103f248d93570fecb6b8999996._comment
new file mode 100644
index 00000000..dc6cc616
--- /dev/null
+++ b/doc/forum/Using_propellor_for_continers_only/comment_1_95e8b7103f248d93570fecb6b8999996._comment
@@ -0,0 +1,20 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2017-03-29T19:09:37Z"
+ content="""
+Sounds like you may want to write a program that uses propellor as a
+library. `Propellor.Engine.mainProperties` is a reasonable
+entry point, just pass it a Host that has the properties you want
+to run.
+
+For example:
+
+ import Propellor
+ import Propellor.Engine
+ import Propellor.Property.DiskImage
+
+ main :: IO ()
+ main = mainProperties $ host "whatever" $ props
+ & imageBuilt "/some/disk.img" ...
+"""]]
diff --git a/doc/forum/Using_propellor_for_continers_only/comment_2_42b45a126cfdf1dfc370b166c8042690._comment b/doc/forum/Using_propellor_for_continers_only/comment_2_42b45a126cfdf1dfc370b166c8042690._comment
new file mode 100644
index 00000000..45cd3e0c
--- /dev/null
+++ b/doc/forum/Using_propellor_for_continers_only/comment_2_42b45a126cfdf1dfc370b166c8042690._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="bardur.arantsson"
+ avatar="http://cdn.libravatar.org/avatar/a0be0039b44d33262b7ae650a0803ad5"
+ subject="comment 2"
+ date="2017-04-06T02:14:58Z"
+ content="""
+I'll try that this weekend, thanks!
+"""]]
diff --git a/doc/forum/Using_propellor_for_continers_only/comment_3_cd4b9b9e160469e9f0b105f6c40a4ef8._comment b/doc/forum/Using_propellor_for_continers_only/comment_3_cd4b9b9e160469e9f0b105f6c40a4ef8._comment
new file mode 100644
index 00000000..fceeedcf
--- /dev/null
+++ b/doc/forum/Using_propellor_for_continers_only/comment_3_cd4b9b9e160469e9f0b105f6c40a4ef8._comment
@@ -0,0 +1,54 @@
+[[!comment format=mdwn
+ username="bardur.arantsson"
+ avatar="http://cdn.libravatar.org/avatar/a0be0039b44d33262b7ae650a0803ad5"
+ subject="comment 3"
+ date="2017-05-12T06:50:49Z"
+ content="""
+Ok, so I've tried to use this to build a Chroot (a reasonable starting point for building containers), using the following program:
+
+ module Main
+ ( main
+ ) where
+
+ import Propellor
+ import Propellor.Engine
+ import Propellor.Property.DiskImage
+ import qualified Propellor.Property.Apt as Apt
+ import qualified Propellor.Property.User as User
+ import Propellor.Property.Chroot
+
+ main :: IO ()
+ main = mainProperties $ host \"whatever\" $ props
+ & provisioned (mychroot \"out\")
+ where
+ mychroot d = debootstrapped mempty d $ props
+ & osDebian Unstable X86_64
+ & Apt.installed [\"linux-image-amd64\"]
+ & User.hasPassword (User \"root\")
+ & User.accountFor (User \"demo\")
+ & User.hasPassword (User \"demo\")
+
+It seems that \"debootstrap\" finishes:
+
+ I: Configuring apt-transport-https...
+ I: Configuring tasksel...
+ I: Configuring tasksel-data...
+ I: Configuring libc-bin...
+ I: Configuring systemd...
+ I: Configuring ca-certificates...
+ I: Base system installed successfully.
+
+But fails immediately afterwards:
+
+ ldd: /usr/local/propellor/propellor: No such file or directory
+ ** warning: user error (ldd [\"/usr/local/propellor/propellor\"] exited 1)
+ whatever chroot out exists ... failed
+ whatever overall ... failed
+
+(I should probably have used a different hostname than \"whatever\", but... whatever :).)
+
+So it seems that the chroot support still expects propellor to be installed on the host system?
+
+I should mention that I've done an extremely small patch to Propellor locally, just to the ChrootBootstrapper instance for ArchLinux to allow it to call debootstrap on Arch Linux -- it seems to exist as a package these days, not sure if it did when that Propellor code was written. Anyway...
+
+"""]]
diff --git a/doc/forum/Using_propellor_for_continers_only/comment_4_9dc985b26c29b9ce21e6c75ec03f6262._comment b/doc/forum/Using_propellor_for_continers_only/comment_4_9dc985b26c29b9ce21e6c75ec03f6262._comment
new file mode 100644
index 00000000..72d7ca83
--- /dev/null
+++ b/doc/forum/Using_propellor_for_continers_only/comment_4_9dc985b26c29b9ce21e6c75ec03f6262._comment
@@ -0,0 +1,21 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 4"""
+ date="2017-05-13T17:42:41Z"
+ content="""
+The way propellor handles running in a chroot or container is it exports
+its binary and support files into the container. This way the
+haskell code can run in a container, rather than being limited to
+only running shell commands in the container, and without needing ghc in
+the container.
+
+It does use the hardcoded `localdir` for that.
+It would certianly be possible to make it use propellor in a different
+location, perhaps using `getExecutablePath`.
+
+Since the git-annex outside the container passes command-line options to
+the one running inside the container to tell it what to do, using
+`mainProperties` would also not work since that does not look at
+command-line options. It would need to use `defaultMain` or
+`processCmdLine` and dispatch itself, or something..
+"""]]
diff --git a/doc/forum/Using_propellor_for_continers_only/comment_5_8552ce821f5a3b386cb9e6ad417670ec._comment b/doc/forum/Using_propellor_for_continers_only/comment_5_8552ce821f5a3b386cb9e6ad417670ec._comment
new file mode 100644
index 00000000..0d9904c5
--- /dev/null
+++ b/doc/forum/Using_propellor_for_continers_only/comment_5_8552ce821f5a3b386cb9e6ad417670ec._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="bardur.arantsson"
+ avatar="http://cdn.libravatar.org/avatar/a0be0039b44d33262b7ae650a0803ad5"
+ subject="comment 5"
+ date="2017-05-25T06:13:14Z"
+ content="""
+I'm not sure I understand all of that, but it sounds like I'll be fighting an uphill battle :). Maybe I'll try something shake-based instead. Thanks for the help.
+"""]]
diff --git a/doc/forum/Why_downloading_package_list_from_hackage.haskell.org__63__/comment_5_61d7ef8a61ac7b922c810825d794da5f._comment b/doc/forum/Why_downloading_package_list_from_hackage.haskell.org__63__/comment_5_61d7ef8a61ac7b922c810825d794da5f._comment
new file mode 100644
index 00000000..35c894b0
--- /dev/null
+++ b/doc/forum/Why_downloading_package_list_from_hackage.haskell.org__63__/comment_5_61d7ef8a61ac7b922c810825d794da5f._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="gueux"
+ avatar="http://cdn.libravatar.org/avatar/2982bac2c2cd94ab3860efb189deafc8"
+ subject="comment 5"
+ date="2017-07-14T10:58:33Z"
+ content="""
+The new \"bootstrapWith (Robustly Stack)\" and \"bootstrapWith OSOnly\" properties completely address my concerns. Thanks!
+"""]]
diff --git a/doc/forum/Why_downloading_package_list_from_hackage.haskell.org__63__/comment_6_ceddc6d118b7ea71ec8f498960a5fe97._comment b/doc/forum/Why_downloading_package_list_from_hackage.haskell.org__63__/comment_6_ceddc6d118b7ea71ec8f498960a5fe97._comment
new file mode 100644
index 00000000..32ed86f8
--- /dev/null
+++ b/doc/forum/Why_downloading_package_list_from_hackage.haskell.org__63__/comment_6_ceddc6d118b7ea71ec8f498960a5fe97._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="gueux"
+ avatar="http://cdn.libravatar.org/avatar/2982bac2c2cd94ab3860efb189deafc8"
+ subject="comment 6"
+ date="2017-07-14T11:16:10Z"
+ content="""
+(I did not try to build propellor again on this 128Mo host yet, though)
+"""]]
diff --git a/doc/forum/Work_on_OS_X.mdwn b/doc/forum/Work_on_OS_X.mdwn
new file mode 100644
index 00000000..e3c5fd64
--- /dev/null
+++ b/doc/forum/Work_on_OS_X.mdwn
@@ -0,0 +1,5 @@
+I'm interested in using Propellor on OS X. I understand that it is not supported though.
+
+Is there anyone doing this? If it was developed, would support for OS X be merged upstream?
+
+Thanks!
diff --git a/doc/forum/Work_on_OS_X/comment_1_6d7d5b89f1de9604718f7973e4b3eeb1._comment b/doc/forum/Work_on_OS_X/comment_1_6d7d5b89f1de9604718f7973e4b3eeb1._comment
new file mode 100644
index 00000000..4eac2063
--- /dev/null
+++ b/doc/forum/Work_on_OS_X/comment_1_6d7d5b89f1de9604718f7973e4b3eeb1._comment
@@ -0,0 +1,20 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2017-04-13T21:36:20Z"
+ content="""
+I got a patch some years back to make propellor compile on OSX.
+I merged it. You might want to get in touch with its author, as
+he may be doing something with propellor on OSX.
+<https://github.com/tittoassini/propellor>
+
+Anyway, I'd probably merge OSX patches, if they were not super
+intrusive. And I don't see why it would be, as propellor already supports
+FreeBSD.
+
+Since `Property` is parameterized by the operating systems it
+supports, it should be easy to start by only porting the core parts
+of propellor, and then port over individual Properties one by one as
+needed. See the commits for the recent FreeBSD port for a nice walkthough
+of the changes you'll want to make.
+"""]]
diff --git a/doc/forum/Work_on_OS_X/comment_2_00b20c240fc13bed6dc54e5b985b41e2._comment b/doc/forum/Work_on_OS_X/comment_2_00b20c240fc13bed6dc54e5b985b41e2._comment
new file mode 100644
index 00000000..aa33c85b
--- /dev/null
+++ b/doc/forum/Work_on_OS_X/comment_2_00b20c240fc13bed6dc54e5b985b41e2._comment
@@ -0,0 +1,17 @@
+[[!comment format=mdwn
+ username="joelmccracken"
+ avatar="http://cdn.libravatar.org/avatar/45175015b9eb3dd3f6c740b3fe920fed"
+ subject="comment 2"
+ date="2017-04-17T17:47:30Z"
+ content="""
+Sounds good. I contacted the person you linked to, have not heard back yet.
+
+
+
+The first issue I ran into is that propellor wants to connect to \"root@<hostname>\", and it doesn't look like this is configurable.
+Would you accept a patch to make this configurable?
+
+Additionally, is this the best place to ask questions about what you would/would not accept?
+
+Thank you!!!
+"""]]
diff --git a/doc/forum/Work_on_OS_X/comment_3_294f4783522a8e4887793aac921ee546._comment b/doc/forum/Work_on_OS_X/comment_3_294f4783522a8e4887793aac921ee546._comment
new file mode 100644
index 00000000..ed654d3f
--- /dev/null
+++ b/doc/forum/Work_on_OS_X/comment_3_294f4783522a8e4887793aac921ee546._comment
@@ -0,0 +1,14 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 3"""
+ date="2017-04-18T00:08:13Z"
+ content="""
+Yes, this is the place. Or you can email me directly, but I prefer to keep
+discussions public.
+
+`propellor --spin` needs a way to run commands as root on the remote host.
+If ssh as root on OSX is not allowed, it would need a way to get to a user
+who can get root, and it would be very annoying if a password needed to be
+entered since each `propellor --spin` actually makes several ssh connections to
+the remote host. Anything that works within these constraints would be ok.
+"""]]
diff --git a/doc/forum/Work_on_OS_X/comment_4_74b579d4d590432b6bd236ccb929cc11._comment b/doc/forum/Work_on_OS_X/comment_4_74b579d4d590432b6bd236ccb929cc11._comment
new file mode 100644
index 00000000..d386c1b5
--- /dev/null
+++ b/doc/forum/Work_on_OS_X/comment_4_74b579d4d590432b6bd236ccb929cc11._comment
@@ -0,0 +1,16 @@
+[[!comment format=mdwn
+ username="joelmccracken"
+ avatar="http://cdn.libravatar.org/avatar/45175015b9eb3dd3f6c740b3fe920fed"
+ subject="comment 4"
+ date="2017-04-20T02:23:06Z"
+ content="""
+So, it turns out that yes, root is a thing on os x... but it is complicated. I'm going to put what I learned here because I think it will be useful, at least for telling folks how to use propellor on os x.
+
+1. Enable the root account. Steps are here: https://support.apple.com/en-us/HT204012
+2. password-authentication as root is disabled -- if you try to `ssh root@localhost`, it wont work. you need a key pair.
+3. use su/sudo to install a public key (probably at `.ssh/id_rsa.pub`) to roots authorized_keys. adapted from: https://discussions.apple.com/thread/4078360?start=0&tstart=0
+4. copy the the pub file to authorized keys: `sudo cp /Users/joel/.ssh/id_rsa.pub /var/root/.ssh/authorized_keys`
+5. you should now be able to `ssh root@localhost` without a password.
+
+I'm not super sure that this is even the best way forward, but lets get this working first, then we'll see.
+"""]]
diff --git a/doc/forum/creating_Bind9_configuration.mdwn b/doc/forum/creating_Bind9_configuration.mdwn
new file mode 100644
index 00000000..5e281394
--- /dev/null
+++ b/doc/forum/creating_Bind9_configuration.mdwn
@@ -0,0 +1,9 @@
+I try to use propellor to deploy a secondary DNS server.
+
+In your configuration, I see nothing to change the `listen-on { 127.0.0.1; };` option, did I miss something?
+
+Also, in `Dns.secondaryFor`, I do not know how to set `confLines` to something else, should I use this function and peel the result until I can change this or shoud I add a `Dns.secondaryFor'` version with an extra argument?
+
+By the way, is it really advisable to use a "minimal config" instead of a full clone?
+
+Thanks!
diff --git a/doc/forum/creating_Bind9_configuration/comment_1_0798f44e1f5a91fbc91c0b472ad92bfa._comment b/doc/forum/creating_Bind9_configuration/comment_1_0798f44e1f5a91fbc91c0b472ad92bfa._comment
new file mode 100644
index 00000000..d1387a22
--- /dev/null
+++ b/doc/forum/creating_Bind9_configuration/comment_1_0798f44e1f5a91fbc91c0b472ad92bfa._comment
@@ -0,0 +1,29 @@
+[[!comment format=mdwn
+ username="Nicolas.Schodet"
+ avatar="http://cdn.libravatar.org/avatar/0d7ec808ec329d04ee9a93c0da3c0089"
+ subject="comment 1"
+ date="2017-08-03T20:52:22Z"
+ content="""
+For the moment I use:
+
+```
+namedOptions :: Property DebianLike
+namedOptions =
+ File.hasContent \"/etc/bind/named.conf.options\" namedOptionsStanza
+ `onChange` Service.reloaded \"bind9\"
+ where
+ namedOptionsStanza =
+ [ \"// automatically generated by propellor\"
+ , \"options {\"
+ , \"\tdirectory \\"/var/cache/bind\\";\"
+ , \"\tdnssec-validation auto;\"
+ , \"\tlisten-on-v6 { any; };\"
+ , \"\tlisten-on { any; };\"
+ , \"\tallow-query { any; };\"
+ , \"\tallow-recursion { localhost; };\"
+ , \"\tallow-transfer { none; };\"
+ , \"\tallow-notify { none; };\"
+ , \"};\"
+ ]
+```
+"""]]
diff --git a/doc/forum/creating_Bind9_configuration/comment_2_f1bffbdd7c2ebab2dd9518ee024e7a92._comment b/doc/forum/creating_Bind9_configuration/comment_2_f1bffbdd7c2ebab2dd9518ee024e7a92._comment
new file mode 100644
index 00000000..71c8b5a4
--- /dev/null
+++ b/doc/forum/creating_Bind9_configuration/comment_2_f1bffbdd7c2ebab2dd9518ee024e7a92._comment
@@ -0,0 +1,18 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 2"""
+ date="2017-08-23T16:00:12Z"
+ content="""
+At least on Debian, bind seems to come configured to listen on all
+interfaces by default, so I have not messed with listen-on settings at all.
+
+confLines seems to have been included in NamedConf to allow for specifying
+additional lines, but there does not seem to be an interface to set it.
+Versions of the 3 dns properties with an additional (NamedConf -> NamedConf)
+parameter woulld be one way; I'd take such a patch.
+
+As to a minimal config vs a full clone, it's up to you. With a full clone
+you can easily modify all of propellor's properties to quicklly deal with
+issues like this.. but then you might have to maintain your patches if you
+don't get them accepted into propellor.
+"""]]
diff --git a/doc/forum/creating_Bind9_configuration/comment_3_6b4d73b17d87d00845fda26431ded422._comment b/doc/forum/creating_Bind9_configuration/comment_3_6b4d73b17d87d00845fda26431ded422._comment
new file mode 100644
index 00000000..c61feaab
--- /dev/null
+++ b/doc/forum/creating_Bind9_configuration/comment_3_6b4d73b17d87d00845fda26431ded422._comment
@@ -0,0 +1,10 @@
+[[!comment format=mdwn
+ username="Nicolas.Schodet"
+ avatar="http://cdn.libravatar.org/avatar/0d7ec808ec329d04ee9a93c0da3c0089"
+ subject="comment 3"
+ date="2017-08-28T14:03:35Z"
+ content="""
+It might be a configuration from my server provider, maybe I should do a clean install :)
+
+If not using a full clone, I also have problem because I cannot use things like Utility.Units.
+"""]]
diff --git a/doc/forum/host_to_deal_with_dpkg::options.mdwn b/doc/forum/host_to_deal_with_dpkg::options.mdwn
new file mode 100644
index 00000000..5faaefe2
--- /dev/null
+++ b/doc/forum/host_to_deal_with_dpkg::options.mdwn
@@ -0,0 +1,41 @@
+[[!meta title "how to deal with dpkg::options"]]
+
+Hello
+
+I try to create a distUpgrade property in order to migrate one of my computer from jessie -> stretch
+
+I started wit this
+
+ distUpgrade :: String -> Property DebianLike
+ distUpgrade p = combineProperties ("apt " ++ p) $ props
+ & Apt.pendingConfigured
+ & Apt.runApt ["-y", "--force-yes", "-o", "Dpkg::Options::=\"--force-confnew\"", p]
+ `assume` MadeChange
+
+But when I try to use this
+
+ ...
+ & distUpgrade dist-upgrade
+
+ I get this error message
+
+ Préconfiguration des paquets...
+ setting xserver-xorg-legacy/xwrapper/allowed_users from configuration file
+ dpkg: erreur: requiert une option d'action
+
+ Utilisez « dpkg --help » pour obtenir de l'aide à propos de l'installation et la désinstallation des paquets [*] ;
+ Utilisez « apt » ou « aptitude » pour gérer les paquets de m1578 mis à jour, 376 nouvellement installés, 72 à enlever et 0 non mis à jour.
+ Il est nécessaire de prendre 0 o/1 458 Mo dans les archives.
+
+I checked that if I run this command on the command line it works
+
+ apt-get -y --force-yes -o Dpkg::Options::="--force-confnew" dist-upgrade
+
+even If I write this it works
+
+ apt-get -y --force-yes -o Dpkg::Options::=\"--force-confnew\" dist-upgrade
+
+So it seems to me that there is a problem with the runApt method or I missed something
+
+thanks
+
diff --git a/doc/forum/host_to_deal_with_dpkg::options/comment_1_641dcb7be62151bdc97fd5e574f334d0._comment b/doc/forum/host_to_deal_with_dpkg::options/comment_1_641dcb7be62151bdc97fd5e574f334d0._comment
new file mode 100644
index 00000000..65756b12
--- /dev/null
+++ b/doc/forum/host_to_deal_with_dpkg::options/comment_1_641dcb7be62151bdc97fd5e574f334d0._comment
@@ -0,0 +1,12 @@
+[[!comment format=mdwn
+ username="picca"
+ avatar="http://cdn.libravatar.org/avatar/7e61c80d28018b10d31f6db7dddb864c"
+ subject="comment 1"
+ date="2017-07-28T15:09:12Z"
+ content="""
+please change the title, I made a mistake
+
+how to deal with ...
+
+sorry
+"""]]
diff --git a/doc/forum/host_to_deal_with_dpkg::options/comment_2_bac8129b570ce216ef9f6aa6c0e12c1e._comment b/doc/forum/host_to_deal_with_dpkg::options/comment_2_bac8129b570ce216ef9f6aa6c0e12c1e._comment
new file mode 100644
index 00000000..39e0ebc3
--- /dev/null
+++ b/doc/forum/host_to_deal_with_dpkg::options/comment_2_bac8129b570ce216ef9f6aa6c0e12c1e._comment
@@ -0,0 +1,9 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 2"""
+ date="2017-07-28T15:45:43Z"
+ content="""
+I doubt that apt's option parser deals with quotes; those are normally
+handled by the shell. runApt does not pass the command through the shell,
+so probably simply removing the quotes from inside the parameter will work.
+"""]]
diff --git a/doc/forum/host_to_deal_with_dpkg::options/comment_3_62d671fb3c787aafcd4d058975208f75._comment b/doc/forum/host_to_deal_with_dpkg::options/comment_3_62d671fb3c787aafcd4d058975208f75._comment
new file mode 100644
index 00000000..4031bd16
--- /dev/null
+++ b/doc/forum/host_to_deal_with_dpkg::options/comment_3_62d671fb3c787aafcd4d058975208f75._comment
@@ -0,0 +1,10 @@
+[[!comment format=mdwn
+ username="picca"
+ avatar="http://cdn.libravatar.org/avatar/7e61c80d28018b10d31f6db7dddb864c"
+ subject="comment 3"
+ date="2017-07-28T15:53:03Z"
+ content="""
+Great it works
+
+thanks a lot
+"""]]
diff --git a/doc/forum/propellor_4.7.6_does_not_compile_on_jessie.mdwn b/doc/forum/propellor_4.7.6_does_not_compile_on_jessie.mdwn
new file mode 100644
index 00000000..b3e6f7c7
--- /dev/null
+++ b/doc/forum/propellor_4.7.6_does_not_compile_on_jessie.mdwn
@@ -0,0 +1,32 @@
+Hello here the error message I got while trying to compile on jessie
+
+ [ 91 of 113] Compiling Propellor.Bootstrap ( src/Propellor/Bootstrap.hs, dist/build/propellor-config/propellor-config-tmp/Propellor/Bootstrap.o ) src/Propellor/Bootstrap.hs:239:22:
+ No instance for (Typeable Bootstrapper)
+ arising from a use of `fromInfo'
+ Possible fix:
+ add an instance declaration for (Typeable Bootstrapper)
+ In the expression: fromInfo (maybe mempty hostInfo mh)
+ In a stmt of a 'do' block:
+ case fromInfo (maybe mempty hostInfo mh) of {
+ NoInfoVal
+ -> do { bs <- getGitConfigValue "propellor.buildsystem";
+ case bs of {
+ Just "stack" -> ...
+ _ -> ... } }
+ InfoVal bs
+ -> case getBuilder bs of {
+ Cabal -> cabalBuild msys
+ Stack -> stackBuild msys } }
+ In the second argument of `($)', namely
+ `do { case fromInfo (maybe mempty hostInfo mh) of {
+ NoInfoVal -> do { ... }
+ InfoVal bs
+ -> case getBuilder bs of {
+ Cabal -> ...
+ Stack -> ... } } }'
+ Warning: The package list for 'hackage.haskell.org' does not exist. Run 'cabal
+ update' to download it.
+ Resolving dependencies...
+ Configuring propellor-4.7.6...
+
+Cheers
diff --git a/doc/forum/propellor_4.7.6_does_not_compile_on_jessie/comment_1_c35f458b4c958f6397fe726f5676b700._comment b/doc/forum/propellor_4.7.6_does_not_compile_on_jessie/comment_1_c35f458b4c958f6397fe726f5676b700._comment
new file mode 100644
index 00000000..98b2d00a
--- /dev/null
+++ b/doc/forum/propellor_4.7.6_does_not_compile_on_jessie/comment_1_c35f458b4c958f6397fe726f5676b700._comment
@@ -0,0 +1,7 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2017-08-23T15:41:55Z"
+ content="""
+I've added a typeable instance for Bootstrapper which should fix that.
+"""]]
diff --git a/doc/forum/propellor_and_gpg2.mdwn b/doc/forum/propellor_and_gpg2.mdwn
new file mode 100644
index 00000000..d78de741
--- /dev/null
+++ b/doc/forum/propellor_and_gpg2.mdwn
@@ -0,0 +1,14 @@
+I had a problem similar to [[Key sign problem]]. Maybe in that case the fix was easy, just supplying the secret key.
+
+In my case this was a fresh install into a new Debian/sid system (so gpg2 is the default) and the failure happened during the propellor --init following the directions in the quick start at <https://propellor.branchable.com/>. During the --init I selected to create a gpg key. The message, after finally getting enough entropy and creating the gpg key, was:
+ error:gpg failed to sign the data
+ fatal: failed to write commit object
+
+So it was frustrating that propellor didn't work out of the box and there were no hints what was wrong with signing commits in git (the error above is from git and doing git commit -S was enough to reproduce it).
+
+The issue has to do with prompting for a passphrase in gpg2. If the agent is running and $GPG_TTY is set correctly you get a prompt and things will work. I was able to convince myself that if the agent wasn't running it would cause this error but it seems that gpg2 requires the agent and automatically starts it so I'm not sure how I managed that.
+
+Initially I was trying propellor before I installed a desktop so I don't know what I had for the gpg agent or how it should have been prompting. There doesn't seem to be much help out there on gpg2 + git failures but I'll keep looking.
+
+Dave
+
diff --git a/doc/forum/propellor_and_gpg2/comment_1_4b732110f59f78f73fdfb745bdd9c0dd._comment b/doc/forum/propellor_and_gpg2/comment_1_4b732110f59f78f73fdfb745bdd9c0dd._comment
new file mode 100644
index 00000000..66c4cffa
--- /dev/null
+++ b/doc/forum/propellor_and_gpg2/comment_1_4b732110f59f78f73fdfb745bdd9c0dd._comment
@@ -0,0 +1,13 @@
+[[!comment format=mdwn
+ username="anselmi@0a9758305bef5e058dd0263fa20a27b334b482c7"
+ nickname="anselmi"
+ avatar="http://cdn.libravatar.org/avatar/65b723eb35eb4e3b05fffafd3e13e0fd"
+ subject="Cache gpg passphrase."
+ date="2016-12-22T17:23:58Z"
+ content="""
+The bottom line on this is that gpg2 (via the agent and pinentry) doesn't prompt correctly when run from git. It does when run directly.
+
+One fix is to set GPG_TTY before running propellor: `export GPG_TTY=$(tty)` or some such.
+
+Anything else that caches the pass phrase in the agent works too since that removes the need to prompt.
+"""]]
diff --git a/doc/forum/propellor_failed_to_sign_the_commit.mdwn b/doc/forum/propellor_failed_to_sign_the_commit.mdwn
new file mode 100644
index 00000000..83a4fd44
--- /dev/null
+++ b/doc/forum/propellor_failed_to_sign_the_commit.mdwn
@@ -0,0 +1,30 @@
+Hello since sometime on my computer gpgv1 -> gpgv2 transition on Debian
+
+I get this error message. (I need to say that I am using a NitroKey Pro for my gpg keys)
+
+ Propellor build ... done
+ error: gpg n'a pas pu signer les données
+ fatal: échec de l'écriture de l'objet commit
+ Git commit ... failed
+
+reading this bug report
+
+ https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=568375
+
+Ifound that I need to define
+
+
+ https://www.gnupg.org/documentation/manuals/gnupg/Common-Problems.html
+
+ The gpg-agent man page nowadays includes the following hint:
+
+ It is important to set the GPG_TTY environment variable in your login
+ shell, for example in the ‘~/.bashrc’ init script:
+
+ export GPG_TTY=$(tty)
+
+don't you think that propellor should define GPG_TTY in order to avoid this problem ?
+
+thanks
+
+Frederic
diff --git a/doc/forum/propellor_failed_to_sign_the_commit/comment_1_c1dab7554841bd88d2109e9d46b31102._comment b/doc/forum/propellor_failed_to_sign_the_commit/comment_1_c1dab7554841bd88d2109e9d46b31102._comment
new file mode 100644
index 00000000..2d2315c0
--- /dev/null
+++ b/doc/forum/propellor_failed_to_sign_the_commit/comment_1_c1dab7554841bd88d2109e9d46b31102._comment
@@ -0,0 +1,30 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2017-07-30T14:51:13Z"
+ content="""
+I guess the problem involves running propellor at a unix tty, not in a
+GUI's virtual terminal?
+
+My limited understanding of `GPG_TTY`, refreshed by re-reading this ooold
+thread <https://bugs.debian.org/316388> is that gpg is normally able to
+detect if it's in a GUI or at a tty, and will prompt in the tty if
+necessary. Where that may fall down is when gpg is run with its stdio
+connected to pipes, since then probably isatty fails. Although in at least
+some cases, gpg apparently then
+[falls back to /dev/tty](https://dev.gnupg.org/T1434).
+
+Propellor runs gpg with stdin and stdout piped to it when eg, decrypting
+the privdata file. I tried `propellor --list-fields` at the linux console
+and it fails there.
+
+But, when I tried `propellor --spin host` at the linux console, that worked
+ok, including making the gpg signed git commit. Of course git is running
+gpg in this case, and perhaps my version of git has its own way to avoid
+this problem.
+
+This does seems like something propellor could work around fairly
+inexpensively.
+
+(See also [[propellor_and_gpg2]].)
+"""]]
diff --git a/doc/forum/propellor_failed_to_sign_the_commit/comment_2_21ff16e0871e7069749cd6c47a6fc8fe._comment b/doc/forum/propellor_failed_to_sign_the_commit/comment_2_21ff16e0871e7069749cd6c47a6fc8fe._comment
new file mode 100644
index 00000000..41120706
--- /dev/null
+++ b/doc/forum/propellor_failed_to_sign_the_commit/comment_2_21ff16e0871e7069749cd6c47a6fc8fe._comment
@@ -0,0 +1,9 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 2"""
+ date="2017-07-30T15:15:45Z"
+ content="""
+It seems that setting `GPG_TTY` does not force gpg to prompt at a tty
+when in a GUI. At least in X with gpg 2.1, I still get a GUI prompt from
+gpg. Good.
+"""]]
diff --git a/doc/forum/propellor_failed_to_sign_the_commit/comment_3_f0e087ed1a80f42d11d34fb215183205._comment b/doc/forum/propellor_failed_to_sign_the_commit/comment_3_f0e087ed1a80f42d11d34fb215183205._comment
new file mode 100644
index 00000000..ae750878
--- /dev/null
+++ b/doc/forum/propellor_failed_to_sign_the_commit/comment_3_f0e087ed1a80f42d11d34fb215183205._comment
@@ -0,0 +1,11 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 3"""
+ date="2017-07-30T15:33:02Z"
+ content="""
+I've made propellor set `GPG_TTY` and verified that this lets gpg prompt
+for the password at the linux console.
+
+Since I was not able to reproduce git commit signing not working, I don't
+know for sure that this fixed that, but imagine it probably would.
+"""]]
diff --git a/doc/haskell_newbie.mdwn b/doc/haskell_newbie.mdwn
index d6e339ed..8f3b60cf 100644
--- a/doc/haskell_newbie.mdwn
+++ b/doc/haskell_newbie.mdwn
@@ -47,13 +47,13 @@ Finally, you need to define the configuration for each host in the list:
[[!format haskell """
mylaptop :: Host
-mylaptop = host "mylaptop.example.com"
+mylaptop = host "mylaptop.example.com" $ props
& osDebian Unstable X86_64
& Apt.stdSourcesList
myserver :: Host
-myserver = host "server.example.com"
- & osDebian (Stable "jessie") X86_64
+myserver = host "server.example.com" $ props
+ & osDebian (Stable "stretch") X86_64
& Apt.stdSourcesList
& Apt.installed ["ssh"]
"""]]
diff --git a/doc/index.mdwn b/doc/index.mdwn
index 52c23021..1e3af9dd 100644
--- a/doc/index.mdwn
+++ b/doc/index.mdwn
@@ -1,7 +1,7 @@
[[!meta title="propellor: deploying properties to hosts with haskell"]]
[[!sidebar content="""
-[[Install]]
+[[Download]]
[API documentation](http://hackage.haskell.org/package/propellor)
[[Other Documentation|documentation]]
[Sample config file](http://git.joeyh.name/?p=propellor.git;a=blob;f=joeyconfig.hs)
diff --git a/doc/install.mdwn b/doc/install.mdwn
deleted file mode 100644
index ad87cedc..00000000
--- a/doc/install.mdwn
+++ /dev/null
@@ -1,4 +0,0 @@
-`git clone git://propellor.branchable.com/ propellor`
-Or get it [from github](https://github.com/joeyh/propellor).
-
-Propellor is recently available in Debian.
diff --git a/doc/news/version_3.1.1.mdwn b/doc/news/version_3.1.1.mdwn
deleted file mode 100644
index b6ef29cf..00000000
--- a/doc/news/version_3.1.1.mdwn
+++ /dev/null
@@ -1,4 +0,0 @@
-propellor 3.1.1 released with [[!toggle text="these changes"]]
-[[!toggleable text="""
- * Haddock build fix.
- Thanks, Sean Whitton"""]] \ No newline at end of file
diff --git a/doc/news/version_3.1.2.mdwn b/doc/news/version_3.1.2.mdwn
deleted file mode 100644
index b54b396a..00000000
--- a/doc/news/version_3.1.2.mdwn
+++ /dev/null
@@ -1,22 +0,0 @@
-propellor 3.1.2 released with [[!toggle text="these changes"]]
-[[!toggleable text="""
- * [ Joey Hess ]
- * Ssh.knownHost: Bug fix: Only fix up the owner of the known\_hosts
- file after it exists.
- * [ Sean Whitton ]
- * Sbuild.keypairInsecurelyGenerated: Improved to be more robust.
- * Pass --allow-unrelated-histories to git merge when run with git 2.9 or
- newer. This fixes the /usr/bin/propellor wrapper with this version of git.
- * Sbuild.built &amp; Sbuild.builtFor no longer require Sbuild.keypairGenerated.
- Transition guide: If you are using sbuild 0.70.0 or newer, you should
- `rm -r /var/lib/sbuild/apt-keys`. Otherwise, you should add either
- Sbuild.keypairGenerated or Sbuild.keypairInsecurelyGenerated to your host.
- * Sbuild haddock improvements:
- - State that we don't support squeeze and Buntish older than trusty.
- This is due to our enhancements, such as eatmydata.
- - State that you need sbuild 0.70.0 or newer to build for stretch.
- This is due to gpg2 hitting Debian stretch.
- - Explain when a keygen is required.
- - Update sample ~/.sbuildrc for sbuild 0.71.0.
- - Add hint for customising chroots with propellor.
- - Update example usage of System type."""]] \ No newline at end of file
diff --git a/doc/news/version_3.2.0.mdwn b/doc/news/version_3.2.0.mdwn
deleted file mode 100644
index bef06b1b..00000000
--- a/doc/news/version_3.2.0.mdwn
+++ /dev/null
@@ -1,17 +0,0 @@
-propellor 3.2.0 released with [[!toggle text="these changes"]]
-[[!toggleable text="""
- * [ Sean Whitton ]
- * Using ccache with Sbuild.built &amp; Sbuild.builtFor is now toggleable: these
- properties now take a parameter of type Sbuild.UseCcache. (API Change)
- * Sbuild.piupartsConf: no longer takes an Apt.Url. (API Change)
- * Sbuild.piupartsConf &amp; Sbuild.piupartsConfFor: does nothing if corresponding
- schroot not built.
- Previously, these properties built the schroot if it was missing.
- * Sbuild.built &amp; Sbuild.piupartsConf: add an additional alias to sid chroots.
- This is for compatibility with `dgit sbuild`.
- * Further improvements to Sbuild.hs haddock.
- * [ Joey Hess ]
- * Tor.hiddenService: Converted port parameter from Int to Port. (API change)
- * Tor.hiddenServiceAvailable: The hidden service hostname file may not
- be available immedaitely after configuring tor; avoid ugly error in
- this case."""]] \ No newline at end of file
diff --git a/doc/news/version_3.2.1.mdwn b/doc/news/version_3.2.1.mdwn
deleted file mode 100644
index 214ef427..00000000
--- a/doc/news/version_3.2.1.mdwn
+++ /dev/null
@@ -1,5 +0,0 @@
-propellor 3.2.1 released with [[!toggle text="these changes"]]
-[[!toggleable text="""
- * Simplify Debootstrap.sourceInstall since #770217 was fixed.
- * Debootstap.installed: Fix inverted logic that made this never install
- debootstrap. Thanks, mithrandi."""]] \ No newline at end of file
diff --git a/doc/news/version_3.2.2.mdwn b/doc/news/version_3.2.2.mdwn
deleted file mode 100644
index 19acc9f7..00000000
--- a/doc/news/version_3.2.2.mdwn
+++ /dev/null
@@ -1,5 +0,0 @@
-propellor 3.2.2 released with [[!toggle text="these changes"]]
-[[!toggleable text="""
- * Added Linode.serialGrub property.
- * Clean up build warnings about redundant constraints when built with ghc 8.0.
- * Added Group.hasUser property. Thanks, Daniel Brooks"""]] \ No newline at end of file
diff --git a/doc/news/version_4.7.3.mdwn b/doc/news/version_4.7.3.mdwn
new file mode 100644
index 00000000..87c58e81
--- /dev/null
+++ b/doc/news/version_4.7.3.mdwn
@@ -0,0 +1,3 @@
+propellor 4.7.3 released with [[!toggle text="these changes"]]
+[[!toggleable text="""
+ * Expand the Trace data type."""]] \ No newline at end of file
diff --git a/doc/news/version_4.7.4.mdwn b/doc/news/version_4.7.4.mdwn
new file mode 100644
index 00000000..982f34b6
--- /dev/null
+++ b/doc/news/version_4.7.4.mdwn
@@ -0,0 +1,7 @@
+propellor 4.7.4 released with [[!toggle text="these changes"]]
+[[!toggleable text="""
+ * Set GPG\_TTY when run at a terminal, so that gpg can do password
+ prompting despite being connected by pipes to propellor (or git).
+ * Rsync: Make rsync display less verbose.
+ * Improve PROPELLOR\_TRACE output so serialized trace values always
+ come on their own line, not mixed with title setting."""]] \ No newline at end of file
diff --git a/doc/news/version_4.7.5.mdwn b/doc/news/version_4.7.5.mdwn
new file mode 100644
index 00000000..f2fbaf84
--- /dev/null
+++ b/doc/news/version_4.7.5.mdwn
@@ -0,0 +1,3 @@
+propellor 4.7.5 released with [[!toggle text="these changes"]]
+[[!toggleable text="""
+ * Avoid crashing when getTerminalName fails due to eg, being in a chroot."""]] \ No newline at end of file
diff --git a/doc/news/version_4.7.6.mdwn b/doc/news/version_4.7.6.mdwn
new file mode 100644
index 00000000..4c8abd97
--- /dev/null
+++ b/doc/news/version_4.7.6.mdwn
@@ -0,0 +1,6 @@
+propellor 4.7.6 released with [[!toggle text="these changes"]]
+[[!toggleable text="""
+ * Sbuild: Add Sbuild.userConfig property.
+ Thanks, Sean Whitton
+ * Locale: Make sure that the locales package is installed when enabling
+ locales."""]] \ No newline at end of file
diff --git a/doc/news/version_4.7.7.mdwn b/doc/news/version_4.7.7.mdwn
new file mode 100644
index 00000000..258f0f23
--- /dev/null
+++ b/doc/news/version_4.7.7.mdwn
@@ -0,0 +1,11 @@
+propellor 4.7.7 released with [[!toggle text="these changes"]]
+[[!toggleable text="""
+ * Locale: Display an error message when /etc/locale.gen does not contain
+ the requested locale.
+ * Attic module is deprecated and will warn when used.
+ Attic is no longer available in Debian and appears to have been
+ mostly supersceded by Borg.
+ * Obnam module is deprecated and will warn when used.
+ Obnam has been retired by its author.
+ * Add Typeable instance to Bootstrapper, fixing build with old versions
+ of ghc. (Previous attempt was incomplete.)"""]] \ No newline at end of file
diff --git a/doc/todo/Add_MonadBaseControl_instance_to_Propellor.mdwn b/doc/todo/Add_MonadBaseControl_instance_to_Propellor.mdwn
new file mode 100644
index 00000000..e044e4d9
--- /dev/null
+++ b/doc/todo/Add_MonadBaseControl_instance_to_Propellor.mdwn
@@ -0,0 +1,3 @@
+I had a specific use-case that ensures a property while using a Consul session via the [consul-haskell package](https://hackage.haskell.org/package/consul-haskell-0.4/docs/Network-Consul.html#v:withSession); in order to make it type check a MonadBaseControl IO instance is needed, so I added one. Hopefully this is generally useful, so I don't need to maintain a forked version of propellor!
+
+Patch is located in the `MonadBaseControl` branch of my cloned git repo `git clone git@github.com:hellertime/propellor.git`
diff --git a/doc/todo/Add_MonadBaseControl_instance_to_Propellor/comment_1_4b0cd7acc6442210a80c547981b5ae45._comment b/doc/todo/Add_MonadBaseControl_instance_to_Propellor/comment_1_4b0cd7acc6442210a80c547981b5ae45._comment
new file mode 100644
index 00000000..b38a015f
--- /dev/null
+++ b/doc/todo/Add_MonadBaseControl_instance_to_Propellor/comment_1_4b0cd7acc6442210a80c547981b5ae45._comment
@@ -0,0 +1,18 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2016-11-30T21:07:26Z"
+ content="""
+I'm not entirely opposed to it, but this does add another two
+dependencies that have to be installed on every host managed by propellor.
+
+Also, I don't really understand the instance MonadBaseControl
+implementation. (And have always had that difficulty with
+monad-control, which is one of the reasons I've stopped using it.)
+This and not having anything to test it with makes me fear maintaining it.
+
+It looks like it would be sufficient make Propellor derive MonadBase IO,
+and then the MonadBaseControl instance could be shipped in another
+package (or even implemented in your config.hs). Does that sound like a
+reasonable compromise?
+"""]]
diff --git a/doc/todo/Add_MonadBaseControl_instance_to_Propellor/comment_2_60d6e06ebada37648df77442733e325f._comment b/doc/todo/Add_MonadBaseControl_instance_to_Propellor/comment_2_60d6e06ebada37648df77442733e325f._comment
new file mode 100644
index 00000000..3233340c
--- /dev/null
+++ b/doc/todo/Add_MonadBaseControl_instance_to_Propellor/comment_2_60d6e06ebada37648df77442733e325f._comment
@@ -0,0 +1,24 @@
+[[!comment format=mdwn
+ username="chris"
+ subject="""comment 2"""
+ date="2016-12-01T18:14:10Z"
+ content="""
+Agree on all points. I would rather not add the dependencies to propellor
+proper either, but such was the requirement for this change. I'd be happy
+enough with the MonadBase IO derivation and implementing this externally,
+no argument here.
+
+As for what it does :) I cribbed the implementation from the Snap server (
+https://github.com/snapframework/snap/blob/
+bda15d0a0f29b0107fd69fbb8b7e8cc5ce5fa7e4/src/Snap/Snaplet/Internal/Types.hs#
+L277),
+and it seems to work, essentially it is a way to take the outer
+transformer, and wrap it inside the inner Monad, but in such a way that the
+inner Monad now has access to the outer transformer !? Yeah, I'm still not
+fully grokking it myself, but it type checks and functions.
+
+Anyway feel free to implement at your leisure, it does seem that I could
+even derive the MonadBase IO instance manually and not have to change
+Propellor in the least, though the auto-derived instance would seem like a
+simple and harmless addition.
+"""]]
diff --git a/doc/todo/Add_MonadBaseControl_instance_to_Propellor/comment_3_45413e6e811c34edc38a6ff70ca7c208._comment b/doc/todo/Add_MonadBaseControl_instance_to_Propellor/comment_3_45413e6e811c34edc38a6ff70ca7c208._comment
new file mode 100644
index 00000000..74a5c8bb
--- /dev/null
+++ b/doc/todo/Add_MonadBaseControl_instance_to_Propellor/comment_3_45413e6e811c34edc38a6ff70ca7c208._comment
@@ -0,0 +1,50 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 3"""
+ date="2016-12-01T18:14:28Z"
+ content="""
+Looking at the lifted-async that is what uses the MonadBaseControl instance
+in your use case, I have some concerns.
+
+Its docs say "All the functions restore the monadic effects in the forked
+computation unless specified otherwise." I think that has bearing on the
+following situation:
+
+Suppose that two Propellor monad actions are run concurrently by this:
+
+ foo `concurrently` bar
+
+Propellor's monad includes a Writer component, that accumulates [EndAction].
+Since they are running concurrently, it seems likely that `foo` and `bar`
+are using separate Writers. Propellor doesn't currently use a State monad,
+but suppose that was added to its stack. Then `foo` and `bar` would
+necessarily, I think, be manipulating independent copies of state.
+
+Now, what happens when `concurrently` finishes running them? We have two
+Writers and/or two States, that need to be merged somehow. I don't see
+anything in the library that lets it do an intelligent merge. (For example,
+it could notice that [EndAction] is a monoid and mappend the two values.)
+
+So, I think when it says it's a restoring the monadic effects, it means it's
+*discarding* any changes that might have been made to the Writer or State.
+
+Is this a large problem for Propellor? Maybe not. EndActions rarely need to
+be added, and in fact only one property in all of Propellor currently adds
+an EndAction. But this could change; Propellor could get state in its
+monad. What then?
+
+Now, I actually dealt with this problem in the
+Propellor.Property.Concurrent module. The code there threads the Writer
+v alues through the concurrent actions and merges them at the end. If
+MonadBaseControl provides a more principled way to do that, which lets
+lifted-async also be used safely, then that part of propellor could perhaps
+be changed to use it.
+
+But, I don't know if this is a problem that MonadBaseControl deals with at
+all. It might be that its design is intended to be used for things like
+`bracket`, where there's no concurrency, and so not as much problem with
+getting different monadic states that need to be merged together. (Although
+in `bracket foo bar baz`, if baz throws an exception part way through,
+there's an interesting question about what to do with any monadic state it
+may have accumulated.)
+"""]]
diff --git a/doc/todo/Arch_Linux_Port.mdwn b/doc/todo/Arch_Linux_Port.mdwn
new file mode 100644
index 00000000..ac3ee4dc
--- /dev/null
+++ b/doc/todo/Arch_Linux_Port.mdwn
@@ -0,0 +1,16 @@
+Hi all, I'm an Arch Linux user and I've been learning Haskell and working on an Arch Liux Port in the last several months. Here's my [GitHub fork](https://github.com/wzhd/propellor/tree/archlinux), and the branch is called archlinux.
+
+Currently, I've added types, modified Bootstrap.hs, and added a Property for the package manager Pacman. I've been using it for a while and it seems to be working.
+
+I've made some addtional minor changes to make propellor compile without errors:
+
+- User.nuked now has type Property Linux
+- OS.cleanInstallOnce now has type Property DebianLike, because one of its dependencies, User.shadowConfig only supports DebianLike
+- tightenTargets is added to Reboot.toDistroKernel to get the expeted type
+- pattern for Arch Linux is added to Debootstrap.extractSuite to silence warning "non-exhaustive pattern match"
+- several properties in Parted and Partition are converted to Property Linux
+- Rsync.installed and Docker.installed now supports Pacman as well
+
+Hope you enjoy it!
+
+> [[merged|done]]; it was indeed enjoyable. thank you! --[[Joey]]
diff --git a/doc/todo/Arch_Linux_Port/comment_1_8e39dc177e21e9e20c1b74b59b9926d2._comment b/doc/todo/Arch_Linux_Port/comment_1_8e39dc177e21e9e20c1b74b59b9926d2._comment
new file mode 100644
index 00000000..11869a2a
--- /dev/null
+++ b/doc/todo/Arch_Linux_Port/comment_1_8e39dc177e21e9e20c1b74b59b9926d2._comment
@@ -0,0 +1,28 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2017-02-03T19:14:41Z"
+ content="""
+Wow, nice work!
+
+Seems that Propellor.Property.Partition.formatted' is still a DebianLike
+property really, since it only supports using apt to install the mkfs
+programs. It will fail at runtime on Arch. So, I think best to keep it
+DebianLike until that's dealt with -- and then the type will be
+`DebianLike + ArchLinux` rather than `LinuxLike`
+
+Same for Propellor.Property.Partition.kpartx.
+
+Several properties that were changed from DebianLike to Linux really
+only support DebianLike and ArchLinux, not all linux distros, so their
+types ought to be `DebianLike + ArchLinux`. This includes Docker.installed,
+Parted.installed, Rsync.installed.
+
+A nicer way to inplement those multi-distro `installed` properties is like
+this:
+
+ installed :: Property (Debian + ArchLinux)
+ installed = Apt.installed ["foo"] `pickOS` Pacman.installed ["foo"]
+
+Make those changes and I will merge it.
+"""]]
diff --git a/doc/todo/Arch_Linux_Port/comment_2_cc4623c156a0d12c88461bc5deec07cd._comment b/doc/todo/Arch_Linux_Port/comment_2_cc4623c156a0d12c88461bc5deec07cd._comment
new file mode 100644
index 00000000..dc6e3eb1
--- /dev/null
+++ b/doc/todo/Arch_Linux_Port/comment_2_cc4623c156a0d12c88461bc5deec07cd._comment
@@ -0,0 +1,18 @@
+[[!comment format=mdwn
+ username="wzhd"
+ avatar="http://cdn.libravatar.org/avatar/d5a499b7c476ca9960cc8dccdf455bae"
+ subject="comment 2"
+ date="2017-02-04T01:53:49Z"
+ content="""
+Thanks!
+
+
+I didn't find the right way to do it; `pickOS` is so much easier than `withOS` !
+
+
+`Propellor.Property.Partition` was modified to get rid of some compiling errors in DiskImage and didn't support anything new. So I removed the changes.
+
+
+Instead, I changed some properties in DiskImage from Linux to DebianLike. Is it the correct way to do it?
+
+"""]]
diff --git a/doc/todo/Arch_Linux_Port/comment_3_d917de766dfe7fded7317d7614d1467f._comment b/doc/todo/Arch_Linux_Port/comment_3_d917de766dfe7fded7317d7614d1467f._comment
new file mode 100644
index 00000000..27ef8078
--- /dev/null
+++ b/doc/todo/Arch_Linux_Port/comment_3_d917de766dfe7fded7317d7614d1467f._comment
@@ -0,0 +1,25 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 3"""
+ date="2017-02-04T20:55:02Z"
+ content="""
+> Instead, I changed some properties in DiskImage from Linux to
+> DebianLike. Is it the correct way to do it?
+
+Looking at it, kpartx is DebianLike-specific, so imageBuiltFrom which uses it
+should be too. The only reason it wasn't marked as DebianLike already and
+was type Linux is because Linux used to be the same as DebianLike and so
+the type checker didn't see a difference. No longer, thanks to your patch.
+
+So, it makes complete sense that you have to change this. You're paying
+the price of blazing the trail of the first non-DebianLike Linux distro in
+Propellor..
+
+---
+
+Looks like your [[!commit 25f6871e1dda3de252fbc6c8ac6962eb0cd9311a]]
+dealt with all my review suggestions. And so, I've merged it.
+
+Unless you have anything else that needs to be done, I'll release
+propellor soon with the added Arch Linux support. Thank you very much!
+"""]]
diff --git a/doc/todo/Arch_Linux_Port/comment_4_924c73c0ab6fb39c9b25ae51facf6bb6._comment b/doc/todo/Arch_Linux_Port/comment_4_924c73c0ab6fb39c9b25ae51facf6bb6._comment
new file mode 100644
index 00000000..f69e2c80
--- /dev/null
+++ b/doc/todo/Arch_Linux_Port/comment_4_924c73c0ab6fb39c9b25ae51facf6bb6._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="wzhd"
+ avatar="http://cdn.libravatar.org/avatar/d5a499b7c476ca9960cc8dccdf455bae"
+ subject="comment 4"
+ date="2017-02-05T00:59:18Z"
+ content="""
+That's great! Thank you so much!
+"""]]
diff --git a/doc/todo/Are_--check_and_--build_on_the_way_in_or_on_the_way_out__63__.mdwn b/doc/todo/Are_--check_and_--build_on_the_way_in_or_on_the_way_out__63__.mdwn
new file mode 100644
index 00000000..52b3b998
--- /dev/null
+++ b/doc/todo/Are_--check_and_--build_on_the_way_in_or_on_the_way_out__63__.mdwn
@@ -0,0 +1,3 @@
+I've managed to do a few useful things with propellor, but it feels a bit rough around the edges to me. It looked at first like the --check and --build options would be useful for checking that my configs would at least compile, but it turns out that --build doesn't even exist and --check just returns without doing anything. Should they just be removed, or do they need more work to finish them?
+
+[[done]]
diff --git a/doc/todo/Are_--check_and_--build_on_the_way_in_or_on_the_way_out__63__/comment_1_7c2b2447254ad44ee1316b47eac130df._comment b/doc/todo/Are_--check_and_--build_on_the_way_in_or_on_the_way_out__63__/comment_1_7c2b2447254ad44ee1316b47eac130df._comment
new file mode 100644
index 00000000..392f0f1c
--- /dev/null
+++ b/doc/todo/Are_--check_and_--build_on_the_way_in_or_on_the_way_out__63__/comment_1_7c2b2447254ad44ee1316b47eac130df._comment
@@ -0,0 +1,12 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2016-12-26T15:55:36Z"
+ content="""
+--check does just what it's supposed to do. This is used during bootstrap
+to notice if the propellor binary has gotten broken by changes to eg system
+libraries.
+
+--build seems to have been added without being implemented. But it does
+seem useful to have a way to simply build propellor so implemented it now.
+"""]]
diff --git a/doc/todo/Are_--check_and_--build_on_the_way_in_or_on_the_way_out__63__/comment_2_b4910f50225a8b763566126861faea11._comment b/doc/todo/Are_--check_and_--build_on_the_way_in_or_on_the_way_out__63__/comment_2_b4910f50225a8b763566126861faea11._comment
new file mode 100644
index 00000000..0c594483
--- /dev/null
+++ b/doc/todo/Are_--check_and_--build_on_the_way_in_or_on_the_way_out__63__/comment_2_b4910f50225a8b763566126861faea11._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="db48x"
+ avatar="http://cdn.libravatar.org/avatar/ad2688127feb555a92154b16d8eeb5d3"
+ subject="aha"
+ date="2016-12-26T21:23:03Z"
+ content="""
+Thanks!
+"""]]
diff --git a/doc/todo/Info_property_to_select_host__39__s_preferred_Apt_mirror.mdwn b/doc/todo/Info_property_to_select_host__39__s_preferred_Apt_mirror.mdwn
new file mode 100644
index 00000000..4cd76383
--- /dev/null
+++ b/doc/todo/Info_property_to_select_host__39__s_preferred_Apt_mirror.mdwn
@@ -0,0 +1,5 @@
+It would be good to have an info property, say `Apt.mirror`, which sets a host's preferred apt mirror. Then all properties in `Propellor.Property.Apt` would use this mirror when generating sources lists, falling back to the `deb.debian.org` default. The value of `Apt.mirror` could be an apt cache on the LAN, or a mirror that is known to be better than the Debian CDN from where the host is located. --[[spwhitton|user/spwhitton]]
+
+[[!tag user/spwhitton]]
+
+> [[merged|done]] thank you! --[[Joey]]
diff --git a/doc/todo/Info_property_to_select_host__39__s_preferred_Apt_mirror/comment_1_ac66a33d71092261a745378c82959e69._comment b/doc/todo/Info_property_to_select_host__39__s_preferred_Apt_mirror/comment_1_ac66a33d71092261a745378c82959e69._comment
new file mode 100644
index 00000000..3734d987
--- /dev/null
+++ b/doc/todo/Info_property_to_select_host__39__s_preferred_Apt_mirror/comment_1_ac66a33d71092261a745378c82959e69._comment
@@ -0,0 +1,7 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2017-02-21T03:07:28Z"
+ content="""
+Very good idea. Happy to merge such a patch.
+"""]]
diff --git a/doc/todo/Info_property_to_select_host__39__s_preferred_Apt_mirror/comment_2_2c2c4817a4259acbc1a63bac2e3fb2e3._comment b/doc/todo/Info_property_to_select_host__39__s_preferred_Apt_mirror/comment_2_2c2c4817a4259acbc1a63bac2e3fb2e3._comment
new file mode 100644
index 00000000..b79ba1c1
--- /dev/null
+++ b/doc/todo/Info_property_to_select_host__39__s_preferred_Apt_mirror/comment_2_2c2c4817a4259acbc1a63bac2e3fb2e3._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="spwhitton"
+ avatar="http://cdn.libravatar.org/avatar/9c3f08f80e67733fd506c353239569eb"
+ subject="merge request"
+ date="2017-03-19T18:42:20Z"
+ content="""
+Please see branch `apt-mirror` of repo `https://git.spwhitton.name/propellor` for an implementation of this.
+"""]]
diff --git a/doc/todo/LVM_logical_volume_creation__44___resize__44___format___38___removal.mdwn b/doc/todo/LVM_logical_volume_creation__44___resize__44___format___38___removal.mdwn
new file mode 100644
index 00000000..0910ef5d
--- /dev/null
+++ b/doc/todo/LVM_logical_volume_creation__44___resize__44___format___38___removal.mdwn
@@ -0,0 +1,7 @@
+I have made a new property to handle logical volume with propellor.
+
+I am not confident my haskell code is good looking as this is my first real life haskell code, can you please have a look?
+
+You can pull the lvm branch at http://git.ni.fr.eu.org/nicolas/propellor.git
+
+Thanks!
diff --git a/doc/todo/LVM_logical_volume_creation__44___resize__44___format___38___removal/comment_1_74c6576b25f74c6e620eb015af8b0f6a._comment b/doc/todo/LVM_logical_volume_creation__44___resize__44___format___38___removal/comment_1_74c6576b25f74c6e620eb015af8b0f6a._comment
new file mode 100644
index 00000000..5982361f
--- /dev/null
+++ b/doc/todo/LVM_logical_volume_creation__44___resize__44___format___38___removal/comment_1_74c6576b25f74c6e620eb015af8b0f6a._comment
@@ -0,0 +1,26 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2017-08-31T22:40:34Z"
+ content="""
+That's a pretty nice job for your first haskell code! And an impressive
+module.
+
+Most of my review comments have to do with improving types.. Which is
+always a nice way to improve already good code. :)
+
+* VolumeGroup and LogicalVolume seem like easy things to mix up.
+ Also, there's never a LogicalVolume without an associated VolumeGroup.
+ So, suggest `newtype VolumeGroup = VolumeGroup String` and
+ `data LogicalVolume = LogicalVolume String VolumeGroup` -- then
+ the user would write something like
+ `LogicalVolume "test" (VolumeGroup "vg0")`
+* Why not make `LvState` contain a `Maybe Partition.Fs` rather than
+ the string value. (This also would move the parsing of filesystem names
+ from `fsMatch` to `lvState` or perhaps to another function it uses.)
+* It seems a bit wrong for `parseSize` to include the rounding
+ to the next extent, which is not really related to parsing.
+ Would be better to split those two things into separate functions.
+
+I feel that this module is fairly close to mergeable.
+"""]]
diff --git a/doc/todo/LVM_logical_volume_creation__44___resize__44___format___38___removal/comment_2_d63d84b56ece233f795d1075aaba887a._comment b/doc/todo/LVM_logical_volume_creation__44___resize__44___format___38___removal/comment_2_d63d84b56ece233f795d1075aaba887a._comment
new file mode 100644
index 00000000..546fe436
--- /dev/null
+++ b/doc/todo/LVM_logical_volume_creation__44___resize__44___format___38___removal/comment_2_d63d84b56ece233f795d1075aaba887a._comment
@@ -0,0 +1,18 @@
+[[!comment format=mdwn
+ username="Nicolas.Schodet"
+ avatar="http://cdn.libravatar.org/avatar/0d7ec808ec329d04ee9a93c0da3c0089"
+ subject="comment 2"
+ date="2017-09-01T21:38:16Z"
+ content="""
+Thanks for your comments.
+
+I also have a problem when running vgs/lvs, they complain about leaked file descriptors. Is it something I can fix?
+
+ File descriptor 10 (/usr/local/propellor/.lock) leaked on vgs invocation. Parent PID 31216: ./dist/build/propellor-config/p
+ File descriptor 11 (pipe:[282601]) leaked on vgs invocation. Parent PID 31216: ./dist/build/propellor-config/p
+ File descriptor 12 (pipe:[282601]) leaked on vgs invocation. Parent PID 31216: ./dist/build/propellor-config/p
+ File descriptor 13 (pipe:[282602]) leaked on vgs invocation. Parent PID 31216: ./dist/build/propellor-config/p
+ File descriptor 14 (pipe:[282602]) leaked on vgs invocation. Parent PID 31216: ./dist/build/propellor-config/p
+
+I have pushed a new version with the suggested fixes.
+"""]]
diff --git a/doc/todo/LVM_logical_volume_creation__44___resize__44___format___38___removal/comment_3_1405e20c8f5dc6e9cca3732e3e368d03._comment b/doc/todo/LVM_logical_volume_creation__44___resize__44___format___38___removal/comment_3_1405e20c8f5dc6e9cca3732e3e368d03._comment
new file mode 100644
index 00000000..76c89ca6
--- /dev/null
+++ b/doc/todo/LVM_logical_volume_creation__44___resize__44___format___38___removal/comment_3_1405e20c8f5dc6e9cca3732e3e368d03._comment
@@ -0,0 +1,25 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 3"""
+ date="2017-09-01T22:32:43Z"
+ content="""
+One way would be to use System.Process's `close_fds` when executing
+vgs/lvs. BTW, I've seen such complaints from lvm before, in some
+situations not involving propellor.
+
+I've made a commit that makes the propellor lock FD be close-on-exec,
+which is generally a good idea for lock FDs anyway. (To prevent some
+long-running daemon process that does not close such FDs keeping the lock
+held.)
+
+My guess is that the other 4 FDs, which are apparently pairs of FDs
+at both sides of a pipe, come from
+System.Console.Concurrent.Internal.bgProcess, which sets up just such a
+pipe. Quite possibly when vgs/lvs are run, it's via that function.
+
+Generally leaking non-lock-related FDs to child processes is not a big
+problem, as long as the child process doesn't write to random FDs (which
+would be pretty bad, but what would ever do that?) ... So I don't know if I
+want to try to chase down every FD used all through propellor to set them
+close-on-exec.
+"""]]
diff --git a/doc/todo/LVM_logical_volume_creation__44___resize__44___format___38___removal/comment_4_20c6734d67fefeb1a8c07730d537e06b._comment b/doc/todo/LVM_logical_volume_creation__44___resize__44___format___38___removal/comment_4_20c6734d67fefeb1a8c07730d537e06b._comment
new file mode 100644
index 00000000..74a8bbe1
--- /dev/null
+++ b/doc/todo/LVM_logical_volume_creation__44___resize__44___format___38___removal/comment_4_20c6734d67fefeb1a8c07730d537e06b._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="Nicolas.Schodet"
+ avatar="http://cdn.libravatar.org/avatar/0d7ec808ec329d04ee9a93c0da3c0089"
+ subject="comment 4"
+ date="2017-09-03T21:00:36Z"
+ content="""
+I can rebase/squash, do you see something else to improve?
+"""]]
diff --git a/doc/todo/Merging_from___47__usr__47__src__47__propellor_broken_now_CHANGELOG_not_a_symlink.mdwn b/doc/todo/Merging_from___47__usr__47__src__47__propellor_broken_now_CHANGELOG_not_a_symlink.mdwn
new file mode 100644
index 00000000..bfba8548
--- /dev/null
+++ b/doc/todo/Merging_from___47__usr__47__src__47__propellor_broken_now_CHANGELOG_not_a_symlink.mdwn
@@ -0,0 +1,36 @@
+In Joey's master branch, `CHANGELOG` is a real file, whereas previously it was a symlink. This breaks the `/usr/src/propellor` newer version check.
+
+Steps to reproduce:
+
+1. Install propellor 3.2.3 or older with apt on Debian or Ubuntu
+2. `propellor --init` and select option `A`
+3. Prepare a pseudorelease: merge Joey's master branch into [my Debian packaging branch](https://git.spwhitton.name/?p=propellor.git;a=shortlog;h=refs/heads/debian), `dch -v3.2.3+gitYYYYMMDD.fffffff`, `dpkg-buildpackage -uc -b`, `debi -u`
+4. `propellor --spin`
+
+I haven't yet tried reproducing this by building a `.deb` from Joey's master branch, rather than my packaging branch. If the problem does not appear using a `.deb` from Joey's master branch, this is an internal Debian problem, rather than an upstream bug. However, perhaps Joey can immediately see a solution.
+
+Sample output:
+
+ Auto-merging src/wrapper.hs
+ Auto-merging src/Utility/UserInfo.hs
+ Auto-merging src/Utility/SystemDirectory.hs
+ Auto-merging src/Utility/Misc.hs
+ Auto-merging src/Utility/FileSystemEncoding.hs
+ Auto-merging src/Utility/Exception.hs
+ Auto-merging src/Propellor/Types/CmdLine.hs
+ Auto-merging src/Propellor/Shim.hs
+ Auto-merging src/Propellor/Property/Gpg.hs
+ Auto-merging src/Propellor/Property/Debootstrap.hs
+ Auto-merging src/Propellor/Property.hs
+ Auto-merging src/Propellor/PrivData.hs
+ Auto-merging src/Propellor/Gpg.hs
+ Auto-merging src/Propellor/CmdLine.hs
+ Auto-merging debian/changelog
+ Auto-merging CHANGELOG
+ CONFLICT (add/add): Merge conflict in CHANGELOG
+ Automatic merge failed; fix conflicts and then commit the result.
+ propellor: Failed to run git ["merge","c590ddd8e2fa87baa409b6c29501d4473555ecfb","-s","recursive","-Xtheirs","--quiet","-m","merging upstream version","--allow-unrelated-histories"]
+ CallStack (from HasCallStack):
+ error, called at src/Propellor/DotDir.hs:425:17 in main:Propellor.DotDir
+
+--spwhitton
diff --git a/doc/todo/Merging_from___47__usr__47__src__47__propellor_broken_now_CHANGELOG_not_a_symlink/comment_1_62b47d7c0530c2988b7e6e998878b920._comment b/doc/todo/Merging_from___47__usr__47__src__47__propellor_broken_now_CHANGELOG_not_a_symlink/comment_1_62b47d7c0530c2988b7e6e998878b920._comment
new file mode 100644
index 00000000..886c2534
--- /dev/null
+++ b/doc/todo/Merging_from___47__usr__47__src__47__propellor_broken_now_CHANGELOG_not_a_symlink/comment_1_62b47d7c0530c2988b7e6e998878b920._comment
@@ -0,0 +1,11 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2017-01-01T21:29:52Z"
+ content="""
+I have reverted that change for now.
+
+I don't think the /usr/src/propellor/ merge has anything specific to do
+with the changelog, so there is probably a general case where that merge
+fails to work. I guess it involves a file's type changing.
+"""]]
diff --git a/doc/todo/Merging_from___47__usr__47__src__47__propellor_broken_now_CHANGELOG_not_a_symlink/comment_2_61463030200038542d293149754d36ed._comment b/doc/todo/Merging_from___47__usr__47__src__47__propellor_broken_now_CHANGELOG_not_a_symlink/comment_2_61463030200038542d293149754d36ed._comment
new file mode 100644
index 00000000..b1b4a037
--- /dev/null
+++ b/doc/todo/Merging_from___47__usr__47__src__47__propellor_broken_now_CHANGELOG_not_a_symlink/comment_2_61463030200038542d293149754d36ed._comment
@@ -0,0 +1,8 @@
+[[!comment format=mdwn
+ username="spwhitton"
+ avatar="http://cdn.libravatar.org/avatar/9c3f08f80e67733fd506c353239569eb"
+ subject="comment 2"
+ date="2017-01-03T09:07:18Z"
+ content="""
+Thanks for looking at this. Yes, it's probably the type-change. There is surely some way to instruct git to DTRT.
+"""]]
diff --git a/doc/todo/PROPELLOR_TRACE_propigation.mdwn b/doc/todo/PROPELLOR_TRACE_propigation.mdwn
new file mode 100644
index 00000000..8f7d6893
--- /dev/null
+++ b/doc/todo/PROPELLOR_TRACE_propigation.mdwn
@@ -0,0 +1,6 @@
+`PROPELLOR_TRACE` is not propigated when spinning a remote host,
+conducting a host, and probably not when provisioning a docker or machined
+container.
+
+It is propgiated when provisioning a chroot. That's all I needed, so I
+didh't bother implementing propigation. --[[Joey]]
diff --git a/doc/todo/Propellor.Property.Versioned_support_asymmetric_RevertableProperty_types.mdwn b/doc/todo/Propellor.Property.Versioned_support_asymmetric_RevertableProperty_types.mdwn
new file mode 100644
index 00000000..c60cd4d6
--- /dev/null
+++ b/doc/todo/Propellor.Property.Versioned_support_asymmetric_RevertableProperty_types.mdwn
@@ -0,0 +1,7 @@
+Currently, this module requires `RevertableProperty t t`.
+That can be annoying, it would be good to support at least
+`RevertablePropery (HasInfo + t) t` and ideally all
+`RevertableProperty t1 t2`
+
+There should be no reason that can't be done; I was just having
+problems getting the type checker happy on the day I wrote it. --[[Joey]]
diff --git a/doc/todo/bug_in_diskimage_finalization.mdwn b/doc/todo/bug_in_diskimage_finalization.mdwn
new file mode 100644
index 00000000..3dc9c437
--- /dev/null
+++ b/doc/todo/bug_in_diskimage_finalization.mdwn
@@ -0,0 +1,13 @@
+DiskImage.imageBuilt has broken and no longer runs the finalization
+properties that get added to the chroot. This includes installing grub, and
+Chroot.noServices etc.
+
+Seems that the `_chroot` info that gets propigated from imageBuilt is
+for the chroot before those properties are added to it. Then when chaining
+into the chroot, `_chroot` info is examined to find the properties to
+ensure.
+
+I have not yet been able to determine what broke it -- I'm sure it used to
+work. --[[Joey]]
+
+> Figured it out, fixed [[done]] --[[Joey]]
diff --git a/doc/todo/differential_update_via_RevertableProperty.mdwn b/doc/todo/differential_update_via_RevertableProperty.mdwn
new file mode 100644
index 00000000..3eb9bc7a
--- /dev/null
+++ b/doc/todo/differential_update_via_RevertableProperty.mdwn
@@ -0,0 +1,146 @@
+Long ago, nomeata pointed out that RevertableProperty required the user to
+keep track of different versions of a Host, in a way that should be able to
+be automated. When the user decides to revert a RevertableProperty, they
+have to keep the reverted property on the Host until propellor runs there,
+and only then can remove it.
+
+What if instead, there was a way to store the old version of a Host
+somewhere. Let's not worry about where or how, but assume we have
+`(old, new) :: (Host, Host)`
+
+Propellor could compare `old` and `new`, and if it finds a
+RevertableProperty in `old` that is not in `new`, add it in reverted form
+to `new'`.
+
+Also, if propellor finds a Property in `old` that is not in `new`, it can
+tell the user that this Property needs to be reverted, but cannot be, so
+`new` won't fully describe the state of the host. --[[Joey]]
+
+----
+
+There are a lot of ways such a capability could be used, especially if
+there were a way to pull the old version of a Host out of a previous
+version of config.hs or something like that. But leaving aside such magic,
+here are some nice use cases:
+
+* Suppose we want to generate several disk images, which are somewhat
+ similar, but differ in some properties. Rather than building a separate
+ chroot for each, we can build a chroot for the first, update the first
+ disk image, compare that with the second and update the chroot
+ accordingly, and so on.
+* When propellor is used to build a OS installer disk image, that installer
+ could know the properties used to create it, and the properties of the
+ system that is desired to be installed. To install, it can rsync the
+ installer disk contents to `/target` and then run propellor in `/target`,
+ differentially updating it as needed.
+
+----
+
+Here's the catch: It can't be implemented currently! The comparison of
+properties needs an `Eq` instance for Property (and RevertableProperty).
+But, a property contains an action in the IO monad, which can't have an
+`Eq` instance, and so there's no good way to compare properties.
+
+Making propellor use an ESDL could get us `Eq`. But it would make it rather
+clumsy to write properties, something like this.
+
+<pre>
+appendfoo f = WriteFile f (ListAppend "foo" (ReadFile f))
+</pre>
+
+(Perhaps a deeply embedded DSL would be better.)
+
+Could a Free monad get us `Eq`? Well, there can apparently be free monads that
+have an `Eq` instance, but I tried building one for a simple teletype, and
+failed, which does not bode well. Here's the code; this fails to compile
+because of a missing instance `(Eq1 ((->) String))`, and of course comparing
+functions for equality is not generally feasible.
+
+<pre>
+{-# LANGUAGE FlexibleContexts, UndecidableInstances #-}
+
+import Control.Monad.Free
+import Prelude.Extras
+
+data TeletypeF x
+ = PutStrLn String x
+ | GetLine (String -> x)
+
+instance Functor TeletypeF where
+ fmap f (PutStrLn str x) = PutStrLn str (f x)
+ fmap f (GetLine k) = GetLine (f . k)
+
+instance (Eq1 ((->) String)) => Eq1 TeletypeF where
+ PutStrLn a x ==# PutStrLn b y = a == b && x == y
+ GetLine a ==# GetLine b = a ==# b
+
+type Teletype = Free TeletypeF
+
+putStrLn' :: String -> Teletype ()
+putStrLn' str = liftF $ PutStrLn str ()
+
+getLine' :: Teletype String
+getLine' = liftF $ GetLine id
+
+foo :: Teletype ()
+foo = do
+ putStrLn' "name?"
+ name <- getLine'
+ putStrLn' ("hello, " ++ name)
+
+fooisfoo :: Bool
+fooisfoo = foo ==# foo
+</pre>
+
+-----
+
+## the best we can do without Eq
+
+Is, perhaps:
+
+ data Version = A | B | C
+ deriving (Enum, Ord)
+
+ foo :: Versioned Hoso
+ foo = versionedHost "foo" $ do
+ ver A someprop
+ <|> othervers otherprop
+ ver A somerevertableprop
+ ver [B, C] newprop
+
+That's ... pretty ok, would hit as least some of the use cases described
+above. Seems to need a Reader+Writer monad to implement it,
+without passing the Version around explicitly.
+
+Is it allowable for `newprop` to not be revertable?
+Once `foo` gets that property, it is never removed if we're moving only
+forwards. On the other hand, perhaps the user will want to roll back to
+version A. Allowing rollbacks seems good, so `inVersion` should only
+accept `RevertableProperty`.
+
+Another interesting case is this:
+
+ foo = versionedHost "foo" $ do
+ ver A bar
+ always otherprop
+ ver [B, C] bar
+
+Is version A of foo identical to verion B? If so, this should be allowed to
+compile even when `bar` cannot be reverted. On the other hand, perhaps
+ordering of the properties matters, in which case the systems are subtly
+different, and there's no way to get from A to B.
+
+It's certianly possible for ordering to matter in propellor properties,
+although it's generally a bug when it does. So, it seems ok for this
+case to be rejected.
+
+As well as `Versioned Host`, it would be possible to have
+`Versioned (Property metatypes)`.
+Indeed, that would make sense to he used internally in the
+examples above. And that allows composition of properties with versioning:
+
+ someprop :: Versioned (Property DebianLike)
+ someprop = versionedProperty $ do
+ ver A foo <|> ver [B, C] bar
+
+> [[done]] in Propellor.Property.Versioned. --[[Joey]]
diff --git a/doc/todo/hostChroot.mdwn b/doc/todo/hostChroot.mdwn
new file mode 100644
index 00000000..6a4df9c1
--- /dev/null
+++ b/doc/todo/hostChroot.mdwn
@@ -0,0 +1,9 @@
+Would be useful to have a `hostChroot :: Host -> Chroot`.
+
+For a Debian host, this would use debootstrapped and pass all the Host's
+properties to it. --[[Joey]]
+
+Would need to make privdata use the context of the input Host. And would
+need to propigate privdata info, but not other info. --[[Joey]]
+
+> [[done]] --[[Joey]]
diff --git a/doc/todo/initial_spin_compile_failure_recovery.mdwn b/doc/todo/initial_spin_compile_failure_recovery.mdwn
new file mode 100644
index 00000000..423b279c
--- /dev/null
+++ b/doc/todo/initial_spin_compile_failure_recovery.mdwn
@@ -0,0 +1,5 @@
+When initial propellor --spin host fails to compile propellor
+perhaps due to a ghc compatability bug, spinning again doesn't fix the
+problem. IIRC /usr/local/propellor has a git repo set up, but no remote
+set, and so the subsequent spin doesn't update it, since propellor is not
+running there to receive a git push into the repo. --[[Joey]]
diff --git a/doc/todo/merge_request:_Timezone.hs.mdwn b/doc/todo/merge_request:_Timezone.hs.mdwn
new file mode 100644
index 00000000..a8ba3eae
--- /dev/null
+++ b/doc/todo/merge_request:_Timezone.hs.mdwn
@@ -0,0 +1,9 @@
+Please consider merging branch `timezone` of repo `https://git.spwhitton.name/propellor`.
+
+Adds `Timezone.configured`.
+
+I think that this works fine on stretch, but on Jessie there is some oddness. For example, if you set the timezone of a host to `US/Arizona`, the apt reconfiguration will put `America/Phoenix` in /etc/timezone, resulting in the property reporting a change every time that it is run. I think this is harmless.
+
+--spwhitton
+
+> [[merged|done]] --[[Joey]]
diff --git a/doc/todo/merge_request:_Timezone.hs/comment_1_9cfb5e48940e58f2064cbb5edf462c06._comment b/doc/todo/merge_request:_Timezone.hs/comment_1_9cfb5e48940e58f2064cbb5edf462c06._comment
new file mode 100644
index 00000000..026b13de
--- /dev/null
+++ b/doc/todo/merge_request:_Timezone.hs/comment_1_9cfb5e48940e58f2064cbb5edf462c06._comment
@@ -0,0 +1,15 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2017-07-16T15:57:20Z"
+ content="""
+I generally consider properties that do work every time to be a minor bug.
+
+I wonder if it would be better to preseed tzdata rather than writing the
+config file. I observe the same substitution from eg, US/Eastern to
+America/New_York in the file when reconfiguring noninteractively,
+but reconfiguring interactively I can select US/Eastern and that gets
+into the file.
+
+Anyway, merged as this is certianly a good starting point.
+"""]]
diff --git a/doc/todo/modify_Apt.pinnedTo_to_pin_a_package_to_multiple_suites_with_different_priorities.mdwn b/doc/todo/modify_Apt.pinnedTo_to_pin_a_package_to_multiple_suites_with_different_priorities.mdwn
new file mode 100644
index 00000000..02be4ad7
--- /dev/null
+++ b/doc/todo/modify_Apt.pinnedTo_to_pin_a_package_to_multiple_suites_with_different_priorities.mdwn
@@ -0,0 +1,7 @@
+Please consider merging the `pin` branch of `https://git.spwhitton.name/propellor` (again).
+
+I've modified `Apt.pinnedTo` so that it can pin an `AptPrefPackage` to multiple suites with different pin priorities. I've included a sample use-case in the function's haddock.
+
+--spwhitton
+
+> merged, [[done]] --[[Joey]]
diff --git a/doc/todo/new_apt_pinning_properties.mdwn b/doc/todo/new_apt_pinning_properties.mdwn
new file mode 100644
index 00000000..8687b58a
--- /dev/null
+++ b/doc/todo/new_apt_pinning_properties.mdwn
@@ -0,0 +1,10 @@
+My branch `pin` of repo `https://git.spwhitton.name/propellor` adds
+
+- `Apt.suiteAvailablePinned`
+- `Apt.pinnedTo`
+- `File.containsBlock`
+- a haddock for `File.containsLines`
+
+There is one TODO in a comment that relates to propellor's algebraic data types. I'd be grateful for help with that. --spwhitton
+
+> merged, thanks. [[done]] --[[Joey]]
diff --git a/doc/todo/new_apt_pinning_properties/comment_1_fd9e6775868eaa8d6aee49d06944ef0c._comment b/doc/todo/new_apt_pinning_properties/comment_1_fd9e6775868eaa8d6aee49d06944ef0c._comment
new file mode 100644
index 00000000..4800608f
--- /dev/null
+++ b/doc/todo/new_apt_pinning_properties/comment_1_fd9e6775868eaa8d6aee49d06944ef0c._comment
@@ -0,0 +1,38 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2017-02-01T20:00:47Z"
+ content="""
+I wonder if it would be better to separate `suiteAvailablePinned`
+into `suiteAvailable` and `suitePinned`? The latter could require
+the former.
+
+`pinnedTo` should probably be DebianLike not UnixLike.
+And its `[String]` parameter ought to be `[Package]`.
+
+Is `File.containsBlock` necessary? Seems that if you care about
+ordering of blocks in the file, you generally should use
+`File.hasContent` to specify the full content. Rather than using
+/etc/apt/preferences.d/10propellor.pref for multiple properties,
+you could use a separate file for each `pinnedTo'` with the parameters
+encoded in the filename.
+
+As to the TODO, I tried adding this:
+
+ robustly' :: RevertableProperty DebianLike DebianLike -> RevertableProperty DebianLike DebianLike
+ robustly' p = p `fallback` (update `before` p)
+
+And the compiler tells me it's wrong because `update` is not revertable.
+But of course, there's no need to revert apt-get update, so this compiles:
+
+ robustly' :: RevertableProperty DebianLike DebianLike -> RevertableProperty DebianLike DebianLike
+ robustly' p = p `fallback` ((update <!> (doNothing :: Property DebianLike)) `before` p)
+
+Cleaning it up left an an exersise for the reader. Might be possible
+to combine `robustly` and `robustly'` into a single function, but I'm
+not able to see how immediately.
+
+However.. Seems to me that whatever you wanted to use `robustly` with to
+spur that TODO, you could just apply it to the first Property of the
+RevertableProperty, and not to the second one?
+"""]]
diff --git a/doc/todo/new_apt_pinning_properties/comment_2_c82f7e83f3fcc7648222d9dbf90e5ddd._comment b/doc/todo/new_apt_pinning_properties/comment_2_c82f7e83f3fcc7648222d9dbf90e5ddd._comment
new file mode 100644
index 00000000..4fd7c824
--- /dev/null
+++ b/doc/todo/new_apt_pinning_properties/comment_2_c82f7e83f3fcc7648222d9dbf90e5ddd._comment
@@ -0,0 +1,66 @@
+[[!comment format=mdwn
+ username="spwhitton"
+ avatar="http://cdn.libravatar.org/avatar/9c3f08f80e67733fd506c353239569eb"
+ subject="reply to review"
+ date="2017-02-02T17:40:11Z"
+ content="""
+Thank you for your feedback, Joey.
+
+> I wonder if it would be better to separate `suiteAvailablePinned`
+> into `suiteAvailable` and `suitePinned`? The latter could require
+> the former.
+
+I see how this could be useful, in particular if you want to make a
+suite like Debian experimental available, which won't cause any packages
+to be automatically upgraded.
+
+However, it makes it less convenient, and perhaps dangerous, to revert a
+pinned suite. For example, suppose on my Debian testing system I have
+`Apt.suitePinned Unstable 100`. If I revert this property, it will
+remove the pin but not remove the source. Then my system might get
+mass-upgraded to sid if I'm not careful.
+
+We couldn't have the revert of `Apt.suitePinned` remove the source
+because then if I have both `& Apt.suiteAvailable Unstable` and `!
+Apt.suitePinned Unstable 100`, the second property would cancel out the
+first, which doesn't make sense.
+
+On balance, I think it's best to keep the current property. A property
+adding sources to apt.sources.d should probably force the user to pick a
+pin value, to avoid any unexpected upgrades.
+
+> `pinnedTo` should probably be DebianLike not UnixLike.
+
+This was my 'TODO'. (Since the property takes a `DebianSuite`, I think
+it should be `Debian` not `DebianLike`.)
+
+I tried applying `tightenTargets` to `pinnedTo`, but that only seems to
+affect one half of the revertable property. Do I need to implement a
+new tightening function?
+
+> And its `[String]` parameter ought to be `[Package]`.
+
+I don't think so. The parameter to `pinnedTo` can be a wildcard
+expression or a regex (per `apt_preferences(5)`). Neither of these are
+accepted by other existing properties that take `[Package]`, such as
+`Apt.installed`. I could add a new type alias, if you prefer.
+
+> Is `File.containsBlock` necessary? Seems that if you care about
+> ordering of blocks in the file, you generally should use
+> `File.hasContent` to specify the full content. Rather than using
+> /etc/apt/preferences.d/10propellor.pref for multiple properties,
+> you could use a separate file for each `pinnedTo'` with the parameters
+> encoded in the filename.
+
+This was what I tried on my first attempt, but it gets very complicated
+if the user passes a wildcard expression or a regex instead of a package
+name. I would need to convert that wildcard expression or regex to a
+cross-platform filename, and the conversion would need to be isomorphic
+to avoid any clashes. The `File.containsBlock` seems more sane than
+that.
+
+> As to the TODO, I tried adding this: [...]
+
+I don't understand how `robustly` is relevant to my TODO -- please see
+above.
+"""]]
diff --git a/doc/todo/new_apt_pinning_properties/comment_3_58d323602f293471ce3d2d9b4d271130._comment b/doc/todo/new_apt_pinning_properties/comment_3_58d323602f293471ce3d2d9b4d271130._comment
new file mode 100644
index 00000000..b0ff271e
--- /dev/null
+++ b/doc/todo/new_apt_pinning_properties/comment_3_58d323602f293471ce3d2d9b4d271130._comment
@@ -0,0 +1,23 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 3"""
+ date="2017-02-02T18:45:01Z"
+ content="""
+That example with reverting one property overriding another property
+is a general problem propellor has with conflicting properties.
+Normally I don't much worry about it, but I agree an accidental mass
+upgrade is a good reason to avoid that problem here.
+
+Yes please add a new type alias for String (or an ADT)
+if Package is not appropriate.
+
+I had misunderstood which function the TODO was for..
+
+Nice surprise that tightenTargets works on RevertableProperty at all.
+Since it does, you should be able to tighten one side, revert, tighten the
+other side, and re-revert. Or, deconstruct the RevertableProperty,
+tighten both sides individually, and reconstruct it.
+
+I've added a Propellor.Property.File.configFileName that
+should be suitable for your purposes, and others..
+"""]]
diff --git a/doc/todo/new_apt_pinning_properties/comment_4_add83ed58963e944ccd705a50e8b5a47._comment b/doc/todo/new_apt_pinning_properties/comment_4_add83ed58963e944ccd705a50e8b5a47._comment
new file mode 100644
index 00000000..9688672b
--- /dev/null
+++ b/doc/todo/new_apt_pinning_properties/comment_4_add83ed58963e944ccd705a50e8b5a47._comment
@@ -0,0 +1,20 @@
+[[!comment format=mdwn
+ username="spwhitton"
+ avatar="http://cdn.libravatar.org/avatar/9c3f08f80e67733fd506c353239569eb"
+ subject="comment 4"
+ date="2017-02-03T04:07:58Z"
+ content="""
+> Yes please add a new type alias for String (or an ADT) if Package is not appropriate.
+
+Propellor won't be parsing any of the regexp or globs, so I've added a new type alias rather than an ADT.
+
+> Nice surprise that tightenTargets works on RevertableProperty at all. Since it does, you should be able to tighten one side, revert, tighten the other side, and re-revert. Or, deconstruct the RevertableProperty, tighten both sides individually, and reconstruct it.
+
+I don't understand what you're getting at with the first of these suggestions.
+
+In any case, now that I'm not using `File.containsBlock`, it's easy to just apply `tightenTargets` to each side.
+
+> I've added a Propellor.Property.File.configFileName that should be suitable for your purposes, and others..
+
+Very nice :) I've updated my branch to use this. I haven't removed `File.containsBlock`, since it might be useful in the future, but you could of course revert the relevant commit.
+"""]]
diff --git a/doc/todo/property_to_install_propellor.mdwn b/doc/todo/property_to_install_propellor.mdwn
new file mode 100644
index 00000000..184977f5
--- /dev/null
+++ b/doc/todo/property_to_install_propellor.mdwn
@@ -0,0 +1,16 @@
+This seems redundant, since propellor must be running to ensure such a
+Property, but a Property to install propellor is useful when eg, creating a
+disk image that itself will need to run propellor. --[[Joey]]
+
+Should support:
+
+* Cloning the git repo propellor is running in. (Using eg `hostChroot`)
+* Cloning some other git repo.
+* Installing the precompiled propellor binary.
+* Installing the propellor haskell library using cabal/stack/apt.
+
+Much of this is already implemented, in non-Property form, in
+Propellor.Bootstrap, but will need adjustments for this new context.
+--[[Joey]]
+
+> [[done]]
diff --git a/doc/todo/property_to_install_propellor/comment_1_b05e9a44e5c7130d9cc928223cd82d78._comment b/doc/todo/property_to_install_propellor/comment_1_b05e9a44e5c7130d9cc928223cd82d78._comment
new file mode 100644
index 00000000..5a826fea
--- /dev/null
+++ b/doc/todo/property_to_install_propellor/comment_1_b05e9a44e5c7130d9cc928223cd82d78._comment
@@ -0,0 +1,16 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2017-04-09T17:42:10Z"
+ content="""
+Making this work when propellor is setting up a chroot is difficult,
+because the localdir is bind mounted into the chroot.
+
+Hmm, `unshare` could be helpful. Run shell commands to clone the localdir
+inside `unshare -m`, prefixed with a `umount localdir`. This way, the bind
+mount is avoided, and it writes "under" it. Limits the commands that can be
+run to set up the localdir to shell commands, but bootstrap already
+operates on terms of shell commands so that seems ok.
+
+`unshare` is linux-specific; comes in util-linux on modern linuxes.
+"""]]
diff --git a/doc/todo/property_to_install_propellor/comment_2_9fea601af57777e1cb49952483f4da63._comment b/doc/todo/property_to_install_propellor/comment_2_9fea601af57777e1cb49952483f4da63._comment
new file mode 100644
index 00000000..f862f79b
--- /dev/null
+++ b/doc/todo/property_to_install_propellor/comment_2_9fea601af57777e1cb49952483f4da63._comment
@@ -0,0 +1,7 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 2"""
+ date="2017-04-09T20:49:04Z"
+ content="""
+Well, seems that `unshare` does not work in a chroot. Hmm.
+"""]]
diff --git a/doc/todo/sbuild_setup_should_use_apt-cacher-ng.mdwn b/doc/todo/sbuild_setup_should_use_apt-cacher-ng.mdwn
new file mode 100644
index 00000000..d37d6806
--- /dev/null
+++ b/doc/todo/sbuild_setup_should_use_apt-cacher-ng.mdwn
@@ -0,0 +1,22 @@
+Please consider merging branch `apt-cacher-ng` of repo `https://git.spwhitton.name/propellor`.
+
+Sample text for changelog/description of changes:
+
+ * Add Apt.proxy property to set a host's apt proxy.
+ * Add Apt.useLocalCacher property to set up apt-cacher-ng.
+ * Rework Sbuild properties to use apt proxies/cachers instead of bind-mounting
+ the host's apt cache. This makes it possible to run more than one build at
+ a time, and lets sbuild run even if apt's cache is locked by the host's apt.
+ - If Apt.proxy is set, it is assumed that the proxy does some sort of
+ caching, and sbuild chroots are set up to use the same proxy.
+ - If Apt.proxy is not set, we install apt-cacher-ng, and point sbuild
+ chroots at the local apt cacher.
+ - Drop Sbuild.piupartsConfFor, Sbuild.piupartsConf, Sbuild.shareAptCache
+ (API change)
+ No longer needed now that we are using apt proxies/cachers.
+ - Update sample config in haddock for Propellor.Property.Sbuild.
+ Please compare both your config.hs and your ~/.sbuildrc against the haddock.
+
+--spwhitton
+
+> merge [[done]] --[[Joey]]
diff --git a/doc/todo/spin_failure_HEAD.mdwn b/doc/todo/spin_failure_HEAD.mdwn
new file mode 100644
index 00000000..1a591b35
--- /dev/null
+++ b/doc/todo/spin_failure_HEAD.mdwn
@@ -0,0 +1,130 @@
+Seen recently on 2 hosts:
+
+ Sending privdata (73139 bytes) to kite.kitenet.net ... done
+ fatal: Couldn't find remote ref HEAD
+ propellor: <stdout>: hPutStr: illegal operation (handle is closed)
+ fatal: The remote end hung up unexpectedly
+ Sending git update to kite.kitenet.net ... failed
+
+Despite the error, HEAD seems to be updated to the commit that is being spun,
+but the rest of the propellor runs doesn't happen. --[[Joey]]
+
+> This was happening spinning kite at my Mom's, but not from home.
+
+> Earlier, it was happening spinning clam from home, when clam had debian
+> oldstable on it (new install).
+>
+> So, transient and/or network-related.. --[[Joey]]
+
+> > Happening again spinning kite over satelite, but not other hosts.
+> > I enabled propellor.debug, and here's what it showed on kite:
+
+ Sending privdata (73139 bytes) to kite.kitenet.net ... done
+ [2017-06-18 16:01:08 EDT] received marked PRIVDATA
+ [2017-06-18 16:01:08 EDT] requested marked GITPUSH
+ [2017-06-18 16:01:11 EDT] received marked GITPUSH
+ [2017-06-18 16:01:11 EDT] command line: GitPush 11 12
+ fatal: Couldn't find remote ref HEAD
+ propellor: <stdout>: hPutStr: illegal operation (handle is closed)
+ fatal: The remote end hung up unexpectedly
+ Sending git update to kite.kitenet.net ... failed
+
+> > Seem that what's failing is "git fetch" when propellor
+> > runs it with --upload-pack used to run propellor --gitpush.
+> >
+> > The "fatal: Couldn't find remote ref HEAD" comes from git fetch,
+> > I think when no HEAD is in the list of remote refs.
+> >
+> > The hPutStr error was a red herring; errorMessage was using
+> > outputConcurrent. After I fixed it to use errorConcurrent,
+> > it displayed the "git fetch from client failed" error message instead.
+> >
+> > Next step is probably to enable `GIT_TRACE_PACKET` debugging
+> > of the git fetch. I did so on kite, but then propellor --spin succeeded.
+> > Here's the debug output I got when it worked, for later comparison
+> > next time it fails. Note the HEAD ref is given first thing.
+
+<pre>
+Sending privdata (73139 bytes) to kite.kitenet.net ... done
+[2017-06-18 16:27:12 EDT] received marked PRIVDATA
+[2017-06-18 16:27:12 EDT] requested marked GITPUSH
+[2017-06-18 16:27:13 EDT] received marked GITPUSH
+[2017-06-18 16:27:13 EDT] command line: GitPush 11 12
+16:27:13.953638 pkt-line.c:80 packet: fetch< 3a3c8a731d169a2768dd243581803dcb7b275049 HEAD\0multi_ack thin-pack side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed symref=HEAD:refs/heads/joeyconfig agent=git/2.11.0
+16:27:13.953781 pkt-line.c:80 packet: fetch< 86b077b7a21efd5484dfaeee3c31fc5f3c151f6c refs/heads/confpairs
+16:27:13.953789 pkt-line.c:80 packet: fetch< e03e4bf0f1e557f87d1fe7e01a6de7866296fce6 refs/heads/d-i
+16:27:13.953795 pkt-line.c:80 packet: fetch< 3a3c8a731d169a2768dd243581803dcb7b275049 refs/heads/joeyconfig
+16:27:13.953801 pkt-line.c:80 packet: fetch< ee56d3793be3a8c0c268d8afdc642ef92b879269 refs/heads/master
+16:27:13.953807 pkt-line.c:80 packet: fetch< 51be061c90ca7539d7f8b804007cd9942f316860 refs/heads/precompiled
+16:27:13.953812 pkt-line.c:80 packet: fetch< 48c0e1107ea4a89a22e71c1cba0bdc238d119a9f refs/heads/resourceconflict
+16:27:13.953818 pkt-line.c:80 packet: fetch< dbfac89a85485f8ca2107792a3ce964c06adefbf refs/heads/typed-os-requirements
+16:27:13.953823 pkt-line.c:80 packet: fetch< 96a4fcf180885788959d7dc136dbef544270fa81 refs/heads/wip-bytestring-privdata
+16:27:13.953829 pkt-line.c:80 packet: fetch< ee35c58303221ddb4c83c33eb12a52c59cd482c2 refs/remotes/abailly/master
+16:27:13.953834 pkt-line.c:80 packet: fetch< baf65fa9fff4b8451ba7f1ee129484723a8deb9b refs/remotes/db48x/fstab-swap
+16:27:13.953839 pkt-line.c:80 packet: fetch< 7d8f9dbf60f8ab345a75c4ee4f8c457d0fde5b43 refs/remotes/db48x/git-in-emtpy-directory
+16:27:13.953844 pkt-line.c:80 packet: fetch< 17abde8439d17d49676f549f357f45eb2adce868 refs/remotes/db48x/master
+16:27:13.953849 pkt-line.c:80 packet: fetch< de50503e4dbdea853e899f01e8828cf4f454dd57 refs/remotes/dgit/dgit/sid
+(omitted 300+ lines of refs)
+16:27:14.352945 pkt-line.c:80 packet: fetch< 0000
+From .
+ * branch HEAD -> FETCH_HEAD
+16:27:14.379922 pkt-line.c:80 packet: fetch> 0000
+Sending git update to kite.kitenet.net ... done
+</pre>
+
+> > Aha! My next spin failed again, with this debug:
+
+<pre>
+Sending privdata (73139 bytes) to kite.kitenet.net ... done
+[2017-06-18 16:31:15 EDT] received marked PRIVDATA
+[2017-06-18 16:31:15 EDT] requested marked GITPUSH
+[2017-06-18 16:31:16 EDT] received marked GITPUSH
+[2017-06-18 16:31:16 EDT] command line: GitPush 11 12
+16:31:16.361717 pkt-line.c:80 packet: fetch< 17abde8439d17d49676f549f357f45eb2adce868 refs/remotes/db48x/master
+<pre>
+
+> > So there's an actual protocol error here; the first 13 lines
+> > of git protocol were not sent.
+> >
+> > Question now is, where is the mangling happening?
+> >
+> > * Fairly sure it's not on the local side's sendGitUpdate,
+> > where "git upload-pack ." is simply run and fed over ssh.
+> > * Could be in gitPushHelper, perhaps it's failing to write
+> > some of the first lines somehow?
+> > * Could be something on the remote side is consuming stdin
+> > that is not supposed to, and eats some of the protocol.
+> >
+> >
+> > I added debug dumping to gitPushHelper, and it seems to be
+> > reading the same truncated data, so it seems the problem is not there.
+> >
+> > Aha! The problem comes from stdin/stdInput confusion here:
+
+ req NeedGitPush gitPushMarker $ \_ -> do
+ hin <- dup stdInput
+ hout <- dup stdOutput
+ hClose stdin
+ hClose stdout
+
+> > A line read from stdin just before the dup gets the first line of the protocol
+> > as expected. But reading from stdInput starts with a later line.
+> > Apparently data is being buffered in the stdin Handle, so gitPushHelper,
+> > which reads from the Fd, does not see it.
+> >
+> > Here's a simple test case. Feeding this 2 lines on stdin will
+> > print the first and then fail with "hGetLine: end of file".
+> > The second line is lost in the buffer. This test case behaves
+> > like that reliably, so I'm surprised propellor only fails sometimes.
+
+ main = do
+ l <- hGetLine stdin
+ print l
+ bob <- fdToHandle stdInput
+ l2 <- hGetLine bob
+ print l2
+
+> > > I thought I'd fixed this by disabling buffering of stdin, but
+> > > it seems not.
+
+> > > > Seems really [[done]] at last! --[[Joey]]
diff --git a/doc/todo/unpropelling_a_host.mdwn b/doc/todo/unpropelling_a_host.mdwn
new file mode 100644
index 00000000..5c31bd90
--- /dev/null
+++ b/doc/todo/unpropelling_a_host.mdwn
@@ -0,0 +1,9 @@
+We discussed at DebConf the need for a property that removes propellor from a host. It would run itself at the end of the spin. It needs to nuke `/usr/local/propellor`. To what extent can it remove propellor's build dependencies? I can see two problems to be resolved before writing any code.
+
+1. There is no standard way to remove cabal and stack packages from `/root` without potentially nuking stuff the user wants to keep. So maybe the property should remove only OS packages? I.e. best used on `OSOnly` hosts/chroots.
+
+2. What if another property on the host installs some or all of those build dependencies? This property would be cancelled out by the unpropellor property. Maybe properties that install packages need to [[set info about the packages that are meant to remain installed|todo/metapackage]]?
+
+The unpropellor property could just nuke `/usr/local/propellor` and leave it at that. But then the sbuild module would need to maintain a list of propellor's build deps to remove from the newly created chroot, which is a third copy of the list..
+
+--spwhitton
diff --git a/doc/todo/usage__47__help_text_improvements.mdwn b/doc/todo/usage__47__help_text_improvements.mdwn
new file mode 100644
index 00000000..80fffb3d
--- /dev/null
+++ b/doc/todo/usage__47__help_text_improvements.mdwn
@@ -0,0 +1,3 @@
+I started out looking at how to make usage.mdwn into a man page, but that's a little more work than I wanted to do tonight. Instead, I added more information to the usage message. Commit is fa0e8d83 on iabak:~db48x/propellor if you want it.
+
+> merged [[done]] tnx --[[Joey]]
diff --git a/doc/todo/usage__47__help_text_improvements/comment_1_66878945cdb57d06849337262d939701._comment b/doc/todo/usage__47__help_text_improvements/comment_1_66878945cdb57d06849337262d939701._comment
new file mode 100644
index 00000000..f30eae46
--- /dev/null
+++ b/doc/todo/usage__47__help_text_improvements/comment_1_66878945cdb57d06849337262d939701._comment
@@ -0,0 +1,13 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 1"""
+ date="2016-12-27T02:46:19Z"
+ content="""
+I don't like the use of tabs in that; it may be that with some terminal
+with an unusual tab stop, the things don't align.
+
+It would probably be simplest to put the description in the line under the
+option.
+
+BTW, the Makefile can build propellor.1 out of usage.mdwn
+"""]]
diff --git a/doc/todo/usage__47__help_text_improvements/comment_2_d531a45851cdef87a8f7b8182b3d04ce._comment b/doc/todo/usage__47__help_text_improvements/comment_2_d531a45851cdef87a8f7b8182b3d04ce._comment
new file mode 100644
index 00000000..62cf1fe4
--- /dev/null
+++ b/doc/todo/usage__47__help_text_improvements/comment_2_d531a45851cdef87a8f7b8182b3d04ce._comment
@@ -0,0 +1,12 @@
+[[!comment format=mdwn
+ username="db48x"
+ avatar="http://cdn.libravatar.org/avatar/ad2688127feb555a92154b16d8eeb5d3"
+ subject="comment 2"
+ date="2016-12-27T06:12:52Z"
+ content="""
+/me facepalms; of course it can. I guess I saw the 'git commit' in the install target and disregarded the rest.
+
+I removed the tabs from the usage. It's a lot longer, but I suppose it gets the job done.
+
+
+"""]]
diff --git a/doc/todo/use_stack_for_remote_building_propellor.mdwn b/doc/todo/use_stack_for_remote_building_propellor.mdwn
index 265596df..8c8751e7 100644
--- a/doc/todo/use_stack_for_remote_building_propellor.mdwn
+++ b/doc/todo/use_stack_for_remote_building_propellor.mdwn
@@ -1,3 +1,16 @@
Among other features [stack](https://github.com/commercialhaskell/stack/) provides a clean and deep dependency management system that even takes care of installing toolchain (ghc, alex, happy, cabal...) in a segregated environment. Building remote propellor with stack would remove the limitation that code should be compilable with stock ghc from package manager. I have done some preliminary work on this feature in my [github clone](https://github.com/abailly/propellor) for propellor, currently from 2.17.2 branch (I wanted to reuse existing properties). The code is mostly in [Bootstrap](https://github.com/abailly/propellor/blob/master/src/Propellor/Bootstrap.hs) and is currently limited to linux systems. Adapting to FreeBsd should be straightforward as this is supported by slack and there are native builds available.
If there is interest in such a feature I would be happy to move it to HEAD and provide a patch.
+
+> I've implemented a bootstrapWith property, which can be added to a Host
+> to make it use stack:
+>
+> & bootstrapWith (Robustly Stack)
+>
+> So, for a propellor install that uses stack entirely, use
+> `stack install propellor` to install it to your laptop,
+> use `propellor --init` to set up `~/.propellor/config,hs`,
+> and in the config file, add the above property to all your
+> Hosts (perhaps using `map` ..).
+>
+> I feel that's enough to call this [[done]]. --[[Joey]]
diff --git a/doc/usage.mdwn b/doc/usage.mdwn
index fec346ae..3d32538f 100644
--- a/doc/usage.mdwn
+++ b/doc/usage.mdwn
@@ -55,6 +55,14 @@ and configured in haskell.
The hostname given to --spin can be a short name, which is
then looked up in the DNS to find the FQDN.
+* propellor --build
+
+ Causes propellor to build itself, checking that your config.hs, etc are
+ valid.
+
+ You do not need to run this as a separate step; propellor automatically
+ builds itself when using things like --spin.
+
* propellor --add-key keyid
Adds a gpg key, which is used to encrypt the privdata.
@@ -66,6 +74,10 @@ and configured in haskell.
using this key. Propellor requires signed commits when pulling from
a central git repository.
+* propellor --rm-key keyid
+
+ Stops encrypting the privdata to a gpg key.
+
* propellor --list-fields
Lists all privdata fields that are used by your propellor configuration.
diff --git a/doc/user/craige.mdwn b/doc/user/craige.mdwn
new file mode 100644
index 00000000..775e2fb0
--- /dev/null
+++ b/doc/user/craige.mdwn
@@ -0,0 +1 @@
+It's been said I was the fourth user :-)
diff --git a/doc/user/spwhitton.mdwn b/doc/user/spwhitton.mdwn
new file mode 100644
index 00000000..f5f92fac
--- /dev/null
+++ b/doc/user/spwhitton.mdwn
@@ -0,0 +1 @@
+Maintainer of propellor's Debian package, and several modules.
diff --git a/joeyconfig.hs b/joeyconfig.hs
index 4c437664..85d323c1 100644
--- a/joeyconfig.hs
+++ b/joeyconfig.hs
@@ -4,6 +4,8 @@ module Main where
import Propellor
import Propellor.Property.Scheduled
+import Propellor.Property.DiskImage
+import Propellor.Property.Chroot
import qualified Propellor.Property.File as File
import qualified Propellor.Property.Apt as Apt
import qualified Propellor.Property.Network as Network
@@ -13,6 +15,7 @@ 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.Fstab as Fstab
import qualified Propellor.Property.Tor as Tor
import qualified Propellor.Property.Dns as Dns
import qualified Propellor.Property.OpenId as OpenId
@@ -25,7 +28,6 @@ import qualified Propellor.Property.Obnam as Obnam
import qualified Propellor.Property.Gpg as Gpg
import qualified Propellor.Property.Systemd as Systemd
import qualified Propellor.Property.Journald as Journald
-import qualified Propellor.Property.Chroot as Chroot
import qualified Propellor.Property.Fail2Ban as Fail2Ban
import qualified Propellor.Property.Aiccu as Aiccu
import qualified Propellor.Property.OS as OS
@@ -36,7 +38,6 @@ import qualified Propellor.Property.SiteSpecific.GitHome as GitHome
import qualified Propellor.Property.SiteSpecific.GitAnnexBuilder as GitAnnexBuilder
import qualified Propellor.Property.SiteSpecific.Branchable as Branchable
import qualified Propellor.Property.SiteSpecific.JoeySites as JoeySites
-import Propellor.Property.DiskImage
main :: IO () -- _ ______`| ,-.__
main = defaultMain hosts -- / \___-=O`/|O`/__| (____.'
@@ -46,14 +47,15 @@ hosts :: [Host] -- * \ | | '--------'
hosts = -- (o) `
[ darkstar
, gnu
+ , dragon
, clam
- , mayfly
- , oyster
, orca
+ , baleen
, honeybee
, kite
, elephant
, beaver
+ , mouse
, pell
, keysafe
] ++ monsters
@@ -69,7 +71,7 @@ testvm = host "testvm.kitenet.net" $ props
& Apt.installed ["ssh"]
& User.hasPassword (User "root")
where
- postinstall :: Property DebianLike
+ postinstall :: Property (HasInfo + DebianLike)
postinstall = propertyList "fixing up after clean install" $ props
& OS.preserveRootSshAuthorized
& OS.preserveResolvConf
@@ -79,41 +81,45 @@ testvm = host "testvm.kitenet.net" $ props
darkstar :: Host
darkstar = host "darkstar.kitenet.net" $ props
+ & osDebian Unstable X86_64
& ipv6 "2001:4830:1600:187::2"
& Aiccu.hasConfig "T18376" "JHZ2-SIXXS"
- & Apt.buildDep ["git-annex"] `period` Daily
+ & User.nuked (User "nosuchuser") User.YesReallyDeleteHome
& JoeySites.dkimMilter
- & JoeySites.alarmClock "*-*-* 7:30" (User "joey")
- "/usr/bin/timeout 45m /home/joey/bin/goodmorning"
+ & JoeySites.postfixSaslPasswordClient
+ -- & JoeySites.alarmClock "*-*-* 7:30" (User "joey")
+ -- "/usr/bin/timeout 45m /home/joey/bin/goodmorning"
& Ssh.userKeys (User "joey") hostContext
[ (SshRsa, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC1YoyHxZwG5Eg0yiMTJLSWJ/+dMM6zZkZiR4JJ0iUfP+tT2bm/lxYompbSqBeiCq+PYcSC67mALxp1vfmdOV//LWlbXfotpxtyxbdTcQbHhdz4num9rJQz1tjsOsxTEheX5jKirFNC5OiKhqwIuNydKWDS9qHGqsKcZQ8p+n1g9Lr3nJVGY7eRRXzw/HopTpwmGmAmb9IXY6DC2k91KReRZAlOrk0287LaK3eCe1z0bu7LYzqqS+w99iXZ/Qs0m9OqAPnHZjWQQ0fN4xn5JQpZSJ7sqO38TBAimM+IHPmy2FTNVVn9zGM+vN1O2xr3l796QmaUG1+XLL0shfR/OZbb joey@darkstar")
]
-
- ! imageBuilt "/tmp/img" c MSDOS (grubBooted PC)
+ & imageBuilt (VirtualBoxPointer "/srv/test.vmdk") mychroot MSDOS
[ partition EXT2 `mountedAt` "/boot"
- `setFlag` BootFlag
, partition EXT4 `mountedAt` "/"
- `mountOpt` errorReadonly
, swapPartition (MegaBytes 256)
]
where
- c d = Chroot.debootstrapped mempty d $ props
+ mychroot d = debootstrapped mempty d $ props
& osDebian Unstable X86_64
- & Hostname.setTo "demo"
& Apt.installed ["linux-image-amd64"]
- & User "root" `User.hasInsecurePassword` "root"
+ & Grub.installed PC
gnu :: Host
gnu = host "gnu.kitenet.net" $ props
- & Apt.buildDep ["git-annex"] `period` Daily
+ & Postfix.satellite
+
+dragon :: Host
+dragon = host "dragon.kitenet.net" $ props
+ & ipv6 "2001:4830:1600:187::2"
+ & JoeySites.dkimMilter
+ & JoeySites.postfixSaslPasswordClient
clam :: Host
clam = host "clam.kitenet.net" $ props
& standardSystem Unstable X86_64
["Unreliable server. Anything here may be lost at any time!" ]
- & ipv4 "167.88.41.194"
+ & ipv4 "45.62.211.6"
& CloudAtCost.decruft
& Ssh.hostKeys hostContext
@@ -122,65 +128,34 @@ clam = host "clam.kitenet.net" $ props
, (SshEcdsa, "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPhfvcOuw0Yt+MnsFc4TI2gWkKi62Eajxz+TgbHMO/uRTYF8c5V8fOI3o+J/3m5+lT0S5o8j8a7xIC3COvi+AVw=")
]
& Apt.unattendedUpgrades
- & Network.ipv6to4
& Systemd.persistentJournal
- & Journald.systemMaxUse "500MiB"
+ & Journald.systemMaxUse "50MiB"
& Tor.isRelay
& Tor.named "kite1"
& Tor.bandwidthRate (Tor.PerMonth "400 GB")
- & Systemd.nspawned webserver
- & File.dirExists "/var/www/html"
- & File.notPresent "/var/www/index.html"
- & "/var/www/html/index.html" `File.hasContent` ["hello, world"]
- & alias "helloworld.kitenet.net"
-
& Systemd.nspawned oldusenetShellBox
& JoeySites.scrollBox
& alias "scroll.joeyh.name"
& alias "us.scroll.joeyh.name"
-mayfly :: Host
-mayfly = host "mayfly.kitenet.net" $ props
- & standardSystem (Stable "jessie") X86_64
- [ "Scratch VM. Contents can change at any time!" ]
- & ipv4 "167.88.36.193"
-
- & CloudAtCost.decruft
- & Apt.unattendedUpgrades
- & Network.ipv6to4
- & Systemd.persistentJournal
- & Journald.systemMaxUse "500MiB"
-
- & Tor.isRelay
- & Tor.named "kite3"
- & Tor.bandwidthRate (Tor.PerMonth "400 GB")
-
-oyster :: Host
-oyster = host "oyster.kitenet.net" $ props
- & standardSystem Unstable X86_64
- [ "Unreliable server. Anything here may be lost at any time!" ]
- & ipv4 "64.137.221.146"
-
- & CloudAtCost.decruft
- & Ssh.hostKeys hostContext
- [ (SshEcdsa, "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBP0ws/IxQegVU0RhqnIm5A/vRSPTO70wD4o2Bd1jL970dTetNyXzvWGe1spEbLjIYSLIO7WvOBSE5RhplBKFMUU=")
- ]
+baleen :: Host
+baleen = host "baleen.kitenet.net" $ props
+ & standardSystem Unstable X86_64 [ "New git-annex build box." ]
+
+ -- Not on public network; ssh access via bounce host.
+ & ipv4 "138.38.77.40"
+
+ -- The root filesystem content may be lost if the VM is resized.
+ -- /dev/vdb contains persistent storage.
+ & Fstab.mounted "auto" "/dev/vdb" "/var/lib/container" mempty
+
& Apt.unattendedUpgrades
- & Network.ipv6to4
+ & Postfix.satellite
+ & Apt.serviceInstalledRunning "ntp"
& Systemd.persistentJournal
- & Journald.systemMaxUse "500MiB"
-
- & Tor.isRelay
- & Tor.named "kite2"
- & Tor.bandwidthRate (Tor.PerMonth "400 GB")
-
- -- Nothing is using http port 80, so listen on
- -- that port for ssh, for traveling on bad networks that
- -- block 22.
- & Ssh.listenPort (Port 80)
orca :: Host
orca = host "orca.kitenet.net" $ props
@@ -206,34 +181,46 @@ orca = host "orca.kitenet.net" $ props
honeybee :: Host
honeybee = host "honeybee.kitenet.net" $ props
- & standardSystem Testing ARMHF [ "Arm git-annex build box." ]
+ & standardSystem Testing ARMHF
+ [ "Home router and arm git-annex build box." ]
- -- I have to travel to get console access, so no automatic
- -- upgrades, and try to be robust.
+ -- Hard to get console access, so no automatic upgrades,
+ -- and try to be robust.
& "/etc/default/rcS" `File.containsLine` "FSCKFIX=yes"
+ -- Cubietruck
& Apt.installed ["flash-kernel"]
& "/etc/flash-kernel/machine" `File.hasContent` ["Cubietech Cubietruck"]
& Apt.installed ["linux-image-armmp"]
- & Network.dhcp "eth0" `requires` Network.cleanInterfacesFile
- & Postfix.satellite
-
- -- ipv6 used for remote access thru firewalls
- & Apt.serviceInstalledRunning "aiccu"
- & ipv6 "2001:4830:1600:187::2"
- -- restart to deal with failure to connect, tunnel issues, etc
- & Cron.job "aiccu restart daily" Cron.Daily (User "root") "/"
- "service aiccu stop; service aiccu start"
+ & Apt.installed ["firmware-brcm80211"]
+ -- Workaround for https://bugs.debian.org/844056
+ `requires` File.hasPrivContent "/lib/firmware/brcm/brcmfmac43362-sdio.txt" anyContext
+ `requires` File.dirExists "/lib/firmware/brcm"
- -- In case compiler needs more than available ram
- & Apt.serviceInstalledRunning "swapspace"
-
- -- No hardware clock.
+ -- No hardware clock
& Apt.serviceInstalledRunning "ntp"
+ & JoeySites.homePowerMonitor
+ (User "joey")
+ (Context "homepower.joeyh.name")
+ (SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMAmVYddg/RgCbIj+cLcEiddeFXaYFnbEJ3uGj9G/EyV joey@honeybee")
+ & JoeySites.homeRouter
+ & Apt.installed ["mtr-tiny", "iftop", "screen"]
+ & Postfix.satellite
+
& Systemd.nspawned (GitAnnexBuilder.autoBuilderContainer
GitAnnexBuilder.armAutoBuilder
- Unstable ARMEL Nothing Cron.Daily "22h")
+ Unstable ARMEL Nothing (Cron.Times "15 10 * * *") "10h")
+ -- Disabled because it does not work, and the old systemd
+ -- in the container uses a ton of CPU
+ ! Systemd.nspawned (GitAnnexBuilder.autoBuilderContainer
+ GitAnnexBuilder.stackAutoBuilder
+ (Stable "jessie") ARMEL (Just "ancient") weekdays "10h")
+ -- In case compiler needs more than available ram
+ & Apt.serviceInstalledRunning "swapspace"
+ where
+ weekdays = Cron.Times "15 10 * * 2-5"
+ -- weekends = Cron.Times "15 10 * * 6-7"
-- This is not a complete description of kite, since it's a
-- multiuser system with eg, user passwords that are not deployed
@@ -242,7 +229,7 @@ kite :: Host
kite = host "kite.kitenet.net" $ props
& standardSystemUnhardened Testing X86_64 [ "Welcome to kite!" ]
& ipv4 "66.228.36.95"
- & ipv6 "2600:3c03::f03c:91ff:fe73:b0d2"
+ -- & ipv6 "2600:3c03::f03c:91ff:fe73:b0d2"
& alias "kitenet.net"
& alias "wren.kitenet.net" -- temporary
& Ssh.hostKeys (Context "kitenet.net")
@@ -252,7 +239,7 @@ kite = host "kite.kitenet.net" $ props
, (SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFZftKMnH/zH29BHMKbcBO4QsgTrstYFVhbrzrlRzBO3")
]
- & Network.static "eth0" `requires` Network.cleanInterfacesFile
+ & Network.preserveStatic "eth0" `requires` Network.cleanInterfacesFile
& Apt.installed ["linux-image-amd64"]
& Linode.serialGrub
& Linode.mlocateEnabled
@@ -262,6 +249,8 @@ kite = host "kite.kitenet.net" $ props
& Journald.systemMaxUse "500MiB"
& Ssh.passwordAuthentication True
& Fail2Ban.installed -- since ssh password authentication is allowed
+ -- Allow ssh -R to forward ports via kite
+ & Ssh.setSshdConfig "GatewayPorts" "clientspecified"
& Apt.serviceInstalledRunning "ntp"
& "/etc/timezone" `File.hasContent` ["US/Eastern"]
@@ -332,7 +321,10 @@ kite = host "kite.kitenet.net" $ props
& JoeySites.oldUseNetServer hosts
& alias "ns4.kitenet.net"
- & myDnsPrimary True "kitenet.net" []
+ & myDnsPrimary True "kitenet.net"
+ [ (RelDomain "mouse-onion", CNAME $ AbsDomain "htieo6yu2qtcn2j3.onion")
+ , (RelDomain "beaver-onion", CNAME $ AbsDomain "tl4xsvaxryjylgxs.onion")
+ ]
& myDnsPrimary True "joeyh.name" []
& myDnsPrimary True "ikiwiki.info" []
& myDnsPrimary True "olduse.net"
@@ -341,6 +333,16 @@ kite = host "kite.kitenet.net" $ props
& alias "ns4.branchable.com"
& branchableSecondary
& Dns.secondaryFor ["animx"] hosts "animx.eu.org"
+ -- Use its own name server (amoung other things this avoids
+ -- spamassassin URIBL_BLOCKED.
+ & "/etc/resolv.conf" `File.hasContent`
+ [ "nameserver 127.0.0.1"
+ , "domain kitenet.net"
+ , "search kitenet.net"
+ ]
+ & alias "debug-me.joeyh.name"
+ -- debug-me installed manually until package is available
+ & Systemd.enabled "debug-me"
-- testing
& Apache.httpsVirtualHost "letsencrypt.joeyh.name" "/var/www/html"
@@ -377,8 +379,7 @@ elephant = host "elephant.kitenet.net" $ props
& Apt.serviceInstalledRunning "swapspace"
& alias "eubackup.kitenet.net"
- & Apt.installed ["obnam", "sshfs", "rsync"]
- & JoeySites.obnamRepos ["pell", "kite"]
+ & Apt.installed ["obnam", "sshfs", "rsync", "borgbackup"]
& JoeySites.githubBackup
& JoeySites.rsyncNetBackup hosts
@@ -417,15 +418,23 @@ elephant = host "elephant.kitenet.net" $ props
beaver :: Host
beaver = host "beaver.kitenet.net" $ props
& ipv6 "2001:4830:1600:195::2"
- & Apt.serviceInstalledRunning "aiccu"
& Apt.installed ["ssh"]
& Ssh.hostPubKey SshDsa "ssh-dss AAAAB3NzaC1kc3MAAACBAIrLX260fY0Jjj/p0syNhX8OyR8hcr6feDPGOj87bMad0k/w/taDSOzpXe0Wet7rvUTbxUjH+Q5wPd4R9zkaSDiR/tCb45OdG6JsaIkmqncwe8yrU+pqSRCxttwbcFe+UU+4AAcinjVedZjVRDj2rRaFPc9BXkPt7ffk8GwEJ31/AAAAFQCG/gOjObsr86vvldUZHCteaJttNQAAAIB5nomvcqOk/TD07DLaWKyG7gAcW5WnfY3WtnvLRAFk09aq1EuiJ6Yba99Zkb+bsxXv89FWjWDg/Z3Psa22JMyi0HEDVsOevy/1sEQ96AGH5ijLzFInfXAM7gaJKXASD7hPbVdjySbgRCdwu0dzmQWHtH+8i1CMVmA2/a5Y/wtlJAAAAIAUZj2US2D378jBwyX1Py7e4sJfea3WSGYZjn4DLlsLGsB88POuh32aOChd1yzF6r6C2sdoPBHQcWBgNGXcx4gF0B5UmyVHg3lIX2NVSG1ZmfuLNJs9iKNu4cHXUmqBbwFYQJBvB69EEtrOw4jSbiTKwHFmqdA/mw1VsMB+khUaVw=="
+ & Tor.installed
+ & Tor.hiddenServiceAvailable "ssh" (Port 22)
& alias "usbackup.kitenet.net"
& JoeySites.backupsBackedupFrom hosts "eubackup.kitenet.net" "/home/joey/lib/backup"
& Apt.serviceInstalledRunning "anacron"
& Cron.niceJob "system disk backed up" Cron.Weekly (User "root") "/"
"rsync -a -x / /home/joey/lib/backup/beaver.kitenet.net/"
+mouse :: Host
+mouse = host "mouse.kitenet.net" $ props
+ & ipv4 "67.223.19.96"
+ & Apt.installed ["ssh"]
+ & Tor.installed
+ & Tor.hiddenServiceAvailable "ssh" (Port 22)
+
-- Branchable is not completely deployed with propellor yet.
pell :: Host
pell = host "pell.branchable.com" $ props
@@ -448,7 +457,8 @@ pell = host "pell.branchable.com" $ props
& alias "dist-bugs.kitenet.net"
& alias "family.kitenet.net"
- & Apt.installed ["linux-image-amd64"]
+ & osDebian (Stable "stretch") X86_64
+ & Apt.installed ["linux-image-686-pae"]
& Apt.unattendedUpgrades
& Branchable.server hosts
& Linode.serialGrub
@@ -458,7 +468,7 @@ keysafe :: Host
keysafe = host "keysafe.joeyh.name" $ props
& ipv4 "139.59.17.168"
& Hostname.sane
- & osDebian (Stable "jessie") X86_64
+ & osDebian (Stable "stretch") X86_64
& Apt.stdSourcesList `onChange` Apt.upgrade
& Apt.unattendedUpgrades
& DigitalOcean.distroKernel
@@ -514,18 +524,11 @@ keysafe = host "keysafe.joeyh.name" $ props
--------------------------- \____, o ,' ----------------------------
---------------------------- '--,___________,' -----------------------------
--- Simple web server, publishing the outside host's /var/www
-webserver :: Systemd.Container
-webserver = Systemd.debContainer "webserver" $ props
- & standardContainer (Stable "jessie")
- & Systemd.bind "/var/www"
- & Apache.installed
-
-- My own openid provider. Uses php, so containerized for security
-- and administrative sanity.
openidProvider :: Systemd.Container
openidProvider = Systemd.debContainer "openid-provider" $ props
- & standardContainer (Stable "jessie")
+ & standardContainer (Stable "stretch")
& alias hn
& OpenId.providerFor [User "joey", User "liw"] hn (Just (Port 8081))
where
@@ -534,7 +537,7 @@ openidProvider = Systemd.debContainer "openid-provider" $ props
-- Exhibit: kite's 90's website on port 1994.
ancientKitenet :: Systemd.Container
ancientKitenet = Systemd.debContainer "ancient-kitenet" $ props
- & standardContainer (Stable "jessie")
+ & standardContainer (Stable "stretch")
& alias hn
& Git.cloned (User "root") "git://kitenet-net.branchable.com/" "/var/www/html"
(Just "remotes/origin/old-kitenet.net")
@@ -548,13 +551,13 @@ ancientKitenet = Systemd.debContainer "ancient-kitenet" $ props
oldusenetShellBox :: Systemd.Container
oldusenetShellBox = Systemd.debContainer "oldusenet-shellbox" $ props
- & standardContainer (Stable "jessie")
+ & standardContainer (Stable "stretch")
& alias "shell.olduse.net"
& JoeySites.oldUseNetShellBox
kiteShellBox :: Systemd.Container
kiteShellBox = Systemd.debContainer "kiteshellbox" $ props
- & standardContainer (Stable "jessie")
+ & standardContainer (Stable "stretch")
& JoeySites.kiteShellBox
type Motd = [String]
@@ -633,14 +636,9 @@ monsters = -- but do want to track their public keys etc.
& Ssh.hostPubKey SshEcdsa "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY="
, host "ns6.gandi.net" $ props
& ipv4 "217.70.177.40"
- , host "turtle.kitenet.net" $ props
- & ipv4 "67.223.19.96"
- & ipv6 "2001:4978:f:2d9::2"
- , host "mouse.kitenet.net" $ props
- & ipv6 "2001:4830:1600:492::2"
, host "animx" $ props
- & ipv4 "76.7.162.101"
& ipv4 "76.7.162.186"
+ & ipv4 "76.7.162.187"
]
diff --git a/privdata/.joeyconfig/keyring.gpg b/privdata/.joeyconfig/keyring.gpg
index 01dd24e7..d5a1c070 100644
--- a/privdata/.joeyconfig/keyring.gpg
+++ b/privdata/.joeyconfig/keyring.gpg
Binary files differ
diff --git a/privdata/.joeyconfig/privdata.gpg b/privdata/.joeyconfig/privdata.gpg
index ef3b128a..81f227ec 100644
--- a/privdata/.joeyconfig/privdata.gpg
+++ b/privdata/.joeyconfig/privdata.gpg
@@ -1,1424 +1,1482 @@
-----BEGIN PGP MESSAGE-----
-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
-=Gbho
+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
+=Wl7d
-----END PGP MESSAGE-----
diff --git a/propellor.cabal b/propellor.cabal
index e5f8b63b..8089c107 100644
--- a/propellor.cabal
+++ b/propellor.cabal
@@ -1,6 +1,6 @@
Name: propellor
-Version: 3.2.3
-Cabal-Version: >= 1.8
+Version: 4.7.7
+Cabal-Version: >= 1.20
License: BSD2
Maintainer: Joey Hess <id@joeyh.name>
Author: Joey Hess
@@ -36,46 +36,49 @@ Description:
It is configured using haskell.
Executable propellor
+ Default-Language: Haskell98
Main-Is: wrapper.hs
GHC-Options: -threaded -Wall -fno-warn-tabs -O0
if impl(ghc >= 8.0)
GHC-Options: -fno-warn-redundant-constraints
- Extensions: TypeOperators
+ Default-Extensions: TypeOperators
Hs-Source-Dirs: src
Build-Depends:
-- propellor needs to support the ghc shipped in Debian stable,
-- and also only depends on packages in Debian stable.
base >= 4.5, base < 5,
- MissingH, directory, filepath, IfElse, process, bytestring, hslogger,
+ directory, filepath, IfElse, process, bytestring, hslogger, split,
unix, unix-compat, ansi-terminal, containers (>= 0.5), network, async,
- time, mtl, transformers, exceptions (>= 0.6), stm, text
+ time, mtl, transformers, exceptions (>= 0.6), stm, text, hashable
Other-Modules:
Propellor.DotDir
Executable propellor-config
- Main-Is: config.hs
+ Default-Language: Haskell98
+ Main-Is: propellor-config.hs
GHC-Options: -threaded -Wall -fno-warn-tabs -O0
if impl(ghc >= 8.0)
GHC-Options: -fno-warn-redundant-constraints
- Extensions: TypeOperators
+ Default-Extensions: TypeOperators
Hs-Source-Dirs: src
Build-Depends:
base >= 4.5, base < 5,
- MissingH, directory, filepath, IfElse, process, bytestring, hslogger,
+ directory, filepath, IfElse, process, bytestring, hslogger, split,
unix, unix-compat, ansi-terminal, containers (>= 0.5), network, async,
- time, mtl, transformers, exceptions (>= 0.6), stm, text
+ time, mtl, transformers, exceptions (>= 0.6), stm, text, hashable
Library
+ Default-Language: Haskell98
GHC-Options: -Wall -fno-warn-tabs -O0
if impl(ghc >= 8.0)
GHC-Options: -fno-warn-redundant-constraints
- Extensions: TypeOperators
+ Default-Extensions: TypeOperators
Hs-Source-Dirs: src
Build-Depends:
base >= 4.5, base < 5,
- MissingH, directory, filepath, IfElse, process, bytestring, hslogger,
+ directory, filepath, IfElse, process, bytestring, hslogger, split,
unix, unix-compat, ansi-terminal, containers (>= 0.5), network, async,
- time, mtl, transformers, exceptions (>= 0.6), stm, text
+ time, mtl, transformers, exceptions (>= 0.6), stm, text, hashable
Exposed-Modules:
Propellor
@@ -87,6 +90,7 @@ Library
Propellor.Property.Apt
Propellor.Property.Apt.PPA
Propellor.Property.Attic
+ Propellor.Property.Bootstrap
Propellor.Property.Borg
Propellor.Property.Ccache
Propellor.Property.Cmd
@@ -110,6 +114,7 @@ Library
Propellor.Property.FreeBSD
Propellor.Property.FreeBSD.Pkg
Propellor.Property.FreeBSD.Poudriere
+ Propellor.Property.FreeDesktop
Propellor.Property.Fstab
Propellor.Property.Git
Propellor.Property.Gpg
@@ -129,12 +134,15 @@ Library
Propellor.Property.Obnam
Propellor.Property.OpenId
Propellor.Property.OS
+ Propellor.Property.Pacman
Propellor.Property.Parted
+ Propellor.Property.Parted.Types
Propellor.Property.Partition
Propellor.Property.Postfix
Propellor.Property.PropellorRepo
Propellor.Property.Prosody
Propellor.Property.Reboot
+ Propellor.Property.Restic
Propellor.Property.Rsync
Propellor.Property.Sbuild
Propellor.Property.Scheduled
@@ -144,10 +152,13 @@ Library
Propellor.Property.Sudo
Propellor.Property.Systemd
Propellor.Property.Systemd.Core
+ Propellor.Property.Timezone
Propellor.Property.Tor
Propellor.Property.Unbound
Propellor.Property.User
Propellor.Property.Uwsgi
+ Propellor.Property.Versioned
+ Propellor.Property.XFCE
Propellor.Property.ZFS
Propellor.Property.ZFS.Process
Propellor.Property.ZFS.Properties
@@ -171,6 +182,8 @@ Library
Propellor.EnsureProperty
Propellor.Exception
Propellor.Types
+ Propellor.Types.Bootloader
+ Propellor.Types.ConfigurableValue
Propellor.Types.Core
Propellor.Types.Chroot
Propellor.Types.CmdLine
@@ -182,6 +195,7 @@ Library
Propellor.Types.Info
Propellor.Types.MetaTypes
Propellor.Types.OS
+ Propellor.Types.PartSpec
Propellor.Types.PrivData
Propellor.Types.Result
Propellor.Types.ResultCheck
@@ -219,10 +233,13 @@ Library
Utility.Process.NonConcurrent
Utility.SafeCommand
Utility.Scheduled
+ Utility.Scheduled
+ Utility.Split
Utility.SystemDirectory
Utility.Table
Utility.ThreadScheduler
Utility.Tmp
+ Utility.Tuple
Utility.UserInfo
System.Console.Concurrent
System.Console.Concurrent.Internal
@@ -230,4 +247,4 @@ Library
source-repository head
type: git
- location: git://git.joeyh.name/propellor.git
+ location: https://git.joeyh.name/git/propellor.git
diff --git a/src/Propellor/Bootstrap.hs b/src/Propellor/Bootstrap.hs
index 2c8fa95a..08af6878 100644
--- a/src/Propellor/Bootstrap.hs
+++ b/src/Propellor/Bootstrap.hs
@@ -1,8 +1,16 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+
module Propellor.Bootstrap (
+ Bootstrapper(..),
+ Builder(..),
+ defaultBootstrapper,
+ getBootstrapper,
bootstrapPropellorCommand,
checkBinaryCommand,
installGitCommand,
buildPropellor,
+ checkDepsCommand,
+ buildCommand,
) where
import Propellor.Base
@@ -14,74 +22,124 @@ import Data.List
type ShellCommand = String
+-- | Different ways that Propellor's dependencies can be installed,
+-- and propellor can be built. The default is `Robustly Cabal`
+--
+-- `Robustly Cabal` and `Robustly Stack` use the OS's native packages
+-- as much as possible to install Cabal, Stack, and propellor's build
+-- dependencies. When necessary, dependencies are built from source
+-- using Cabal or Stack rather than using the OS's native packages.
+--
+-- `OSOnly` uses the OS's native packages of Cabal and all of propellor's
+-- build dependencies. It may not work on all systems.
+data Bootstrapper = Robustly Builder | OSOnly
+ deriving (Show, Typeable)
+
+data Builder = Cabal | Stack
+ deriving (Show, Typeable)
+
+defaultBootstrapper :: Bootstrapper
+defaultBootstrapper = Robustly Cabal
+
+-- | Gets the Bootstrapper for the Host propellor is running on.
+getBootstrapper :: Propellor Bootstrapper
+getBootstrapper = go <$> askInfo
+ where
+ go NoInfoVal = defaultBootstrapper
+ go (InfoVal bs) = bs
+
+getBuilder :: Bootstrapper -> Builder
+getBuilder (Robustly b) = b
+getBuilder OSOnly = Cabal
+
-- Shell command line to ensure propellor is bootstrapped and ready to run.
-- Should be run inside the propellor config dir, and will install
-- all necessary build dependencies and build propellor.
-bootstrapPropellorCommand :: Maybe System -> ShellCommand
-bootstrapPropellorCommand msys = checkDepsCommand msys ++
+bootstrapPropellorCommand :: Bootstrapper -> Maybe System -> ShellCommand
+bootstrapPropellorCommand bs msys = checkDepsCommand bs msys ++
"&& if ! test -x ./propellor; then "
- ++ buildCommand ++
- "; fi;" ++ checkBinaryCommand
+ ++ buildCommand bs ++
+ "; fi;" ++ checkBinaryCommand bs
-- Use propellor --check to detect if the local propellor binary has
-- stopped working (eg due to library changes), and must be rebuilt.
-checkBinaryCommand :: ShellCommand
-checkBinaryCommand = "if test -x ./propellor && ! ./propellor --check; then " ++ go ++ "; fi"
+checkBinaryCommand :: Bootstrapper -> ShellCommand
+checkBinaryCommand bs = "if test -x ./propellor && ! ./propellor --check; then " ++ go (getBuilder bs) ++ "; fi"
where
- go = intercalate " && "
+ go Cabal = intercalate " && "
[ "cabal clean"
- , buildCommand
+ , buildCommand bs
+ ]
+ go Stack = intercalate " && "
+ [ "stack clean"
+ , buildCommand bs
]
-buildCommand :: ShellCommand
-buildCommand = intercalate " && "
- [ "cabal configure"
- , "cabal build propellor-config"
- , "ln -sf dist/build/propellor-config/propellor-config propellor"
- ]
+buildCommand :: Bootstrapper -> ShellCommand
+buildCommand bs = intercalate " && " (go (getBuilder bs))
+ where
+ go Cabal =
+ [ "cabal configure"
+ , "cabal build propellor-config"
+ , "ln -sf dist/build/propellor-config/propellor-config propellor"
+ ]
+ go Stack =
+ [ "stack build :propellor-config"
+ , "ln -sf $(stack path --dist-dir)/build/propellor-config/propellor-config propellor"
+ ]
--- Run cabal configure to check if all dependencies are installed;
--- if not, run the depsCommand.
-checkDepsCommand :: Maybe System -> ShellCommand
-checkDepsCommand sys = "if ! cabal configure >/dev/null 2>&1; then " ++ depsCommand sys ++ "; fi"
+-- Check if all dependencies are installed; if not, run the depsCommand.
+checkDepsCommand :: Bootstrapper -> Maybe System -> ShellCommand
+checkDepsCommand bs sys = go (getBuilder bs)
+ where
+ go Cabal = "if ! cabal configure >/dev/null 2>&1; then " ++ depsCommand bs sys ++ "; fi"
+ go Stack = "if ! stack build --dry-run >/dev/null 2>&1; then " ++ depsCommand bs sys ++ "; fi"
--- Install build dependencies of propellor.
---
--- First, try to install ghc, cabal, gnupg, and all haskell libraries that
--- propellor uses from OS packages.
+-- Install build dependencies of propellor, using the specified
+-- Bootstrapper.
--
+-- When bootstrapping Robustly, first try to install the builder,
+-- and all haskell libraries that propellor uses from OS packages.
-- Some packages may not be available in some versions of Debian
-- (eg, Debian wheezy lacks async), or propellor may need a newer version.
--- So, as a second step, cabal is used to install all dependencies.
+-- So, as a second step, any other dependencies are installed from source
+-- using the builder.
--
-- Note: May succeed and leave some deps not installed.
-depsCommand :: Maybe System -> ShellCommand
-depsCommand msys = "( " ++ intercalate " ; " (concat [osinstall, cabalinstall]) ++ " ) || true"
+depsCommand :: Bootstrapper -> Maybe System -> ShellCommand
+depsCommand bs msys = "( " ++ intercalate " ; " (go bs) ++ ") || true"
where
- osinstall = case msys of
- Just (System (FreeBSD _) _) -> map pkginstall fbsddeps
- Just (System (Debian _ _) _) -> useapt
- Just (System (Buntish _) _) -> useapt
- -- assume a debian derived system when not specified
- Nothing -> useapt
-
- useapt = "apt-get update" : map aptinstall debdeps
-
- cabalinstall =
+ go (Robustly Cabal) = osinstall Cabal ++
[ "cabal update"
, "cabal install --only-dependencies"
+ ]
+ go (Robustly Stack) = osinstall Stack ++
+ [ "stack setup"
+ , "stack build --only-dependencies :propellor-config"
]
+ go OSOnly = osinstall Cabal
+
+ osinstall builder = case msys of
+ Just (System (FreeBSD _) _) -> map pkginstall (fbsddeps builder)
+ Just (System (ArchLinux) _) -> map pacmaninstall (archlinuxdeps builder)
+ Just (System (Debian _ _) _) -> useapt builder
+ Just (System (Buntish _) _) -> useapt builder
+ -- assume a Debian derived system when not specified
+ Nothing -> useapt builder
+
+ useapt builder = "apt-get update" : map aptinstall (debdeps builder)
aptinstall p = "DEBIAN_FRONTEND=noninteractive apt-get -qq --no-upgrade --no-install-recommends -y install " ++ p
pkginstall p = "ASSUME_ALWAYS_YES=yes pkg install " ++ p
+ pacmaninstall p = "pacman -S --noconfirm --needed " ++ p
-- This is the same deps listed in debian/control.
- debdeps =
+ debdeps Cabal =
[ "gnupg"
, "ghc"
, "cabal-install"
, "libghc-async-dev"
- , "libghc-missingh-dev"
+ , "libghc-split-dev"
, "libghc-hslogger-dev"
, "libghc-unix-compat-dev"
, "libghc-ansi-terminal-dev"
@@ -92,14 +150,19 @@ depsCommand msys = "( " ++ intercalate " ; " (concat [osinstall, cabalinstall])
, "libghc-exceptions-dev"
, "libghc-stm-dev"
, "libghc-text-dev"
- , "make"
+ , "libghc-hashable-dev"
+ ]
+ debdeps Stack =
+ [ "gnupg"
+ , "haskell-stack"
]
- fbsddeps =
+
+ fbsddeps Cabal =
[ "gnupg"
, "ghc"
, "hs-cabal-install"
, "hs-async"
- , "hs-MissingH"
+ , "hs-split"
, "hs-hslogger"
, "hs-unix-compat"
, "hs-ansi-terminal"
@@ -110,7 +173,35 @@ depsCommand msys = "( " ++ intercalate " ; " (concat [osinstall, cabalinstall])
, "hs-exceptions"
, "hs-stm"
, "hs-text"
- , "gmake"
+ , "hs-hashable"
+ ]
+ fbsddeps Stack =
+ [ "gnupg"
+ , "stack"
+ ]
+
+ archlinuxdeps Cabal =
+ [ "gnupg"
+ , "ghc"
+ , "cabal-install"
+ , "haskell-async"
+ , "haskell-split"
+ , "haskell-hslogger"
+ , "haskell-unix-compat"
+ , "haskell-ansi-terminal"
+ , "haskell-hackage-security"
+ , "haskell-ifelse"
+ , "haskell-network"
+ , "haskell-mtl"
+ , "haskell-transformers-base"
+ , "haskell-exceptions"
+ , "haskell-stm"
+ , "haskell-text"
+ , "hashell-hashable"
+ ]
+ archlinuxdeps Stack =
+ [ "gnupg"
+ , "stack"
]
installGitCommand :: Maybe System -> ShellCommand
@@ -121,31 +212,39 @@ installGitCommand msys = case msys of
[ "ASSUME_ALWAYS_YES=yes pkg update"
, "ASSUME_ALWAYS_YES=yes pkg install git"
]
+ (Just (System (ArchLinux) _)) -> use
+ [ "pacman -S --noconfirm --needed git"]
-- assume a debian derived system when not specified
Nothing -> use apt
where
- use cmds = "if ! git --version >/dev/null; then " ++ intercalate " && " cmds ++ "; fi"
+ use cmds = "if ! git --version >/dev/null 2>&1; then " ++ intercalate " && " cmds ++ "; fi"
apt =
[ "apt-get update"
, "DEBIAN_FRONTEND=noninteractive apt-get -qq --no-install-recommends --no-upgrade -y install git"
]
+-- Build propellor, and symlink the built binary to ./propellor.
+--
+-- When the Host has a Buildsystem specified it is used. If none is
+-- specified, look at git config propellor.buildsystem.
buildPropellor :: Maybe Host -> IO ()
-buildPropellor mh = unlessM (actionMessage "Propellor build" (build msys)) $
+buildPropellor mh = unlessM (actionMessage "Propellor build" build) $
errorMessage "Propellor build failed!"
where
msys = case fmap (fromInfo . hostInfo) mh of
Just (InfoVal sys) -> Just sys
_ -> Nothing
--- Build propellor using cabal or stack, and symlink propellor to the
--- built binary.
-build :: Maybe System -> IO Bool
-build msys = catchBoolIO $ do
- bs <- getGitConfigValue "propellor.buildsystem"
- case bs of
- Just "stack" -> stackBuild msys
- _ -> cabalBuild msys
+ build = catchBoolIO $ do
+ case fromInfo (maybe mempty hostInfo mh) of
+ NoInfoVal -> do
+ bs <- getGitConfigValue "propellor.buildsystem"
+ case bs of
+ Just "stack" -> stackBuild msys
+ _ -> cabalBuild msys
+ InfoVal bs -> case getBuilder bs of
+ Cabal -> cabalBuild msys
+ Stack -> stackBuild msys
-- For speed, only runs cabal configure when it's not been run before.
-- If the build fails cabal may need to have configure re-run.
@@ -178,7 +277,7 @@ cabalBuild msys = do
, case msys of
Nothing -> return False
Just sys ->
- boolSystem "sh" [Param "-c", Param (depsCommand (Just sys))]
+ boolSystem "sh" [Param "-c", Param (depsCommand (Robustly Cabal) (Just sys))]
<&&> cabal ["configure"]
)
cabal_build = cabal ["build", "propellor-config"]
diff --git a/src/Propellor/CmdLine.hs b/src/Propellor/CmdLine.hs
index fc256109..bd01b34c 100644
--- a/src/Propellor/CmdLine.hs
+++ b/src/Propellor/CmdLine.hs
@@ -19,26 +19,41 @@ import Propellor.Types.CmdLine
import qualified Propellor.Property.Docker as Docker
import qualified Propellor.Property.Chroot as Chroot
import qualified Propellor.Shim as Shim
+import Utility.FileSystemEncoding
usage :: Handle -> IO ()
usage h = hPutStrLn h $ unlines
[ "Usage:"
- , " propellor --init"
- , " propellor"
- , " propellor hostname"
- , " propellor --spin targethost [--via relayhost]"
- , " propellor --add-key keyid"
- , " propellor --rm-key keyid"
- , " propellor --list-fields"
- , " propellor --dump field context"
- , " propellor --edit field context"
- , " propellor --set field context"
- , " propellor --unset field context"
- , " propellor --unset-unused"
- , " propellor --merge"
- , " propellor --build"
- , " propellor --check"
- ]
+ , " with no arguments, provision the current host"
+ , ""
+ , " --init"
+ , " initialize ~/.propellor"
+ , " hostname"
+ , " provision the current host as if it had the specified hostname"
+ , " --spin targethost [--via relayhost]"
+ , " provision the specified host"
+ , " --build"
+ , " recompile using your current config"
+ , " --add-key keyid"
+ , " add an additional signing key to the private data"
+ , " --rm-key keyid"
+ , " remove a signing key from the private data"
+ , " --list-fields"
+ , " list private data fields"
+ , " --set field context"
+ , " set a private data field"
+ , " --unset field context"
+ , " clear a private data field"
+ , " --unset-unused"
+ , " clear unused fields from the private data"
+ , " --dump field context"
+ , " show the content of a private data field"
+ , " --edit field context"
+ , " edit the content of a private data field"
+ , " --merge"
+ , " combine multiple spins into a single git commit"
+ , " --check"
+ , " double-check that propellor can actually run here"]
usageError :: [String] -> IO a
usageError ps = do
@@ -54,6 +69,7 @@ processCmdLine = go =<< getArgs
<$> mapM hostname (reverse hs)
<*> pure (Just r)
_ -> Spin <$> mapM hostname ps <*> pure Nothing
+ go ("--build":[]) = return Build
go ("--add-key":k:[]) = return $ AddKey k
go ("--rm-key":k:[]) = return $ RmKey k
go ("--set":f:c:[]) = withprivfield f c Set
@@ -94,6 +110,8 @@ data CanRebuild = CanRebuild | NoRebuild
-- | Runs propellor on hosts, as controlled by command-line options.
defaultMain :: [Host] -> IO ()
defaultMain hostlist = withConcurrentOutput $ do
+ useFileSystemEncoding
+ setupGpgEnv
Shim.cleanEnv
checkDebugMode
cmdline <- processCmdLine
@@ -102,6 +120,7 @@ defaultMain hostlist = withConcurrentOutput $ do
where
go cr (Serialized cmdline) = go cr cmdline
go _ Check = return ()
+ go cr Build = buildFirst Nothing cr Build $ return ()
go _ (Set field context) = setPrivData field context
go _ (Unset field context) = unsetPrivData field context
go _ (UnsetUnused) = unsetPrivDataUnused hostlist
@@ -186,7 +205,7 @@ updateFirst h canrebuild cmdline next = ifM hasOrigin
, next
)
--- If changes can be fetched from origin, Builds propellor (when allowed)
+-- If changes can be fetched from origin, builds propellor (when allowed)
-- and re-execs the updated propellor binary to continue.
-- Otherwise, runs the IO action to continue.
updateFirst' :: Maybe Host -> CanRebuild -> CmdLine -> IO () -> IO ()
diff --git a/src/Propellor/Container.hs b/src/Propellor/Container.hs
index 26194456..a805add8 100644
--- a/src/Propellor/Container.hs
+++ b/src/Propellor/Container.hs
@@ -51,15 +51,30 @@ propagateContainer
)
=> String
-> c
+ -> (PropagateInfo -> Bool)
-> Property metatypes
-> Property metatypes
-propagateContainer containername c prop = prop
+propagateContainer containername c wanted prop = prop
`addChildren` map convert (containerProperties c)
where
convert p =
- let n = property (getDesc p) (getSatisfy p) :: Property UnixLike
+ let n = property'' (getDesc p) (getSatisfy p) :: Property UnixLike
n' = n
`setInfoProperty` mapInfo (forceHostContext containername)
- (propagatableInfo (getInfo p))
+ (propagatableInfo wanted (getInfo p))
`addChildren` map convert (getChildren p)
in toChildProperty n'
+
+-- | Filters out parts of the Info that should not propagate out of a
+-- container.
+propagatableInfo :: (PropagateInfo -> Bool) -> Info -> Info
+propagatableInfo wanted (Info l) = Info $
+ filter (\(InfoEntry a) -> wanted (propagateInfo a)) l
+
+normalContainerInfo :: PropagateInfo -> Bool
+normalContainerInfo PropagatePrivData = True
+normalContainerInfo (PropagateInfo b) = b
+
+onlyPrivData :: PropagateInfo -> Bool
+onlyPrivData PropagatePrivData = True
+onlyPrivData (PropagateInfo _) = False
diff --git a/src/Propellor/DotDir.hs b/src/Propellor/DotDir.hs
index 21a9cdb7..f42c0575 100644
--- a/src/Propellor/DotDir.hs
+++ b/src/Propellor/DotDir.hs
@@ -47,10 +47,10 @@ disthead = distdir </> "head"
upstreambranch :: String
upstreambranch = "upstream/master"
--- Using the github mirror of the main propellor repo because
+-- Using the joeyh.name mirror of the main propellor repo because
-- it is accessible over https for better security.
netrepo :: String
-netrepo = "https://github.com/joeyh/propellor.git"
+netrepo = "https://git.joeyh.name/git/propellor.git"
dotPropellor :: IO FilePath
dotPropellor = do
@@ -316,7 +316,7 @@ minimalConfig = do
]
stackResolver :: String
-stackResolver = "lts-5.10"
+stackResolver = "lts-8.22"
fullClone :: IO Result
fullClone = do
diff --git a/src/Propellor/Engine.hs b/src/Propellor/Engine.hs
index 8958da6b..b4dc66ce 100644
--- a/src/Propellor/Engine.hs
+++ b/src/Propellor/Engine.hs
@@ -8,6 +8,8 @@ module Propellor.Engine (
fromHost,
fromHost',
onlyProcess,
+ chainPropellor,
+ runChainPropellor,
) where
import System.Exit
@@ -17,7 +19,9 @@ import "mtl" Control.Monad.RWS.Strict
import System.PosixCompat
import System.Posix.IO
import System.FilePath
+import System.Console.Concurrent
import Control.Applicative
+import Control.Concurrent.Async
import Prelude
import Propellor.Types
@@ -28,6 +32,8 @@ import Propellor.Exception
import Propellor.Info
import Utility.Exception
import Utility.Directory
+import Utility.Process
+import Utility.PartialPrelude
-- | Gets the Properties of a Host, and ensures them all,
-- with nice display of what's being done.
@@ -66,7 +72,9 @@ ensureChildProperties ps = ensure ps NoChange
ensure [] rs = return rs
ensure (p:ls) rs = do
hn <- asks hostName
- r <- actionMessageOn hn (getDesc p) (catchPropellor $ getSatisfy p)
+ r <- maybe (pure NoChange)
+ (actionMessageOn hn (getDesc p) . catchPropellor)
+ (getSatisfy p)
ensure ls (r <> rs)
-- | Lifts an action into the context of a different host.
@@ -89,8 +97,59 @@ onlyProcess lockfile a = bracket lock unlock (const a)
lock = do
createDirectoryIfMissing True (takeDirectory lockfile)
l <- createFile lockfile stdFileMode
+ setFdOption l CloseOnExec True
setLock l (WriteLock, AbsoluteSeek, 0, 0)
`catchIO` const alreadyrunning
return l
unlock = closeFd
alreadyrunning = error "Propellor is already running on this host!"
+
+-- | Chains to a propellor sub-Process, forwarding its output on to the
+-- display, except for the last line which is a Result.
+chainPropellor :: CreateProcess -> IO Result
+chainPropellor p =
+ -- We want to use outputConcurrent to display output
+ -- as it's received. If only stdout were captured,
+ -- concurrent-output would buffer all outputConcurrent.
+ -- Also capturing stderr avoids that problem.
+ withOEHandles createProcessSuccess p $ \(outh, errh) -> do
+ (r, ()) <- processChainOutput outh
+ `concurrently` forwardChainError errh
+ return r
+
+-- | Reads and displays each line from the Handle, except for the last line
+-- which is a Result.
+processChainOutput :: Handle -> IO Result
+processChainOutput h = go Nothing
+ where
+ go lastline = do
+ v <- catchMaybeIO (hGetLine h)
+ case v of
+ Nothing -> case lastline of
+ Nothing -> do
+ return FailedChange
+ Just l -> case readish l of
+ Just r -> pure r
+ Nothing -> do
+ outputConcurrent (l ++ "\n")
+ return FailedChange
+ Just s -> do
+ outputConcurrent $
+ maybe "" (\l -> if null l then "" else l ++ "\n") lastline
+ go (Just s)
+
+forwardChainError :: Handle -> IO ()
+forwardChainError h = do
+ v <- catchMaybeIO (hGetLine h)
+ case v of
+ Nothing -> return ()
+ Just s -> do
+ errorConcurrent (s ++ "\n")
+ forwardChainError h
+
+-- | Used by propellor sub-Processes that are run by chainPropellor.
+runChainPropellor :: Host -> Propellor Result -> IO ()
+runChainPropellor h a = do
+ r <- runPropellor h a
+ flushConcurrentOutput
+ putStrLn $ "\n" ++ show r
diff --git a/src/Propellor/EnsureProperty.hs b/src/Propellor/EnsureProperty.hs
index 30dfd5ad..ad74bfa8 100644
--- a/src/Propellor/EnsureProperty.hs
+++ b/src/Propellor/EnsureProperty.hs
@@ -46,7 +46,7 @@ ensureProperty
=> OuterMetaTypesWitness outer
-> Property (MetaTypes inner)
-> Propellor Result
-ensureProperty _ = catchPropellor . getSatisfy
+ensureProperty _ = maybe (return NoChange) catchPropellor . getSatisfy
-- The name of this was chosen to make type errors a bit more understandable.
type family Cannot_ensureProperty_WithInfo (l :: [a]) :: Bool
@@ -62,7 +62,7 @@ property'
-> (OuterMetaTypesWitness metatypes -> Propellor Result)
-> Property (MetaTypes metatypes)
property' d a =
- let p = Property sing d (a (outerMetaTypesWitness p)) mempty mempty
+ let p = Property sing d (Just (a (outerMetaTypesWitness p))) mempty mempty
in p
-- | Used to provide the metatypes of a Property to calls to
diff --git a/src/Propellor/Gpg.hs b/src/Propellor/Gpg.hs
index fd2fca79..c48bc060 100644
--- a/src/Propellor/Gpg.hs
+++ b/src/Propellor/Gpg.hs
@@ -1,8 +1,9 @@
module Propellor.Gpg where
import System.IO
+import System.Posix.IO
+import System.Posix.Terminal
import Data.Maybe
-import Data.List.Utils
import Control.Monad
import Control.Applicative
import Prelude
@@ -16,9 +17,32 @@ import Utility.Process.NonConcurrent
import Utility.Monad
import Utility.Misc
import Utility.Tmp
-import Utility.FileSystemEncoding
import Utility.Env
import Utility.Directory
+import Utility.Split
+import Utility.Exception
+
+-- | When at a tty, set GPG_TTY to point to the tty device. This is needed
+-- so that when gpg is run with stio connected to a pipe, it is still able
+-- to display password prompts at the console.
+--
+-- This should not prevent gpg from using the GUI for prompting when one is
+-- available.
+setupGpgEnv :: IO ()
+setupGpgEnv = checkhandles [stdInput, stdOutput, stdError]
+ where
+ checkhandles [] = return ()
+ checkhandles (h:hs) = do
+ isterm <- queryTerminal h
+ if isterm
+ then do
+ v <- tryNonAsync $ getTerminalName h
+ case v of
+ Right ttyname ->
+ -- do not overwrite
+ setEnv "GPG_TTY" ttyname False
+ Left _ -> checkhandles hs
+ else checkhandles hs
type KeyId = String
@@ -183,7 +207,7 @@ gpgDecrypt :: FilePath -> IO String
gpgDecrypt f = do
gpgbin <- getGpgBin
ifM (doesFileExist f)
- ( writeReadProcessEnv gpgbin ["--decrypt", f] Nothing Nothing (Just fileEncoding)
+ ( writeReadProcessEnv gpgbin ["--decrypt", f] Nothing Nothing Nothing
, return ""
)
@@ -201,6 +225,4 @@ gpgEncrypt f s = do
encrypted <- writeReadProcessEnv gpgbin opts Nothing (Just writer) Nothing
viaTmp writeFile f encrypted
where
- writer h = do
- fileEncoding h
- hPutStr h s
+ writer h = hPutStr h s
diff --git a/src/Propellor/Info.hs b/src/Propellor/Info.hs
index 3d7f07a5..ed6c2d85 100644
--- a/src/Propellor/Info.hs
+++ b/src/Propellor/Info.hs
@@ -3,6 +3,7 @@
module Propellor.Info (
osDebian,
osBuntish,
+ osArchLinux,
osFreeBSD,
setInfoProperty,
addInfoProperty,
@@ -83,13 +84,13 @@ askInfo = asks (fromInfo . hostInfo)
-- It also lets the type checker know that all the properties of the
-- host must support Debian.
--
--- > & osDebian (Stable "jessie") X86_64
+-- > & osDebian (Stable "stretch") X86_64
osDebian :: DebianSuite -> Architecture -> Property (HasInfo + Debian)
osDebian = osDebian' Linux
-- Use to specify a different `DebianKernel` than the default `Linux`
--
--- > & osDebian' KFreeBSD (Stable "jessie") X86_64
+-- > & osDebian' KFreeBSD (Stable "stretch") X86_64
osDebian' :: DebianKernel -> DebianSuite -> Architecture -> Property (HasInfo + Debian)
osDebian' kernel suite arch = tightenTargets $ os (System (Debian kernel suite) arch)
@@ -106,6 +107,10 @@ osBuntish release arch = tightenTargets $ os (System (Buntish release) arch)
osFreeBSD :: FreeBSDRelease -> Architecture -> Property (HasInfo + FreeBSD)
osFreeBSD release arch = tightenTargets $ os (System (FreeBSD release) arch)
+-- | Specifies that a host's operating system is Arch Linux
+osArchLinux :: Architecture -> Property (HasInfo + ArchLinux)
+osArchLinux arch = tightenTargets $ os (System (ArchLinux) arch)
+
os :: System -> Property (HasInfo + UnixLike)
os system = pureInfoProperty ("Operating " ++ show system) (InfoVal system)
diff --git a/src/Propellor/Message.hs b/src/Propellor/Message.hs
index 97573516..0f42e417 100644
--- a/src/Propellor/Message.hs
+++ b/src/Propellor/Message.hs
@@ -5,6 +5,8 @@
-- the messages will be displayed sequentially.
module Propellor.Message (
+ Trace(..),
+ parseTrace,
getMessageHandle,
isConsole,
forceConsole,
@@ -14,7 +16,6 @@ module Propellor.Message (
infoMessage,
errorMessage,
stopPropellorMessage,
- processChainOutput,
messagesDone,
createProcessConcurrent,
withConcurrentOutput,
@@ -22,6 +23,7 @@ module Propellor.Message (
import System.Console.ANSI
import System.IO
+import Control.Monad.IfElse
import Control.Monad.IO.Class (liftIO, MonadIO)
import System.IO.Unsafe (unsafePerformIO)
import Control.Concurrent
@@ -31,12 +33,26 @@ import Prelude
import Propellor.Types
import Propellor.Types.Exception
-import Utility.PartialPrelude
import Utility.Monad
+import Utility.Env
import Utility.Exception
+import Utility.PartialPrelude
+
+-- | Serializable tracing. Export `PROPELLOR_TRACE=1` in the environment to
+-- make propellor emit these to stdout, in addition to its other output.
+data Trace
+ = ActionStart (Maybe HostName) Desc
+ | ActionEnd (Maybe HostName) Desc Result
+ deriving (Read, Show)
+
+-- | Given a line read from propellor, if it's a serialized Trace,
+-- parses it.
+parseTrace :: String -> Maybe Trace
+parseTrace = readish
data MessageHandle = MessageHandle
{ isConsole :: Bool
+ , traceEnabled :: Bool
}
-- | A shared global variable for the MessageHandle.
@@ -45,11 +61,16 @@ globalMessageHandle :: MVar MessageHandle
globalMessageHandle = unsafePerformIO $
newMVar =<< MessageHandle
<$> catchDefaultIO False (hIsTerminalDevice stdout)
+ <*> ((== Just "1") <$> getEnv "PROPELLOR_TRACE")
-- | Gets the global MessageHandle.
getMessageHandle :: IO MessageHandle
getMessageHandle = readMVar globalMessageHandle
+trace :: Trace -> IO ()
+trace t = whenM (traceEnabled <$> getMessageHandle) $
+ putStrLn $ show t
+
-- | Force console output. This can be used when stdout is not directly
-- connected to a console, but is eventually going to be displayed at a
-- console.
@@ -65,16 +86,17 @@ whenConsole s = ifM (isConsole <$> getMessageHandle)
-- | Shows a message while performing an action, with a colored status
-- display.
-actionMessage :: (MonadIO m, MonadMask m, ActionResult r) => Desc -> m r -> m r
+actionMessage :: (MonadIO m, MonadMask m, ActionResult r, ToResult r) => Desc -> m r -> m r
actionMessage = actionMessage' Nothing
-- | Shows a message while performing an action on a specified host,
-- with a colored status display.
-actionMessageOn :: (MonadIO m, MonadMask m, ActionResult r) => HostName -> Desc -> m r -> m r
+actionMessageOn :: (MonadIO m, MonadMask m, ActionResult r, ToResult r) => HostName -> Desc -> m r -> m r
actionMessageOn = actionMessage' . Just
-actionMessage' :: (MonadIO m, ActionResult r) => Maybe HostName -> Desc -> m r -> m r
+actionMessage' :: (MonadIO m, ActionResult r, ToResult r) => Maybe HostName -> Desc -> m r -> m r
actionMessage' mhn desc a = do
+ liftIO $ trace $ ActionStart mhn desc
liftIO $ outputConcurrent
=<< whenConsole (setTitleCode $ "propellor: " ++ desc)
@@ -88,6 +110,7 @@ actionMessage' mhn desc a = do
, let (msg, intensity, color) = getActionResult r
in colorLine intensity color msg
]
+ liftIO $ trace $ ActionEnd mhn desc (toResult r)
return r
where
@@ -102,7 +125,7 @@ actionMessage' mhn desc a = do
warningMessage :: MonadIO m => String -> m ()
warningMessage s = liftIO $
- outputConcurrent =<< colorLine Vivid Magenta ("** warning: " ++ s)
+ errorConcurrent =<< colorLine Vivid Magenta ("** warning: " ++ s)
infoMessage :: MonadIO m => [String] -> m ()
infoMessage ls = liftIO $ outputConcurrent $ concatMap (++ "\n") ls
@@ -113,7 +136,7 @@ infoMessage ls = liftIO $ outputConcurrent $ concatMap (++ "\n") ls
-- property fail. Propellor will continue to the next property.
errorMessage :: MonadIO m => String -> m a
errorMessage s = liftIO $ do
- outputConcurrent =<< colorLine Vivid Red ("** error: " ++ s)
+ errorConcurrent =<< colorLine Vivid Red ("** error: " ++ s)
-- Normally this exception gets caught and is not displayed,
-- and propellor continues. So it's only displayed if not
-- caught, and so we say, cannot continue.
@@ -142,27 +165,6 @@ colorLine intensity color msg = concat <$> sequence
, pure "\n"
]
--- | Reads and displays each line from the Handle, except for the last line
--- which is a Result.
-processChainOutput :: Handle -> IO Result
-processChainOutput h = go Nothing
- where
- go lastline = do
- v <- catchMaybeIO (hGetLine h)
- case v of
- Nothing -> case lastline of
- Nothing -> do
- return FailedChange
- Just l -> case readish l of
- Just r -> pure r
- Nothing -> do
- outputConcurrent (l ++ "\n")
- return FailedChange
- Just s -> do
- outputConcurrent $
- maybe "" (\l -> if null l then "" else l ++ "\n") lastline
- go (Just s)
-
-- | Called when all messages about properties have been printed.
messagesDone :: IO ()
messagesDone = outputConcurrent
diff --git a/src/Propellor/PrivData.hs b/src/Propellor/PrivData.hs
index 2e9cdbab..516eda03 100644
--- a/src/Propellor/PrivData.hs
+++ b/src/Propellor/PrivData.hs
@@ -57,7 +57,6 @@ import Utility.Misc
import Utility.FileMode
import Utility.Env
import Utility.Table
-import Utility.FileSystemEncoding
import Utility.Directory
-- | Allows a Property to access the value of a specific PrivDataField,
@@ -171,7 +170,6 @@ getPrivData field context m = do
setPrivData :: PrivDataField -> Context -> IO ()
setPrivData field context = do
putStrLn "Enter private data on stdin; ctrl-D when done:"
- fileEncoding stdin
setPrivDataTo field context . PrivData =<< hGetContentsStrict stdin
unsetPrivData :: PrivDataField -> Context -> IO ()
@@ -274,7 +272,7 @@ readPrivData :: String -> PrivMap
readPrivData = fromMaybe M.empty . readish
readPrivDataFile :: FilePath -> IO PrivMap
-readPrivDataFile f = readPrivData <$> readFileStrictAnyEncoding f
+readPrivDataFile f = readPrivData <$> readFileStrict f
makePrivDataDir :: IO ()
makePrivDataDir = createDirectoryIfMissing False privDataDir
@@ -283,10 +281,10 @@ newtype PrivInfo = PrivInfo
{ fromPrivInfo :: S.Set (PrivDataField, Maybe PrivDataSourceDesc, HostContext) }
deriving (Eq, Ord, Show, Typeable, Monoid)
--- PrivInfo is propagated out of containers, so that propellor can see which
--- hosts need it.
+-- PrivInfo always propagates out of containers, so that propellor
+-- can see which hosts need it.
instance IsInfo PrivInfo where
- propagateInfo _ = True
+ propagateInfo _ = PropagatePrivData
-- | Sets the context of any privdata that uses HostContext to the
-- provided name.
diff --git a/src/Propellor/Property.hs b/src/Propellor/Property.hs
index ae4fc914..55e688ab 100644
--- a/src/Propellor/Property.hs
+++ b/src/Propellor/Property.hs
@@ -16,7 +16,6 @@ module Propellor.Property (
, check
, fallback
, revert
- , applyToList
-- * Property descriptions
, describe
, (==>)
@@ -51,10 +50,10 @@ import Data.Monoid
import Control.Monad.IfElse
import "mtl" Control.Monad.RWS.Strict
import System.Posix.Files
-import qualified Data.Hash.MD5 as MD5
+import Data.Maybe
import Data.List
+import Data.Hashable
import Control.Applicative
-import Data.Foldable hiding (and, elem)
import Prelude
import Propellor.Types
@@ -66,8 +65,8 @@ import Propellor.Info
import Propellor.EnsureProperty
import Utility.Exception
import Utility.Monad
-import Utility.Misc
import Utility.Directory
+import Utility.Misc
-- | Makes a perhaps non-idempotent Property be idempotent by using a flag
-- file to indicate whether it has run before.
@@ -120,13 +119,15 @@ onChange
-> CombinedType x y
onChange = combineWith combiner revertcombiner
where
- combiner p hook = do
+ combiner (Just p) (Just hook) = Just $ do
r <- p
case r of
MadeChange -> do
r' <- hook
return $ r <> r'
_ -> return r
+ combiner (Just p) Nothing = Just p
+ combiner Nothing _ = Nothing
revertcombiner = (<>)
-- | Same as `onChange` except that if property y fails, a flag file
@@ -144,24 +145,30 @@ onChangeFlagOnFail
-> CombinedType x y
onChangeFlagOnFail flagfile = combineWith combiner revertcombiner
where
- combiner s1 s2 = do
+ combiner (Just s1) s2 = Just $ do
r1 <- s1
case r1 of
MadeChange -> flagFailed s2
_ -> ifM (liftIO $ doesFileExist flagfile)
- (flagFailed s2
+ ( flagFailed s2
, return r1
)
+ combiner Nothing _ = Nothing
+
revertcombiner = (<>)
- flagFailed s = do
+
+ flagFailed (Just s) = do
r <- s
liftIO $ case r of
FailedChange -> createFlagFile
_ -> removeFlagFile
return r
+ flagFailed Nothing = return NoChange
+
createFlagFile = unlessM (doesFileExist flagfile) $ do
createDirectoryIfMissing True (takeDirectory flagfile)
writeFile flagfile ""
+
removeFlagFile = whenM (doesFileExist flagfile) $ removeFile flagfile
-- | Changes the description of a property.
@@ -178,11 +185,13 @@ infixl 1 ==>
fallback :: (Combines p1 p2) => p1 -> p2 -> CombinedType p1 p2
fallback = combineWith combiner revertcombiner
where
- combiner a1 a2 = do
+ combiner (Just a1) (Just a2) = Just $ do
r <- a1
if r == FailedChange
then a2
else return r
+ combiner (Just a1) Nothing = Just a1
+ combiner Nothing _ = Nothing
revertcombiner = (<>)
-- | Indicates that a Property may change a particular file. When the file
@@ -220,12 +229,12 @@ changesFile p f = checkResult getstat comparestat p
-- Changes to mtime etc that do not change file content are treated as
-- NoChange.
changesFileContent :: Checkable p i => p i -> FilePath -> Property i
-changesFileContent p f = checkResult getmd5 comparemd5 p
+changesFileContent p f = checkResult gethash comparehash p
where
- getmd5 = catchMaybeIO $ MD5.md5 . MD5.Str <$> readFileStrictAnyEncoding f
- comparemd5 oldmd5 = do
- newmd5 <- getmd5
- return $ if oldmd5 == newmd5 then NoChange else MadeChange
+ gethash = catchMaybeIO $ hash <$> readFileStrict f
+ comparehash oldhash = do
+ newhash <- gethash
+ return $ if oldhash == newhash then NoChange else MadeChange
-- | Determines if the first file is newer than the second file.
--
@@ -263,7 +272,7 @@ isNewerThan x y = do
--
-- For example:
--
--- > upgraded :: UnixLike
+-- > upgraded :: Property (DebianLike + FreeBSD)
-- > upgraded = (Apt.upgraded `pickOS` Pkg.upgraded)
-- > `describe` "OS upgraded"
--
@@ -292,9 +301,9 @@ pickOS a b = c `addChildren` [toChildProperty a, toChildProperty b]
-- are added as children, so their info will propigate.
c = withOS (getDesc a) $ \_ o ->
if matching o a
- then getSatisfy a
+ then maybe (pure NoChange) id (getSatisfy a)
else if matching o b
- then getSatisfy b
+ then maybe (pure NoChange) id (getSatisfy b)
else unsupportedOS'
matching Nothing _ = False
matching (Just o) p =
@@ -308,8 +317,8 @@ pickOS a b = c `addChildren` [toChildProperty a, toChildProperty b]
--
-- > myproperty :: Property Debian
-- > myproperty = withOS "foo installed" $ \w o -> case o of
--- > (Just (System (Debian (Stable release)) arch)) -> ensureProperty w ...
--- > (Just (System (Debian suite) arch)) -> ensureProperty w ...
+-- > (Just (System (Debian kernel (Stable release)) arch)) -> ensureProperty w ...
+-- > (Just (System (Debian kernel suite) arch)) -> ensureProperty w ...
-- > _ -> unsupportedOS'
--
-- Note that the operating system specifics may not be declared for all hosts,
@@ -343,22 +352,17 @@ unsupportedOS' = go =<< getOS
revert :: RevertableProperty setup undo -> RevertableProperty undo setup
revert (RevertableProperty p1 p2) = RevertableProperty p2 p1
--- | Apply a property to each element of a list.
-applyToList
- :: (Foldable t, Functor t, Combines p p, p ~ CombinedType p p)
- => (b -> p)
- -> t b
- -> p
-prop `applyToList` xs = Data.Foldable.foldr1 before $ prop <$> xs
-
makeChange :: IO () -> Propellor Result
makeChange a = liftIO a >> return MadeChange
noChange :: Propellor Result
noChange = return NoChange
+-- | A no-op property.
+--
+-- This is the same as `mempty` from the `Monoid` instance.
doNothing :: SingI t => Property (MetaTypes t)
-doNothing = property "noop property" noChange
+doNothing = mempty
-- | Registers an action that should be run at the very end, after
-- propellor has checks all the properties of a host.
diff --git a/src/Propellor/Property/Apache.hs b/src/Propellor/Property/Apache.hs
index f321143f..854d0eaa 100644
--- a/src/Propellor/Property/Apache.hs
+++ b/src/Propellor/Property/Apache.hs
@@ -64,6 +64,24 @@ modEnabled modname = enable <!> disable
`onChange` reloaded
isenabled = boolSystem "a2query" [Param "-q", Param "-m", Param modname]
+-- | Control whether an apache configuration file is enabled.
+--
+-- The String is the base name of the configuration, eg "charset" or "gitweb".
+confEnabled :: String -> RevertableProperty DebianLike DebianLike
+confEnabled confname = enable <!> disable
+ where
+ enable = check (not <$> isenabled)
+ (cmdProperty "a2enconf" ["--quiet", confname])
+ `describe` ("apache configuration enabled " ++ confname)
+ `requires` installed
+ `onChange` reloaded
+ disable = check isenabled
+ (cmdProperty "a2disconf" ["--quiet", confname])
+ `describe` ("apache configuration disabled " ++ confname)
+ `requires` installed
+ `onChange` reloaded
+ isenabled = boolSystem "a2query" [Param "-q", Param "-c", Param confname]
+
-- | Make apache listen on the specified ports.
--
-- Note that ports are also specified inside a site's config file,
@@ -72,7 +90,7 @@ listenPorts :: [Port] -> Property DebianLike
listenPorts ps = "/etc/apache2/ports.conf" `File.hasContent` map portline ps
`onChange` restarted
where
- portline port = "Listen " ++ fromPort port
+ portline port = "Listen " ++ val port
-- This is a list of config files because different versions of apache
-- use different filenames. Propellor simply writes them all.
@@ -135,8 +153,8 @@ virtualHost domain port docroot = virtualHost' domain port docroot []
-- | Like `virtualHost` but with additional config lines added.
virtualHost' :: Domain -> Port -> WebRoot -> [ConfigLine] -> RevertableProperty DebianLike DebianLike
virtualHost' domain port docroot addedcfg = siteEnabled domain $
- [ "<VirtualHost *:" ++ fromPort port ++ ">"
- , "ServerName " ++ domain ++ ":" ++ fromPort port
+ [ "<VirtualHost *:" ++ val port ++ ">"
+ , "ServerName " ++ domain ++ ":" ++ val port
, "DocumentRoot " ++ docroot
, "ErrorLog /var/log/apache2/error.log"
, "LogLevel warn"
@@ -171,7 +189,7 @@ httpsVirtualHost' domain docroot letos addedcfg = setup <!> teardown
`requires` modEnabled "ssl"
`before` setuphttps
teardown = siteDisabled domain
- setuphttp = siteEnabled' domain $
+ setuphttp = (siteEnabled' domain $
-- The sslconffile is only created after letsencrypt gets
-- the cert. The "*" is needed to make apache not error
-- when the file doesn't exist.
@@ -183,27 +201,27 @@ httpsVirtualHost' domain docroot letos addedcfg = setup <!> teardown
, "RewriteRule ^/.well-known/(.*) - [L]"
-- Everything else redirects to https
, "RewriteRule ^/(.*) https://" ++ domain ++ "/$1 [L,R,NE]"
- ]
+ ])
+ `requires` File.dirExists (takeDirectory cf)
setuphttps = LetsEncrypt.letsEncrypt letos domain docroot
`onChange` postsetuphttps
postsetuphttps = combineProperties (domain ++ " ssl cert installed") $ props
- & File.dirExists (takeDirectory cf)
& File.hasContent cf sslvhost
`onChange` reloaded
-- always reload since the cert has changed
& reloaded
where
- cf = sslconffile "letsencrypt"
sslvhost = vhost (Port 443)
[ "SSLEngine on"
, "SSLCertificateFile " ++ LetsEncrypt.certFile domain
, "SSLCertificateKeyFile " ++ LetsEncrypt.privKeyFile domain
, "SSLCertificateChainFile " ++ LetsEncrypt.chainFile domain
]
+ cf = sslconffile "letsencrypt"
sslconffile s = "/etc/apache2/sites-available/ssl/" ++ domain ++ "/" ++ s ++ ".conf"
vhost p ls =
- [ "<VirtualHost *:" ++ fromPort p ++">"
- , "ServerName " ++ domain ++ ":" ++ fromPort p
+ [ "<VirtualHost *:" ++ val p ++">"
+ , "ServerName " ++ domain ++ ":" ++ val p
, "DocumentRoot " ++ docroot
, "ErrorLog /var/log/apache2/error.log"
, "LogLevel warn"
diff --git a/src/Propellor/Property/Apt.hs b/src/Propellor/Property/Apt.hs
index 196fb345..5630d83a 100644
--- a/src/Propellor/Property/Apt.hs
+++ b/src/Propellor/Property/Apt.hs
@@ -1,9 +1,11 @@
{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE DeriveDataTypeable #-}
module Propellor.Property.Apt where
import Data.Maybe
import Data.List
+import Data.Typeable
import System.IO
import Control.Monad
import Control.Applicative
@@ -13,6 +15,40 @@ import Propellor.Base
import qualified Propellor.Property.File as File
import qualified Propellor.Property.Service as Service
import Propellor.Property.File (Line)
+import Propellor.Types.Info
+
+data HostMirror = HostMirror Url
+ deriving (Eq, Show, Typeable)
+
+data HostAptProxy = HostAptProxy Url
+ deriving (Eq, Show, Typeable)
+
+-- | Indicate host's preferred apt mirror
+mirror :: Url -> Property (HasInfo + UnixLike)
+mirror u = pureInfoProperty (u ++ " apt mirror selected")
+ (InfoVal (HostMirror u))
+
+getMirror :: Propellor Url
+getMirror = do
+ mirrorInfo <- getMirrorInfo
+ osInfo <- getOS
+ return $ case (osInfo, mirrorInfo) of
+ (_, Just (HostMirror u)) -> u
+ (Just (System (Debian _ _) _), _) ->
+ "http://deb.debian.org/debian"
+ (Just (System (Buntish _) _), _) ->
+ "mirror://mirrors.ubuntu.com/"
+ (Just (System dist _), _) ->
+ error ("no Apt mirror defined for " ++ show dist)
+ _ -> error "no Apt mirror defined for this host or OS"
+ where
+ getMirrorInfo :: Propellor (Maybe HostMirror)
+ getMirrorInfo = fromInfoVal <$> askInfo
+
+withMirror :: Desc -> (Url -> Property DebianLike) -> Property DebianLike
+withMirror desc mkp = property' desc $ \w -> do
+ u <- getMirror
+ ensureProperty w (mkp u)
sourcesList :: FilePath
sourcesList = "/etc/apt/sources.list"
@@ -37,8 +73,8 @@ stableUpdatesSuite (Stable s) = Just (s ++ "-updates")
stableUpdatesSuite _ = Nothing
debLine :: String -> Url -> [Section] -> Line
-debLine suite mirror sections = unwords $
- ["deb", mirror, suite] ++ sections
+debLine suite url sections = unwords $
+ ["deb", url, suite] ++ sections
srcLine :: Line -> Line
srcLine l = case words l of
@@ -61,11 +97,8 @@ binandsrc url suite = catMaybes
bs <- backportSuite suite
return $ debLine bs url stdSections
-debCdn :: SourcesGenerator
-debCdn = binandsrc "http://httpredir.debian.org/debian"
-
-kernelOrg :: SourcesGenerator
-kernelOrg = binandsrc "http://mirrors.kernel.org/debian"
+stdArchiveLines :: Propellor SourcesGenerator
+stdArchiveLines = return . binandsrc =<< getMirror
-- | Only available for Stable and Testing
securityUpdates :: SourcesGenerator
@@ -75,11 +108,9 @@ securityUpdates suite
in [l, srcLine l]
| otherwise = []
--- | Makes sources.list have a standard content using the Debian mirror CDN,
--- with the Debian suite configured by the os.
---
--- Since the CDN is sometimes unreliable, also adds backup lines using
--- kernel.org.
+-- | Makes sources.list have a standard content using the Debian mirror CDN
+-- (or other host specified using the `mirror` property), with the
+-- Debian suite configured by the os.
stdSourcesList :: Property Debian
stdSourcesList = withOS "standard sources.list" $ \w o -> case o of
(Just (System (Debian _ suite) _)) ->
@@ -94,11 +125,62 @@ stdSourcesListFor suite = stdSourcesList' suite []
-- 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 Debian
-stdSourcesList' suite more = tightenTargets $ setSourcesList
- (concatMap (\gen -> gen suite) generators)
- `describe` ("standard sources.list for " ++ show suite)
+stdSourcesList' suite more = tightenTargets $
+ withMirror desc $ \u -> setSourcesList
+ (concatMap (\gen -> gen suite) (generators u))
where
- generators = [debCdn, kernelOrg, securityUpdates] ++ more
+ generators u = [binandsrc u, securityUpdates] ++ more
+ desc = ("standard sources.list for " ++ show suite)
+
+type PinPriority = Int
+
+-- | Adds an apt source for a suite, and pins that suite to a given pin value
+-- (see apt_preferences(5)). Revert to drop the source and unpin the suite.
+--
+-- If the requested suite is the host's OS suite, the suite is pinned, but no
+-- source is added. That apt source should already be available, or you can use
+-- a property like 'Apt.stdSourcesList'.
+suiteAvailablePinned
+ :: DebianSuite
+ -> PinPriority
+ -> RevertableProperty Debian Debian
+suiteAvailablePinned s pin = available <!> unavailable
+ where
+ available :: Property Debian
+ available = tightenTargets $ combineProperties (desc True) $ props
+ & File.hasContent prefFile (suitePinBlock "*" s pin)
+ & setSourcesFile
+
+ unavailable :: Property Debian
+ unavailable = tightenTargets $ combineProperties (desc False) $ props
+ & File.notPresent sourcesFile
+ `onChange` update
+ & File.notPresent prefFile
+
+ setSourcesFile :: Property Debian
+ setSourcesFile = tightenTargets $ withMirror (desc True) $ \u ->
+ withOS (desc True) $ \w o -> case o of
+ (Just (System (Debian _ hostSuite) _))
+ | s /= hostSuite -> ensureProperty w $
+ File.hasContent sourcesFile (sources u)
+ `onChange` update
+ _ -> noChange
+
+ -- Unless we are pinning a backports suite, filter out any backports
+ -- sources that were added by our generators. The user probably doesn't
+ -- want those to be pinned to the same value
+ sources u = dropBackports $ concatMap (\gen -> gen s) (generators u)
+ where
+ dropBackports
+ | "-backports" `isSuffixOf` (showSuite s) = id
+ | otherwise = filter (not . isInfixOf "-backports")
+
+ generators u = [binandsrc u, securityUpdates]
+ prefFile = "/etc/apt/preferences.d/20" ++ showSuite s ++ ".pref"
+ sourcesFile = "/etc/apt/sources.list.d/" ++ showSuite s ++ ".list"
+
+ desc True = "Debian " ++ showSuite s ++ " pinned, priority " ++ show pin
+ desc False = "Debian " ++ showSuite s ++ " not pinned"
setSourcesList :: [Line] -> Property DebianLike
setSourcesList ls = sourcesList `File.hasContent` ls `onChange` update
@@ -196,6 +278,50 @@ buildDepIn dir = cmdPropertyEnv "sh" ["-c", cmd] noninteractiveEnv
where
cmd = "cd '" ++ dir ++ "' && mk-build-deps debian/control --install --tool 'apt-get -y --no-install-recommends' --remove"
+-- | The name of a package, a glob to match the names of packages, or a regexp
+-- surrounded by slashes to match the names of packages. See
+-- apt_preferences(5), "Regular expressions and glob(7) syntax"
+type AptPackagePref = String
+
+-- | Pins a list of packages, package wildcards and/or regular expressions to a
+-- list of suites and corresponding pin priorities (see apt_preferences(5)).
+-- Revert to unpin.
+--
+-- Each package, package wildcard or regular expression will be pinned to all of
+-- the specified suites.
+--
+-- Note that this will have no effect unless there is an apt source for each of
+-- the suites. One way to add an apt source is 'Apt.suiteAvailablePinned'.
+--
+-- For example, to obtain Emacs Lisp addon packages not present in your release
+-- of Debian from testing, falling back to sid if they're not available in
+-- testing, you could use
+--
+-- > & Apt.suiteAvailablePinned Testing (-10)
+-- > & Apt.suiteAvailablePinned Unstable (-10)
+-- > & ["elpa-*"] `Apt.pinnedTo` [(Testing, 100), (Unstable, 50)]
+pinnedTo
+ :: [AptPackagePref]
+ -> [(DebianSuite, PinPriority)]
+ -> RevertableProperty Debian Debian
+pinnedTo ps pins = mconcat (map (\p -> pinnedTo' p pins) ps)
+ `describe` unwords (("pinned to " ++ showSuites):ps)
+ where
+ showSuites = intercalate "," $ showSuite . fst <$> pins
+
+pinnedTo'
+ :: AptPackagePref
+ -> [(DebianSuite, PinPriority)]
+ -> RevertableProperty Debian Debian
+pinnedTo' p pins =
+ (tightenTargets $ prefFile `File.hasContent` prefs)
+ <!> (tightenTargets $ File.notPresent prefFile)
+ where
+ prefs = foldr step [] pins
+ step (suite, pin) ls = ls ++ suitePinBlock p suite pin ++ [""]
+ prefFile = "/etc/apt/preferences.d/10propellor_"
+ ++ File.configFileName p <.> "pref"
+
-- | Package installation may fail becuse the archive has changed.
-- Run an update in that case and retry.
robustly :: Property DebianLike -> Property DebianLike
@@ -349,5 +475,40 @@ hasForeignArch arch = check notAdded (add `before` update)
add = cmdProperty "dpkg" ["--add-architecture", arch]
`assume` MadeChange
+-- | Disable the use of PDiffs for machines with high-bandwidth connections.
+noPDiffs :: Property DebianLike
+noPDiffs = tightenTargets $ "/etc/apt/apt.conf.d/20pdiffs" `File.hasContent`
+ [ "Acquire::PDiffs \"false\";" ]
+
+suitePin :: DebianSuite -> String
+suitePin s = prefix s ++ showSuite s
+ where
+ prefix (Stable _) = "n="
+ prefix _ = "a="
+
+suitePinBlock :: AptPackagePref -> DebianSuite -> PinPriority -> [Line]
+suitePinBlock p suite pin =
+ [ "Explanation: This file added by propellor"
+ , "Package: " ++ p
+ , "Pin: release " ++ suitePin suite
+ , "Pin-Priority: " ++ val pin
+ ]
+
dpkgStatus :: FilePath
dpkgStatus = "/var/lib/dpkg/status"
+
+-- | Set apt's proxy
+proxy :: Url -> Property (HasInfo + DebianLike)
+proxy u = tightenTargets $
+ proxyInfo `before` proxyConfig `describe` desc
+ where
+ proxyInfo = pureInfoProperty desc (InfoVal (HostAptProxy u))
+ proxyConfig = "/etc/apt/apt.conf.d/20proxy" `File.hasContent`
+ [ "Acquire::HTTP::Proxy \"" ++ u ++ "\";" ]
+ desc = (u ++ " apt proxy selected")
+
+-- | Cause apt to proxy downloads via an apt cacher on localhost
+useLocalCacher :: Property (HasInfo + DebianLike)
+useLocalCacher = proxy "http://localhost:3142"
+ `requires` serviceInstalledRunning "apt-cacher-ng"
+ `describe` "apt uses local apt cacher"
diff --git a/src/Propellor/Property/Apt/PPA.hs b/src/Propellor/Property/Apt/PPA.hs
index 49fa9fa7..a8f7db15 100644
--- a/src/Propellor/Property/Apt/PPA.hs
+++ b/src/Propellor/Property/Apt/PPA.hs
@@ -6,10 +6,11 @@ module Propellor.Property.Apt.PPA where
import Data.List
import Control.Applicative
import Prelude
-import Data.String.Utils
import Data.String (IsString(..))
+
import Propellor.Base
import qualified Propellor.Property.Apt as Apt
+import Utility.Split
-- | Ensure software-properties-common is installed.
installed :: Property DebianLike
@@ -25,8 +26,8 @@ data PPA = PPA
, ppaArchive :: String -- ^ The name of the archive.
} deriving (Eq, Ord)
-instance Show PPA where
- show p = concat ["ppa:", ppaAccount p, "/", ppaArchive p]
+instance ConfigurableValue PPA where
+ val p = concat ["ppa:", ppaAccount p, "/", ppaArchive p]
instance IsString PPA where
-- | Parse strings like "ppa:zfs-native/stable" into a PPA.
@@ -40,9 +41,9 @@ instance IsString PPA where
-- | Adds a PPA to the local system repositories.
addPpa :: PPA -> Property DebianLike
addPpa p =
- cmdPropertyEnv "apt-add-repository" ["--yes", show p] Apt.noninteractiveEnv
+ cmdPropertyEnv "apt-add-repository" ["--yes", val p] Apt.noninteractiveEnv
`assume` MadeChange
- `describe` ("Added PPA " ++ (show p))
+ `describe` ("Added PPA " ++ (val p))
`requires` installed
-- | A repository key ID to be downloaded with apt-key.
@@ -52,14 +53,11 @@ data AptKeyId = AptKeyId
, akiServer :: String
} deriving (Eq, Ord)
-instance Show AptKeyId where
- show k = unwords ["Apt Key", akiName k, akiId k, "from", akiServer k]
-
-- | Adds an 'AptKeyId' from the specified GPG server.
addKeyId :: AptKeyId -> Property DebianLike
addKeyId keyId =
check keyTrusted akcmd
- `describe` (unwords ["Add third-party Apt key", show keyId])
+ `describe` (unwords ["Add third-party Apt key", desc keyId])
where
akcmd =
tightenTargets $ cmdProperty "apt-key" ["adv", "--keyserver", akiServer keyId, "--recv-keys", akiId keyId]
@@ -72,10 +70,12 @@ addKeyId keyId =
nkid = take 8 (akiId keyId)
in
(isInfixOf [nkid] . pks) <$> readProcess "apt-key" ["list"]
+ desc k = unwords ["Apt Key", akiName k, akiId k, "from", akiServer k]
-- | An Apt source line that apt-add-repository will just add to
--- sources.list. It's also an instance of both 'Show' and 'IsString' to make
--- using 'OverloadedStrings' in the configuration file easier.
+-- sources.list. It's also an instance of both 'ConfigurableValue'
+-- and 'IsString' to make using 'OverloadedStrings' in the configuration
+-- file easier.
--
-- | FIXME there's apparently an optional "options" fragment that I've
-- definitely not parsed here.
@@ -85,8 +85,8 @@ data AptSource = AptSource
, asComponents :: [String] -- ^ The list of components to install from this repository.
} deriving (Eq, Ord)
-instance Show AptSource where
- show asrc = unwords ["deb", asURL asrc, asSuite asrc, unwords . asComponents $ asrc]
+instance ConfigurableValue AptSource where
+ val asrc = unwords ["deb", asURL asrc, asSuite asrc, unwords . asComponents $ asrc]
instance IsString AptSource where
fromString s =
@@ -103,7 +103,7 @@ addRepository :: AptRepository -> Property DebianLike
addRepository (AptRepositoryPPA p) = addPpa p
addRepository (AptRepositorySource src) =
check repoExists addSrc
- `describe` unwords ["Adding APT repository", show src]
+ `describe` unwords ["Adding APT repository", val src]
`requires` installed
where
allSourceLines =
@@ -112,4 +112,4 @@ addRepository (AptRepositorySource src) =
. filter (not . isPrefixOf "#")
. filter (/= "") . lines <$> allSourceLines
repoExists = isInfixOf [src] <$> activeSources
- addSrc = cmdProperty "apt-add-source" [show src]
+ addSrc = cmdProperty "apt-add-source" [val src]
diff --git a/src/Propellor/Property/Attic.hs b/src/Propellor/Property/Attic.hs
index 4415f8c0..8ab5546b 100644
--- a/src/Propellor/Property/Attic.hs
+++ b/src/Propellor/Property/Attic.hs
@@ -1,8 +1,12 @@
-- | Maintainer: Félix Sipma <felix+propellor@gueux.org>
--
-- Support for the Attic backup tool <https://attic-backup.org/>
+--
+-- This module is deprecated because Attic is not available in debian
+-- stable any longer (so the installed property no longer works), and it
+-- appears to have been mostly supersceded by Borg.
-module Propellor.Property.Attic
+module Propellor.Property.Attic {-# DEPRECATED "Use Borg instead" #-}
( installed
, repoExists
, init
@@ -104,7 +108,7 @@ backup' dir backupdir crontimes extraargs kp = cronjob
where
desc = backupdir ++ " attic backup"
cronjob = Cron.niceJob ("attic_backup" ++ dir) crontimes (User "root") "/" $
- "flock " ++ shellEscape lockfile ++ " sh -c " ++ backupcmd
+ "flock " ++ shellEscape lockfile ++ " sh -c " ++ shellEscape backupcmd
lockfile = "/var/lock/propellor-attic.lock"
backupcmd = intercalate ";" $
createCommand
@@ -131,11 +135,11 @@ backup' dir backupdir crontimes extraargs kp = cronjob
-- passed to the `backup` property, they will run attic prune to clean out
-- generations not specified here.
keepParam :: KeepPolicy -> AtticParam
-keepParam (KeepHours n) = "--keep-hourly=" ++ show n
-keepParam (KeepDays n) = "--keep-daily=" ++ show n
-keepParam (KeepWeeks n) = "--keep-daily=" ++ show n
-keepParam (KeepMonths n) = "--keep-monthly=" ++ show n
-keepParam (KeepYears n) = "--keep-yearly=" ++ show n
+keepParam (KeepHours n) = "--keep-hourly=" ++ val n
+keepParam (KeepDays n) = "--keep-daily=" ++ val n
+keepParam (KeepWeeks n) = "--keep-daily=" ++ val n
+keepParam (KeepMonths n) = "--keep-monthly=" ++ val n
+keepParam (KeepYears n) = "--keep-yearly=" ++ val n
-- | Policy for backup generations to keep. For example, KeepDays 30 will
-- keep the latest backup for each day when a backup was made, and keep the
diff --git a/src/Propellor/Property/Bootstrap.hs b/src/Propellor/Property/Bootstrap.hs
new file mode 100644
index 00000000..f0759dae
--- /dev/null
+++ b/src/Propellor/Property/Bootstrap.hs
@@ -0,0 +1,144 @@
+-- | This module contains properties that configure how Propellor
+-- bootstraps to run itself on a Host.
+
+module Propellor.Property.Bootstrap (
+ Bootstrapper(..),
+ Builder(..),
+ bootstrapWith,
+ RepoSource(..),
+ bootstrappedFrom,
+ clonedFrom
+) where
+
+import Propellor.Base
+import Propellor.Bootstrap
+import Propellor.Types.Info
+import Propellor.Property.Chroot
+
+import Data.List
+import qualified Data.ByteString as B
+
+-- | This property can be used to configure the `Bootstrapper` that is used
+-- to bootstrap propellor on a Host. For example, if you want to use
+-- stack:
+--
+-- > host "example.com" $ props
+-- > & bootstrapWith (Robustly Stack)
+--
+-- When `bootstrappedFrom` is used in a `Chroot` or other `Container`,
+-- this property can also be added to the chroot to configure it.
+bootstrapWith :: Bootstrapper -> Property (HasInfo + UnixLike)
+bootstrapWith b = pureInfoProperty desc (InfoVal b)
+ where
+ desc = "propellor bootstrapped with " ++ case b of
+ Robustly Stack -> "stack"
+ Robustly Cabal -> "cabal"
+ OSOnly -> "OS packages only"
+
+-- | Where a propellor repository should be bootstrapped from.
+data RepoSource
+ = GitRepoUrl String
+ | GitRepoOutsideChroot
+ -- ^ When used in a chroot, this copies the git repository from
+ -- outside the chroot, including its configuration.
+
+-- | Bootstraps a propellor installation into
+-- /usr/local/propellor/
+--
+-- Normally, propellor is bootstrapped by eg, using propellor --spin,
+-- and so this property is not generally needed.
+--
+-- This property only does anything when used inside a Chroot or other
+-- Container. This is particularly useful inside a chroot used to build a
+-- disk image, to make the disk image have propellor installed.
+--
+-- The git repository is cloned (or pulled to update if it already exists).
+--
+-- All build dependencies are installed, using distribution packages
+-- or falling back to using cabal or stack.
+bootstrappedFrom :: RepoSource -> Property Linux
+bootstrappedFrom reposource = check inChroot $
+ go `requires` clonedFrom reposource
+ where
+ go :: Property Linux
+ go = property "Propellor bootstrapped" $ do
+ system <- getOS
+ bootstrapper <- getBootstrapper
+ assumeChange $ exposeTrueLocaldir $ const $
+ runShellCommand $ buildShellCommand
+ [ "cd " ++ localdir
+ , checkDepsCommand bootstrapper system
+ , buildCommand bootstrapper
+ ]
+
+-- | Clones the propellor repository into /usr/local/propellor/
+--
+-- If the propellor repo has already been cloned, pulls to get it
+-- up-to-date.
+clonedFrom :: RepoSource -> Property Linux
+clonedFrom reposource = case reposource of
+ GitRepoOutsideChroot -> go `onChange` copygitconfig
+ _ -> go
+ where
+ go :: Property Linux
+ go = property ("Propellor repo cloned from " ++ sourcedesc) $
+ ifM needclone (makeclone, updateclone)
+
+ makeclone = do
+ let tmpclone = localdir ++ ".tmpclone"
+ system <- getOS
+ assumeChange $ exposeTrueLocaldir $ \sysdir -> do
+ let originloc = case reposource of
+ GitRepoUrl s -> s
+ GitRepoOutsideChroot -> sysdir
+ runShellCommand $ buildShellCommand
+ [ installGitCommand system
+ , "rm -rf " ++ tmpclone
+ , "git clone " ++ shellEscape originloc ++ " " ++ tmpclone
+ , "mkdir -p " ++ localdir
+ -- This is done rather than deleting
+ -- the old localdir, because if it is bound
+ -- mounted from outside the chroot, deleting
+ -- it after unmounting in unshare will remove
+ -- the bind mount outside the unshare.
+ , "(cd " ++ tmpclone ++ " && tar c .) | (cd " ++ localdir ++ " && tar x)"
+ , "rm -rf " ++ tmpclone
+ ]
+
+ updateclone = assumeChange $ exposeTrueLocaldir $ const $
+ runShellCommand $ buildShellCommand
+ [ "cd " ++ localdir
+ , "git pull"
+ ]
+
+ -- Copy the git config of the repo outside the chroot into the
+ -- chroot. This way it has the same remote urls, and other git
+ -- configuration.
+ copygitconfig :: Property Linux
+ copygitconfig = property ("Propellor repo git config copied from outside the chroot") $ do
+ let gitconfig = localdir </> ".git" </> "config"
+ cfg <- liftIO $ B.readFile gitconfig
+ exposeTrueLocaldir $ const $
+ liftIO $ B.writeFile gitconfig cfg
+ return MadeChange
+
+ needclone = (inChroot <&&> truelocaldirisempty)
+ <||> (liftIO (not <$> doesDirectoryExist localdir))
+
+ truelocaldirisempty = exposeTrueLocaldir $ const $
+ runShellCommand ("test ! -d " ++ localdir ++ "/.git")
+
+ sourcedesc = case reposource of
+ GitRepoUrl s -> s
+ GitRepoOutsideChroot -> localdir ++ " outside the chroot"
+
+assumeChange :: Propellor Bool -> Propellor Result
+assumeChange a = do
+ ok <- a
+ return (cmdResult ok <> MadeChange)
+
+buildShellCommand :: [String] -> String
+buildShellCommand = intercalate "&&" . map (\c -> "(" ++ c ++ ")")
+
+runShellCommand :: String -> Propellor Bool
+runShellCommand s = liftIO $ boolSystem "sh" [ Param "-c", Param s]
diff --git a/src/Propellor/Property/Borg.hs b/src/Propellor/Property/Borg.hs
index 16030562..ace7a48b 100644
--- a/src/Propellor/Property/Borg.hs
+++ b/src/Propellor/Property/Borg.hs
@@ -92,8 +92,8 @@ restored dir backupdir = go `requires` installed
-- > ["--exclude=/srv/git/tobeignored"]
-- > [Borg.KeepDays 7, Borg.KeepWeeks 4, Borg.KeepMonths 6, Borg.KeepYears 1]
--
--- Note that this property does not make borg encrypt the backup
--- repository.
+-- Note that this property does not initialize the backup repository,
+-- so that will need to be done once, before-hand.
--
-- Since borg uses a fair amount of system resources, only one borg
-- backup job will be run at a time. Other jobs will wait their turns to
@@ -110,7 +110,7 @@ backup' dir backupdir crontimes extraargs kp = cronjob
where
desc = backupdir ++ " borg backup"
cronjob = Cron.niceJob ("borg_backup" ++ dir) crontimes (User "root") "/" $
- "flock " ++ shellEscape lockfile ++ " sh -c " ++ backupcmd
+ "flock " ++ shellEscape lockfile ++ " sh -c " ++ shellEscape backupcmd
lockfile = "/var/lock/propellor-borg.lock"
backupcmd = intercalate ";" $
createCommand
@@ -137,11 +137,11 @@ backup' dir backupdir crontimes extraargs kp = cronjob
-- passed to the `backup` property, they will run borg prune to clean out
-- generations not specified here.
keepParam :: KeepPolicy -> BorgParam
-keepParam (KeepHours n) = "--keep-hourly=" ++ show n
-keepParam (KeepDays n) = "--keep-daily=" ++ show n
-keepParam (KeepWeeks n) = "--keep-daily=" ++ show n
-keepParam (KeepMonths n) = "--keep-monthly=" ++ show n
-keepParam (KeepYears n) = "--keep-yearly=" ++ show n
+keepParam (KeepHours n) = "--keep-hourly=" ++ val n
+keepParam (KeepDays n) = "--keep-daily=" ++ val n
+keepParam (KeepWeeks n) = "--keep-daily=" ++ val n
+keepParam (KeepMonths n) = "--keep-monthly=" ++ val n
+keepParam (KeepYears n) = "--keep-yearly=" ++ val n
-- | Policy for backup generations to keep. For example, KeepDays 30 will
-- keep the latest backup for each day when a backup was made, and keep the
diff --git a/src/Propellor/Property/Ccache.hs b/src/Propellor/Property/Ccache.hs
index c0b8d539..a2bef117 100644
--- a/src/Propellor/Property/Ccache.hs
+++ b/src/Propellor/Property/Ccache.hs
@@ -76,7 +76,7 @@ limitToParams NoLimit = []
limitToParams (MaxSize s) = case maxSizeParam s of
Just param -> [Right param]
Nothing -> [Left $ "unable to parse data size " ++ s]
-limitToParams (MaxFiles f) = [Right $ "--max-files=" ++ show f]
+limitToParams (MaxFiles f) = [Right $ "--max-files=" ++ val f]
limitToParams (l1 :+ l2) = limitToParams l1 <> limitToParams l2
-- | Configures a ccache in /var/cache for a group
diff --git a/src/Propellor/Property/Chroot.hs b/src/Propellor/Property/Chroot.hs
index cb693a73..9e8bcd2f 100644
--- a/src/Propellor/Property/Chroot.hs
+++ b/src/Propellor/Property/Chroot.hs
@@ -4,12 +4,14 @@ module Propellor.Property.Chroot (
debootstrapped,
bootstrapped,
provisioned,
+ hostChroot,
Chroot(..),
ChrootBootstrapper(..),
Debootstrapped(..),
ChrootTarball(..),
noServices,
inChroot,
+ exposeTrueLocaldir,
-- * Internal use
provisioned',
propagateChrootInfo,
@@ -31,27 +33,28 @@ import qualified Propellor.Property.File as File
import qualified Propellor.Shim as Shim
import Propellor.Property.Mount
import Utility.FileMode
+import Utility.Split
import qualified Data.Map as M
-import Data.List.Utils
import System.Posix.Directory
-import System.Console.Concurrent
-- | Specification of a chroot. Normally you'll use `debootstrapped` or
--- `bootstrapped` to construct a Chroot value.
+-- `bootstrapped` or `hostChroot` to construct a Chroot value.
data Chroot where
- Chroot :: ChrootBootstrapper b => FilePath -> b -> Host -> Chroot
+ Chroot :: ChrootBootstrapper b => FilePath -> b -> InfoPropagator -> Host -> Chroot
instance IsContainer Chroot where
- containerProperties (Chroot _ _ h) = containerProperties h
- containerInfo (Chroot _ _ h) = containerInfo h
- setContainerProperties (Chroot loc b h) ps = Chroot loc b (setContainerProperties h ps)
+ containerProperties (Chroot _ _ _ h) = containerProperties h
+ containerInfo (Chroot _ _ _ h) = containerInfo h
+ setContainerProperties (Chroot loc b p h) ps =
+ let h' = setContainerProperties h ps
+ in Chroot loc b p h'
chrootSystem :: Chroot -> Maybe System
chrootSystem = fromInfoVal . fromInfo . containerInfo
instance Show Chroot where
- show c@(Chroot loc _ _) = "Chroot " ++ loc ++ " " ++ show (chrootSystem c)
+ show c@(Chroot loc _ _ _) = "Chroot " ++ loc ++ " " ++ show (chrootSystem c)
-- | Class of things that can do initial bootstrapping of an operating
-- System in a chroot.
@@ -93,6 +96,7 @@ instance ChrootBootstrapper Debootstrapped where
buildchroot (Debootstrapped cf) system loc = case system of
(Just s@(System (Debian _ _) _)) -> Right $ debootstrap s
(Just s@(System (Buntish _) _)) -> Right $ debootstrap s
+ (Just (System ArchLinux _)) -> Left "Arch Linux not supported by debootstrap."
(Just (System (FreeBSD _) _)) -> Left "FreeBSD not supported by debootstrap."
Nothing -> Left "Cannot debootstrap; OS not specified"
where
@@ -114,7 +118,9 @@ debootstrapped conf = bootstrapped (Debootstrapped conf)
-- | Defines a Chroot at the given location, bootstrapped with the
-- specified ChrootBootstrapper.
bootstrapped :: ChrootBootstrapper b => b -> FilePath -> Props metatypes -> Chroot
-bootstrapped bootstrapper location ps = Chroot location bootstrapper (host location ps)
+bootstrapped bootstrapper location ps = c
+ where
+ c = Chroot location bootstrapper propagateChrootInfo (host location ps)
-- | Ensures that the chroot exists and is provisioned according to its
-- properties.
@@ -123,15 +129,14 @@ bootstrapped bootstrapper location ps = Chroot location bootstrapper (host locat
-- is first unmounted. Note that it does not ensure that any processes
-- that might be running inside the chroot are stopped.
provisioned :: Chroot -> RevertableProperty (HasInfo + Linux) Linux
-provisioned c = provisioned' (propagateChrootInfo c) c False
+provisioned c = provisioned' c False
provisioned'
- :: (Property Linux -> Property (HasInfo + Linux))
- -> Chroot
+ :: Chroot
-> Bool
-> RevertableProperty (HasInfo + Linux) Linux
-provisioned' propigator c@(Chroot loc bootstrapper _) systemdonly =
- (propigator $ setup `describe` chrootDesc c "exists")
+provisioned' c@(Chroot loc bootstrapper infopropigator _) systemdonly =
+ (infopropigator c normalContainerInfo $ setup `describe` chrootDesc c "exists")
<!>
(teardown `describe` chrootDesc c "removed")
where
@@ -150,17 +155,20 @@ provisioned' propigator c@(Chroot loc bootstrapper _) systemdonly =
property ("removed " ++ loc) $
makeChange (removeChroot loc)
-propagateChrootInfo :: Chroot -> Property Linux -> Property (HasInfo + Linux)
-propagateChrootInfo c@(Chroot location _ _) p = propagateContainer location c $
- p `setInfoProperty` chrootInfo c
+type InfoPropagator = Chroot -> (PropagateInfo -> Bool) -> Property Linux -> Property (HasInfo + Linux)
+
+propagateChrootInfo :: InfoPropagator
+propagateChrootInfo c@(Chroot location _ _ _) pinfo p =
+ propagateContainer location c pinfo $
+ p `setInfoProperty` chrootInfo c
chrootInfo :: Chroot -> Info
-chrootInfo (Chroot loc _ h) = mempty `addInfo`
+chrootInfo (Chroot loc _ _ h) = mempty `addInfo`
mempty { _chroots = M.singleton loc h }
-- | Propellor is run inside the chroot to provision it.
propellChroot :: Chroot -> ([String] -> IO (CreateProcess, IO ())) -> Bool -> Property UnixLike
-propellChroot c@(Chroot loc _ _) mkproc systemdonly = property (chrootDesc c "provisioned") $ do
+propellChroot c@(Chroot loc _ _ _) mkproc systemdonly = property (chrootDesc c "provisioned") $ do
let d = localdir </> shimdir c
let me = localdir </> "propellor"
shim <- liftIO $ ifM (doesDirectoryExist d)
@@ -192,14 +200,12 @@ propellChroot c@(Chroot loc _ _) mkproc systemdonly = property (chrootDesc c "pr
, "--continue"
, show cmd
]
- let p' = p { env = Just pe }
- r <- liftIO $ withHandle StdoutHandle createProcessSuccess p'
- processChainOutput
+ r <- liftIO $ chainPropellor (p { env = Just pe })
liftIO cleanup
return r
toChain :: HostName -> Chroot -> Bool -> IO CmdLine
-toChain parenthost (Chroot loc _ _) systemdonly = do
+toChain parenthost (Chroot loc _ _ _) systemdonly = do
onconsole <- isConsole <$> getMessageHandle
return $ ChrootChain parenthost loc systemdonly onconsole
@@ -214,17 +220,16 @@ chain hostlist (ChrootChain hn loc systemdonly onconsole) =
go h = do
changeWorkingDirectory localdir
when onconsole forceConsole
- onlyProcess (provisioningLock loc) $ do
- r <- runPropellor (setInChroot h) $ ensureChildProperties $
- if systemdonly
- then [toChildProperty Systemd.installed]
- else hostProperties h
- flushConcurrentOutput
- putStrLn $ "\n" ++ show r
+ onlyProcess (provisioningLock loc) $
+ runChainPropellor (setInChroot h) $
+ ensureChildProperties $
+ if systemdonly
+ then [toChildProperty Systemd.installed]
+ else hostProperties h
chain _ _ = errorMessage "bad chain command"
inChrootProcess :: Bool -> Chroot -> [String] -> IO (CreateProcess, IO ())
-inChrootProcess keepprocmounted (Chroot loc _ _) cmd = do
+inChrootProcess keepprocmounted (Chroot loc _ _ _) cmd = do
mountproc
return (proc "chroot" (loc:cmd), cleanup)
where
@@ -244,13 +249,13 @@ provisioningLock :: FilePath -> FilePath
provisioningLock containerloc = "chroot" </> mungeloc containerloc ++ ".lock"
shimdir :: Chroot -> FilePath
-shimdir (Chroot loc _ _) = "chroot" </> mungeloc loc ++ ".shim"
+shimdir (Chroot loc _ _ _) = "chroot" </> mungeloc loc ++ ".shim"
mungeloc :: FilePath -> String
mungeloc = replace "/" "_"
chrootDesc :: Chroot -> String -> String
-chrootDesc (Chroot loc _ _) desc = "chroot " ++ loc ++ " " ++ desc
+chrootDesc (Chroot loc _ _ _) desc = "chroot " ++ loc ++ " " ++ desc
-- | Adding this property to a chroot prevents daemons and other services
-- from being started, which is often something you want to prevent when
@@ -286,3 +291,54 @@ setInChroot h = h { hostInfo = hostInfo h `addInfo` InfoVal (InChroot True) }
newtype InChroot = InChroot Bool
deriving (Typeable, Show)
+
+-- | Runs an action with the true localdir exposed,
+-- not the one bind-mounted into a chroot. The action is passed the
+-- path containing the contents of the localdir outside the chroot.
+--
+-- In a chroot, this is accomplished by temporily bind mounting the localdir
+-- to a temp directory, to preserve access to the original bind mount. Then
+-- we unmount the localdir to expose the true localdir. Finally, to cleanup,
+-- the temp directory is bind mounted back to the localdir.
+exposeTrueLocaldir :: (FilePath -> Propellor a) -> Propellor a
+exposeTrueLocaldir a = ifM inChroot
+ ( withTmpDirIn (takeDirectory localdir) "propellor.tmp" $ \tmpdir ->
+ bracket_
+ (movebindmount localdir tmpdir)
+ (movebindmount tmpdir localdir)
+ (a tmpdir)
+ , a localdir
+ )
+ where
+ movebindmount from to = liftIO $ do
+ run "mount" [Param "--bind", File from, File to]
+ -- Have to lazy unmount, because the propellor process
+ -- is running in the localdir that it's unmounting..
+ run "umount" [Param "-l", File from]
+ -- We were in the old localdir; move to the new one after
+ -- flipping the bind mounts. Otherwise, commands that try
+ -- to access the cwd will fail because it got umounted out
+ -- from under.
+ changeWorkingDirectory "/"
+ changeWorkingDirectory localdir
+ run cmd ps = unlessM (boolSystem cmd ps) $
+ error $ "exposeTrueLocaldir failed to run " ++ show (cmd, ps)
+
+-- | Generates a Chroot that has all the properties of a Host.
+--
+-- Note that it's possible to create loops using this, where a host
+-- contains a Chroot containing itself etc. Such loops will be detected at
+-- runtime.
+hostChroot :: ChrootBootstrapper bootstrapper => Host -> bootstrapper -> FilePath -> Chroot
+hostChroot h bootstrapper d = chroot
+ where
+ chroot = Chroot d bootstrapper pinfo h
+ pinfo = propagateHostChrootInfo h
+
+-- This is different than propagateChrootInfo in that Info using
+-- HostContext is not made to use the name of the chroot as its context,
+-- but instead uses the hostname of the Host.
+propagateHostChrootInfo :: Host -> InfoPropagator
+propagateHostChrootInfo h c pinfo p =
+ propagateContainer (hostName h) c pinfo $
+ p `setInfoProperty` chrootInfo c
diff --git a/src/Propellor/Property/Cmd.hs b/src/Propellor/Property/Cmd.hs
index 6b84acb5..f2de1a27 100644
--- a/src/Propellor/Property/Cmd.hs
+++ b/src/Propellor/Property/Cmd.hs
@@ -33,6 +33,7 @@ module Propellor.Property.Cmd (
Script,
scriptProperty,
userScriptProperty,
+ cmdResult,
-- * Lower-level interface for running commands
CommandParam(..),
boolSystem,
diff --git a/src/Propellor/Property/Concurrent.hs b/src/Propellor/Property/Concurrent.hs
index e69dc17d..e729d0cb 100644
--- a/src/Propellor/Property/Concurrent.hs
+++ b/src/Propellor/Property/Concurrent.hs
@@ -64,10 +64,13 @@ concurrently p1 p2 = (combineWith go go p1 p2)
-- Increase the number of capabilities right up to the number of
-- processors, so that A `concurrently` B `concurrently` C
-- runs all 3 properties on different processors when possible.
- go a1 a2 = do
+ go (Just a1) (Just a2) = Just $ do
n <- liftIO getNumProcessors
withCapabilities n $
concurrentSatisfy a1 a2
+ go (Just a1) Nothing = Just a1
+ go Nothing (Just a2) = Just a2
+ go Nothing Nothing = Nothing
-- | Ensures all the properties in the list, with a specified amount of
-- concurrency.
@@ -101,9 +104,9 @@ concurrentList getn d (Props ps) = property d go `addChildren` ps
Nothing -> return r
Just p -> do
hn <- asks hostName
- r' <- actionMessageOn hn
- (getDesc p)
- (getSatisfy p)
+ r' <- case getSatisfy p of
+ Nothing -> return NoChange
+ Just a -> actionMessageOn hn (getDesc p) a
worker q (r <> r')
-- | Run an action with the number of capabiities increased as necessary to
diff --git a/src/Propellor/Property/Conductor.hs b/src/Propellor/Property/Conductor.hs
index 8aa18d20..cfeb5aa7 100644
--- a/src/Propellor/Property/Conductor.hs
+++ b/src/Propellor/Property/Conductor.hs
@@ -323,15 +323,15 @@ instance Show NotConductorFor where
show (NotConductorFor l) = "NotConductorFor " ++ show (map hostName l)
instance IsInfo ConductorFor where
- propagateInfo _ = False
+ propagateInfo _ = PropagateInfo False
instance IsInfo NotConductorFor where
- propagateInfo _ = False
+ propagateInfo _ = PropagateInfo False
-- Added to Info when a host has been orchestrated.
newtype Orchestrated = Orchestrated Any
deriving (Typeable, Monoid, Show)
instance IsInfo Orchestrated where
- propagateInfo _ = False
+ propagateInfo _ = PropagateInfo False
isOrchestrated :: Orchestrated -> Bool
isOrchestrated (Orchestrated v) = getAny v
diff --git a/src/Propellor/Property/ConfFile.hs b/src/Propellor/Property/ConfFile.hs
index b49c626e..76d52bd9 100644
--- a/src/Propellor/Property/ConfFile.hs
+++ b/src/Propellor/Property/ConfFile.hs
@@ -9,8 +9,10 @@ module Propellor.Property.ConfFile (
IniSection,
IniKey,
containsIniSetting,
+ lacksIniSetting,
hasIniSection,
lacksIniSection,
+ iniFileContains,
) where
import Propellor.Base
@@ -92,6 +94,19 @@ containsIniSetting f (header, key, value) = adjustIniSection
go (l:ls) = if isKeyVal l then confline : ls else l : go ls
isKeyVal x = (filter (/= ' ') . takeWhile (/= '=')) x `elem` [key, '#':key]
+-- | Removes a key=value setting from a section of an .ini file.
+-- Note that the section heading is left in the file, so this is not a
+-- perfect reversion of containsIniSetting.
+lacksIniSetting :: FilePath -> (IniSection, IniKey, String) -> Property UnixLike
+lacksIniSetting f (header, key, value) = adjustIniSection
+ (f ++ " section [" ++ header ++ "] lacks " ++ key ++ "=" ++ value)
+ header
+ (filter (/= confline))
+ id
+ f
+ where
+ confline = key ++ "=" ++ value
+
-- | Ensures that a .ini file exists and contains a section
-- with a given key=value list of settings.
hasIniSection :: FilePath -> IniSection -> [(IniKey, String)] -> Property UnixLike
@@ -114,3 +129,13 @@ lacksIniSection f header = adjustIniSection
(const []) -- remove all lines of section
id -- add no lines if section is missing
f
+
+-- | Specifies the whole content of a .ini file.
+--
+-- Revertijg this causes the file not to exist.
+iniFileContains :: FilePath -> [(IniSection, [(IniKey, String)])] -> RevertableProperty UnixLike UnixLike
+iniFileContains f l = f `hasContent` content <!> notPresent f
+ where
+ content = concatMap sectioncontent l
+ sectioncontent (section, keyvalues) = iniHeader section :
+ map (\(key, value) -> key ++ "=" ++ value) keyvalues
diff --git a/src/Propellor/Property/Cron.hs b/src/Propellor/Property/Cron.hs
index 0966a7e5..ab700a9d 100644
--- a/src/Propellor/Property/Cron.hs
+++ b/src/Propellor/Property/Cron.hs
@@ -80,7 +80,8 @@ niceJob desc times user cddir command = job desc times user cddir
-- | Installs a cron job to run propellor.
runPropellor :: Times -> Property UnixLike
-runPropellor times = withOS "propellor cron job" $ \w o ->
+runPropellor times = withOS "propellor cron job" $ \w o -> do
+ bootstrapper <- getBootstrapper
ensureProperty w $
niceJob "propellor" times (User "root") localdir
- (bootstrapPropellorCommand o ++ "; ./propellor")
+ (bootstrapPropellorCommand bootstrapper o ++ "; ./propellor")
diff --git a/src/Propellor/Property/DebianMirror.hs b/src/Propellor/Property/DebianMirror.hs
index d8a9c423..ad15f9a2 100644
--- a/src/Propellor/Property/DebianMirror.hs
+++ b/src/Propellor/Property/DebianMirror.hs
@@ -79,7 +79,7 @@ data DebianMirror = DebianMirror
mkDebianMirror :: FilePath -> Cron.Times -> DebianMirror
mkDebianMirror dir crontimes = DebianMirror
- { _debianMirrorHostName = "httpredir.debian.org"
+ { _debianMirrorHostName = "deb.debian.org"
, _debianMirrorDir = dir
, _debianMirrorSuites = []
, _debianMirrorArchitectures = []
diff --git a/src/Propellor/Property/Debootstrap.hs b/src/Propellor/Property/Debootstrap.hs
index f8cb6e0e..e21bcdff 100644
--- a/src/Propellor/Property/Debootstrap.hs
+++ b/src/Propellor/Property/Debootstrap.hs
@@ -96,6 +96,7 @@ built' installprop target system@(System _ arch) config =
extractSuite :: System -> Maybe String
extractSuite (System (Debian _ s) _) = Just $ Apt.showSuite s
extractSuite (System (Buntish r) _) = Just r
+extractSuite (System (ArchLinux) _) = Nothing
extractSuite (System (FreeBSD _) _) = Nothing
-- | Ensures debootstrap is installed.
@@ -148,7 +149,7 @@ sourceInstall' = withTmpDir "debootstrap" $ \tmpd -> do
. filter ("debootstrap_" `isInfixOf`)
. filter (".tar." `isInfixOf`)
. extractUrls baseurl <$>
- readFileStrictAnyEncoding indexfile
+ readFileStrict indexfile
nukeFile indexfile
tarfile <- case urls of
diff --git a/src/Propellor/Property/DiskImage.hs b/src/Propellor/Property/DiskImage.hs
index 06dfa69c..6c1a572c 100644
--- a/src/Propellor/Property/DiskImage.hs
+++ b/src/Propellor/Property/DiskImage.hs
@@ -8,71 +8,95 @@ module Propellor.Property.DiskImage (
-- * Partition specification
module Propellor.Property.DiskImage.PartSpec,
-- * Properties
- DiskImage,
+ DiskImage(..),
+ RawDiskImage(..),
+ VirtualBoxPointer(..),
imageBuilt,
imageRebuilt,
imageBuiltFrom,
imageExists,
- -- * Finalization
- Finalization,
- grubBooted,
Grub.BIOS(..),
- noFinalization,
) where
import Propellor.Base
import Propellor.Property.DiskImage.PartSpec
import Propellor.Property.Chroot (Chroot)
import Propellor.Property.Chroot.Util (removeChroot)
+import Propellor.Property.Mount
import qualified Propellor.Property.Chroot as Chroot
import qualified Propellor.Property.Grub as Grub
import qualified Propellor.Property.File as File
import qualified Propellor.Property.Apt as Apt
import Propellor.Property.Parted
-import Propellor.Property.Mount
import Propellor.Property.Fstab (SwapPartition(..), genFstab)
import Propellor.Property.Partition
import Propellor.Property.Rsync
+import Propellor.Types.Info
+import Propellor.Types.Bootloader
import Propellor.Container
import Utility.Path
+import Utility.FileMode
-import Data.List (isPrefixOf, isInfixOf, sortBy)
+import Data.List (isPrefixOf, isInfixOf, sortBy, unzip4)
import Data.Function (on)
import qualified Data.Map.Strict as M
import qualified Data.ByteString.Lazy as L
import System.Posix.Files
-type DiskImage = FilePath
+-- | Type class of disk image formats.
+class DiskImage d where
+ -- | Get the location where the raw disk image should be stored.
+ rawDiskImage :: d -> RawDiskImage
+ -- | Describe the disk image (for display to the user)
+ describeDiskImage :: d -> String
+ -- | Convert the raw disk image file in the
+ -- `rawDiskImage` location into the desired disk image format.
+ -- For best efficiency, the raw disk imasge file should be left
+ -- unchanged on disk.
+ buildDiskImage :: d -> RevertableProperty DebianLike Linux
+
+-- | A raw disk image, that can be written directly out to a disk.
+newtype RawDiskImage = RawDiskImage FilePath
+
+instance DiskImage RawDiskImage where
+ rawDiskImage = id
+ describeDiskImage (RawDiskImage f) = f
+ buildDiskImage (RawDiskImage _) = doNothing <!> doNothing
+
+-- | A virtualbox .vmdk file, which contains a pointer to the raw disk
+-- image. This can be built very quickly.
+newtype VirtualBoxPointer = VirtualBoxPointer FilePath
+
+instance DiskImage VirtualBoxPointer where
+ rawDiskImage (VirtualBoxPointer f) = RawDiskImage $
+ dropExtension f ++ ".img"
+ describeDiskImage (VirtualBoxPointer f) = f
+ buildDiskImage (VirtualBoxPointer vmdkfile) = (setup <!> cleanup)
+ `describe` (vmdkfile ++ " built")
+ where
+ setup = cmdProperty "VBoxManage"
+ [ "internalcommands", "createrawvmdk"
+ , "-filename", vmdkfile
+ , "-rawdisk", diskimage
+ ]
+ `changesFile` vmdkfile
+ `onChange` File.mode vmdkfile (combineModes (ownerWriteMode : readModes))
+ `requires` Apt.installed ["virtualbox"]
+ `requires` File.notPresent vmdkfile
+ cleanup = tightenTargets $ File.notPresent vmdkfile
+ RawDiskImage diskimage = rawDiskImage (VirtualBoxPointer vmdkfile)
-- | Creates a bootable disk image.
--
-- First the specified Chroot is set up, and its properties are satisfied.
--
-- Then, the disk image is set up, and the chroot is copied into the
--- appropriate partition(s) of it.
---
--- Example use:
---
--- > import Propellor.Property.DiskImage
---
--- > let chroot d = Chroot.debootstrapped mempty d
--- > & osDebian Unstable X86_64
--- > & Apt.installed ["linux-image-amd64"]
--- > & User.hasPassword (User "root")
--- > & User.accountFor (User "demo")
--- > & User.hasPassword (User "demo")
--- > & User.hasDesktopGroups (User "demo")
--- > & ...
--- > in imageBuilt "/srv/images/foo.img" chroot
--- > MSDOS (grubBooted PC)
--- > [ partition EXT2 `mountedAt` "/boot"
--- > `setFlag` BootFlag
--- > , partition EXT4 `mountedAt` "/"
--- > `addFreeSpace` MegaBytes 100
--- > `mountOpt` errorReadonly
--- > , swapPartition (MegaBytes 256)
--- > ]
+-- appropriate partition(s) of it.
--
+-- The partitions default to being sized just large enough to fit the files
+-- from the chroot. You can use `addFreeSpace` to make them a bit larger
+-- than that, or `setSize` to use a fixed size.
+--
-- Note that the disk image file is reused if it already exists,
-- to avoid expensive IO to generate a new one. And, it's updated in-place,
-- so its contents are undefined during the build process.
@@ -81,39 +105,95 @@ type DiskImage = FilePath
-- chroot while the disk image is being built, which should prevent any
-- daemons that are included from being started on the system that is
-- building the disk image.
-imageBuilt :: DiskImage -> (FilePath -> Chroot) -> TableType -> Finalization -> [PartSpec] -> RevertableProperty (HasInfo + Linux) Linux
+--
+-- Example use:
+--
+-- > import Propellor.Property.DiskImage
+-- > import Propellor.Property.Chroot
+-- >
+-- > foo = host "foo.example.com" $ props
+-- > & imageBuilt (RawDiskImage "/srv/diskimages/disk.img") mychroot
+-- > MSDOS
+-- > [ partition EXT2 `mountedAt` "/boot"
+-- > `setFlag` BootFlag
+-- > , partition EXT4 `mountedAt` "/"
+-- > `addFreeSpace` MegaBytes 100
+-- > `mountOpt` errorReadonly
+-- > , swapPartition (MegaBytes 256)
+-- > ]
+-- > where
+-- > mychroot d = debootstrapped mempty d $ props
+-- > & osDebian Unstable X86_64
+-- > & Apt.installed ["linux-image-amd64"]
+-- > & Grub.installed PC
+-- > & User.hasPassword (User "root")
+-- > & User.accountFor (User "demo")
+-- > & User.hasPassword (User "demo")
+-- > & User.hasDesktopGroups (User "demo")
+-- > & ...
+--
+-- This can also be used with `Chroot.hostChroot` to build a disk image
+-- that has all the properties of a Host. For example:
+--
+-- > foo :: Host
+-- > foo = host "foo.example.com" $ props
+-- > & imageBuilt (RawDiskImage "/srv/diskimages/bar-disk.img")
+-- > (hostChroot bar (Debootstrapped mempty))
+-- > MSDOS
+-- > [ partition EXT2 `mountedAt` "/boot"
+-- > `setFlag` BootFlag
+-- > , partition EXT4 `mountedAt` "/"
+-- > `addFreeSpace` MegaBytes 5000
+-- > , swapPartition (MegaBytes 256)
+-- > ]
+-- >
+-- > bar :: Host
+-- > bar = host "bar.example.com" $ props
+-- > & osDebian Unstable X86_64
+-- > & Apt.installed ["linux-image-amd64"]
+-- > & Grub.installed PC
+-- > & hasPassword (User "root")
+imageBuilt :: DiskImage d => d -> (FilePath -> Chroot) -> TableType -> [PartSpec ()] -> RevertableProperty (HasInfo + DebianLike) Linux
imageBuilt = imageBuilt' False
-- | Like 'built', but the chroot is deleted and rebuilt from scratch each
-- time. This is more expensive, but useful to ensure reproducible results
-- when the properties of the chroot have been changed.
-imageRebuilt :: DiskImage -> (FilePath -> Chroot) -> TableType -> Finalization -> [PartSpec] -> RevertableProperty (HasInfo + Linux) Linux
+imageRebuilt :: DiskImage d => d -> (FilePath -> Chroot) -> TableType -> [PartSpec ()] -> RevertableProperty (HasInfo + DebianLike) Linux
imageRebuilt = imageBuilt' True
-imageBuilt' :: Bool -> DiskImage -> (FilePath -> Chroot) -> TableType -> Finalization -> [PartSpec] -> RevertableProperty (HasInfo + Linux) Linux
-imageBuilt' rebuild img mkchroot tabletype final partspec =
+imageBuilt' :: DiskImage d => Bool -> d -> (FilePath -> Chroot) -> TableType -> [PartSpec ()] -> RevertableProperty (HasInfo + DebianLike) Linux
+imageBuilt' rebuild img mkchroot tabletype partspec =
imageBuiltFrom img chrootdir tabletype final partspec
`requires` Chroot.provisioned chroot
`requires` (cleanrebuild <!> (doNothing :: Property UnixLike))
`describe` desc
where
- desc = "built disk image " ++ img
+ desc = "built disk image " ++ describeDiskImage img
+ RawDiskImage imgfile = rawDiskImage img
cleanrebuild :: Property Linux
cleanrebuild
| rebuild = property desc $ do
liftIO $ removeChroot chrootdir
return MadeChange
| otherwise = doNothing
- chrootdir = img ++ ".chroot"
+ chrootdir = imgfile ++ ".chroot"
chroot =
- let c = mkchroot chrootdir
+ let c = propprivdataonly $ mkchroot chrootdir
in setContainerProps c $ containerProps c
-- Before ensuring any other properties of the chroot,
-- avoid starting services. Reverted by imageFinalized.
&^ Chroot.noServices
- -- First stage finalization.
- & fst final
& cachesCleaned
+ -- Only propagate privdata Info from this chroot, nothing else.
+ propprivdataonly (Chroot.Chroot d b ip h) =
+ Chroot.Chroot d b (\c _ -> ip c onlyPrivData) h
+ -- Pick boot loader finalization based on which bootloader is
+ -- installed.
+ final = case fromInfo (containerInfo chroot) of
+ [GrubInstalled] -> grubBooted
+ [] -> unbootable "no bootloader is installed"
+ _ -> unbootable "multiple bootloaders are installed; don't know which to use"
-- | This property is automatically added to the chroot when building a
-- disk image. It cleans any caches of information that can be omitted;
@@ -124,13 +204,14 @@ cachesCleaned = "cache cleaned" ==> (Apt.cacheCleaned `pickOS` skipit)
skipit = doNothing :: Property UnixLike
-- | Builds a disk image from the contents of a chroot.
-imageBuiltFrom :: DiskImage -> FilePath -> TableType -> Finalization -> [PartSpec] -> RevertableProperty (HasInfo + Linux) UnixLike
+imageBuiltFrom :: DiskImage d => d -> FilePath -> TableType -> Finalization -> [PartSpec ()] -> RevertableProperty (HasInfo + DebianLike) Linux
imageBuiltFrom img chrootdir tabletype final partspec = mkimg <!> rmimg
where
- desc = img ++ " built from " ++ chrootdir
+ desc = describeDiskImage img ++ " built from " ++ chrootdir
+ dest@(RawDiskImage imgfile) = rawDiskImage img
mkimg = property' desc $ \w -> do
- -- unmount helper filesystems such as proc from the chroot
- -- before getting sizes
+ -- Unmount helper filesystems such as proc from the chroot
+ -- first; don't want to include the contents of those.
liftIO $ unmountBelow chrootdir
szm <- M.mapKeys (toSysDir chrootdir) . M.map toPartSize
<$> liftIO (dirSizes chrootdir)
@@ -139,18 +220,20 @@ imageBuiltFrom img chrootdir tabletype final partspec = mkimg <!> rmimg
let (mnts, mntopts, parttable) = fitChrootSize tabletype partspec $
map (calcsz mnts) mnts
ensureProperty w $
- imageExists img (partTableSize parttable)
+ imageExists' dest parttable
`before`
- partitioned YesReallyDeleteDiskContents img parttable
+ kpartx imgfile (mkimg' mnts mntopts parttable)
`before`
- kpartx img (mkimg' mnts mntopts parttable)
+ buildDiskImage img
mkimg' mnts mntopts parttable devs =
partitionsPopulated chrootdir mnts mntopts devs
`before`
imageFinalized final mnts mntopts devs parttable
- rmimg = File.notPresent img
+ rmimg = undoRevertableProperty (buildDiskImage img)
+ `before` undoRevertableProperty (imageExists' dest dummyparttable)
+ dummyparttable = PartTable tabletype []
-partitionsPopulated :: FilePath -> [Maybe MountPoint] -> [MountOpts] -> [LoopDev] -> Property Linux
+partitionsPopulated :: FilePath -> [Maybe MountPoint] -> [MountOpts] -> [LoopDev] -> Property DebianLike
partitionsPopulated chrootdir mnts mntopts devs = property' desc $ \w ->
mconcat $ zipWith3 (go w) mnts mntopts devs
where
@@ -179,10 +262,10 @@ partitionsPopulated chrootdir mnts mntopts devs = property' desc $ \w ->
-- The constructor for each Partition is passed the size of the files
-- from the chroot that will be put in that partition.
-fitChrootSize :: TableType -> [PartSpec] -> [PartSize] -> ([Maybe MountPoint], [MountOpts], PartTable)
+fitChrootSize :: TableType -> [PartSpec ()] -> [PartSize] -> ([Maybe MountPoint], [MountOpts], PartTable)
fitChrootSize tt l basesizes = (mounts, mountopts, parttable)
where
- (mounts, mountopts, sizers) = unzip3 l
+ (mounts, mountopts, sizers, _) = unzip4 l
parttable = PartTable tt (zipWith id sizers basesizes)
-- | Generates a map of the sizes of the contents of
@@ -219,8 +302,8 @@ getMountSz szm l (Just mntpt) =
-- If the file doesn't exist, or is too small, creates a new one, full of 0's.
--
-- If the file is too large, truncates it down to the specified size.
-imageExists :: FilePath -> ByteSize -> Property Linux
-imageExists img sz = property ("disk image exists" ++ img) $ liftIO $ do
+imageExists :: RawDiskImage -> ByteSize -> Property Linux
+imageExists (RawDiskImage img) isz = property ("disk image exists" ++ img) $ liftIO $ do
ms <- catchMaybeIO $ getFileStatus img
case ms of
Just s
@@ -231,21 +314,47 @@ imageExists img sz = property ("disk image exists" ++ img) $ liftIO $ do
_ -> do
L.writeFile img (L.replicate (fromIntegral sz) 0)
return MadeChange
+ where
+ sz = ceiling (fromInteger isz / sectorsize) * ceiling sectorsize
+ -- Disks have a sector size, and making a disk image not
+ -- aligned to a sector size will confuse some programs.
+ -- Common sector sizes are 512 and 4096; use 4096 as it's larger.
+ sectorsize = 4096 :: Double
--- | A pair of properties. The first property is satisfied within the
--- chroot, and is typically used to download the boot loader.
+-- | Ensure that disk image file exists and is partitioned.
--
--- The second property is run after the disk image is created,
--- with its populated partition tree mounted in the provided
--- location from the provided loop devices. This will typically
--- take care of installing the boot loader to the image.
+-- Avoids repartitioning the disk image, when a file of the right size
+-- already exists, and it has the same PartTable.
+imageExists' :: RawDiskImage -> PartTable -> RevertableProperty DebianLike UnixLike
+imageExists' dest@(RawDiskImage img) parttable = (setup <!> cleanup) `describe` desc
+ where
+ desc = "disk image exists " ++ img
+ parttablefile = img ++ ".parttable"
+ setup = property' desc $ \w -> do
+ oldparttable <- liftIO $ catchDefaultIO "" $ readFileStrict parttablefile
+ res <- ensureProperty w $ imageExists dest (partTableSize parttable)
+ if res == NoChange && oldparttable == show parttable
+ then return NoChange
+ else if res == FailedChange
+ then return FailedChange
+ else do
+ liftIO $ writeFile parttablefile (show parttable)
+ ensureProperty w $ partitioned YesReallyDeleteDiskContents img parttable
+ cleanup = File.notPresent img
+ `before`
+ File.notPresent parttablefile
+
+-- | A property that is run after the disk image is created, with
+-- its populated partition tree mounted in the provided
+-- location from the provided loop devices. This is typically used to
+-- install a boot loader in the image's superblock.
--
--- It's ok if the second property leaves additional things mounted
+-- It's ok if the property leaves additional things mounted
-- in the partition tree.
-type Finalization = (Property Linux, (FilePath -> [LoopDev] -> Property Linux))
+type Finalization = (FilePath -> [LoopDev] -> Property Linux)
imageFinalized :: Finalization -> [Maybe MountPoint] -> [MountOpts] -> [LoopDev] -> PartTable -> Property Linux
-imageFinalized (_, final) mnts mntopts devs (PartTable _ parts) =
+imageFinalized final mnts mntopts devs (PartTable _ parts) =
property' "disk image finalized" $ \w ->
withTmpDir "mnt" $ \top ->
go w top `finally` liftIO (unmountall top)
@@ -289,48 +398,27 @@ imageFinalized (_, final) mnts mntopts devs (PartTable _ parts) =
allowservices top = nukeFile (top ++ "/usr/sbin/policy-rc.d")
-noFinalization :: Finalization
-noFinalization = (doNothing, \_ _ -> doNothing)
+unbootable :: String -> Finalization
+unbootable msg = \_ _ -> property desc $ do
+ warningMessage (desc ++ ": " ++ msg)
+ return FailedChange
+ where
+ desc = "image is not bootable"
-- | Makes grub be the boot loader of the disk image.
-grubBooted :: Grub.BIOS -> Finalization
-grubBooted bios = (Grub.installed' bios, boots)
+--
+-- This does not install the grub package. You will need to add
+-- the `Grub.installed` property to the chroot.
+grubBooted :: Finalization
+grubBooted mnt loopdevs = Grub.bootsMounted mnt wholediskloopdev
+ `describe` "disk image boots using grub"
where
- boots mnt loopdevs = combineProperties "disk image boots using grub" $ props
- -- bind mount host /dev so grub can access the loop devices
- & bindMount "/dev" (inmnt "/dev")
- & mounted "proc" "proc" (inmnt "/proc") mempty
- & mounted "sysfs" "sys" (inmnt "/sys") mempty
- -- update the initramfs so it gets the uuid of the root partition
- & inchroot "update-initramfs" ["-u"]
- `assume` MadeChange
- -- work around for http://bugs.debian.org/802717
- & check haveosprober (inchroot "chmod" ["-x", osprober])
- & inchroot "update-grub" []
- `assume` MadeChange
- & check haveosprober (inchroot "chmod" ["+x", osprober])
- & inchroot "grub-install" [wholediskloopdev]
- `assume` MadeChange
- -- sync all buffered changes out to the disk image
- -- may not be necessary, but seemed needed sometimes
- -- when using the disk image right away.
- & cmdProperty "sync" []
- `assume` NoChange
- where
- -- cannot use </> since the filepath is absolute
- inmnt f = mnt ++ f
-
- inchroot cmd ps = cmdProperty "chroot" ([mnt, cmd] ++ ps)
-
- haveosprober = doesFileExist (inmnt osprober)
- osprober = "/etc/grub.d/30_os-prober"
-
- -- It doesn't matter which loopdev we use; all
- -- come from the same disk image, and it's the loop dev
- -- for the whole disk image we seek.
- wholediskloopdev = case loopdevs of
- (l:_) -> wholeDiskLoopDev l
- [] -> error "No loop devs provided!"
+ -- It doesn't matter which loopdev we use; all
+ -- come from the same disk image, and it's the loop dev
+ -- for the whole disk image we seek.
+ wholediskloopdev = case loopdevs of
+ (l:_) -> wholeDiskLoopDev l
+ [] -> error "No loop devs provided!"
isChild :: FilePath -> Maybe MountPoint -> Bool
isChild mntpt (Just d)
diff --git a/src/Propellor/Property/DiskImage/PartSpec.hs b/src/Propellor/Property/DiskImage/PartSpec.hs
index 4b05df03..55249889 100644
--- a/src/Propellor/Property/DiskImage/PartSpec.hs
+++ b/src/Propellor/Property/DiskImage/PartSpec.hs
@@ -1,32 +1,28 @@
-- | Disk image partition specification and combinators.
+-- Partitions in disk images default to being sized large enough to hold
+-- the files that appear in the directory where the partition is to be
+-- mounted. Plus a fudge factor, since filesystems have some space
+-- overhead.
+
module Propellor.Property.DiskImage.PartSpec (
+ module Propellor.Types.PartSpec,
module Propellor.Property.DiskImage.PartSpec,
- Partition,
- PartSize(..),
- PartFlag(..),
- TableType(..),
- Fs(..),
- MountPoint,
+ module Propellor.Property.Parted.Types,
+ module Propellor.Property.Partition,
) where
import Propellor.Base
import Propellor.Property.Parted
-import Propellor.Property.Mount
+import Propellor.Types.PartSpec
+import Propellor.Property.Parted.Types
+import Propellor.Property.Partition (Fs(..))
--- | Specifies a mount point, mount options, and a constructor for a Partition.
---
--- The size that is eventually provided is the amount of space needed to
--- hold the files that appear in the directory where the partition is to be
--- mounted. Plus a fudge factor, since filesystems have some space
--- overhead.
-type PartSpec = (Maybe MountPoint, MountOpts, PartSize -> Partition)
-
--- | Partitions that are not to be mounted (ie, LinuxSwap), or that have
--- no corresponding directory in the chroot will have 128 MegaBytes
--- provided as a default size.
-defSz :: PartSize
-defSz = MegaBytes 128
+-- | Adds additional free space to the partition.
+addFreeSpace :: PartSpec t -> PartSize -> PartSpec t
+addFreeSpace (mp, o, p, t) freesz = (mp, o, p', t)
+ where
+ p' = \sz -> p (sz <> freesz)
-- | Add 2% for filesystem overhead. Rationalle for picking 2%:
-- A filesystem with 1% overhead might just sneak by as acceptable.
@@ -35,47 +31,3 @@ defSz = MegaBytes 128
-- Add an additional 200 mb for temp files, journals, etc.
fudge :: PartSize -> PartSize
fudge (MegaBytes n) = MegaBytes (n + n `div` 100 * 2 + 3 + 200)
-
--- | Specifies a swap partition of a given size.
-swapPartition :: PartSize -> PartSpec
-swapPartition sz = (Nothing, mempty, const (mkPartition LinuxSwap sz))
-
--- | Specifies a partition with a given filesystem.
---
--- The partition is not mounted anywhere by default; use the combinators
--- below to configure it.
-partition :: Fs -> PartSpec
-partition fs = (Nothing, mempty, mkPartition fs)
-
--- | Specifies where to mount a partition.
-mountedAt :: PartSpec -> FilePath -> PartSpec
-mountedAt (_, o, p) mp = (Just mp, o, p)
-
--- | Specifies a mount option, such as "noexec"
-mountOpt :: ToMountOpts o => PartSpec -> o -> PartSpec
-mountOpt (mp, o, p) o' = (mp, o <> toMountOpts o', p)
-
--- | Mount option to make a partition be remounted readonly when there's an
--- error accessing it.
-errorReadonly :: MountOpts
-errorReadonly = toMountOpts "errors=remount-ro"
-
--- | Adds additional free space to the partition.
-addFreeSpace :: PartSpec -> PartSize -> PartSpec
-addFreeSpace (mp, o, p) freesz = (mp, o, \sz -> p (sz <> freesz))
-
--- | Forced a partition to be a specific size, instead of scaling to the
--- size needed for the files in the chroot.
-setSize :: PartSpec -> PartSize -> PartSpec
-setSize (mp, o, p) sz = (mp, o, const (p sz))
-
--- | Sets a flag on the partition.
-setFlag :: PartSpec -> PartFlag -> PartSpec
-setFlag s f = adjustp s $ \p -> p { partFlags = (f, True):partFlags p }
-
--- | Makes a MSDOS partition be Extended, rather than Primary.
-extended :: PartSpec -> PartSpec
-extended s = adjustp s $ \p -> p { partType = Extended }
-
-adjustp :: PartSpec -> (Partition -> Partition) -> PartSpec
-adjustp (mp, o, p) f = (mp, o, f . p)
diff --git a/src/Propellor/Property/Dns.hs b/src/Propellor/Property/Dns.hs
index 2e2710a6..889aece5 100644
--- a/src/Propellor/Property/Dns.hs
+++ b/src/Propellor/Property/Dns.hs
@@ -250,7 +250,7 @@ confStanza c =
cfgline f v = "\t" ++ f ++ " " ++ v ++ ";"
ipblock name l =
[ "\t" ++ name ++ " {" ] ++
- (map (\ip -> "\t\t" ++ fromIPAddr ip ++ ";") l) ++
+ (map (\ip -> "\t\t" ++ val ip ++ ";") l) ++
[ "\t};" ]
mastersblock
| null (confMasters c) = []
@@ -307,17 +307,17 @@ rValue :: Record -> Maybe String
rValue (Address (IPv4 addr)) = Just addr
rValue (Address (IPv6 addr)) = Just addr
rValue (CNAME d) = Just $ dValue d
-rValue (MX pri d) = Just $ show pri ++ " " ++ dValue d
+rValue (MX pri d) = Just $ val pri ++ " " ++ dValue d
rValue (NS d) = Just $ dValue d
rValue (SRV priority weight port target) = Just $ unwords
- [ show priority
- , show weight
- , show port
+ [ val priority
+ , val weight
+ , val port
, dValue target
]
rValue (SSHFP x y s) = Just $ unwords
- [ show x
- , show y
+ [ val x
+ , val y
, s
]
rValue (INCLUDE f) = Just f
diff --git a/src/Propellor/Property/Docker.hs b/src/Propellor/Property/Docker.hs
index 2ef97438..66418253 100644
--- a/src/Propellor/Property/Docker.hs
+++ b/src/Propellor/Property/Docker.hs
@@ -55,21 +55,22 @@ import Propellor.Container
import qualified Propellor.Property.File as File
import qualified Propellor.Property.Apt as Apt
import qualified Propellor.Property.Cmd as Cmd
+import qualified Propellor.Property.Pacman as Pacman
import qualified Propellor.Shim as Shim
import Utility.Path
import Utility.ThreadScheduler
+import Utility.Split
import Control.Concurrent.Async hiding (link)
import System.Posix.Directory
import System.Posix.Process
import Prelude hiding (init)
import Data.List hiding (init)
-import Data.List.Utils
import qualified Data.Map as M
import System.Console.Concurrent
-installed :: Property DebianLike
-installed = Apt.installed ["docker.io"]
+installed :: Property (DebianLike + ArchLinux)
+installed = Apt.installed ["docker.io"] `pickOS` Pacman.installed ["docker"]
-- | Configures docker with an authentication file, so that images can be
-- pushed to index.docker.io. Optional.
@@ -183,8 +184,9 @@ imagePulled ctr = pulled `describe` msg
image = getImageName ctr
propagateContainerInfo :: Container -> Property (HasInfo + Linux) -> Property (HasInfo + Linux)
-propagateContainerInfo ctr@(Container _ h) p = propagateContainer cn ctr $
- p `addInfoProperty` dockerinfo
+propagateContainerInfo ctr@(Container _ h) p =
+ propagateContainer cn ctr normalContainerInfo $
+ p `addInfoProperty` dockerinfo
where
dockerinfo = dockerInfo $
mempty { _dockerContainers = M.singleton cn h }
@@ -322,7 +324,7 @@ class Publishable p where
toPublish :: p -> String
instance Publishable (Bound Port) where
- toPublish p = fromPort (hostSide p) ++ ":" ++ fromPort (containerSide p)
+ toPublish p = val (hostSide p) ++ ":" ++ val (containerSide p)
-- | string format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort
instance Publishable String where
@@ -574,8 +576,7 @@ provisionContainer cid = containerDesc cid $ property "provisioned" $ liftIO $ d
let p = inContainerProcess cid
(if isConsole msgh then ["-it"] else [])
(shim : params)
- r <- withHandle StdoutHandle createProcessSuccess p $
- processChainOutput
+ r <- chainPropellor p
when (r /= FailedChange) $
setProvisionedFlag cid
return r
@@ -594,10 +595,9 @@ chain hostlist hn s = case toContainerId s of
where
go cid h = do
changeWorkingDirectory localdir
- onlyProcess (provisioningLock cid) $ do
- r <- runPropellor h $ ensureChildProperties $ hostProperties h
- flushConcurrentOutput
- putStrLn $ "\n" ++ show r
+ onlyProcess (provisioningLock cid) $
+ runChainPropellor h $
+ ensureChildProperties $ hostProperties h
stopContainer :: ContainerId -> IO Bool
stopContainer cid = boolSystem dockercmd [Param "stop", Param $ fromContainerId cid ]
@@ -659,10 +659,10 @@ listImages :: IO [ImageUID]
listImages = map ImageUID . lines <$> readProcess dockercmd ["images", "--all", "--quiet"]
runProp :: String -> RunParam -> Property (HasInfo + Linux)
-runProp field val = tightenTargets $ pureInfoProperty (param) $
+runProp field v = tightenTargets $ pureInfoProperty (param) $
mempty { _dockerRunParams = [DockerRunParam (\_ -> "--"++param)] }
where
- param = field++"="++val
+ param = field++"="++v
genProp :: String -> (HostName -> RunParam) -> Property (HasInfo + Linux)
genProp field mkval = tightenTargets $ pureInfoProperty field $
diff --git a/src/Propellor/Property/File.hs b/src/Propellor/Property/File.hs
index 95fc6f81..3293599a 100644
--- a/src/Propellor/Property/File.hs
+++ b/src/Propellor/Property/File.hs
@@ -1,4 +1,4 @@
-{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE FlexibleInstances, FlexibleContexts #-}
module Propellor.Property.File where
@@ -6,8 +6,10 @@ import Propellor.Base
import Utility.FileMode
import qualified Data.ByteString.Lazy as L
+import Data.List (isInfixOf, isPrefixOf)
import System.Posix.Files
import System.Exit
+import Data.Char
type Line = String
@@ -18,14 +20,42 @@ f `hasContent` newcontent = fileProperty
(\_oldcontent -> newcontent) f
-- | Ensures that a line is present in a file, adding it to the end if not.
+--
+-- For example:
+--
+-- > & "/etc/default/daemon.conf" `File.containsLine` ("cachesize = " ++ val 1024)
+--
+-- The above example uses `val` to serialize a `ConfigurableValue`
containsLine :: FilePath -> Line -> Property UnixLike
f `containsLine` l = f `containsLines` [l]
+-- | Ensures that a list of lines are present in a file, adding any that are not
+-- to the end of the file.
+--
+-- Note that this property does not guarantee that the lines will appear
+-- consecutively, nor in the order specified. If you need either of these, use
+-- 'File.containsBlock'.
containsLines :: FilePath -> [Line] -> Property UnixLike
f `containsLines` ls = fileProperty (f ++ " contains:" ++ show ls) go f
where
go content = content ++ filter (`notElem` content) ls
+-- | Ensures that a block of consecutive lines is present in a file, adding it
+-- to the end if not. Revert to ensure that the block is not present (though
+-- the lines it contains could be present, non-consecutively).
+containsBlock :: FilePath -> [Line] -> RevertableProperty UnixLike UnixLike
+f `containsBlock` ls =
+ fileProperty (f ++ " contains block:" ++ show ls) add f
+ <!> fileProperty (f ++ " lacks block:" ++ show ls) remove f
+ where
+ add content
+ | ls `isInfixOf` content = content
+ | otherwise = content ++ ls
+ remove [] = []
+ remove content@(x:xs)
+ | ls `isPrefixOf` content = remove (drop (length ls) content)
+ | otherwise = x : remove xs
+
-- | 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.
@@ -75,11 +105,11 @@ hasPrivContent' writemode source f context =
-- | Replaces the content of a file with the transformed content of another file
basedOn :: FilePath -> (FilePath, [Line] -> [Line]) -> Property UnixLike
-f `basedOn` (f', a) = property' desc $ \o -> do
- tmpl <- liftIO $ readFile f'
+f `basedOn` (src, a) = property' desc $ \o -> do
+ tmpl <- liftIO $ readFile src
ensureProperty o $ fileProperty desc (\_ -> a $ lines $ tmpl) f
where
- desc = f ++ " is based on " ++ f'
+ desc = f ++ " is based on " ++ src
-- | Removes a file. Does not remove symlinks or non-plain-files.
notPresent :: FilePath -> Property UnixLike
@@ -120,23 +150,26 @@ link `isSymlinkedTo` (LinkTarget target) = property desc $
-- | Ensures that a file is a copy of another (regular) file.
isCopyOf :: FilePath -> FilePath -> Property UnixLike
-f `isCopyOf` f' = property desc $ go =<< (liftIO $ tryIO $ getFileStatus f')
+f `isCopyOf` src = property desc $ go =<< (liftIO $ tryIO $ getFileStatus src)
where
- desc = f ++ " is copy of " ++ f'
+ desc = f ++ " is copy of " ++ src
go (Right stat) = if isRegularFile stat
- then gocmp =<< (liftIO $ cmp)
- else warningMessage (f' ++ " is not a regular file") >>
+ then ifM (liftIO $ doesFileExist f)
+ ( gocmp =<< (liftIO $ cmp)
+ , doit
+ )
+ else warningMessage (src ++ " is not a regular file") >>
return FailedChange
go (Left e) = warningMessage (show e) >> return FailedChange
- cmp = safeSystem "cmp" [Param "-s", Param "--", File f, File f']
+ cmp = safeSystem "cmp" [Param "-s", Param "--", File f, File src]
gocmp ExitSuccess = noChange
gocmp (ExitFailure 1) = doit
gocmp _ = warningMessage "cmp failed" >> return FailedChange
- doit = makeChange $ copy f' `viaStableTmp` f
- copy src dest = unlessM (runcp src dest) $ errorMessage "cp failed"
- runcp src dest = boolSystem "cp"
+ doit = makeChange $ copy `viaStableTmp` f
+ copy dest = unlessM (runcp dest) $ errorMessage "cp failed"
+ runcp dest = boolSystem "cp"
[Param "--preserve=all", Param "--", File src, File dest]
-- | Ensures that a file/dir has the specified owner and group.
@@ -147,6 +180,20 @@ ownerGroup f (User owner) (Group group) = p `describe` (f ++ " owner " ++ og)
`changesFile` f
og = owner ++ ":" ++ group
+-- | Given a base directory, and a relative path under that
+-- directory, applies a property to each component of the path in turn,
+-- starting with the base directory.
+--
+-- For example, to make a file owned by a user, making sure their home
+-- directory and the subdirectories to it are also owned by them:
+--
+-- > "/home/user/program/file" `hasContent` ["foo"]
+-- > `before` applyPath "/home/user" ".config/program/file"
+-- > (\f -> ownerGroup f (User "user") (Group "user"))
+applyPath :: Monoid (Property metatypes) => FilePath -> FilePath -> (FilePath -> Property metatypes) -> Property metatypes
+applyPath basedir relpath mkp = mconcat $
+ map mkp (scanl (</>) basedir (splitPath relpath))
+
-- | Ensures that a file/dir has the specfied mode.
mode :: FilePath -> FileMode -> Property UnixLike
mode f v = p `changesFile` f
@@ -221,3 +268,51 @@ viaStableTmp a f = bracketIO setup cleanup go
go tmpfile = do
a tmpfile
liftIO $ rename tmpfile f
+
+-- | Generates a base configuration file name from a String, which
+-- can be put in a configuration directory, such as
+-- </etc/apt/sources.list.d/>
+--
+-- The generated file name is limited to using ASCII alphanumerics,
+-- \'_\' and \'.\' , so that programs that only accept a limited set of
+-- characters will accept it. Any other characters will be encoded
+-- in escaped form.
+--
+-- Some file extensions, such as ".old" may be filtered out by
+-- programs that use configuration directories. To avoid such problems,
+-- it's a good idea to add an static prefix and extension to the
+-- result of this function. For example:
+--
+-- > aptConf foo = "/etc/apt/apt.conf.d" </> "propellor_" ++ configFileName foo <.> ".conf"
+configFileName :: String -> FilePath
+configFileName = concatMap escape
+ where
+ escape c
+ | isAscii c && isAlphaNum c = [c]
+ | c == '.' = [c]
+ | otherwise = '_' : show (ord c)
+
+-- | Applies configFileName to any value that can be shown.
+showConfigFileName :: Show v => v -> FilePath
+showConfigFileName = configFileName . show
+
+-- | Inverse of showConfigFileName.
+readConfigFileName :: Read v => FilePath -> Maybe v
+readConfigFileName = readish . unescape
+ where
+ unescape [] = []
+ unescape ('_':cs) = case break (not . isDigit) cs of
+ ([], _) -> '_' : unescape cs
+ (ns, cs') -> case readish ns of
+ Nothing -> '_' : ns ++ unescape cs'
+ Just n -> chr n : unescape cs'
+ unescape (c:cs) = c : unescape cs
+
+data Overwrite = OverwriteExisting | PreserveExisting
+
+-- | When passed PreserveExisting, only ensures the property when the file
+-- does not exist.
+checkOverwrite :: Overwrite -> FilePath -> (FilePath -> Property i) -> Property i
+checkOverwrite OverwriteExisting f mkp = mkp f
+checkOverwrite PreserveExisting f mkp =
+ check (not <$> doesFileExist f) (mkp f)
diff --git a/src/Propellor/Property/Firejail.hs b/src/Propellor/Property/Firejail.hs
index b7841e07..6e877683 100644
--- a/src/Propellor/Property/Firejail.hs
+++ b/src/Propellor/Property/Firejail.hs
@@ -22,7 +22,7 @@ installed = Apt.installed ["firejail"]
--
-- See "DESKTOP INTEGRATION" in firejail(1).
jailed :: [String] -> Property DebianLike
-jailed ps = (jailed' `applyToList` ps)
+jailed ps = mconcat (map jailed' ps)
`requires` installed
`describe` unwords ("firejail jailed":ps)
diff --git a/src/Propellor/Property/Firewall.hs b/src/Propellor/Property/Firewall.hs
index 3ea19ffa..736a4458 100644
--- a/src/Propellor/Property/Firewall.hs
+++ b/src/Propellor/Property/Firewall.hs
@@ -15,7 +15,6 @@ module Propellor.Property.Firewall (
TCPFlag(..),
Frequency(..),
IPWithMask(..),
- fromIPWithMask
) where
import Data.Monoid
@@ -44,16 +43,16 @@ rule c tb tg rs = property ("firewall rule: " <> show r) addIpTable
toIpTable :: Rule -> [CommandParam]
toIpTable r = map Param $
- fromChain (ruleChain r) :
+ val (ruleChain r) :
toIpTableArg (ruleRules r) ++
- ["-t", fromTable (ruleTable r), "-j", fromTarget (ruleTarget r)]
+ ["-t", val (ruleTable r), "-j", val (ruleTarget r)]
toIpTableArg :: Rules -> [String]
toIpTableArg Everything = []
toIpTableArg (Proto proto) = ["-p", map toLower $ show proto]
-toIpTableArg (DPort port) = ["--dport", fromPort port]
+toIpTableArg (DPort port) = ["--dport", val port]
toIpTableArg (DPortRange (portf, portt)) =
- ["--dport", fromPort portf ++ ":" ++ fromPort portt]
+ ["--dport", val portf ++ ":" ++ val portt]
toIpTableArg (InIFace iface) = ["-i", iface]
toIpTableArg (OutIFace iface) = ["-o", iface]
toIpTableArg (Ctstate states) =
@@ -64,12 +63,12 @@ toIpTableArg (Ctstate states) =
toIpTableArg (ICMPType i) =
[ "-m"
, "icmp"
- , "--icmp-type", fromICMPTypeMatch i
+ , "--icmp-type", val i
]
toIpTableArg (RateLimit f) =
[ "-m"
, "limit"
- , "--limit", fromFrequency f
+ , "--limit", val f
]
toIpTableArg (TCPFlags m c) =
[ "-m"
@@ -87,30 +86,30 @@ toIpTableArg (GroupOwner (Group g)) =
]
toIpTableArg (Source ipwm) =
[ "-s"
- , intercalate "," (map fromIPWithMask ipwm)
+ , intercalate "," (map val ipwm)
]
toIpTableArg (Destination ipwm) =
[ "-d"
- , intercalate "," (map fromIPWithMask ipwm)
+ , intercalate "," (map val ipwm)
]
toIpTableArg (NotDestination ipwm) =
[ "!"
, "-d"
- , intercalate "," (map fromIPWithMask ipwm)
+ , intercalate "," (map val ipwm)
]
toIpTableArg (NatDestination ip mport) =
[ "--to-destination"
- , fromIPAddr ip ++ maybe "" (\p -> ":" ++ fromPort p) mport
+ , val ip ++ maybe "" (\p -> ":" ++ val p) mport
]
toIpTableArg (r :- r') = toIpTableArg r <> toIpTableArg r'
data IPWithMask = IPWithNoMask IPAddr | IPWithIPMask IPAddr IPAddr | IPWithNumMask IPAddr Int
deriving (Eq, Show)
-fromIPWithMask :: IPWithMask -> String
-fromIPWithMask (IPWithNoMask ip) = fromIPAddr ip
-fromIPWithMask (IPWithIPMask ip ipm) = fromIPAddr ip ++ "/" ++ fromIPAddr ipm
-fromIPWithMask (IPWithNumMask ip m) = fromIPAddr ip ++ "/" ++ show m
+instance ConfigurableValue IPWithMask where
+ val (IPWithNoMask ip) = val ip
+ val (IPWithIPMask ip ipm) = val ip ++ "/" ++ val ipm
+ val (IPWithNumMask ip m) = val ip ++ "/" ++ val m
data Rule = Rule
{ ruleChain :: Chain
@@ -122,33 +121,33 @@ data Rule = Rule
data Table = Filter | Nat | Mangle | Raw | Security
deriving (Eq, Show)
-fromTable :: Table -> String
-fromTable Filter = "filter"
-fromTable Nat = "nat"
-fromTable Mangle = "mangle"
-fromTable Raw = "raw"
-fromTable Security = "security"
+instance ConfigurableValue Table where
+ val Filter = "filter"
+ val Nat = "nat"
+ val Mangle = "mangle"
+ val Raw = "raw"
+ val Security = "security"
data Target = ACCEPT | REJECT | DROP | LOG | TargetCustom String
deriving (Eq, Show)
-fromTarget :: Target -> String
-fromTarget ACCEPT = "ACCEPT"
-fromTarget REJECT = "REJECT"
-fromTarget DROP = "DROP"
-fromTarget LOG = "LOG"
-fromTarget (TargetCustom t) = t
+instance ConfigurableValue Target where
+ val ACCEPT = "ACCEPT"
+ val REJECT = "REJECT"
+ val DROP = "DROP"
+ val LOG = "LOG"
+ val (TargetCustom t) = t
data Chain = INPUT | OUTPUT | FORWARD | PREROUTING | POSTROUTING | ChainCustom String
deriving (Eq, Show)
-fromChain :: Chain -> String
-fromChain INPUT = "INPUT"
-fromChain OUTPUT = "OUTPUT"
-fromChain FORWARD = "FORWARD"
-fromChain PREROUTING = "PREROUTING"
-fromChain POSTROUTING = "POSTROUTING"
-fromChain (ChainCustom c) = c
+instance ConfigurableValue Chain where
+ val INPUT = "INPUT"
+ val OUTPUT = "OUTPUT"
+ val FORWARD = "FORWARD"
+ val PREROUTING = "PREROUTING"
+ val POSTROUTING = "POSTROUTING"
+ val (ChainCustom c) = c
data Proto = TCP | UDP | ICMP
deriving (Eq, Show)
@@ -159,15 +158,15 @@ data ConnectionState = ESTABLISHED | RELATED | NEW | INVALID
data ICMPTypeMatch = ICMPTypeName String | ICMPTypeCode Int
deriving (Eq, Show)
-fromICMPTypeMatch :: ICMPTypeMatch -> String
-fromICMPTypeMatch (ICMPTypeName t) = t
-fromICMPTypeMatch (ICMPTypeCode c) = show c
+instance ConfigurableValue ICMPTypeMatch where
+ val (ICMPTypeName t) = t
+ val (ICMPTypeCode c) = val c
data Frequency = NumBySecond Int
deriving (Eq, Show)
-fromFrequency :: Frequency -> String
-fromFrequency (NumBySecond n) = show n ++ "/second"
+instance ConfigurableValue Frequency where
+ val (NumBySecond n) = val n ++ "/second"
type TCPFlagMask = [TCPFlag]
diff --git a/src/Propellor/Property/FreeBSD/Pkg.hs b/src/Propellor/Property/FreeBSD/Pkg.hs
index 704c1db9..77bf5768 100644
--- a/src/Propellor/Property/FreeBSD/Pkg.hs
+++ b/src/Propellor/Property/FreeBSD/Pkg.hs
@@ -39,7 +39,7 @@ pkgCmd cmd args =
newtype PkgUpdate = PkgUpdate String
deriving (Typeable, Monoid, Show)
instance IsInfo PkgUpdate where
- propagateInfo _ = False
+ propagateInfo _ = PropagateInfo False
pkgUpdated :: PkgUpdate -> Bool
pkgUpdated (PkgUpdate _) = True
@@ -55,8 +55,9 @@ update =
newtype PkgUpgrade = PkgUpgrade String
deriving (Typeable, Monoid, Show)
+
instance IsInfo PkgUpgrade where
- propagateInfo _ = False
+ propagateInfo _ = PropagateInfo False
pkgUpgraded :: PkgUpgrade -> Bool
pkgUpgraded (PkgUpgrade _) = True
diff --git a/src/Propellor/Property/FreeBSD/Poudriere.hs b/src/Propellor/Property/FreeBSD/Poudriere.hs
index 58477468..378c5530 100644
--- a/src/Propellor/Property/FreeBSD/Poudriere.hs
+++ b/src/Propellor/Property/FreeBSD/Poudriere.hs
@@ -19,8 +19,9 @@ poudriereConfigPath = "/usr/local/etc/poudriere.conf"
newtype PoudriereConfigured = PoudriereConfigured String
deriving (Typeable, Monoid, Show)
+
instance IsInfo PoudriereConfigured where
- propagateInfo _ = False
+ propagateInfo _ = PropagateInfo False
poudriereConfigured :: PoudriereConfigured -> Bool
poudriereConfigured (PoudriereConfigured _) = True
@@ -68,7 +69,7 @@ jail j@(Jail name version arch) = tightenTargets $
nx <- liftIO $ not <$> jailExists j
return $ c && nx
- (cmd, args) = poudriereCommand "jail" ["-c", "-j", name, "-a", show arch, "-v", show version]
+ (cmd, args) = poudriereCommand "jail" ["-c", "-j", name, "-a", val arch, "-v", val version]
createJail = cmdProperty cmd args
in
check chk createJail
@@ -101,9 +102,10 @@ data PoudriereZFS = PoudriereZFS ZFS.ZFS ZFS.ZFSProperties
data Jail = Jail String FBSDVersion PoudriereArch
data PoudriereArch = I386 | AMD64 deriving (Eq)
-instance Show PoudriereArch where
- show I386 = "i386"
- show AMD64 = "amd64"
+
+instance ConfigurableValue PoudriereArch where
+ val I386 = "i386"
+ val AMD64 = "amd64"
fromArchitecture :: Architecture -> PoudriereArch
fromArchitecture X86_64 = AMD64
@@ -127,7 +129,7 @@ instance ToShellConfigLines PoudriereZFS where
toAssoc (PoudriereZFS (ZFS.ZFS (ZFS.ZPool pool) dataset) _) =
[ ("NO_ZFS", "no")
, ("ZPOOL", pool)
- , ("ZROOTFS", show dataset)
+ , ("ZROOTFS", val dataset)
]
type ConfigLine = String
diff --git a/src/Propellor/Property/FreeDesktop.hs b/src/Propellor/Property/FreeDesktop.hs
new file mode 100644
index 00000000..75dcbdfa
--- /dev/null
+++ b/src/Propellor/Property/FreeDesktop.hs
@@ -0,0 +1,29 @@
+-- | Freedesktop.org configuration file properties.
+
+module Propellor.Property.FreeDesktop where
+
+import Propellor.Base
+import Propellor.Property.ConfFile
+
+desktopFile :: String -> FilePath
+desktopFile s = s ++ ".desktop"
+
+-- | Name used in a desktop file; user visible.
+type Name = String
+
+-- | Command that a dekstop file runs. May include parameters.
+type Exec = String
+
+-- | Specifies an autostart file. By default it will be located in the
+-- system-wide autostart directory.
+autostart :: FilePath -> Name -> Exec -> RevertableProperty UnixLike UnixLike
+autostart f n e = ("/etc/xdg/autostart" </> f) `iniFileContains`
+ [ ("Desktop Entry",
+ [ ("Type", "Application")
+ , ("Version", "1.0")
+ , ("Name", n)
+ , ("Comment", "Autostart")
+ , ("Terminal", "False")
+ , ("Exec", e)
+ ] )
+ ]
diff --git a/src/Propellor/Property/Fstab.hs b/src/Propellor/Property/Fstab.hs
index 60f11d8e..29b85426 100644
--- a/src/Propellor/Property/Fstab.hs
+++ b/src/Propellor/Property/Fstab.hs
@@ -24,19 +24,32 @@ import Utility.Table
-- Note that if anything else is already mounted at the `MountPoint`, it
-- will be left as-is by this property.
mounted :: FsType -> Source -> MountPoint -> MountOpts -> Property Linux
-mounted fs src mnt opts = tightenTargets $
- "/etc/fstab" `File.containsLine` l
- `describe` (mnt ++ " mounted by fstab")
+mounted fs src mnt opts = tightenTargets $
+ listed fs src mnt opts
`onChange` mountnow
where
- l = intercalate "\t" [src, mnt, fs, formatMountOpts opts, dump, passno]
- dump = "0"
- passno = "2"
-- This use of mountPoints, which is linux-only, is why this
-- property currently only supports linux.
mountnow = check (notElem mnt <$> mountPoints) $
cmdProperty "mount" [mnt]
+-- | Ensures that </etc/fstab> contains a line mounting the specified
+-- `Source` on the specified `MountPoint`. Does not ensure that it's
+-- currently `mounted`.
+listed :: FsType -> Source -> MountPoint -> MountOpts -> Property UnixLike
+listed fs src mnt opts = "/etc/fstab" `File.containsLine` l
+ `describe` (mnt ++ " mounted by fstab")
+ where
+ l = intercalate "\t" [src, mnt, fs, formatMountOpts opts, dump, passno]
+ dump = "0"
+ passno = "2"
+
+-- | Ensures that </etc/fstab> contains a line enabling the specified
+-- `Source` to be used as swap space, and that it's enabled.
+swap :: Source -> Property Linux
+swap src = listed "swap" src "none" mempty
+ `onChange` swapOn src
+
newtype SwapPartition = SwapPartition FilePath
-- | Replaces </etc/fstab> with a file that should cause the currently
@@ -77,8 +90,8 @@ genFstab mnts swaps mnttransform = do
, pure "0"
, pure (if mnt == "/" then "1" else "2")
]
- getswapcfg (SwapPartition swap) = sequence
- [ fromMaybe swap <$> getM (\a -> a swap)
+ getswapcfg (SwapPartition s) = sequence
+ [ fromMaybe s <$> getM (\a -> a s)
[ uuidprefix getSourceUUID
, sourceprefix getSourceLabel
]
diff --git a/src/Propellor/Property/Gpg.hs b/src/Propellor/Property/Gpg.hs
index 74e9df5a..27baa4ba 100644
--- a/src/Propellor/Property/Gpg.hs
+++ b/src/Propellor/Property/Gpg.hs
@@ -2,7 +2,6 @@ module Propellor.Property.Gpg where
import Propellor.Base
import qualified Propellor.Property.Apt as Apt
-import Utility.FileSystemEncoding
import System.PosixCompat
@@ -35,7 +34,6 @@ keyImported key@(GpgKeyId keyid) user@(User u) = prop
( return NoChange
, makeChange $ withHandle StdinHandle createProcessSuccess
(proc "su" ["-c", "gpg --import", u]) $ \h -> do
- fileEncoding h
hPutStr h (unlines keylines)
hClose h
)
diff --git a/src/Propellor/Property/Grub.hs b/src/Propellor/Property/Grub.hs
index a03fc5a0..d0516dc8 100644
--- a/src/Propellor/Property/Grub.hs
+++ b/src/Propellor/Property/Grub.hs
@@ -3,6 +3,10 @@ module Propellor.Property.Grub where
import Propellor.Base
import qualified Propellor.Property.File as File
import qualified Propellor.Property.Apt as Apt
+import Propellor.Property.Mount
+import Propellor.Property.Chroot (inChroot)
+import Propellor.Types.Info
+import Propellor.Types.Bootloader
-- | Eg, \"hd0,0\" or \"xen/xvda1\"
type GrubDevice = String
@@ -18,9 +22,10 @@ data BIOS = PC | EFI64 | EFI32 | Coreboot | Xen
-- | Installs the grub package. This does not make grub be used as the
-- bootloader.
--
--- This includes running update-grub.
-installed :: BIOS -> Property DebianLike
-installed bios = installed' bios `onChange` mkConfig
+-- This includes running update-grub, unless it's run in a chroot.
+installed :: BIOS -> Property (HasInfo + DebianLike)
+installed bios = installed' bios
+ `onChange` (check (not <$> inChroot) mkConfig)
-- Run update-grub, to generate the grub boot menu. It will be
-- automatically updated when kernel packages are installed.
@@ -29,11 +34,11 @@ mkConfig = tightenTargets $ cmdProperty "update-grub" []
`assume` MadeChange
-- | Installs grub; does not run update-grub.
-installed' :: BIOS -> Property Linux
-installed' bios = (aptinstall `pickOS` unsupportedOS)
+installed' :: BIOS -> Property (HasInfo + DebianLike)
+installed' bios = setInfoProperty aptinstall
+ (toInfo [GrubInstalled])
`describe` "grub package installed"
where
- aptinstall :: Property DebianLike
aptinstall = Apt.installed [debpkg]
debpkg = case bios of
PC -> "grub-pc"
@@ -64,12 +69,12 @@ boots dev = tightenTargets $ cmdProperty "grub-install" [dev]
--
-- The rootdev should be in the form "hd0", while the bootdev is in the form
-- "xen/xvda".
-chainPVGrub :: GrubDevice -> GrubDevice -> TimeoutSecs -> Property DebianLike
+chainPVGrub :: GrubDevice -> GrubDevice -> TimeoutSecs -> Property (HasInfo + DebianLike)
chainPVGrub rootdev bootdev timeout = combineProperties desc $ props
& File.dirExists "/boot/grub"
& "/boot/grub/menu.lst" `File.hasContent`
[ "default 1"
- , "timeout " ++ show timeout
+ , "timeout " ++ val timeout
, ""
, "title grub-xen shim"
, "root (" ++ rootdev ++ ")"
@@ -85,3 +90,54 @@ chainPVGrub rootdev bootdev timeout = combineProperties desc $ props
xenshim = scriptProperty ["grub-mkimage --prefix '(" ++ bootdev ++ ")/boot/grub' -c /boot/load.cf -O x86_64-xen /usr/lib/grub/x86_64-xen/*.mod > /boot/xen-shim"]
`assume` MadeChange
`describe` "/boot-xen-shim"
+
+-- | This is a version of `boots` that makes grub boot the system mounted
+-- at a particular directory. The OSDevice should be the underlying disk
+-- device that grub will be installed to (generally a whole disk,
+-- not a partition).
+bootsMounted :: FilePath -> OSDevice -> Property Linux
+bootsMounted mnt wholediskdev = combineProperties desc $ props
+ -- remove mounts that are done below to make sure the right thing
+ -- gets mounted
+ & cleanupmounts
+ -- bind mount host /dev so grub can access the loop devices
+ & bindMount "/dev" (inmnt "/dev")
+ & mounted "proc" "proc" (inmnt "/proc") mempty
+ & mounted "sysfs" "sys" (inmnt "/sys") mempty
+ -- update the initramfs so it gets the uuid of the root partition
+ & inchroot "update-initramfs" ["-u"]
+ `assume` MadeChange
+ -- work around for http://bugs.debian.org/802717
+ & check haveosprober (inchroot "chmod" ["-x", osprober])
+ & inchroot "update-grub" []
+ `assume` MadeChange
+ & check haveosprober (inchroot "chmod" ["+x", osprober])
+ & inchroot "grub-install" [wholediskdev]
+ `assume` MadeChange
+ & cleanupmounts
+ -- sync all buffered changes out to the disk in case it's
+ -- used right away
+ & cmdProperty "sync" []
+ `assume` NoChange
+ where
+ desc = "grub boots " ++ wholediskdev
+
+ -- cannot use </> since the filepath is absolute
+ inmnt f = mnt ++ f
+
+ inchroot cmd ps = cmdProperty "chroot" ([mnt, cmd] ++ ps)
+
+ haveosprober = doesFileExist (inmnt osprober)
+ osprober = "/etc/grub.d/30_os-prober"
+
+ cleanupmounts :: Property Linux
+ cleanupmounts = property desc $ liftIO $ do
+ cleanup "/sys"
+ cleanup "/proc"
+ cleanup "/dev"
+ return NoChange
+ where
+ cleanup m =
+ let mp = inmnt m
+ in whenM (isMounted mp) $
+ umountLazy mp
diff --git a/src/Propellor/Property/HostingProvider/Linode.hs b/src/Propellor/Property/HostingProvider/Linode.hs
index fca3df63..ebe8d261 100644
--- a/src/Propellor/Property/HostingProvider/Linode.hs
+++ b/src/Propellor/Property/HostingProvider/Linode.hs
@@ -8,7 +8,7 @@ import Utility.FileMode
-- | Configures grub to use the serial console as set up by Linode.
-- Useful when running a distribution supplied kernel.
-- <https://www.linode.com/docs/tools-reference/custom-kernels-distros/run-a-distribution-supplied-kernel-with-kvm>
-serialGrub :: Property DebianLike
+serialGrub :: Property (HasInfo + DebianLike)
serialGrub = "/etc/default/grub" `File.containsLines`
[ "GRUB_CMDLINE_LINUX=\"console=ttyS0,19200n8\""
, "GRUB_DISABLE_LINUX_UUID=true"
@@ -17,11 +17,12 @@ serialGrub = "/etc/default/grub" `File.containsLines`
]
`onChange` Grub.mkConfig
`requires` Grub.installed Grub.PC
+ `describe` "GRUB configured for Linode serial console"
-- | Linode's pv-grub-x86_64 (only used for its older XEN instances)
-- does not support booting recent Debian kernels compressed
-- with xz. This sets up pv-grub chaining to enable it.
-chainPVGrub :: Grub.TimeoutSecs -> Property DebianLike
+chainPVGrub :: Grub.TimeoutSecs -> Property (HasInfo + DebianLike)
chainPVGrub = Grub.chainPVGrub "hd0" "xen/xvda"
-- | Linode disables mlocate's cron job's execute permissions,
diff --git a/src/Propellor/Property/Hostname.hs b/src/Propellor/Property/Hostname.hs
index e1342d91..1eb9d690 100644
--- a/src/Propellor/Property/Hostname.hs
+++ b/src/Propellor/Property/Hostname.hs
@@ -3,9 +3,9 @@ module Propellor.Property.Hostname where
import Propellor.Base
import qualified Propellor.Property.File as File
import Propellor.Property.Chroot (inChroot)
+import Utility.Split
import Data.List
-import Data.List.Utils
-- | Ensures that the hostname is set using best practices, to whatever
-- name the `Host` has.
diff --git a/src/Propellor/Property/LightDM.hs b/src/Propellor/Property/LightDM.hs
index 339fa9a3..d471d314 100644
--- a/src/Propellor/Property/LightDM.hs
+++ b/src/Propellor/Property/LightDM.hs
@@ -10,7 +10,12 @@ installed :: Property DebianLike
installed = Apt.installed ["lightdm"]
-- | Configures LightDM to skip the login screen and autologin as a user.
-autoLogin :: User -> Property UnixLike
-autoLogin (User u) = "/etc/lightdm/lightdm.conf" `ConfFile.containsIniSetting`
- ("SeatDefaults", "autologin-user", u)
- `describe` "lightdm autologin"
+autoLogin :: User -> RevertableProperty DebianLike DebianLike
+autoLogin (User u) = (setup <!> cleanup)
+ `describe` ("lightdm autologin for " ++ u)
+ where
+ cf = "/etc/lightdm/lightdm.conf"
+ setting = ("Seat:*", "autologin-user", u)
+ setup = cf `ConfFile.containsIniSetting` setting
+ `requires` installed
+ cleanup = tightenTargets $ cf `ConfFile.lacksIniSetting` setting
diff --git a/src/Propellor/Property/List.hs b/src/Propellor/Property/List.hs
index 0eec04c7..758e51ce 100644
--- a/src/Propellor/Property/List.hs
+++ b/src/Propellor/Property/List.hs
@@ -43,6 +43,13 @@ propertyList desc (Props ps) =
-- | Combines a list of properties, resulting in one property that
-- ensures each in turn. Stops if a property fails.
+--
+-- > combineProperties "foo" $ props
+-- > & bar
+-- > & baz
+--
+-- This is similar to using `mconcat` with a list of properties,
+-- except it can combine together different types of properties.
combineProperties :: SingI metatypes => Desc -> Props (MetaTypes metatypes) -> Property (MetaTypes metatypes)
combineProperties desc (Props ps) =
property desc (combineSatisfy cs NoChange)
@@ -53,7 +60,7 @@ combineProperties desc (Props ps) =
combineSatisfy :: [ChildProperty] -> Result -> Propellor Result
combineSatisfy [] rs = return rs
combineSatisfy (p:ps) rs = do
- r <- catchPropellor $ getSatisfy p
+ r <- maybe (return NoChange) catchPropellor (getSatisfy p)
case r of
FailedChange -> return FailedChange
_ -> combineSatisfy ps (r <> rs)
diff --git a/src/Propellor/Property/Locale.hs b/src/Propellor/Property/Locale.hs
index b7cf242c..53091fc9 100644
--- a/src/Propellor/Property/Locale.hs
+++ b/src/Propellor/Property/Locale.hs
@@ -4,6 +4,7 @@ module Propellor.Property.Locale where
import Propellor.Base
import Propellor.Property.File
+import qualified Propellor.Property.Apt as Apt
import Data.List (isPrefixOf)
@@ -50,7 +51,8 @@ locale `isSelectedFor` vars = do
-- Per Debian bug #684134 we cannot ensure a locale is generated by means of
-- Apt.reConfigure. So localeAvailable edits /etc/locale.gen manually.
available :: Locale -> RevertableProperty DebianLike DebianLike
-available locale = ensureAvailable <!> ensureUnavailable
+available locale = ensureAvailable `requires` Apt.installed ["locales"]
+ <!> ensureUnavailable
where
f = "/etc/locale.gen"
desc = (locale ++ " locale generated")
@@ -61,7 +63,7 @@ available locale = ensureAvailable <!> ensureUnavailable
then ensureProperty w $
fileProperty desc (foldr uncomment []) f
`onChange` regenerate
- else return FailedChange -- locale unavailable for generation
+ else error $ "locale " ++ locale ++ " is not present in /etc/locale.gen, even in commented out form; cannot generate"
ensureUnavailable :: Property DebianLike
ensureUnavailable = tightenTargets $
fileProperty (locale ++ " locale not generated") (foldr comment []) f
diff --git a/src/Propellor/Property/Logcheck.hs b/src/Propellor/Property/Logcheck.hs
index ced9fce2..8eaf56fd 100644
--- a/src/Propellor/Property/Logcheck.hs
+++ b/src/Propellor/Property/Logcheck.hs
@@ -16,21 +16,21 @@ import qualified Propellor.Property.File as File
data ReportLevel = Workstation | Server | Paranoid
type Service = String
-instance Show ReportLevel where
- show Workstation = "workstation"
- show Server = "server"
- show Paranoid = "paranoid"
+instance ConfigurableValue ReportLevel where
+ val Workstation = "workstation"
+ val Server = "server"
+ val Paranoid = "paranoid"
-- The common prefix used by default in syslog lines.
defaultPrefix :: String
defaultPrefix = "^\\w{3} [ :[:digit:]]{11} [._[:alnum:]-]+ "
ignoreFilePath :: ReportLevel -> Service -> FilePath
-ignoreFilePath t n = "/etc/logcheck/ignore.d." ++ (show t) </> n
+ignoreFilePath t n = "/etc/logcheck/ignore.d." ++ (val t) </> n
ignoreLines :: ReportLevel -> Service -> [String] -> Property UnixLike
ignoreLines t n ls = (ignoreFilePath t n) `File.containsLines` ls
- `describe` ("logcheck ignore lines for " ++ n ++ "(" ++ (show t) ++ ")")
+ `describe` ("logcheck ignore lines for " ++ n ++ "(" ++ val t ++ ")")
installed :: Property DebianLike
installed = Apt.installed ["logcheck"]
diff --git a/src/Propellor/Property/Mount.hs b/src/Propellor/Property/Mount.hs
index 026509a9..2c4d9620 100644
--- a/src/Propellor/Property/Mount.hs
+++ b/src/Propellor/Property/Mount.hs
@@ -40,6 +40,9 @@ formatMountOpts (MountOpts []) = "defaults"
formatMountOpts (MountOpts l) = intercalate "," l
-- | Mounts a device, without listing it in </etc/fstab>.
+--
+-- Note that this property will fail if the device is already mounted
+-- at the MountPoint.
mounted :: FsType -> Source -> MountPoint -> MountOpts -> Property UnixLike
mounted fs src mnt opts = property (mnt ++ " mounted") $
toResult <$> liftIO (mount fs src mnt opts)
@@ -52,6 +55,17 @@ bindMount src dest = tightenTargets $
`assume` MadeChange
`describe` ("bind mounted " ++ src ++ " to " ++ dest)
+-- | Enables swapping to a device, which must be formatted already as a swap
+-- partition.
+swapOn :: Source -> RevertableProperty Linux Linux
+swapOn mnt = tightenTargets doswapon <!> tightenTargets doswapoff
+ where
+ swaps = lines <$> readProcess "swapon" ["--show=NAME"]
+ doswapon = check (notElem mnt <$> swaps) $
+ cmdProperty "swapon" [mnt]
+ doswapoff = check (elem mnt <$> swaps) $
+ cmdProperty "swapoff" [mnt]
+
mount :: FsType -> Source -> MountPoint -> MountOpts -> IO Bool
mount fs src mnt opts = boolSystem "mount" $
[ Param "-t", Param fs
@@ -64,6 +78,10 @@ mount fs src mnt opts = boolSystem "mount" $
mountPoints :: IO [MountPoint]
mountPoints = lines <$> readProcess "findmnt" ["-rn", "--output", "target"]
+-- | Checks if anything is mounted at the MountPoint.
+isMounted :: MountPoint -> IO Bool
+isMounted mnt = isJust <$> getFsType mnt
+
-- | Finds all filesystems mounted inside the specified directory.
mountPointsBelow :: FilePath -> IO [MountPoint]
mountPointsBelow target = filter (\p -> simplifyPath p /= simplifyPath target)
@@ -115,12 +133,15 @@ blkidTag tag dev = catchDefaultIO Nothing $
-- | Unmounts a device or mountpoint,
-- lazily so any running processes don't block it.
+--
+-- Note that this will fail if it's not mounted.
umountLazy :: FilePath -> IO ()
umountLazy mnt =
unlessM (boolSystem "umount" [ Param "-l", Param mnt ]) $
stopPropellorMessage $ "failed unmounting " ++ mnt
--- | Unmounts anything mounted inside the specified directory.
+-- | Unmounts anything mounted inside the specified directory,
+-- not including the directory itself.
unmountBelow :: FilePath -> IO ()
unmountBelow d = do
submnts <- mountPointsBelow d
diff --git a/src/Propellor/Property/Munin.hs b/src/Propellor/Property/Munin.hs
index dd74d91b..6dab25ef 100644
--- a/src/Propellor/Property/Munin.hs
+++ b/src/Propellor/Property/Munin.hs
@@ -46,8 +46,8 @@ hostListFragment' hs os = concatMap muninHost hs
where
muninHost :: Host -> [String]
muninHost h = [ "[" ++ (hostName h) ++ "]"
- , " address " ++ maybe (hostName h) (fromIPAddr . fst) (hOverride h)
- ] ++ (maybe [] (\x -> [" port " ++ (fromPort $ snd x)]) (hOverride h)) ++ [""]
+ , " address " ++ maybe (hostName h) (val . fst) (hOverride h)
+ ] ++ (maybe [] (\x -> [" port " ++ (val $ snd x)]) (hOverride h)) ++ [""]
hOverride :: Host -> Maybe (IPAddr, Port)
hOverride h = lookup (hostName h) os
diff --git a/src/Propellor/Property/Network.hs b/src/Propellor/Property/Network.hs
index 9ed9e591..b581fa3f 100644
--- a/src/Propellor/Property/Network.hs
+++ b/src/Propellor/Property/Network.hs
@@ -7,6 +7,9 @@ import Data.Char
type Interface = String
+-- | Options to put in a stanza of an ifupdown interfaces file.
+type InterfaceOptions = [(String, String)]
+
ifUp :: Interface -> Property DebianLike
ifUp iface = tightenTargets $ cmdProperty "ifup" [iface]
`assume` MadeChange
@@ -19,27 +22,57 @@ ifUp iface = tightenTargets $ cmdProperty "ifup" [iface]
--
-- No interfaces are brought up or down by this property.
cleanInterfacesFile :: Property DebianLike
-cleanInterfacesFile = tightenTargets $ hasContent interfacesFile
- [ "# Deployed by propellor, do not edit."
- , ""
- , "source-directory interfaces.d"
+cleanInterfacesFile = interfaceFileContains interfacesFile
+ [ "source-directory interfaces.d"
, ""
, "# The loopback network interface"
, "auto lo"
, "iface lo inet loopback"
]
+ []
`describe` ("clean " ++ interfacesFile)
-- | Configures an interface to get its address via dhcp.
dhcp :: Interface -> Property DebianLike
-dhcp iface = tightenTargets $ hasContent (interfaceDFile iface)
+dhcp iface = dhcp' iface mempty
+
+dhcp' :: Interface -> InterfaceOptions -> Property DebianLike
+dhcp' iface options = interfaceFileContains (interfaceDFile iface)
[ "auto " ++ iface
, "iface " ++ iface ++ " inet dhcp"
- ]
+ ] options
`describe` ("dhcp " ++ iface)
`requires` interfacesDEnabled
--- | Writes a static interface file for the specified interface.
+newtype Gateway = Gateway IPAddr
+
+-- | Configures an interface with a static address and gateway.
+static :: Interface -> IPAddr -> Maybe Gateway -> Property DebianLike
+static iface addr gateway = static' iface addr gateway mempty
+
+static' :: Interface -> IPAddr -> Maybe Gateway -> InterfaceOptions -> Property DebianLike
+static' iface addr gateway options =
+ interfaceFileContains (interfaceDFile iface) headerlines options'
+ `describe` ("static IP address for " ++ iface)
+ `requires` interfacesDEnabled
+ where
+ headerlines =
+ [ "auto " ++ iface
+ , "iface " ++ iface ++ " " ++ inet ++ " static"
+ ]
+ options' = catMaybes
+ [ Just $ ("address", val addr)
+ , case gateway of
+ Just (Gateway gaddr) ->
+ Just ("gateway", val gaddr)
+ Nothing -> Nothing
+ ] ++ options
+ inet = case addr of
+ IPv4 _ -> "inet"
+ IPv6 _ -> "inet6"
+
+-- | Writes a static interface file for the specified interface
+-- to preserve its current configuration.
--
-- The interface has to be up already. It could have been brought up by
-- DHCP, or by other means. The current ipv4 addresses
@@ -50,8 +83,8 @@ dhcp iface = tightenTargets $ hasContent (interfaceDFile iface)
--
-- (ipv6 addresses are not included because it's assumed they come up
-- automatically in most situations.)
-static :: Interface -> Property DebianLike
-static iface = tightenTargets $
+preserveStatic :: Interface -> Property DebianLike
+preserveStatic iface = tightenTargets $
check (not <$> doesFileExist f) setup
`describe` desc
`requires` interfacesDEnabled
@@ -84,13 +117,13 @@ static iface = tightenTargets $
-- | 6to4 ipv6 connection, should work anywhere
ipv6to4 :: Property DebianLike
-ipv6to4 = tightenTargets $ hasContent (interfaceDFile "sit0")
- [ "# Deployed by propellor, do not edit."
+ipv6to4 = tightenTargets $ interfaceFileContains (interfaceDFile "sit0")
+ [ "auto sit0"
, "iface sit0 inet6 static"
- , "\taddress 2002:5044:5531::1"
- , "\tnetmask 64"
- , "\tgateway ::192.88.99.1"
- , "auto sit0"
+ ]
+ [ ("address", "2002:5044:5531::1")
+ , ("netmask", "64")
+ , ("gateway", "::192.88.99.1")
]
`describe` "ipv6to4"
`requires` interfacesDEnabled
@@ -114,3 +147,10 @@ interfacesDEnabled :: Property DebianLike
interfacesDEnabled = tightenTargets $
containsLine interfacesFile "source-directory interfaces.d"
`describe` "interfaces.d directory enabled"
+
+interfaceFileContains :: FilePath -> [String] -> InterfaceOptions -> Property DebianLike
+interfaceFileContains f headerlines options = tightenTargets $ hasContent f $
+ warning : headerlines ++ map fmt options
+ where
+ fmt (k, v) = "\t" ++ k ++ " " ++ v
+ warning = "# Deployed by propellor, do not edit."
diff --git a/src/Propellor/Property/OS.hs b/src/Propellor/Property/OS.hs
index d974cfbc..c31bef7b 100644
--- a/src/Propellor/Property/OS.hs
+++ b/src/Propellor/Property/OS.hs
@@ -64,7 +64,7 @@ import Control.Exception (throw)
-- > & User.accountFor "joey"
-- > & User.hasSomePassword "joey"
-- > -- rest of system properties here
-cleanInstallOnce :: Confirmation -> Property Linux
+cleanInstallOnce :: Confirmation -> Property DebianLike
cleanInstallOnce confirmation = check (not <$> doesFileExist flagfile) $
go `requires` confirmed "clean install confirmed" confirmation
where
@@ -207,7 +207,7 @@ preserveNetwork = go `requires` Network.cleanInterfacesFile
["route", "list", "scope", "global"]
case words <$> headMaybe ls of
Just ("default":"via":_:"dev":iface:_) ->
- ensureProperty w $ Network.static iface
+ ensureProperty w $ Network.preserveStatic iface
_ -> do
warningMessage "did not find any default ipv4 route"
return FailedChange
diff --git a/src/Propellor/Property/Obnam.hs b/src/Propellor/Property/Obnam.hs
index 5bf3ff06..7943b46e 100644
--- a/src/Propellor/Property/Obnam.hs
+++ b/src/Propellor/Property/Obnam.hs
@@ -1,6 +1,9 @@
-- | Support for the Obnam backup tool <http://obnam.org/>
+--
+-- This module is deprecated because Obnam has been retired by its
+-- author.
-module Propellor.Property.Obnam where
+module Propellor.Property.Obnam {-# DEPRECATED "Obnam has been retired; time to transition to something else" #-} where
import Propellor.Base
import qualified Propellor.Property.Apt as Apt
@@ -150,7 +153,7 @@ keepParam ps = "--keep=" ++ intercalate "," (map go ps)
go (KeepWeeks n) = mk n 'w'
go (KeepMonths n) = mk n 'm'
go (KeepYears n) = mk n 'y'
- mk n c = show n ++ [c]
+ mk n c = val n ++ [c]
isKeepParam :: ObnamParam -> Bool
isKeepParam p = "--keep=" `isPrefixOf` p
diff --git a/src/Propellor/Property/OpenId.hs b/src/Propellor/Property/OpenId.hs
index 0abf38a6..00daa57d 100644
--- a/src/Propellor/Property/OpenId.hs
+++ b/src/Propellor/Property/OpenId.hs
@@ -28,7 +28,7 @@ providerFor users hn mp = propertyList desc $ props
where
baseurl = hn ++ case mp of
Nothing -> ""
- Just p -> ':' : fromPort p
+ Just p -> ':' : val p
url = "http://"++baseurl++"/simpleid"
desc = "openid provider " ++ url
setbaseurl l
diff --git a/src/Propellor/Property/Pacman.hs b/src/Propellor/Property/Pacman.hs
new file mode 100644
index 00000000..60ed4bea
--- /dev/null
+++ b/src/Propellor/Property/Pacman.hs
@@ -0,0 +1,68 @@
+-- | Maintainer: Zihao Wang <dev@wzhd.org>
+--
+-- Support for the Pacman package manager <https://www.archlinux.org/pacman/>
+
+module Propellor.Property.Pacman where
+
+import Propellor.Base
+
+runPacman :: [String] -> UncheckedProperty ArchLinux
+runPacman ps = tightenTargets $ cmdProperty "pacman" ps
+
+-- | Have pacman update its lists of packages, but without upgrading anything.
+update :: Property ArchLinux
+update = combineProperties ("pacman update") $ props
+ & runPacman ["-Sy", "--noconfirm"]
+ `assume` MadeChange
+
+upgrade :: Property ArchLinux
+upgrade = combineProperties ("pacman upgrade") $ props
+ & runPacman ["-Syu", "--noconfirm"]
+ `assume` MadeChange
+
+type Package = String
+
+installed :: [Package] -> Property ArchLinux
+installed = installed' ["--noconfirm"]
+
+installed' :: [String] -> [Package] -> Property ArchLinux
+installed' params ps = check (not <$> isInstalled' ps) go
+ `describe` unwords ("pacman installed":ps)
+ where
+ go = runPacman (params ++ ["-S"] ++ ps)
+
+removed :: [Package] -> Property ArchLinux
+removed ps = check (any (== IsInstalled) <$> getInstallStatus ps)
+ (runPacman (["-R", "--noconfirm"] ++ ps))
+ `describe` unwords ("pacman removed":ps)
+
+isInstalled :: Package -> IO Bool
+isInstalled p = isInstalled' [p]
+
+isInstalled' :: [Package] -> IO Bool
+isInstalled' ps = all (== IsInstalled) <$> getInstallStatus ps
+
+data InstallStatus = IsInstalled | NotInstalled
+ deriving (Show, Eq)
+
+{- Returns the InstallStatus of packages that are installed
+ - or known and not installed. If a package is not known at all to apt
+ - or dpkg, it is not included in the list. -}
+getInstallStatus :: [Package] -> IO [InstallStatus]
+getInstallStatus ps = mapMaybe id <$> mapM status ps
+ where
+ status :: Package -> IO (Maybe InstallStatus)
+ status p = do
+ ifM (succeeds "pacman" ["-Q", p])
+ (return (Just IsInstalled),
+ ifM (succeeds "pacman" ["-Sp", p])
+ (return (Just NotInstalled),
+ return Nothing))
+
+succeeds :: String -> [String] -> IO Bool
+succeeds cmd args = (quietProcess >> return True)
+ `catchIO` (\_ -> return False)
+ where
+ quietProcess :: IO ()
+ quietProcess = withQuietOutput createProcessSuccess p
+ p = (proc cmd args)
diff --git a/src/Propellor/Property/Parted.hs b/src/Propellor/Property/Parted.hs
index bc8a256d..43744142 100644
--- a/src/Propellor/Property/Parted.hs
+++ b/src/Propellor/Property/Parted.hs
@@ -1,6 +1,7 @@
{-# LANGUAGE FlexibleContexts #-}
module Propellor.Property.Parted (
+ -- * Types
TableType(..),
PartTable(..),
partTableSize,
@@ -15,136 +16,30 @@ module Propellor.Property.Parted (
Partition.MkfsOpts,
PartType(..),
PartFlag(..),
- Eep(..),
+ -- * Properties
partitioned,
parted,
+ Eep(..),
installed,
+ -- * PartSpec combinators
+ calcPartTable,
+ DiskSize(..),
+ DiskPart,
+ module Propellor.Types.PartSpec,
+ DiskSpaceUse(..),
+ useDiskSpace,
) where
import Propellor.Base
+import Propellor.Property.Parted.Types
import qualified Propellor.Property.Apt as Apt
+import qualified Propellor.Property.Pacman as Pacman
import qualified Propellor.Property.Partition as Partition
+import Propellor.Types.PartSpec
import Utility.DataUnits
-import Data.Char
-import System.Posix.Files
-
-class PartedVal a where
- val :: a -> String
-
--- | Types of partition tables supported by parted.
-data TableType = MSDOS | GPT | AIX | AMIGA | BSD | DVH | LOOP | MAC | PC98 | SUN
- deriving (Show)
-
-instance PartedVal TableType where
- val = map toLower . show
-
--- | A disk's partition table.
-data PartTable = PartTable TableType [Partition]
- deriving (Show)
-
-instance Monoid PartTable where
- -- | default TableType is MSDOS
- mempty = PartTable MSDOS []
- -- | uses the TableType of the second parameter
- mappend (PartTable _l1 ps1) (PartTable l2 ps2) = PartTable l2 (ps1 ++ ps2)
-
--- | Gets the total size of the disk specified by the partition table.
-partTableSize :: PartTable -> ByteSize
-partTableSize (PartTable _ ps) = fromPartSize $
- -- add 1 megabyte to hold the partition table itself
- mconcat (MegaBytes 1 : map partSize ps)
-
--- | A partition on the disk.
-data Partition = Partition
- { partType :: PartType
- , partSize :: PartSize
- , partFs :: Partition.Fs
- , partMkFsOpts :: Partition.MkfsOpts
- , partFlags :: [(PartFlag, Bool)] -- ^ flags can be set or unset (parted may set some flags by default)
- , partName :: Maybe String -- ^ optional name for partition (only works for GPT, PC98, MAC)
- }
- deriving (Show)
-
--- | Makes a Partition with defaults for non-important values.
-mkPartition :: Partition.Fs -> PartSize -> Partition
-mkPartition fs sz = Partition
- { partType = Primary
- , partSize = sz
- , partFs = fs
- , partMkFsOpts = []
- , partFlags = []
- , partName = Nothing
- }
-
--- | Type of a partition.
-data PartType = Primary | Logical | Extended
- deriving (Show)
-
-instance PartedVal PartType where
- val Primary = "primary"
- val Logical = "logical"
- val Extended = "extended"
-
--- | All partition sizing is done in megabytes, so that parted can
--- automatically lay out the partitions.
---
--- Note that these are SI megabytes, not mebibytes.
-newtype PartSize = MegaBytes Integer
- deriving (Show)
-
-instance PartedVal PartSize where
- val (MegaBytes n)
- | n > 0 = show n ++ "MB"
- -- parted can't make partitions smaller than 1MB;
- -- avoid failure in edge cases
- | otherwise = show "1MB"
--- | Rounds up to the nearest MegaByte.
-toPartSize :: ByteSize -> PartSize
-toPartSize b = MegaBytes $ ceiling (fromInteger b / 1000000 :: Double)
-
-fromPartSize :: PartSize -> ByteSize
-fromPartSize (MegaBytes b) = b * 1000000
-
-instance Monoid PartSize where
- mempty = MegaBytes 0
- mappend (MegaBytes a) (MegaBytes b) = MegaBytes (a + b)
-
-reducePartSize :: PartSize -> PartSize -> PartSize
-reducePartSize (MegaBytes a) (MegaBytes b) = MegaBytes (a - b)
-
--- | Flags that can be set on a partition.
-data PartFlag = BootFlag | RootFlag | SwapFlag | HiddenFlag | RaidFlag | LvmFlag | LbaFlag | LegacyBootFlag | IrstFlag | EspFlag | PaloFlag
- deriving (Show)
-
-instance PartedVal PartFlag where
- val BootFlag = "boot"
- val RootFlag = "root"
- val SwapFlag = "swap"
- val HiddenFlag = "hidden"
- val RaidFlag = "raid"
- val LvmFlag = "lvm"
- val LbaFlag = "lba"
- val LegacyBootFlag = "legacy_boot"
- val IrstFlag = "irst"
- val EspFlag = "esp"
- val PaloFlag = "palo"
-
-instance PartedVal Bool where
- val True = "on"
- val False = "off"
-
-instance PartedVal Partition.Fs where
- val Partition.EXT2 = "ext2"
- val Partition.EXT3 = "ext3"
- val Partition.EXT4 = "ext4"
- val Partition.BTRFS = "btrfs"
- val Partition.REISERFS = "reiserfs"
- val Partition.XFS = "xfs"
- val Partition.FAT = "fat"
- val Partition.VFAT = "vfat"
- val Partition.NTFS = "ntfs"
- val Partition.LinuxSwap = "linux-swap"
+import System.Posix.Files
+import Data.List (genericLength)
data Eep = YesReallyDeleteDiskContents
@@ -167,19 +62,19 @@ partitioned eep disk (PartTable tabletype parts) = property' desc $ \w -> do
partedparams = concat $ mklabel : mkparts (1 :: Integer) mempty parts []
format (p, dev) = Partition.formatted' (partMkFsOpts p)
Partition.YesReallyFormatPartition (partFs p) dev
- mklabel = ["mklabel", val tabletype]
+ mklabel = ["mklabel", pval tabletype]
mkflag partnum (f, b) =
[ "set"
, show partnum
- , val f
- , val b
+ , pval f
+ , pval b
]
mkpart partnum offset p =
[ "mkpart"
- , val (partType p)
- , val (partFs p)
- , val offset
- , val (offset <> partSize p)
+ , pval (partType p)
+ , pval (partFs p)
+ , pval offset
+ , pval (offset <> partSize p)
] ++ case partName p of
Just n -> ["name", show partnum, n]
Nothing -> []
@@ -192,12 +87,76 @@ partitioned eep disk (PartTable tabletype parts) = property' desc $ \w -> do
--
-- Parted is run in script mode, so it will never prompt for input.
-- It is asked to use cylinder alignment for the disk.
-parted :: Eep -> FilePath -> [String] -> Property DebianLike
+parted :: Eep -> FilePath -> [String] -> Property (DebianLike + ArchLinux)
parted YesReallyDeleteDiskContents disk ps = p `requires` installed
where
p = cmdProperty "parted" ("--script":"--align":"cylinder":disk:ps)
`assume` MadeChange
-- | Gets parted installed.
-installed :: Property DebianLike
-installed = Apt.installed ["parted"]
+installed :: Property (DebianLike + ArchLinux)
+installed = Apt.installed ["parted"] `pickOS` Pacman.installed ["parted"]
+
+-- | Gets the total size of the disk specified by the partition table.
+partTableSize :: PartTable -> ByteSize
+partTableSize (PartTable _ ps) = fromPartSize $
+ mconcat (partitionTableOverhead : map partSize ps)
+
+-- | Some disk is used to store the partition table itself. Assume less
+-- than 1 mb.
+partitionTableOverhead :: PartSize
+partitionTableOverhead = MegaBytes 1
+
+-- | Calculate a partition table, for a given size of disk.
+--
+-- For example:
+--
+-- > calcPartTable (DiskSize (1024 * 1024 * 1024 * 100)) MSDOS
+-- > [ partition EXT2 `mountedAt` "/boot"
+-- > `setSize` MegaBytes 256
+-- > `setFlag` BootFlag
+-- > , partition EXT4 `mountedAt` "/"
+-- > `useDisk` RemainingSpace
+-- > ]
+calcPartTable :: DiskSize -> TableType -> [PartSpec DiskPart] -> PartTable
+calcPartTable (DiskSize disksize) tt l = PartTable tt (map go l)
+ where
+ go (_, _, mkpart, FixedDiskPart) = mkpart defSz
+ go (_, _, mkpart, DynamicDiskPart (Percent p)) = mkpart $ toPartSize $
+ diskremainingafterfixed * fromIntegral p `div` 100
+ go (_, _, mkpart, DynamicDiskPart RemainingSpace) = mkpart $ toPartSize $
+ diskremaining `div` genericLength (filter isremainingspace l)
+ diskremainingafterfixed =
+ disksize - sumsizes (filter isfixed l)
+ diskremaining =
+ disksize - sumsizes (filter (not . isremainingspace) l)
+ sumsizes = sum . map fromPartSize . (partitionTableOverhead :) .
+ map (partSize . go)
+ isfixed (_, _, _, FixedDiskPart) = True
+ isfixed _ = False
+ isremainingspace (_, _, _, DynamicDiskPart RemainingSpace) = True
+ isremainingspace _ = False
+
+-- | Size of a disk, in bytes.
+newtype DiskSize = DiskSize ByteSize
+ deriving (Show)
+
+data DiskPart = FixedDiskPart | DynamicDiskPart DiskSpaceUse
+
+data DiskSpaceUse = Percent Int | RemainingSpace
+
+instance Monoid DiskPart
+ where
+ mempty = FixedDiskPart
+ mappend FixedDiskPart FixedDiskPart = FixedDiskPart
+ mappend (DynamicDiskPart (Percent a)) (DynamicDiskPart (Percent b)) = DynamicDiskPart (Percent (a + b))
+ mappend (DynamicDiskPart RemainingSpace) (DynamicDiskPart RemainingSpace) = DynamicDiskPart RemainingSpace
+ mappend (DynamicDiskPart (Percent a)) _ = DynamicDiskPart (Percent a)
+ mappend _ (DynamicDiskPart (Percent b)) = DynamicDiskPart (Percent b)
+ mappend (DynamicDiskPart RemainingSpace) _ = DynamicDiskPart RemainingSpace
+ mappend _ (DynamicDiskPart RemainingSpace) = DynamicDiskPart RemainingSpace
+
+-- | Make a partition use some percentage of the size of the disk
+-- (less all fixed size partitions), or the remaining space in the disk.
+useDiskSpace :: PartSpec DiskPart -> DiskSpaceUse -> PartSpec DiskPart
+useDiskSpace (mp, o, p, _) diskuse = (mp, o, p, DynamicDiskPart diskuse)
diff --git a/src/Propellor/Property/Parted/Types.hs b/src/Propellor/Property/Parted/Types.hs
new file mode 100644
index 00000000..3350e008
--- /dev/null
+++ b/src/Propellor/Property/Parted/Types.hs
@@ -0,0 +1,119 @@
+module Propellor.Property.Parted.Types where
+
+import Propellor.Base
+import qualified Propellor.Property.Partition as Partition
+import Utility.DataUnits
+
+import Data.Char
+
+class PartedVal a where
+ pval :: a -> String
+
+-- | Types of partition tables supported by parted.
+data TableType = MSDOS | GPT | AIX | AMIGA | BSD | DVH | LOOP | MAC | PC98 | SUN
+ deriving (Show)
+
+instance PartedVal TableType where
+ pval = map toLower . show
+
+-- | A disk's partition table.
+data PartTable = PartTable TableType [Partition]
+ deriving (Show)
+
+instance Monoid PartTable where
+ -- | default TableType is MSDOS
+ mempty = PartTable MSDOS []
+ -- | uses the TableType of the second parameter
+ mappend (PartTable _l1 ps1) (PartTable l2 ps2) = PartTable l2 (ps1 ++ ps2)
+
+-- | A partition on the disk.
+data Partition = Partition
+ { partType :: PartType
+ , partSize :: PartSize
+ , partFs :: Partition.Fs
+ , partMkFsOpts :: Partition.MkfsOpts
+ , partFlags :: [(PartFlag, Bool)] -- ^ flags can be set or unset (parted may set some flags by default)
+ , partName :: Maybe String -- ^ optional name for partition (only works for GPT, PC98, MAC)
+ }
+ deriving (Show)
+
+-- | Makes a Partition with defaults for non-important values.
+mkPartition :: Partition.Fs -> PartSize -> Partition
+mkPartition fs sz = Partition
+ { partType = Primary
+ , partSize = sz
+ , partFs = fs
+ , partMkFsOpts = []
+ , partFlags = []
+ , partName = Nothing
+ }
+
+-- | Type of a partition.
+data PartType = Primary | Logical | Extended
+ deriving (Show)
+
+instance PartedVal PartType where
+ pval Primary = "primary"
+ pval Logical = "logical"
+ pval Extended = "extended"
+
+-- | All partition sizing is done in megabytes, so that parted can
+-- automatically lay out the partitions.
+--
+-- Note that these are SI megabytes, not mebibytes.
+newtype PartSize = MegaBytes Integer
+ deriving (Show)
+
+instance PartedVal PartSize where
+ pval (MegaBytes n)
+ | n > 0 = val n ++ "MB"
+ -- parted can't make partitions smaller than 1MB;
+ -- avoid failure in edge cases
+ | otherwise = "1MB"
+
+-- | Rounds up to the nearest MegaByte.
+toPartSize :: ByteSize -> PartSize
+toPartSize b = MegaBytes $ ceiling (fromInteger b / 1000000 :: Double)
+
+fromPartSize :: PartSize -> ByteSize
+fromPartSize (MegaBytes b) = b * 1000000
+
+instance Monoid PartSize where
+ mempty = MegaBytes 0
+ mappend (MegaBytes a) (MegaBytes b) = MegaBytes (a + b)
+
+reducePartSize :: PartSize -> PartSize -> PartSize
+reducePartSize (MegaBytes a) (MegaBytes b) = MegaBytes (a - b)
+
+-- | Flags that can be set on a partition.
+data PartFlag = BootFlag | RootFlag | SwapFlag | HiddenFlag | RaidFlag | LvmFlag | LbaFlag | LegacyBootFlag | IrstFlag | EspFlag | PaloFlag
+ deriving (Show)
+
+instance PartedVal PartFlag where
+ pval BootFlag = "boot"
+ pval RootFlag = "root"
+ pval SwapFlag = "swap"
+ pval HiddenFlag = "hidden"
+ pval RaidFlag = "raid"
+ pval LvmFlag = "lvm"
+ pval LbaFlag = "lba"
+ pval LegacyBootFlag = "legacy_boot"
+ pval IrstFlag = "irst"
+ pval EspFlag = "esp"
+ pval PaloFlag = "palo"
+
+instance PartedVal Bool where
+ pval True = "on"
+ pval False = "off"
+
+instance PartedVal Partition.Fs where
+ pval Partition.EXT2 = "ext2"
+ pval Partition.EXT3 = "ext3"
+ pval Partition.EXT4 = "ext4"
+ pval Partition.BTRFS = "btrfs"
+ pval Partition.REISERFS = "reiserfs"
+ pval Partition.XFS = "xfs"
+ pval Partition.FAT = "fat"
+ pval Partition.VFAT = "vfat"
+ pval Partition.NTFS = "ntfs"
+ pval Partition.LinuxSwap = "linux-swap"
diff --git a/src/Propellor/Property/Partition.hs b/src/Propellor/Property/Partition.hs
index 2bf5b927..679675b7 100644
--- a/src/Propellor/Property/Partition.hs
+++ b/src/Propellor/Property/Partition.hs
@@ -9,6 +9,7 @@ import Utility.Applicative
import System.Posix.Files
import Data.List
+import Data.Char
-- | Filesystems etc that can be used for a partition.
data Fs = EXT2 | EXT3 | EXT4 | BTRFS | REISERFS | XFS | FAT | VFAT | NTFS | LinuxSwap
@@ -81,11 +82,26 @@ kpartx diskimage mkprop = go `requires` Apt.installed ["kpartx"]
return r
cleanup = void $ liftIO $ boolSystem "kpartx" [Param "-d", File diskimage]
+-- kpartx's output includes the device for the loop partition, and some
+-- information about the whole disk loop device. In earlier versions,
+-- this was simply the path to the loop device. But, in kpartx 0.6,
+-- this changed to the major:minor of the block device. Either is handled
+-- by this parser.
kpartxParse :: String -> [LoopDev]
kpartxParse = mapMaybe (finddev . words) . lines
where
- finddev ("add":"map":ld:_:_:_:_:wd:_) = Just $ LoopDev
- { partitionLoopDev = "/dev/mapper/" ++ ld
- , wholeDiskLoopDev = wd
- }
+ finddev ("add":"map":ld:_:_:_:_:s:_) = do
+ wd <- if isAbsolute s
+ then Just s
+ -- A loop partition name loop0pn corresponds to
+ -- /dev/loop0. It would be more robust to check
+ -- that the major:minor matches, but haskell's
+ -- unix library lacks a way to do that.
+ else case takeWhile isDigit (dropWhile (not . isDigit) ld) of
+ [] -> Nothing
+ n -> Just $ "/dev" </> "loop" ++ n
+ Just $ LoopDev
+ { partitionLoopDev = "/dev/mapper/" ++ ld
+ , wholeDiskLoopDev = wd
+ }
finddev _ = Nothing
diff --git a/src/Propellor/Property/Reboot.hs b/src/Propellor/Property/Reboot.hs
index 31731dc2..909d87fb 100644
--- a/src/Propellor/Property/Reboot.hs
+++ b/src/Propellor/Property/Reboot.hs
@@ -59,7 +59,7 @@ atEnd force resultok = property "scheduled reboot at end of propellor run" $ do
-- See 'Propellor.Property.HostingProvider.DigitalOcean'
-- for an example of how to do this.
toDistroKernel :: Property DebianLike
-toDistroKernel = check (not <$> runningInstalledKernel) now
+toDistroKernel = tightenTargets $ check (not <$> runningInstalledKernel) now
`describe` "running installed kernel"
-- | Given a kernel version string @v@, reboots immediately if the running
@@ -78,15 +78,16 @@ toKernelNewerThan ver =
property' ("reboot to kernel newer than " ++ ver) $ \w -> do
wantV <- tryReadVersion ver
runningV <- tryReadVersion =<< liftIO runningKernelVersion
- installedV <- maximum <$>
- (mapM tryReadVersion =<< liftIO installedKernelVersions)
if runningV >= wantV then noChange
- else if installedV >= wantV
- then ensureProperty w now
- else errorMessage $
- "kernel newer than "
- ++ ver
- ++ " not installed"
+ else maximum <$> installedVs >>= \installedV ->
+ if installedV >= wantV
+ then ensureProperty w now
+ else errorMessage $
+ "kernel newer than "
+ ++ ver
+ ++ " not installed"
+ where
+ installedVs = mapM tryReadVersion =<< liftIO installedKernelVersions
runningInstalledKernel :: IO Bool
runningInstalledKernel = do
diff --git a/src/Propellor/Property/Restic.hs b/src/Propellor/Property/Restic.hs
new file mode 100644
index 00000000..d9d4d4be
--- /dev/null
+++ b/src/Propellor/Property/Restic.hs
@@ -0,0 +1,202 @@
+-- | Maintainer: Félix Sipma <felix+propellor@gueux.org>
+--
+-- Support for the restic backup tool <https://github.com/restic/restic>
+
+module Propellor.Property.Restic
+ ( ResticRepo (..)
+ , installed
+ , repoExists
+ , init
+ , restored
+ , backup
+ , backup'
+ , KeepPolicy (..)
+ ) where
+
+import Propellor.Base hiding (init)
+import Prelude hiding (init)
+import qualified Propellor.Property.Apt as Apt
+import qualified Propellor.Property.Cron as Cron
+import qualified Propellor.Property.File as File
+import Data.List (intercalate)
+
+type Url = String
+
+type ResticParam = String
+
+data ResticRepo
+ = Direct FilePath
+ | SFTP User HostName FilePath
+ | REST Url
+
+instance ConfigurableValue ResticRepo where
+ val (Direct fp) = fp
+ val (SFTP u h fp) = "sftp:" ++ val u ++ "@" ++ val h ++ ":" ++ fp
+ val (REST url) = "rest:" ++ url
+
+installed :: Property DebianLike
+installed = withOS desc $ \w o -> case o of
+ (Just (System (Debian _ (Stable "jessie")) _)) -> ensureProperty w $
+ Apt.installedBackport ["restic"]
+ _ -> ensureProperty w $
+ Apt.installed ["restic"]
+ where
+ desc = "installed restic"
+
+repoExists :: ResticRepo -> IO Bool
+repoExists repo = boolSystem "restic"
+ [ Param "-r"
+ , File (val repo)
+ , Param "--password-file"
+ , File (getPasswordFile repo)
+ , Param "snapshots"
+ ]
+
+passwordFileDir :: FilePath
+passwordFileDir = "/etc/restic-keys"
+
+getPasswordFile :: ResticRepo -> FilePath
+getPasswordFile repo = passwordFileDir </> File.configFileName (val repo)
+
+passwordFileConfigured :: ResticRepo -> Property (HasInfo + UnixLike)
+passwordFileConfigured repo = propertyList "restic password file" $ props
+ & File.dirExists passwordFileDir
+ & File.mode passwordFileDir 0O2700
+ & getPasswordFile repo `File.hasPrivContent` hostContext
+
+-- | Inits a new restic repository
+init :: ResticRepo -> Property (HasInfo + DebianLike)
+init repo = check (not <$> repoExists repo) (cmdProperty "restic" initargs)
+ `requires` installed
+ `requires` passwordFileConfigured repo
+ where
+ initargs =
+ [ "-r"
+ , val repo
+ , "--password-file"
+ , getPasswordFile repo
+ , "init"
+ ]
+
+-- | Restores a directory from a restic 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 -> ResticRepo -> Property (HasInfo + DebianLike)
+restored dir repo = go
+ `requires` init repo
+ where
+ go :: Property DebianLike
+ go = property (dir ++ " restored by restic") $ ifM (liftIO needsRestore)
+ ( do
+ warningMessage $ dir ++ " is empty/missing; restoring from backup ..."
+ liftIO restore
+ , noChange
+ )
+
+ needsRestore = null <$> catchDefaultIO [] (dirContents dir)
+
+ restore = withTmpDirIn (takeDirectory dir) "restic-restore" $ \tmpdir -> do
+ ok <- boolSystem "restic"
+ [ Param "-r"
+ , File (val repo)
+ , Param "--password-file"
+ , File (getPasswordFile repo)
+ , Param "restore"
+ , Param "latest"
+ , Param "--target"
+ , File tmpdir
+ ]
+ let restoreddir = tmpdir ++ "/" ++ dir
+ ifM (pure ok <&&> doesDirectoryExist restoreddir)
+ ( do
+ void $ tryIO $ removeDirectory dir
+ renameDirectory restoreddir dir
+ return MadeChange
+ , return FailedChange
+ )
+
+-- | Installs a cron job that causes a given directory to be backed
+-- up, by running restic 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.
+-- For example:
+--
+-- > & Restic.backup "/srv/git"
+-- > (Restic.SFTP (User root) (HostName myserver) /mnt/backup/git.restic")
+-- > Cron.Daily
+-- > ["--exclude=/srv/git/tobeignored"]
+-- > [Restic.KeepDays 7, Restic.KeepWeeks 4, Restic.KeepMonths 6, Restic.KeepYears 1]
+--
+-- Since restic uses a fair amount of system resources, only one restic
+-- backup job will be run at a time. Other jobs will wait their turns to
+-- run.
+backup :: FilePath -> ResticRepo -> Cron.Times -> [ResticParam] -> [KeepPolicy] -> Property (HasInfo + DebianLike)
+backup dir repo crontimes extraargs kp = backup' [dir] repo crontimes extraargs kp
+ `requires` restored dir repo
+
+-- | Does a backup, but does not automatically restore.
+backup' :: [FilePath] -> ResticRepo -> Cron.Times -> [ResticParam] -> [KeepPolicy] -> Property (HasInfo + DebianLike)
+backup' dirs repo crontimes extraargs kp = cronjob
+ `describe` desc
+ `requires` init repo
+ where
+ desc = val repo ++ " restic backup"
+ cronjob = Cron.niceJob ("restic_backup" ++ intercalate "_" dirs) crontimes (User "root") "/" $
+ "flock " ++ shellEscape lockfile ++ " sh -c " ++ shellEscape backupcmd
+ lockfile = "/var/lock/propellor-restic.lock"
+ backupcmd = intercalate " && " $
+ createCommand
+ : if null kp then [] else [pruneCommand]
+ createCommand = unwords $
+ [ "restic"
+ , "-r"
+ , shellEscape (val repo)
+ , "--password-file"
+ , shellEscape (getPasswordFile repo)
+ ]
+ ++ map shellEscape extraargs ++
+ [ "backup" ]
+ ++ map shellEscape dirs
+ pruneCommand = unwords $
+ [ "restic"
+ , "-r"
+ , shellEscape (val repo)
+ , "--password-file"
+ , shellEscape (getPasswordFile repo)
+ , "forget"
+ , "--prune"
+ ]
+ ++
+ map keepParam kp
+
+-- | Constructs a ResticParam that specifies which old backup generations to
+-- keep. By default, all generations are kept. However, when this parameter is
+-- passed to the `backup` property, they will run restic prune to clean out
+-- generations not specified here.
+keepParam :: KeepPolicy -> ResticParam
+keepParam (KeepLast n) = "--keep-last=" ++ val n
+keepParam (KeepHours n) = "--keep-hourly=" ++ val n
+keepParam (KeepDays n) = "--keep-daily=" ++ val n
+keepParam (KeepWeeks n) = "--keep-weekly=" ++ val n
+keepParam (KeepMonths n) = "--keep-monthly=" ++ val n
+keepParam (KeepYears n) = "--keep-yearly=" ++ val n
+
+-- | Policy for backup generations to keep. For example, KeepDays 30 will
+-- keep the latest backup for each day when a backup was made, and keep the
+-- last 30 such backups. When multiple KeepPolicies are combined together,
+-- backups meeting any policy are kept. See restic's man page for details.
+data KeepPolicy
+ = KeepLast Int
+ | KeepHours Int
+ | KeepDays Int
+ | KeepWeeks Int
+ | KeepMonths Int
+ | KeepYears Int
diff --git a/src/Propellor/Property/Rsync.hs b/src/Propellor/Property/Rsync.hs
index b40396de..d922e79f 100644
--- a/src/Propellor/Property/Rsync.hs
+++ b/src/Propellor/Property/Rsync.hs
@@ -2,6 +2,7 @@ module Propellor.Property.Rsync where
import Propellor.Base
import qualified Propellor.Property.Apt as Apt
+import qualified Propellor.Property.Pacman as Pacman
type Src = FilePath
type Dest = FilePath
@@ -16,7 +17,7 @@ filesUnder d = Pattern (d ++ "/*")
-- | Ensures that the Dest directory exists and has identical contents as
-- the Src directory.
-syncDir :: Src -> Dest -> Property DebianLike
+syncDir :: Src -> Dest -> Property (DebianLike + ArchLinux)
syncDir = syncDirFiltered []
data Filter
@@ -43,9 +44,9 @@ newtype Pattern = Pattern String
-- Rsync checks each name to be transferred against its list of Filter
-- rules, and the first matching one is acted on. If no matching rule
-- is found, the file is processed.
-syncDirFiltered :: [Filter] -> Src -> Dest -> Property DebianLike
+syncDirFiltered :: [Filter] -> Src -> Dest -> Property (DebianLike + ArchLinux)
syncDirFiltered filters src dest = rsync $
- [ "-av"
+ [ "-a"
-- Add trailing '/' to get rsync to sync the Dest directory,
-- rather than a subdir inside it, which it will do without a
-- trailing '/'.
@@ -53,10 +54,13 @@ syncDirFiltered filters src dest = rsync $
, addTrailingPathSeparator dest
, "--delete"
, "--delete-excluded"
- , "--quiet"
+ , "--info=progress2"
] ++ map toRsync filters
-rsync :: [String] -> Property DebianLike
+rsync :: [String] -> Property (DebianLike + ArchLinux)
rsync ps = cmdProperty "rsync" ps
`assume` MadeChange
- `requires` Apt.installed ["rsync"]
+ `requires` installed
+
+installed :: Property (DebianLike + ArchLinux)
+installed = Apt.installed ["rsync"] `pickOS` Pacman.installed ["rsync"]
diff --git a/src/Propellor/Property/Sbuild.hs b/src/Propellor/Property/Sbuild.hs
index c3e55bbf..23f3b311 100644
--- a/src/Propellor/Property/Sbuild.hs
+++ b/src/Propellor/Property/Sbuild.hs
@@ -20,12 +20,10 @@ Debian stretch, which older sbuild can't handle.
Suggested usage in @config.hs@:
-> & Apt.installed ["piuparts", "autopkgtest"]
+> & Apt.installed ["piuparts", "autopkgtest", "lintian"]
> & Sbuild.builtFor (System (Debian Linux Unstable) X86_32) Sbuild.UseCcache
-> & Sbuild.piupartsConfFor (System (Debian Linux Unstable) X86_32)
> & Sbuild.updatedFor (System (Debian Linux Unstable) X86_32) `period` Weekly 1
> & Sbuild.usableBy (User "spwhitton")
-> & Sbuild.shareAptCache
> & Schroot.overlaysInTmpfs
If you are using sbuild older than 0.70.0, you also need:
@@ -34,15 +32,13 @@ If you are using sbuild older than 0.70.0, you also need:
In @~/.sbuildrc@ (sbuild 0.71.0 or newer):
-> $run_piuparts = 1;
> $piuparts_opts = [
+> '--no-eatmydata',
> '--schroot',
-> '%r-%a-piuparts',
+> '%r-%a-sbuild',
> '--fail-if-inadequate',
-> '--fail-on-broken-symlinks',
> ];
>
-> $run_autopkgtest = 1;
> $autopkgtest_root_args = "";
> $autopkgtest_opts = ["--", "schroot", "%r-%a-sbuild"];
@@ -53,9 +49,9 @@ propellor spin pulls in a lot of dependencies. This could defeat
using sbuild to determine if you've included all necessary build
dependencies in your source package control file.
-Nevertheless, the chroot that @sbuild-createchroot(1)@ creates might
-not meet your needs. For example, you might need to enable an apt
-cacher. In that case you can do something like this in @config.hs@:
+Nevertheless, the chroot that @sbuild-createchroot(1)@ creates might not meet
+your needs. For example, you might need to enable apt's https support. In that
+case you can do something like this in @config.hs@:
> & Sbuild.built (System (Debian Linux Unstable) X86_32) `before` mySetup
> where
@@ -74,20 +70,19 @@ module Propellor.Property.Sbuild (
UseCcache(..),
built,
updated,
- piupartsConf,
builtFor,
updatedFor,
- piupartsConfFor,
-- * Global sbuild configuration
-- blockNetwork,
installed,
keypairGenerated,
keypairInsecurelyGenerated,
- shareAptCache,
usableBy,
+ userConfig,
) where
import Propellor.Base
+import Propellor.Types.Info
import Propellor.Property.Debootstrap (extractSuite)
import Propellor.Property.Chroot.Util
import qualified Propellor.Property.Apt as Apt
@@ -98,10 +93,10 @@ import qualified Propellor.Property.File as File
import qualified Propellor.Property.Schroot as Schroot
import qualified Propellor.Property.Reboot as Reboot
import qualified Propellor.Property.User as User
-
import Utility.FileMode
+import Utility.Split
+
import Data.List
-import Data.List.Utils
type Suite = String
@@ -111,8 +106,8 @@ type Suite = String
-- the same suite and the same architecture, so neither do we
data SbuildSchroot = SbuildSchroot Suite Architecture
-instance Show SbuildSchroot where
- show (SbuildSchroot suite arch) = suite ++ "-" ++ architectureToDebianArchString arch
+instance ConfigurableValue SbuildSchroot where
+ val (SbuildSchroot suite arch) = suite ++ "-" ++ architectureToDebianArchString arch
-- | Whether an sbuild schroot should use ccache during builds
--
@@ -128,9 +123,9 @@ data UseCcache = UseCcache | NoCcache
builtFor :: System -> UseCcache -> RevertableProperty DebianLike UnixLike
builtFor sys cc = go <!> deleted
where
- go = property' ("sbuild schroot for " ++ show sys) $
- \w -> case (schrootFromSystem sys, stdMirror sys) of
- (Just s, Just u) -> ensureProperty w $
+ go = Apt.withMirror goDesc $ \u -> property' goDesc $ \w ->
+ case schrootFromSystem sys of
+ Just s -> ensureProperty w $
setupRevertableProperty $ built s u cc
_ -> errorMessage
("don't know how to debootstrap " ++ show sys)
@@ -139,6 +134,7 @@ builtFor sys cc = go <!> deleted
Just s -> ensureProperty w $
undoRevertableProperty $ built s "dummy" cc
Nothing -> noChange
+ goDesc = "sbuild schroot for " ++ show sys
-- | Build and configure a schroot for use with sbuild
built :: SbuildSchroot -> Apt.Url -> UseCcache -> RevertableProperty DebianLike UnixLike
@@ -146,12 +142,13 @@ built s@(SbuildSchroot suite arch) mirror cc =
((go `before` enhancedConf)
`requires` ccacheMaybePrepared cc
`requires` installed
- `requires` overlaysKernel)
+ `requires` overlaysKernel
+ `requires` cleanupOldConfig)
<!> deleted
where
go :: Property DebianLike
go = check (unpopulated (schrootRoot s) <||> ispartial) $
- property' ("built sbuild schroot for " ++ show s) make
+ property' ("built sbuild schroot for " ++ val s) make
make w = do
de <- liftIO standardPathEnv
let params = Param <$>
@@ -170,22 +167,49 @@ built s@(SbuildSchroot suite arch) mirror cc =
-- TODO we should kill any sessions still using the chroot
-- before destroying it (as suggested by sbuild-destroychroot)
deleted = check (not <$> unpopulated (schrootRoot s)) $
- property ("no sbuild schroot for " ++ show s) $ do
+ property ("no sbuild schroot for " ++ val s) $ do
liftIO $ removeChroot $ schrootRoot s
liftIO $ nukeFile
- ("/etc/sbuild/chroot" </> show s ++ "-sbuild")
+ ("/etc/sbuild/chroot" </> val s ++ "-sbuild")
makeChange $ nukeFile (schrootConf s)
enhancedConf =
- combineProperties ("enhanced schroot conf for " ++ show s) $ props
+ combineProperties ("enhanced schroot conf for " ++ val s) $ props
& aliasesLine
+ -- set up an apt proxy/cacher
+ & proxyCacher
-- enable ccache and eatmydata for speed
& ConfFile.containsIniSetting (schrootConf s)
- ( show s ++ "-sbuild"
+ ( val s ++ "-sbuild"
, "command-prefix"
, intercalate "," commandPrefix
)
+ -- set the apt proxy inside the chroot. If the host has an apt proxy
+ -- set, assume that it does some sort of caching. Otherwise, set up a
+ -- local apt-cacher-ng instance
+ --
+ -- (if we didn't assume that the apt proxy does some sort of caching,
+ -- we'd need to complicate the Apt.HostAptProxy type to indicate whether
+ -- the proxy caches, and if it doesn't, set up apt-cacher-ng as an
+ -- intermediary proxy between the chroot's apt and the Apt.HostAptProxy
+ -- proxy. This complexity is more likely to cause problems than help
+ -- anyone)
+ proxyCacher :: Property DebianLike
+ proxyCacher = property' "set schroot apt proxy" $ \w -> do
+ proxyInfo <- getProxyInfo
+ ensureProperty w $ case proxyInfo of
+ Just (Apt.HostAptProxy u) -> setChrootProxy u
+ Nothing -> (Apt.serviceInstalledRunning "apt-cacher-ng"
+ `before` setChrootProxy "http://localhost:3142")
+ where
+ getProxyInfo :: Propellor (Maybe Apt.HostAptProxy)
+ getProxyInfo = fromInfoVal <$> askInfo
+ setChrootProxy :: Apt.Url -> Property DebianLike
+ setChrootProxy u = tightenTargets $ File.hasContent
+ (schrootRoot s </> "etc/apt/apt.conf.d/20proxy")
+ [ "Acquire::HTTP::Proxy \"" ++ u ++ "\";" ]
+
-- if we're building a sid chroot, add useful aliases
-- In order to avoid more than one schroot getting the same aliases, we
-- only do this if the arch of the chroot equals the host arch.
@@ -196,7 +220,7 @@ built s@(SbuildSchroot suite arch) mirror cc =
then ensureProperty w $
ConfFile.containsIniSetting
(schrootConf s)
- ( show s ++ "-sbuild"
+ ( val s ++ "-sbuild"
, "aliases"
, aliases
)
@@ -217,6 +241,21 @@ built s@(SbuildSchroot suite arch) mirror cc =
Reboot.toKernelNewerThan "3.18"
else noChange
+ -- clean up config from earlier versions of this module
+ cleanupOldConfig :: Property UnixLike
+ cleanupOldConfig =
+ property' "old sbuild module config cleaned up" $ \w -> do
+ void $ ensureProperty w $
+ check (doesFileExist fstab)
+ (File.lacksLine fstab aptCacheLine)
+ void $ liftIO . tryIO $ removeDirectoryRecursive profile
+ void $ liftIO $ nukeFile (schrootPiupartsConf s)
+ -- assume this did nothing
+ noChange
+ where
+ fstab = "/etc/schroot/sbuild/fstab"
+ profile = "/etc/schroot/piuparts"
+
-- A failed debootstrap run will leave a debootstrap directory;
-- recover by deleting it and trying again.
ispartial = ifM (doesDirectoryExist (schrootRoot s </> "debootstrap"))
@@ -263,7 +302,7 @@ updatedFor system = property' ("updated sbuild schroot for " ++ show system) $
updated :: SbuildSchroot -> Property DebianLike
updated s@(SbuildSchroot suite arch) =
check (doesDirectoryExist (schrootRoot s)) $ go
- `describe` ("updated schroot for " ++ show s)
+ `describe` ("updated schroot for " ++ val s)
`requires` installed
where
go :: Property DebianLike
@@ -283,13 +322,13 @@ updated s@(SbuildSchroot suite arch) =
-- given suite and architecture, so we don't need the suffix to be random.
fixConfFile :: SbuildSchroot -> Property UnixLike
fixConfFile s@(SbuildSchroot suite arch) =
- property' ("schroot for " ++ show s ++ " config file fixed") $ \w -> do
+ property' ("schroot for " ++ val s ++ " config file fixed") $ \w -> do
confs <- liftIO $ dirContents dir
let old = concat $ filter (tempPrefix `isPrefixOf`) confs
liftIO $ moveFile old new
liftIO $ moveFile
- ("/etc/sbuild/chroot" </> show s ++ "-propellor")
- ("/etc/sbuild/chroot" </> show s ++ "-sbuild")
+ ("/etc/sbuild/chroot" </> val s ++ "-propellor")
+ ("/etc/sbuild/chroot" </> val s ++ "-sbuild")
ensureProperty w $
File.fileProperty "replace dummy suffix" (map munge) new
where
@@ -298,92 +337,6 @@ fixConfFile s@(SbuildSchroot suite arch) =
tempPrefix = dir </> suite ++ "-" ++ architectureToDebianArchString arch ++ "-propellor-"
munge = replace "-propellor]" "-sbuild]"
--- | Create a corresponding schroot config file for use with piuparts
---
--- This function is a convenience wrapper around 'piupartsConf', allowing the
--- user to identify the schroot using the 'System' type. See that function's
--- documentation for why you might want to use this property, and sample config.
-piupartsConfFor :: System -> Property DebianLike
-piupartsConfFor sys = property' ("piuparts schroot conf for " ++ show sys) $
- \w -> case schrootFromSystem sys of
- Just s -> ensureProperty w $ piupartsConf s
- _ -> errorMessage
- ("don't know how to debootstrap " ++ show sys)
-
--- | Create a corresponding schroot config file for use with piuparts
---
--- This is useful because:
---
--- - piuparts will clear out the apt cache which makes 'shareAptCache' much less
--- useful
---
--- - piuparts itself invokes eatmydata, so the command-prefix setting in our
--- regular schroot config would force the user to pass @--no-eatmydata@ to
--- piuparts in their @~/.sbuildrc@, which is inconvenient.
---
--- To make use of this new schroot config, you can put something like this in
--- your ~/.sbuildrc (sbuild 0.71.0 or newer):
---
--- > $run_piuparts = 1;
--- > $piuparts_opts = [
--- > '--schroot',
--- > '%r-%a-piuparts',
--- > '--fail-if-inadequate',
--- > '--fail-on-broken-symlinks',
--- > ];
---
--- This property has no effect if the corresponding sbuild schroot does not
--- exist (i.e. you also need 'Sbuild.built' or 'Sbuild.builtFor').
-piupartsConf :: SbuildSchroot -> Property DebianLike
-piupartsConf s@(SbuildSchroot _ arch) =
- check (doesFileExist (schrootConf s)) go
- `requires` installed
- where
- go :: Property DebianLike
- go = property' desc $ \w -> do
- aliases <- aliasesLine
- ensureProperty w $ combineProperties desc $ props
- & check (not <$> doesFileExist f)
- (File.basedOn f (schrootConf s, map munge))
- & ConfFile.containsIniSetting f
- (sec, "profile", "piuparts")
- & ConfFile.containsIniSetting f
- (sec, "aliases", aliases)
- & ConfFile.containsIniSetting f
- (sec, "command-prefix", "")
- & File.dirExists dir
- & File.isSymlinkedTo (dir </> "copyfiles")
- (File.LinkTarget $ orig </> "copyfiles")
- & File.isSymlinkedTo (dir </> "nssdatabases")
- (File.LinkTarget $ orig </> "nssdatabases")
- & File.basedOn (dir </> "fstab")
- (orig </> "fstab", filter (/= aptCacheLine))
-
- orig = "/etc/schroot/sbuild"
- dir = "/etc/schroot/piuparts"
- sec = show s ++ "-piuparts"
- f = schrootPiupartsConf s
- munge = replace "-sbuild]" "-piuparts]"
- desc = "piuparts schroot conf for " ++ show s
-
- -- normally the piuparts schroot conf has no aliases, but we have to add
- -- one, for dgit compatibility, if this is the default sid chroot
- aliasesLine = sidHostArchSchroot s >>= \isSidHostArchSchroot ->
- return $ if isSidHostArchSchroot
- then "UNRELEASED-"
- ++ architectureToDebianArchString arch
- ++ "-piuparts"
- else ""
-
--- | Bind-mount /var/cache/apt/archives in all sbuild chroots so that the host
--- system and the chroot share the apt cache
---
--- This speeds up builds by avoiding unnecessary downloads of build
--- dependencies.
-shareAptCache :: Property DebianLike
-shareAptCache = File.containsLine "/etc/schroot/sbuild/fstab" aptCacheLine
- `requires` installed
- `describe` "sbuild schroots share host apt cache"
aptCacheLine :: String
aptCacheLine = "/var/cache/apt/archives /var/cache/apt/archives none rw,bind 0 0"
@@ -493,6 +446,35 @@ ccachePrepared = propertyList "sbuild group ccache configured" $ props
-- [Firewall.IPWithNumMask (IPv4 "127.0.0.1") 8])
-- `requires` installed -- sbuild group must exist
+-- | Maintain recommended ~/.sbuildrc for a user, and adds them to the
+-- sbuild group
+--
+-- You probably want a custom ~/.sbuildrc on your workstation, but
+-- this property is handy for quickly setting up build boxes.
+userConfig :: User -> Property DebianLike
+userConfig user@(User u) = go
+ `requires` usableBy user
+ `requires` Apt.installed ["piuparts", "autopkgtest", "lintian"]
+ where
+ go :: Property DebianLike
+ go = property' ("~/.sbuildrc for " ++ u) $ \w -> do
+ h <- liftIO (User.homedir user)
+ ensureProperty w $ File.hasContent (h </> ".sbuildrc")
+ [ "$run_lintian = 1;"
+ , ""
+ , "$run_piuparts = 1;"
+ , "$piuparts_opts = ["
+ , " '--no-eatmydata',"
+ , " '--schroot',"
+ , " '%r-%a-sbuild',"
+ , " '--fail-if-inadequate',"
+ , " ];"
+ , ""
+ , "$run_autopkgtest = 1;"
+ , "$autopkgtest_root_args = \"\";"
+ , "$autopkgtest_opts = [\"--\", \"schroot\", \"%r-%a-sbuild\"];"
+ ]
+
-- ==== utility functions ====
schrootFromSystem :: System -> Maybe SbuildSchroot
@@ -500,11 +482,6 @@ schrootFromSystem system@(System _ arch) =
extractSuite system
>>= \suite -> return $ SbuildSchroot suite arch
-stdMirror :: System -> Maybe Apt.Url
-stdMirror (System (Debian _ _) _) = Just "http://httpredir.debian.org/debian"
-stdMirror (System (Buntish _) _) = Just "mirror://mirrors.ubuntu.com/"
-stdMirror _ = Nothing
-
schrootRoot :: SbuildSchroot -> FilePath
schrootRoot (SbuildSchroot s a) = "/srv/chroot" </> s ++ "-" ++ architectureToDebianArchString a
@@ -527,7 +504,7 @@ schrootPiupartsConf (SbuildSchroot s a) =
sidHostArchSchroot :: SbuildSchroot -> Propellor Bool
sidHostArchSchroot (SbuildSchroot suite arch) = do
maybeOS <- getOS
- case maybeOS of
- Nothing -> return False
+ return $ case maybeOS of
+ Nothing -> False
Just (System _ hostArch) ->
- return $ suite == "unstable" && hostArch == arch
+ suite == "unstable" && hostArch == arch
diff --git a/src/Propellor/Property/SiteSpecific/Branchable.hs b/src/Propellor/Property/SiteSpecific/Branchable.hs
index 239bcbeb..ce679083 100644
--- a/src/Propellor/Property/SiteSpecific/Branchable.hs
+++ b/src/Propellor/Property/SiteSpecific/Branchable.hs
@@ -8,6 +8,8 @@ import qualified Propellor.Property.Ssh as Ssh
import qualified Propellor.Property.Postfix as Postfix
import qualified Propellor.Property.Gpg as Gpg
import qualified Propellor.Property.Sudo as Sudo
+import qualified Propellor.Property.Borg as Borg
+import qualified Propellor.Property.Cron as Cron
server :: [Host] -> Property (HasInfo + DebianLike)
server hosts = propertyList "branchable server" $ props
@@ -37,18 +39,24 @@ server hosts = propertyList "branchable server" $ props
& Postfix.installed
& Postfix.mainCf ("mailbox_command", "procmail -a \"$EXTENSION\"")
- -- Obnam is run by a cron job in ikiwiki-hosting.
- & "/etc/obnam.conf" `File.hasContent`
- [ "[config]"
- , "repository = sftp://joey@eubackup.kitenet.net/home/joey/lib/backup/pell.obnam"
- , "log = /var/log/obnam.log"
- , "encrypt-with = " ++ obnamkey
- , "log-level = info"
- , "log-max = 1048576"
- , "keep = 7d,5w,12m"
- , "upload-queue-size = 128"
- , "lru-size = 128"
+ & Borg.backup "/" "joey@eubackup.kitenet.net:/home/joey/lib/backup/branchable/pell.borg" Cron.Daily
+ [ "--exclude=/proc/*"
+ , "--exclude=/sys/*"
+ , "--exclude=/run/*"
+ , "--exclude=/tmp/*"
+ , "--exclude=/var/tmp/*"
+ , "--exclude=/var/backups/ikiwiki-hosting-web/*"
+ , "--exclude=/var/cache/*"
+ , "--exclude=/home/*/source/*"
+ , "--exclude=/home/*/public_html/*"
+ , "--exclude=/home/*/.git/*"
]
+ [ Borg.KeepDays 7
+ , Borg.KeepWeeks 5
+ , Borg.KeepMonths 12
+ , Borg.KeepYears 1
+ ]
+ -- gpg key that can be used to decrypt the borg backup key
& Gpg.keyImported (Gpg.GpgKeyId obnamkey) (User "root")
& Ssh.userKeys (User "root") (Context "branchable.com")
[ (SshRsa, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC2PqTSupwncqeffNwZQXacdEWp7L+TxllIxH7WjfRMb3U74mQxWI0lwqLVW6Fox430DvhSqF1y5rJBvTHh4i49Tc9lZ7mwAxA6jNOP6bmdfteaKKYmUw5qwtJW0vISBFu28qBO11Nq3uJ1D3Oj6N+b3mM/0D3Y3NoGgF8+2dLdi81u9+l6AQ5Jsnozi2Ni/Osx2oVGZa+IQDO6gX8VEP4OrcJFNJe8qdnvItcGwoivhjbIfzaqNNvswKgGzhYLOAS5KT8HsjvIpYHWkyQ5QUX7W/lqGSbjP+6B8C3tkvm8VLXbmaD+aSkyCaYbuoXC2BoJdS7Jh8phKMwPJmdYVepn")
diff --git a/src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs b/src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs
index d40964b3..bd4d0928 100644
--- a/src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs
+++ b/src/Propellor/Property/SiteSpecific/GitAnnexBuilder.hs
@@ -143,15 +143,15 @@ stackAutoBuilder suite arch flavor =
stackInstalled :: Property Linux
stackInstalled = withOS "stack installed" $ \w o ->
case o of
- (Just (System (Debian Linux (Stable "jessie")) X86_32)) ->
- ensureProperty w $ manualinstall X86_32
+ (Just (System (Debian Linux (Stable "jessie")) arch)) ->
+ ensureProperty w $ manualinstall arch
_ -> ensureProperty w $ Apt.installed ["haskell-stack"]
where
-- Warning: Using a binary downloaded w/o validation.
manualinstall :: Architecture -> Property Linux
manualinstall arch = tightenTargets $ check (not <$> doesFileExist binstack) $
propertyList "stack installed from upstream tarball" $ props
- & cmdProperty "wget" ["https://www.stackage.org/stack/linux-" ++ architectureToDebianArchString arch, "-O", tmptar]
+ & cmdProperty "wget" ["https://www.stackage.org/stack/linux-" ++ archname, "-O", tmptar]
`assume` MadeChange
& File.dirExists tmpdir
& cmdProperty "tar" ["xf", tmptar, "-C", tmpdir, "--strip-components=1"]
@@ -160,6 +160,15 @@ stackInstalled = withOS "stack installed" $ \w o ->
`assume` MadeChange
& cmdProperty "rm" ["-rf", tmpdir, tmptar]
`assume` MadeChange
+ where
+ -- See https://www.stackage.org/stack/ for the list of
+ -- binaries.
+ archname = case arch of
+ X86_32 -> "i386"
+ X86_64 -> "x86_64"
+ ARMHF -> "arm"
+ -- Probably not available.
+ a -> architectureToDebianArchString a
binstack = "/usr/bin/stack"
tmptar = "/root/stack.tar.gz"
tmpdir = "/root/stack"
diff --git a/src/Propellor/Property/SiteSpecific/JoeySites.hs b/src/Propellor/Property/SiteSpecific/JoeySites.hs
index d8991cb1..d4263031 100644
--- a/src/Propellor/Property/SiteSpecific/JoeySites.hs
+++ b/src/Propellor/Property/SiteSpecific/JoeySites.hs
@@ -19,13 +19,14 @@ import qualified Propellor.Property.Obnam as Obnam
import qualified Propellor.Property.Apache as Apache
import qualified Propellor.Property.Postfix as Postfix
import qualified Propellor.Property.Systemd as Systemd
+import qualified Propellor.Property.Network as Network
import qualified Propellor.Property.Fail2Ban as Fail2Ban
import qualified Propellor.Property.LetsEncrypt as LetsEncrypt
import Utility.FileMode
+import Utility.Split
import Data.List
import System.Posix.Files
-import Data.String.Utils
scrollBox :: Property (HasInfo + DebianLike)
scrollBox = propertyList "scroll server" $ props
@@ -78,7 +79,8 @@ scrollBox = propertyList "scroll server" $ props
`onChange` Ssh.restarted
& User.shellSetTo (User "scroll") s
& User.hasPassword (User "scroll")
- & Apt.serviceInstalledRunning "telnetd"
+ -- telnetd attracted password crackers, so disabled
+ & Apt.removed ["telnetd"]
& Apt.installed ["shellinabox"]
& File.hasContent "/etc/default/shellinabox"
[ "# Deployed by propellor"
@@ -227,23 +229,29 @@ gitServer hosts = propertyList "git.kitenet.net setup" $ props
`requires` Ssh.knownHost hosts "usw-s002.rsync.net" (User "root")
`requires` Ssh.authorizedKeys (User "family") (Context "git.kitenet.net")
`requires` User.accountFor (User "family")
- & Apt.installed ["git", "rsync", "gitweb"]
+ & Apt.installed ["git", "rsync", "cgit"]
& Apt.installed ["git-annex"]
& Apt.installed ["kgb-client"]
& File.hasPrivContentExposed "/etc/kgb-bot/kgb-client.conf" anyContext
`requires` File.dirExists "/etc/kgb-bot/"
& 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" (User "joey")
- -- I keep the website used for gitweb checked into git..
- & Git.cloned (User "root") "/srv/git/joey/git.kitenet.net.git" "/srv/web/git.kitenet.net" Nothing
+ & "/etc/cgitrc" `File.hasContent`
+ [ "clone-url=https://git.joeyh.name/git/$CGIT_REPO_URL git://git.joeyh.name/$CGIT_REPO_URL"
+ , "css=/cgit-css/cgit.css"
+ , "logo=/cgit-css/cgit.png"
+ , "enable-http-clone=1"
+ , "root-title=Joey's git repositories"
+ , "root-desc="
+ , "enable-index-owner=0"
+ , "snapshots=tar.gz"
+ , "enable-git-config=1"
+ , "scan-path=/srv/git"
+ ]
+ `describe` "cgit configured"
+ -- I keep the website used for git.kitenet.net/git.joeyh.name checked into git..
+ & Git.cloned (User "joey") "/srv/git/joey/git.kitenet.net.git" "/srv/web/git.kitenet.net" Nothing
+ -- Don't need global apache configuration for cgit.
+ ! Apache.confEnabled "cgit"
& website "git.kitenet.net"
& website "git.joeyh.name"
& Apache.modEnabled "cgi"
@@ -313,9 +321,9 @@ apacheSite hn middle = Apache.siteEnabled hn $ apachecfg hn middle
apachecfg :: HostName -> Apache.ConfigFile -> Apache.ConfigFile
apachecfg hn middle =
- [ "<VirtualHost *:"++show port++">"
+ [ "<VirtualHost *:" ++ val port ++ ">"
, " ServerAdmin grue@joeyh.name"
- , " ServerName "++hn++":"++show port
+ , " ServerName "++hn++":" ++ val port
]
++ middle ++
[ ""
@@ -328,7 +336,7 @@ apachecfg hn middle =
, "</VirtualHost>"
]
where
- port = 80 :: Int
+ port = Port 80
gitAnnexDistributor :: Property (HasInfo + DebianLike)
gitAnnexDistributor = combineProperties "git-annex distributor, including rsync server and signer" $ props
@@ -369,7 +377,7 @@ tmp = propertyList "tmp.joeyh.name" $ props
-- (Obsolete; need to revert this.)
pumpRss :: Property DebianLike
pumpRss = Cron.job "pump rss" (Cron.Times "15 * * * *") (User "joey") "/srv/web/tmp.joeyh.name/"
- "wget https://rss.io.jpope.org/feed/joeyh@identi.ca.atom -O pump.atom.new --no-check-certificate 2>/dev/null; sed 's/ & / /g' pump.atom.new > pump.atom"
+ "wget https://pump2rss.com/feed/joeyh@identi.ca.atom -O pump.atom.new --no-check-certificate 2>/dev/null; sed 's/ & / /g' pump.atom.new > pump.atom"
ircBouncer :: Property (HasInfo + DebianLike)
ircBouncer = propertyList "IRC bouncer" $ props
@@ -404,8 +412,6 @@ githubBackup = propertyList "github-backup box" $ props
& githubKeys
& Cron.niceJob "github-backup run" (Cron.Times "30 4 * * *") (User "joey")
"/home/joey/lib/backup" backupcmd
- & Cron.niceJob "gitriddance" (Cron.Times "30 4 * * *") (User "joey")
- "/home/joey/lib/backup" gitriddancecmd
where
backupcmd = intercalate "&&" $
[ "mkdir -p github"
@@ -413,11 +419,6 @@ githubBackup = propertyList "github-backup box" $ props
, ". $HOME/.github-keys"
, "github-backup joeyh"
]
- gitriddancecmd = intercalate "&&" $
- [ "cd github"
- , ". $HOME/.github-keys"
- ] ++ map gitriddance githubMirrors
- gitriddance (r, msg) = "(cd " ++ r ++ " && gitriddance " ++ shellEscape msg ++ ")"
githubKeys :: Property (HasInfo + UnixLike)
githubKeys =
@@ -426,19 +427,6 @@ githubKeys =
`onChange` File.ownerGroup f (User "joey") (Group "joey")
--- these repos are only mirrored on github, I don't want
--- all the proprietary features
-githubMirrors :: [(String, String)]
-githubMirrors =
- [ ("ikiwiki", plzuseurl "http://ikiwiki.info/todo/")
- , ("git-annex", plzuseurl "http://git-annex.branchable.com/todo/")
- , ("myrepos", plzuseurl "http://myrepos.branchable.com/todo/")
- , ("propellor", plzuseurl "http://propellor.branchable.com/todo/")
- , ("etckeeper", plzuseurl "http://etckeeper.branchable.com/todo/")
- ]
- where
- plzuseurl u = "Please submit changes to " ++ u ++ " instead of using github pull requests, which are not part of my workflow. Just open a todo item there and link to a git repository containing your changes. Did you know, git is a distributed system? The git repository doesn't even need to be on github! Please send any complaints to Github; they don't allow turning off pull requests or redirecting them elsewhere. -- A robot acting on behalf of Joey Hess"
-
rsyncNetBackup :: [Host] -> Property DebianLike
rsyncNetBackup hosts = Cron.niceJob "rsync.net copied in daily" (Cron.Times "30 5 * * *")
(User "joey") "/home/joey/lib/backup" "mkdir -p rsync.net && rsync --delete -az 2318@usw-s002.rsync.net: rsync.net"
@@ -452,16 +440,6 @@ backupsBackedupFrom hosts srchost destdir = Cron.niceJob desc
desc = "backups copied from " ++ srchost ++ " on boot"
cmd = "sleep 30m && rsync -az --bwlimit=300K --partial --delete " ++ srchost ++ ":lib/backup/ " ++ destdir </> srchost
-obnamRepos :: [String] -> Property UnixLike
-obnamRepos rs = propertyList ("obnam repos for " ++ unwords rs) $
- toProps (mkbase : map mkrepo rs)
- where
- mkbase = mkdir "/home/joey/lib/backup"
- `requires` mkdir "/home/joey/lib"
- mkrepo r = mkdir ("/home/joey/lib/backup/" ++ r ++ ".obnam")
- mkdir d = File.dirExists d
- `before` File.ownerGroup d (User "joey") (Group "joey")
-
podcatcher :: Property DebianLike
podcatcher = Cron.niceJob "podcatcher run hourly" (Cron.Times "55 * * * *")
(User "joey") "/home/joey/lib/sound/podcasts"
@@ -586,8 +564,8 @@ kiteMailServer = propertyList "kitenet.net mail server" $ props
, "# Enable postgrey."
, "smtpd_recipient_restrictions = permit_tls_clientcerts,permit_sasl_authenticated,,permit_mynetworks,reject_unauth_destination,check_policy_service inet:127.0.0.1:10023"
- , "# Enable spamass-milter, amavis-milter, opendkim"
- , "smtpd_milters = unix:/spamass/spamass.sock unix:amavis/amavis.sock inet:localhost:8891"
+ , "# Enable spamass-milter, amavis-milter (opendkim is not enabled because it causes mails forwarded from eg gmail to be rejected)"
+ , "smtpd_milters = unix:/spamass/spamass.sock unix:amavis/amavis.sock"
, "# opendkim is used for outgoing mail"
, "non_smtpd_milters = inet:localhost:8891"
, "milter_connect_macros = j {daemon_name} v {if_name} _"
@@ -694,6 +672,10 @@ dkimInstalled = go `onChange` Service.restarted "opendkim"
& File.ownerGroup "/etc/mail/dkim.key" (User "opendkim") (Group "opendkim")
& "/etc/default/opendkim" `File.containsLine`
"SOCKET=\"inet:8891@localhost\""
+ `onChange`
+ (cmdProperty "/lib/opendkim/opendkim.service.generate" []
+ `assume` MadeChange)
+ `onChange` Service.restarted "opendkim"
& "/etc/opendkim.conf" `File.containsLines`
[ "KeyFile /etc/mail/dkim.key"
, "SubDomains yes"
@@ -707,9 +689,20 @@ dkimInstalled = go `onChange` Service.restarted "opendkim"
domainKey :: (BindDomain, Record)
domainKey = (RelDomain "mail._domainkey", TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCc+/rfzNdt5DseBBmfB3C6sVM7FgVvf4h1FeCfyfwPpVcmPdW6M2I+NtJsbRkNbEICxiP6QY2UM0uoo9TmPqLgiCCG2vtuiG6XMsS0Y/gGwqKM7ntg/7vT1Go9vcquOFFuLa5PnzpVf8hB9+PMFdS4NPTvWL2c5xxshl/RJzICnQIDAQAB")
-hasJoeyCAChain :: Property (HasInfo + UnixLike)
-hasJoeyCAChain = "/etc/ssl/certs/joeyca.pem" `File.hasPrivContentExposed`
- Context "joeyca.pem"
+postfixSaslPasswordClient :: Property (HasInfo + DebianLike)
+postfixSaslPasswordClient = combineProperties "postfix uses SASL password to authenticate with smarthost" $ props
+ & Postfix.satellite
+ & Postfix.mappedFile "/etc/postfix/sasl_passwd"
+ (`File.hasPrivContent` (Context "kitenet.net"))
+ & Postfix.mainCfFile `File.containsLines`
+ [ "# TLS setup for SASL auth to kite"
+ , "smtp_sasl_auth_enable = yes"
+ , "smtp_tls_security_level = encrypt"
+ , "smtp_sasl_tls_security_options = noanonymous"
+ , "relayhost = [kitenet.net]"
+ , "smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd"
+ ]
+ `onChange` Postfix.reloaded
hasPostfixCert :: Context -> Property (HasInfo + UnixLike)
hasPostfixCert ctx = combineProperties "postfix tls cert installed" $ props
@@ -790,6 +783,15 @@ legacyWebSites = propertyList "legacy web sites" $ props
, "# Redirect all to joeyh.name."
, "rewriterule (.*) http://joeyh.name$1 [r]"
]
+ & alias "homepower.joeyh.name"
+ & apacheSite "homepower.joeyh.name"
+ [ "DocumentRoot /srv/web/homepower.joeyh.name"
+ , "<Directory /srv/web/homepower.joeyh.name>"
+ , " Options Indexes ExecCGI"
+ , " AllowOverride None"
+ , Apache.allowAll
+ , "</Directory>"
+ ]
where
kitenetcfg =
-- /var/www is empty
@@ -891,7 +893,7 @@ userDirHtml = File.fileProperty "apache userdir is html" (map munge) conf
-- <http://joeyh.name/blog/entry/a_programmable_alarm_clock_using_systemd/>
--
-- oncalendar example value: "*-*-* 7:30"
-alarmClock :: String -> User -> String -> Property DebianLike
+alarmClock :: String -> User -> String -> Property Linux
alarmClock oncalendar (User user) command = combineProperties "goodmorning timer installed" $ props
& "/etc/systemd/system/goodmorning.timer" `File.hasContent`
[ "[Unit]"
@@ -925,3 +927,124 @@ alarmClock oncalendar (User user) command = combineProperties "goodmorning timer
& Systemd.started "goodmorning.timer"
& "/etc/systemd/logind.conf" `ConfFile.containsIniSetting`
("Login", "LidSwitchIgnoreInhibited", "no")
+
+-- My home power monitor.
+homePowerMonitor :: IsContext c => User -> c -> (SshKeyType, Ssh.PubKeyText) -> Property (HasInfo + DebianLike)
+homePowerMonitor user ctx sshkey = propertyList "home power monitor" $ props
+ & Apache.installed
+ & Apt.installed ["python2", "python-pymodbus"]
+ & File.ownerGroup "/var/www/html" user (userGroup user)
+ & Git.cloned user "git://git.kitenet.net/joey/homepower" d Nothing
+ `onChange` buildpoller
+ & Systemd.enabled servicename
+ `requires` serviceinstalled
+ `onChange` Systemd.started servicename
+ & Cron.niceJob "homepower upload"
+ (Cron.Times "1 * * * *") user d rsynccommand
+ `requires` Ssh.userKeyAt (Just sshkeyfile) user ctx sshkey
+ where
+ d = "/var/www/html/homepower"
+ sshkeyfile = d </> ".ssh/key"
+ buildpoller = userScriptProperty (User "joey")
+ [ "cd " ++ d
+ , "make"
+ ]
+ `assume` MadeChange
+ `requires` Apt.installed ["ghc", "make"]
+ servicename = "homepower"
+ servicefile = "/etc/systemd/system/" ++ servicename ++ ".service"
+ serviceinstalled = servicefile `File.hasContent`
+ [ "[Unit]"
+ , "Description=home power monitor"
+ , ""
+ , "[Service]"
+ , "ExecStart=" ++ d ++ "/poller"
+ , "WorkingDirectory=" ++ d
+ , "User=joey"
+ , "Group=joey"
+ , ""
+ , "[Install]"
+ , "WantedBy=multi-user.target"
+ ]
+ -- Only upload when eth0 is up; eg the satellite internet is up.
+ -- Any changes to the rsync command will need my .authorized_keys
+ -- rsync server command to be updated too.
+ rsynccommand = "if ip route | grep '^default' | grep -q eth0; then rsync -e 'ssh -i" ++ sshkeyfile ++ "' -avz rrds/recent/ joey@kitenet.net:/srv/web/homepower.joeyh.name/rrds/recent/; fi"
+
+-- My home router, running hostapd and dnsmasq for wlan0,
+-- with eth0 connected to a satellite modem, and a fallback ppp connection.
+homeRouter :: Property (HasInfo + DebianLike)
+homeRouter = propertyList "home router" $ props
+ & Network.static "wlan0" (IPv4 "10.1.1.1") Nothing
+ `requires` Network.cleanInterfacesFile
+ & Apt.serviceInstalledRunning "hostapd"
+ `requires` File.hasContent "/etc/hostapd/hostapd.conf"
+ [ "interface=wlan0"
+ , "ssid=house"
+ , "hw_mode=g"
+ , "channel=8"
+ ]
+ `requires` File.dirExists "/lib/hostapd"
+ & Apt.serviceInstalledRunning "dnsmasq"
+ `requires` File.hasContent "/etc/dnsmasq.conf"
+ [ "domain-needed"
+ , "bogus-priv"
+ , "interface=wlan0"
+ , "domain=kitenet.net"
+ , "dhcp-range=10.1.1.100,10.1.1.150,24h"
+ , "no-hosts"
+ , "address=/honeybee.kitenet.net/10.1.1.1"
+ ]
+ `requires` File.hasContent "/etc/resolv.conf"
+ [ "domain kitenet.net"
+ , "search kitenet.net"
+ , "nameserver 8.8.8.8"
+ , "nameserver 8.8.4.4"
+ ]
+ & ipmasq "wlan0"
+ & Apt.serviceInstalledRunning "netplug"
+ & Network.dhcp' "eth0"
+ -- When satellite is down, fall back to dialup
+ [ ("pre-up", "poff -a || true")
+ , ("post-down", "pon")
+ ]
+ `requires` Network.cleanInterfacesFile
+ & Apt.installed ["ppp"]
+ `before` File.hasContent "/etc/ppp/peers/provider"
+ [ "user \"joeyh@arczip.com\""
+ , "connect \"/usr/sbin/chat -v -f /etc/chatscripts/pap -T 9734111\""
+ , "/dev/ttyACM0"
+ , "115200"
+ , "noipdefault"
+ , "defaultroute"
+ , "persist"
+ , "noauth"
+ ]
+ `before` File.hasPrivContent "/etc/ppp/pap-secrets" (Context "joeyh@arczip.com")
+
+-- | Enable IP masqerading, on whatever other interfaces come up than the
+-- provided intif.
+ipmasq :: String -> Property DebianLike
+ipmasq intif = File.hasContent ifupscript
+ [ "#!/bin/sh"
+ , "INTIF=" ++ intif
+ , "if [ \"$IFACE\" = $INTIF ] || [ \"$IFACE\" = lo ]; then"
+ , "exit 0"
+ , "fi"
+ , "iptables -F"
+ , "iptables -A FORWARD -i $IFACE -o $INTIF -m state --state ESTABLISHED,RELATED -j ACCEPT"
+ , "iptables -A FORWARD -i $INTIF -o $IFACE -j ACCEPT"
+ , "iptables -t nat -A POSTROUTING -o $IFACE -j MASQUERADE"
+ , "echo 1 > /proc/sys/net/ipv4/ip_forward"
+ ]
+ `before` scriptmode ifupscript
+ `before` File.hasContent pppupscript
+ [ "#!/bin/sh"
+ , "IFACE=$PPP_IFACE " ++ ifupscript
+ ]
+ `before` scriptmode pppupscript
+ `requires` Apt.installed ["iptables"]
+ where
+ ifupscript = "/etc/network/if-up.d/ipmasq"
+ pppupscript = "/etc/ppp/ip-up.d/ipmasq"
+ scriptmode f = f `File.mode` combineModes (readModes ++ executeModes)
diff --git a/src/Propellor/Property/Ssh.hs b/src/Propellor/Property/Ssh.hs
index bce522f6..fd89f97a 100644
--- a/src/Propellor/Property/Ssh.hs
+++ b/src/Propellor/Property/Ssh.hs
@@ -69,11 +69,11 @@ setSshdConfigBool :: ConfigKeyword -> Bool -> Property DebianLike
setSshdConfigBool setting allowed = setSshdConfig setting (sshBool allowed)
setSshdConfig :: ConfigKeyword -> String -> Property DebianLike
-setSshdConfig setting val = File.fileProperty desc f sshdConfig
+setSshdConfig setting v = File.fileProperty desc f sshdConfig
`onChange` restarted
where
- desc = unwords [ "ssh config:", setting, val ]
- cfgline = setting ++ " " ++ val
+ desc = unwords [ "ssh config:", setting, v ]
+ cfgline = setting ++ " " ++ v
wantedline s
| s == cfgline = True
| (setting ++ " ") `isPrefixOf` s = False
@@ -120,7 +120,7 @@ dotFile f user = do
listenPort :: Port -> RevertableProperty DebianLike DebianLike
listenPort port = enable <!> disable
where
- portline = "Port " ++ fromPort port
+ portline = "Port " ++ val port
enable = sshdConfig `File.containsLine` portline
`describe` ("ssh listening on " ++ portline)
`onChange` restarted
@@ -227,7 +227,7 @@ newtype HostKeyInfo = HostKeyInfo
deriving (Eq, Ord, Typeable, Show)
instance IsInfo HostKeyInfo where
- propagateInfo _ = False
+ propagateInfo _ = PropagateInfo False
instance Monoid HostKeyInfo where
mempty = HostKeyInfo M.empty
@@ -248,7 +248,7 @@ newtype UserKeyInfo = UserKeyInfo
deriving (Eq, Ord, Typeable, Show)
instance IsInfo UserKeyInfo where
- propagateInfo _ = False
+ propagateInfo _ = PropagateInfo False
instance Monoid UserKeyInfo where
mempty = UserKeyInfo M.empty
diff --git a/src/Propellor/Property/Sudo.hs b/src/Propellor/Property/Sudo.hs
index 45ab8af2..1614801d 100644
--- a/src/Propellor/Property/Sudo.hs
+++ b/src/Propellor/Property/Sudo.hs
@@ -9,23 +9,33 @@ 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 :: User -> Property DebianLike
-enabledFor user@(User u) = go `requires` Apt.installed ["sudo"]
+enabledFor :: User -> RevertableProperty DebianLike DebianLike
+enabledFor user@(User u) = setup `requires` Apt.installed ["sudo"] <!> cleanup
where
- go :: Property UnixLike
- go = property' desc $ \w -> do
+ setup :: Property UnixLike
+ setup = property' desc $ \w -> do
locked <- liftIO $ isLockedPassword user
ensureProperty w $
fileProperty desc
(modify locked . filter (wanted locked))
- "/etc/sudoers"
- desc = u ++ " is sudoer"
+ sudoers
+ where
+ desc = u ++ " is sudoer"
+
+ cleanup :: Property DebianLike
+ cleanup = tightenTargets $
+ fileProperty desc (filter notuserline) sudoers
+ where
+ desc = u ++ " is not sudoer"
+
+ sudoers = "/etc/sudoers"
sudobaseline = u ++ " ALL=(ALL:ALL)"
+ notuserline l = not (sudobaseline `isPrefixOf` l)
sudoline True = sudobaseline ++ " NOPASSWD:ALL"
sudoline False = sudobaseline ++ " ALL"
wanted locked l
-- TOOD: Full sudoers file format parse..
- | not (sudobaseline `isPrefixOf` l) = True
+ | notuserline l = True
| "NOPASSWD" `isInfixOf` l = locked
| otherwise = True
modify locked ls
diff --git a/src/Propellor/Property/Systemd.hs b/src/Propellor/Property/Systemd.hs
index 78529f73..51d1313c 100644
--- a/src/Propellor/Property/Systemd.hs
+++ b/src/Propellor/Property/Systemd.hs
@@ -55,9 +55,9 @@ import qualified Propellor.Property.Apt as Apt
import qualified Propellor.Property.File as File
import Propellor.Property.Systemd.Core
import Utility.FileMode
+import Utility.Split
import Data.List
-import Data.List.Utils
import qualified Data.Map as M
type ServiceName = String
@@ -259,7 +259,7 @@ debContainer name ps = container name $ \d -> Chroot.debootstrapped mempty d ps
-- Reverting this property stops the container, removes the systemd unit,
-- and deletes the chroot and all its contents.
nspawned :: Container -> RevertableProperty (HasInfo + Linux) Linux
-nspawned c@(Container name (Chroot.Chroot loc builder _) h) =
+nspawned c@(Container name (Chroot.Chroot loc builder _ _) h) =
p `describe` ("nspawned " ++ name)
where
p :: RevertableProperty (HasInfo + Linux) Linux
@@ -271,7 +271,7 @@ nspawned c@(Container name (Chroot.Chroot loc builder _) h) =
-- Chroot provisioning is run in systemd-only mode,
-- which sets up the chroot and ensures systemd and dbus are
-- installed, but does not handle the other properties.
- chrootprovisioned = Chroot.provisioned' (Chroot.propagateChrootInfo chroot) chroot True
+ chrootprovisioned = Chroot.provisioned' chroot True
-- Use nsenter to enter container and and run propellor to
-- finish provisioning.
@@ -281,56 +281,44 @@ nspawned c@(Container name (Chroot.Chroot loc builder _) h) =
<!>
doNothing
- chroot = Chroot.Chroot loc builder h
+ chroot = Chroot.Chroot loc builder Chroot.propagateChrootInfo h
--- | Sets up the service file for the container, and then starts
--- it running.
+-- | Sets up the service files for the container, using the
+-- systemd-nspawn@.service template, and starts it running.
nspawnService :: Container -> ChrootCfg -> RevertableProperty Linux Linux
nspawnService (Container name _ _) cfg = setup <!> teardown
where
service = nspawnServiceName name
- servicefile = "/etc/systemd/system/multi-user.target.wants" </> service
-
- servicefilecontent = do
- ls <- lines <$> readFile "/lib/systemd/system/systemd-nspawn@.service"
- return $ unlines $
- "# deployed by propellor" : map addparams ls
- addparams l
- | "ExecStart=" `isPrefixOf` l = unwords $
- [ "ExecStart = /usr/bin/systemd-nspawn"
- , "--quiet"
- , "--keep-unit"
- , "--boot"
- , "--directory=" ++ containerDir name
- , "--machine=%i"
- ] ++ nspawnServiceParams cfg
- | otherwise = l
-
- goodservicefile = (==)
- <$> servicefilecontent
- <*> catchDefaultIO "" (readFile servicefile)
-
- writeservicefile :: Property Linux
- writeservicefile = property servicefile $ makeChange $ do
- c <- servicefilecontent
- File.viaStableTmp (\t -> writeFile t c) servicefile
-
- setupservicefile :: Property Linux
- setupservicefile = check (not <$> goodservicefile) $
- -- if it's running, it has the wrong configuration,
- -- so stop it
- stopped service
- `requires` daemonReloaded
- `requires` writeservicefile
+ overridedir = "/etc/systemd/system" </> nspawnServiceName name ++ ".d"
+ overridefile = overridedir </> "local.conf"
+ overridecontent =
+ [ "[Service]"
+ , "# Reset ExecStart from the template"
+ , "ExecStart="
+ , "ExecStart=/usr/bin/systemd-nspawn " ++ unwords nspawnparams
+ ]
+ nspawnparams =
+ [ "--quiet"
+ , "--keep-unit"
+ , "--boot"
+ , "--directory=" ++ containerDir name
+ , "--machine=" ++ name
+ ] ++ nspawnServiceParams cfg
+
+ overrideconfigured = File.hasContent overridefile overridecontent
+ `onChange` daemonReloaded
+ `requires` File.dirExists overridedir
setup :: Property Linux
setup = started service
- `requires` setupservicefile
+ `requires` enabled service
+ `requires` overrideconfigured
`requires` machined
teardown :: Property Linux
- teardown = check (doesFileExist servicefile) $
- disabled service `requires` stopped service
+ teardown = stopped service
+ `before` disabled service
+ `before` File.notPresent overridefile
nspawnServiceParams :: ChrootCfg -> [String]
nspawnServiceParams NoChrootCfg = []
@@ -421,7 +409,7 @@ class Publishable a where
toPublish :: a -> String
instance Publishable Port where
- toPublish port = fromPort port
+ toPublish port = val port
instance Publishable (Bound Port) where
toPublish v = toPublish (hostSide v) ++ ":" ++ toPublish (containerSide v)
diff --git a/src/Propellor/Property/Timezone.hs b/src/Propellor/Property/Timezone.hs
new file mode 100644
index 00000000..96a5e59c
--- /dev/null
+++ b/src/Propellor/Property/Timezone.hs
@@ -0,0 +1,21 @@
+-- | Maintainer: Sean Whitton <spwhitton@spwhitton.name>
+
+module Propellor.Property.Timezone where
+
+import Propellor.Base
+import qualified Propellor.Property.Apt as Apt
+import qualified Propellor.Property.File as File
+
+-- | A timezone from /usr/share/zoneinfo
+type Timezone = String
+
+-- | Sets the system's timezone
+configured :: Timezone -> Property DebianLike
+configured zone = File.hasContent "/etc/timezone" [zone]
+ `onChange` update
+ `describe` (zone ++ " timezone configured")
+ where
+ update = Apt.reConfigure "tzdata" mempty
+ -- work around a bug in recent tzdata. See
+ -- https://bugs.launchpad.net/ubuntu/+source/tzdata/+bug/1554806/
+ `requires` File.notPresent "/etc/localtime"
diff --git a/src/Propellor/Property/Tor.hs b/src/Propellor/Property/Tor.hs
index ea9f39ed..8794bc7f 100644
--- a/src/Propellor/Property/Tor.hs
+++ b/src/Propellor/Property/Tor.hs
@@ -53,12 +53,20 @@ named n = configured [("Nickname", n')]
where
n' = saneNickname n
+-- | Configures tor with secret_id_key, ed25519_master_id_public_key,
+-- and ed25519_master_id_secret_key from privdata.
torPrivKey :: Context -> Property (HasInfo + DebianLike)
-torPrivKey context = f `File.hasPrivContent` context
- `onChange` File.ownerGroup f user (userGroup user)
+torPrivKey context = mconcat (map go keyfiles)
+ `onChange` restarted
`requires` torPrivKeyDirExists
where
- f = torPrivKeyDir </> "secret_id_key"
+ keyfiles = map (torPrivKeyDir </>)
+ [ "secret_id_key"
+ , "ed25519_master_id_public_key"
+ , "ed25519_master_id_secret_key"
+ ]
+ go f = f `File.hasPrivContent` context
+ `onChange` File.ownerGroup f user (userGroup user)
torPrivKeyDirExists :: Property DebianLike
torPrivKeyDirExists = File.dirExists torPrivKeyDir
@@ -124,22 +132,30 @@ bandwidthRate' s divby = case readSize dataUnits s of
-- If used without `hiddenServiceData`, tor will generate a new
-- private key.
hiddenService :: HiddenServiceName -> Port -> Property DebianLike
-hiddenService hn (Port port) = ConfFile.adjustSection
- (unwords ["hidden service", hn, "available on port", show port])
+hiddenService hn port = hiddenService' hn [port]
+
+hiddenService' :: HiddenServiceName -> [Port] -> Property DebianLike
+hiddenService' hn ports = ConfFile.adjustSection
+ (unwords ["hidden service", hn, "available on ports", intercalate "," (map val ports')])
(== oniondir)
(not . isPrefixOf "HiddenServicePort")
- (const [oniondir, onionport])
- (++ [oniondir, onionport])
+ (const (oniondir : onionports))
+ (++ oniondir : onionports)
mainConfig
`onChange` restarted
where
oniondir = unwords ["HiddenServiceDir", varLib </> hn]
- onionport = unwords ["HiddenServicePort", show port, "127.0.0.1:" ++ show port]
+ onionports = map onionport ports'
+ ports' = sort ports
+ onionport port = unwords ["HiddenServicePort", val port, "127.0.0.1:" ++ val port]
-- | Same as `hiddenService` but also causes propellor to display
-- the onion address of the hidden service.
hiddenServiceAvailable :: HiddenServiceName -> Port -> Property DebianLike
-hiddenServiceAvailable hn port = hiddenServiceHostName $ hiddenService hn port
+hiddenServiceAvailable hn port = hiddenServiceAvailable' hn [port]
+
+hiddenServiceAvailable' :: HiddenServiceName -> [Port] -> Property DebianLike
+hiddenServiceAvailable' hn ports = hiddenServiceHostName $ hiddenService' hn ports
where
hiddenServiceHostName p = adjustPropertySatisfy p $ \satisfy -> do
r <- satisfy
diff --git a/src/Propellor/Property/Unbound.hs b/src/Propellor/Property/Unbound.hs
index 23a5b30d..470aad7e 100644
--- a/src/Propellor/Property/Unbound.hs
+++ b/src/Propellor/Property/Unbound.hs
@@ -133,10 +133,10 @@ genAddress dom ttl addr = case addr of
IPv6 _ -> genAddress' "AAAA" dom ttl addr
genAddress' :: String -> BindDomain -> Maybe Int -> IPAddr -> String
-genAddress' recordtype dom ttl addr = dValue dom ++ " " ++ maybe "" (\ttl' -> show ttl' ++ " ") ttl ++ "IN " ++ recordtype ++ " " ++ fromIPAddr addr
+genAddress' recordtype dom ttl addr = dValue dom ++ " " ++ maybe "" (\ttl' -> val ttl' ++ " ") ttl ++ "IN " ++ recordtype ++ " " ++ val addr
genMX :: BindDomain -> Int -> BindDomain -> String
-genMX dom priority dest = dValue dom ++ " " ++ "MX" ++ " " ++ show priority ++ " " ++ dValue dest
+genMX dom priority dest = dValue dom ++ " " ++ "MX" ++ " " ++ val priority ++ " " ++ dValue dest
genPTR :: BindDomain -> ReverseIP -> String
genPTR dom revip = revip ++ ". " ++ "PTR" ++ " " ++ dValue dom
diff --git a/src/Propellor/Property/User.hs b/src/Propellor/Property/User.hs
index 76eae647..0b5bdddc 100644
--- a/src/Propellor/Property/User.hs
+++ b/src/Propellor/Property/User.hs
@@ -22,17 +22,18 @@ systemAccountFor :: User -> Property DebianLike
systemAccountFor user@(User u) = systemAccountFor' user Nothing (Just (Group u))
systemAccountFor' :: User -> Maybe FilePath -> Maybe Group -> Property DebianLike
-systemAccountFor' (User u) mhome mgroup = tightenTargets $ check nouser go
+systemAccountFor' (User u) mhome mgroup = case mgroup of
+ Nothing -> prop
+ Just g -> prop
+ `requires` systemGroup g
`describe` ("system account for " ++ u)
where
+ prop = tightenTargets $ check nouser go
nouser = isNothing <$> catchMaybeIO (getUserEntryForName u)
go = cmdProperty "adduser" $
- [ "--system" ]
+ [ "--system", "--home" ]
++
- "--home" : maybe
- ["/nonexistent", "--no-create-home"]
- ( \h -> [ h ] )
- mhome
+ maybe ["/nonexistent", "--no-create-home"] ( \h -> [h] ) mhome
++
maybe [] ( \(Group g) -> ["--ingroup", g] ) mgroup
++
@@ -42,8 +43,18 @@ systemAccountFor' (User u) mhome mgroup = tightenTargets $ check nouser go
, u
]
+systemGroup :: Group -> Property UnixLike
+systemGroup (Group g) = check nogroup go
+ `describe` ("system account for " ++ g)
+ where
+ nogroup = isNothing <$> catchMaybeIO (getGroupEntryForName g)
+ go = cmdProperty "addgroup"
+ [ "--system"
+ , g
+ ]
+
-- | Removes user home directory!! Use with caution.
-nuked :: User -> Eep -> Property DebianLike
+nuked :: User -> Eep -> Property Linux
nuked user@(User u) _ = tightenTargets $ check hashomedir go
`describe` ("nuked user " ++ u)
where
@@ -97,8 +108,12 @@ setPassword getpassword = getpassword $ go
-- | Makes a user's password be the passed String. Highly insecure:
-- The password is right there in your config file for anyone to see!
hasInsecurePassword :: User -> String -> Property DebianLike
-hasInsecurePassword u@(User n) p = property (n ++ " has insecure password") $
- chpasswd u p []
+hasInsecurePassword u@(User n) p = go
+ `requires` shadowConfig True
+ where
+ go :: Property DebianLike
+ go = property (n ++ " has insecure password") $
+ chpasswd u p []
chpasswd :: User -> String -> [String] -> Propellor Result
chpasswd (User user) v ps = makeChange $ withHandle StdinHandle createProcessSuccess
@@ -107,7 +122,7 @@ chpasswd (User user) v ps = makeChange $ withHandle StdinHandle createProcessSuc
hClose h
lockedPassword :: User -> Property DebianLike
-lockedPassword user@(User u) = tightenTargets $
+lockedPassword user@(User u) = tightenTargets $
check (not <$> isLockedPassword user) go
`describe` ("locked " ++ u ++ " password")
where
diff --git a/src/Propellor/Property/Versioned.hs b/src/Propellor/Property/Versioned.hs
new file mode 100644
index 00000000..87673c64
--- /dev/null
+++ b/src/Propellor/Property/Versioned.hs
@@ -0,0 +1,124 @@
+{-# LANGUAGE RankNTypes, FlexibleContexts, TypeFamilies #-}
+
+-- | Versioned properties and hosts.
+--
+-- When importing and using this module, you will need to enable some
+-- language extensions:
+--
+-- > {-# LANGUAGE RankNTypes, FlexibleContexts, TypeFamilies #-}
+--
+-- This module takes advantage of `RevertableProperty` to let propellor
+-- switch cleanly between versions. The way it works is all revertable
+-- properties for other versions than the current version are first
+-- reverted, and then propellor ensures the property for the current
+-- version. This method should work for any combination of revertable
+-- properties.
+--
+-- For example:
+--
+-- > demo :: Versioned Int (RevertableProperty DebianLike DebianLike)
+-- > demo ver =
+-- > ver ( (== 1) --> Apache.modEnabled "foo"
+-- > `requires` Apache.modEnabled "foosupport"
+-- > <|> (== 2) --> Apache.modEnabled "bar"
+-- > <|> (> 2) --> Apache.modEnabled "baz"
+-- > )
+-- >
+-- > foo :: Host
+-- > foo = host "foo.example.com" $ props
+-- > & demo `version` (2 :: Int)
+--
+-- Similarly, a whole Host can be versioned. For example:
+--
+-- > bar :: Versioned Int Host
+-- > bar ver = host "bar.example.com" $ props
+-- > & osDebian Unstable X86_64
+-- > & ver ( (== 1) --> Apache.modEnabled "foo"
+-- > <|> (== 2) --> Apache.modEnabled "bar"
+-- > )
+-- > & ver ( (>= 2) --> Apt.unattendedUpgrades )
+--
+-- Note that some versioning of revertable properties may cause
+-- propellor to do a lot of unnecessary work each time it's run.
+-- Here's an example of such a problem:
+--
+-- > slow :: Versioned Int -> RevertableProperty DebianLike DebianLike
+-- > slow ver =
+-- > ver ( (== 1) --> (Apt.installed "foo" <!> Apt.removed "foo")
+-- > <|> (== 2) --> (Apt.installed "bar" <!> Apt.removed "bar")
+-- > )
+--
+-- Suppose that package bar depends on package foo. Then at version 2,
+-- propellor will remove package foo in order to revert version 1, only
+-- to re-install it since version 2 also needs it installed.
+
+module Propellor.Property.Versioned (Versioned, version, (-->), (<|>)) where
+
+import Propellor
+import Propellor.Types.Core
+
+import Data.List
+
+-- | Something that has multiple versions of type `v`.
+type Versioned v t = VersionedBy v -> t
+
+type VersionedBy v
+ = forall metatypes. Combines (RevertableProperty metatypes metatypes) (RevertableProperty metatypes metatypes)
+ => (CombinedType (RevertableProperty metatypes metatypes) (RevertableProperty metatypes metatypes) ~ RevertableProperty metatypes metatypes)
+ => (VerSpec v metatypes -> RevertableProperty metatypes metatypes)
+
+-- | Access a particular version of a Versioned value.
+version :: (Versioned v t) -> v -> t
+version f v = f (processVerSpec v)
+
+-- A specification of versions.
+--
+-- Why is this not a simple list like
+-- [(v -> Bool, RevertableProperty metatypes metatypes)] ?
+-- Using a list would mean the empty list would need to be dealt with,
+-- and processVerSpec does not have a Monoid instance for
+-- RevertableProperty metatypes metatypes in scope, and due to the way the
+-- Versioned type works, the compiler cannot find such an instance.
+--
+-- Also, using this data type allows a nice syntax for creating
+-- VerSpecs, via the `<&>` and `alt` functions.
+data VerSpec v metatypes
+ = Base (v -> Bool, RevertableProperty metatypes metatypes)
+ | More (v -> Bool, RevertableProperty metatypes metatypes) (VerSpec v metatypes)
+
+processVerSpec
+ :: Combines (RevertableProperty metatypes metatypes) (RevertableProperty metatypes metatypes)
+ => (CombinedType (RevertableProperty metatypes metatypes) (RevertableProperty metatypes metatypes) ~ RevertableProperty metatypes metatypes)
+ => v
+ -> VerSpec v metatypes
+ -> RevertableProperty metatypes metatypes
+processVerSpec v s = combinedp s
+ `describe` intercalate " and " (combineddesc s [])
+ where
+ combinedp (Base (c, p))
+ | c v = p
+ | otherwise = revert p
+ combinedp (More (c, p) vs)
+ | c v = combinedp vs `before` p
+ | otherwise = revert p `before` combinedp vs
+ combineddesc (Base (c, p)) l
+ | c v = getDesc p : l
+ | otherwise = getDesc (revert p) : l
+ combineddesc (More (c, p) vs) l
+ | c v = getDesc p : combineddesc vs l
+ | otherwise = getDesc (revert p) : combineddesc vs l
+
+-- | Specify a function that checks the version, and what
+-- `RevertableProperty` to use if the version matches.
+(-->) :: (v -> Bool) -> RevertableProperty metatypes metatypes -> VerSpec v metatypes
+c --> p = Base (c, p)
+
+-- | Add an alternate version.
+(<|>) :: VerSpec v metatypes -> VerSpec v metatypes -> VerSpec v metatypes
+Base a <|> Base b = More a (Base b)
+Base a <|> More b c = More a (More b c)
+More b c <|> Base a = More a (More b c)
+More a b <|> More c d = More a (More c (b <|> d))
+
+infixl 8 -->
+infixl 2 <|>
diff --git a/src/Propellor/Property/XFCE.hs b/src/Propellor/Property/XFCE.hs
new file mode 100644
index 00000000..dc57660f
--- /dev/null
+++ b/src/Propellor/Property/XFCE.hs
@@ -0,0 +1,41 @@
+module Propellor.Property.XFCE where
+
+import Propellor.Base
+import qualified Propellor.Property.Apt as Apt
+import qualified Propellor.Property.File as File
+import qualified Propellor.Property.User as User
+
+installed :: Property DebianLike
+installed = Apt.installed ["task-xfce-desktop"]
+ `describe` "XFCE desktop installed"
+
+-- | Minimal install of XFCE, with a terminal emulator and panel,
+-- and X and network-manager, but not any of the extra apps.
+installedMin :: Property DebianLike
+installedMin = Apt.installedMin ["xfce4", "xfce4-terminal", "task-desktop"]
+ `describe` "minimal XFCE desktop installed"
+
+-- | Installs network-manager-gnome, which is the way to get
+-- network-manager to manage networking in XFCE too.
+networkManager :: Property DebianLike
+networkManager = Apt.installedMin ["network-manager-gnome"]
+
+-- | Normally at first login, XFCE asks what kind of panel the user wants.
+-- This enables the default configuration noninteractively.
+defaultPanelFor :: User -> File.Overwrite -> Property DebianLike
+defaultPanelFor u@(User username) overwrite = property' desc $ \w -> do
+ home <- liftIO $ User.homedir u
+ ensureProperty w (go home)
+ where
+ desc = "default XFCE panel for " ++ username
+ basecf = ".config" </> "xfce4" </> "xfconf"
+ </> "xfce-perchannel-xml" </> "xfce4-panel.xml"
+ -- This location is probably Debian-specific.
+ defcf = "/etc/xdg/xfce4/panel/default.xml"
+ go :: FilePath -> Property DebianLike
+ go home = tightenTargets $
+ File.checkOverwrite overwrite (home </> basecf) $ \cf ->
+ cf `File.isCopyOf` defcf
+ `before` File.applyPath home basecf
+ (\f -> File.ownerGroup f u (userGroup u))
+ `requires` Apt.installed ["xfce4-panel"]
diff --git a/src/Propellor/Property/ZFS/Process.hs b/src/Propellor/Property/ZFS/Process.hs
index 372bac6d..42b23df2 100644
--- a/src/Propellor/Property/ZFS/Process.hs
+++ b/src/Propellor/Property/ZFS/Process.hs
@@ -5,7 +5,8 @@
module Propellor.Property.ZFS.Process where
import Propellor.Base
-import Data.String.Utils (split)
+import Utility.Split
+
import Data.List
-- | Gets the properties of a ZFS volume.
diff --git a/src/Propellor/Shim.hs b/src/Propellor/Shim.hs
index 27545afb..811ae7f0 100644
--- a/src/Propellor/Shim.hs
+++ b/src/Propellor/Shim.hs
@@ -9,7 +9,6 @@ module Propellor.Shim (setup, cleanEnv, file) where
import Propellor.Base
import Utility.LinuxMkLibs
import Utility.FileMode
-import Utility.FileSystemEncoding
import Data.List
import System.Posix.Files
@@ -57,7 +56,6 @@ shebang = "#!/bin/sh"
checkAlreadyShimmed :: FilePath -> IO FilePath -> IO FilePath
checkAlreadyShimmed f nope = ifM (doesFileExist f)
( withFile f ReadMode $ \h -> do
- fileEncoding h
s <- hGetLine h
if s == shebang
then return f
diff --git a/src/Propellor/Spin.hs b/src/Propellor/Spin.hs
index c6699961f..aeaa4643 100644
--- a/src/Propellor/Spin.hs
+++ b/src/Propellor/Spin.hs
@@ -87,12 +87,15 @@ spin' mprivdata relay target hst = do
-- And now we can run it.
unlessM (boolSystemNonConcurrent "ssh" (map Param $ cacheparams ++ ["-t", sshtarget, shellWrap runcmd])) $
- error "remote propellor failed"
+ giveup "remote propellor failed"
where
hn = fromMaybe target relay
sys = case fromInfo (hostInfo hst) of
InfoVal o -> Just o
NoInfoVal -> Nothing
+ bootstrapper = case fromInfo (hostInfo hst) of
+ NoInfoVal -> defaultBootstrapper
+ InfoVal bs -> bs
relaying = relay == Just target
viarelay = isJust relay && not relaying
@@ -109,7 +112,7 @@ spin' mprivdata relay target hst = do
updatecmd = intercalate " && "
[ "cd " ++ localdir
- , bootstrapPropellorCommand sys
+ , bootstrapPropellorCommand bootstrapper sys
, if viarelay
then "./propellor --continue " ++
shellEscape (show (Relay target))
@@ -169,7 +172,7 @@ getSshTarget target hst
warningMessage $ "DNS seems out of date for " ++ target ++ " (" ++ why ++ "); using IP address from configuration instead."
return ip
- configips = map fromIPAddr $ mapMaybe getIPAddr $
+ configips = map val $ mapMaybe getIPAddr $
S.toList $ fromDnsInfo $ fromInfo $ hostInfo hst
-- Update the privdata, repo url, and git repo over the ssh
@@ -186,26 +189,8 @@ update forhost = do
writeFileProtected privfile
whenM hasGitRepo $
- req NeedGitPush gitPushMarker $ \_ -> do
- hin <- dup stdInput
- hout <- dup stdOutput
- hClose stdin
- hClose stdout
- -- Not using git pull because git 2.5.0 badly
- -- broke its option parser.
- unlessM (boolSystemNonConcurrent "git" (pullparams hin hout)) $
- errorMessage "git fetch from client failed"
- unlessM (boolSystemNonConcurrent "git" [Param "merge", Param "FETCH_HEAD"]) $
- errorMessage "git merge from client failed"
+ gitPullFromUpdateServer
where
- pullparams hin hout =
- [ Param "fetch"
- , Param "--progress"
- , Param "--upload-pack"
- , Param $ "./propellor --gitpush " ++ show hin ++ " " ++ show hout
- , Param "."
- ]
-
-- When --spin --relay is run, get a privdata file
-- to be relayed to the target host.
privfile = maybe privDataLocal privDataRelay forhost
@@ -336,31 +321,6 @@ sendPrecompiled hn = void $ actionMessage "Uploading locally compiled propellor
, "rm -f " ++ remotetarball
]
--- Shim for git push over the propellor ssh channel.
--- Reads from stdin and sends it to hout;
--- reads from hin and sends it to stdout.
-gitPushHelper :: Fd -> Fd -> IO ()
-gitPushHelper hin hout = void $ fromstdin `concurrently` tostdout
- where
- fromstdin = do
- h <- fdToHandle hout
- connect stdin h
- tostdout = do
- h <- fdToHandle hin
- connect h stdout
- connect fromh toh = do
- hSetBinaryMode fromh True
- hSetBinaryMode toh True
- b <- B.hGetSome fromh 40960
- if B.null b
- then do
- hClose fromh
- hClose toh
- else do
- B.hPut toh b
- hFlush toh
- connect fromh toh
-
mergeSpin :: IO ()
mergeSpin = do
branch <- getCurrentBranch
@@ -388,3 +348,68 @@ findLastNonSpinCommit = do
spinCommitMessage :: String
spinCommitMessage = "propellor spin"
+
+-- Stdin and stdout are connected to the updateServer over ssh.
+-- Request that it run git upload-pack, and connect that up to a git fetch
+-- to receive the data.
+gitPullFromUpdateServer :: IO ()
+gitPullFromUpdateServer = req NeedGitPush gitPushMarker $ \_ -> do
+ -- IO involving stdin can cause data to be buffered in the Handle
+ -- (even when it's set NoBuffering), but we need to pass a FD to
+ -- git fetch containing all of stdin after the gitPushMarker,
+ -- including any that has been buffered.
+ --
+ -- To do so, create a pipe, and forward stdin, including any
+ -- buffered part, through it.
+ (pread, pwrite) <- System.Posix.IO.createPipe
+ -- Note that there is a race between the createPipe and setting
+ -- CloseOnExec. Another processess forked here would inherit
+ -- pwrite and perhaps keep it open. However, propellor is not
+ -- running concurrent threads at this point, so this is ok.
+ setFdOption pwrite CloseOnExec True
+ hwrite <- fdToHandle pwrite
+ forwarder <- async $ stdin *>* hwrite
+ let hin = pread
+ hout <- dup stdOutput
+ hClose stdout
+ -- Not using git pull because git 2.5.0 badly
+ -- broke its option parser.
+ unlessM (boolSystemNonConcurrent "git" (fetchparams hin hout)) $
+ errorMessage "git fetch from client failed"
+ wait forwarder
+ unlessM (boolSystemNonConcurrent "git" [Param "merge", Param "FETCH_HEAD"]) $
+ errorMessage "git merge from client failed"
+ where
+ fetchparams hin hout =
+ [ Param "fetch"
+ , Param "--progress"
+ , Param "--upload-pack"
+ , Param $ "./propellor --gitpush " ++ show hin ++ " " ++ show hout
+ , Param "."
+ ]
+
+-- Shim for git push over the propellor ssh channel.
+-- Reads from stdin and sends it to hout;
+-- reads from hin and sends it to stdout.
+gitPushHelper :: Fd -> Fd -> IO ()
+gitPushHelper hin hout = void $ fromstdin `concurrently` tostdout
+ where
+ fromstdin = do
+ h <- fdToHandle hout
+ stdin *>* h
+ tostdout = do
+ h <- fdToHandle hin
+ h *>* stdout
+
+-- Forward data from one handle to another.
+(*>*) :: Handle -> Handle -> IO ()
+fromh *>* toh = do
+ b <- B.hGetSome fromh 40960
+ if B.null b
+ then do
+ hClose fromh
+ hClose toh
+ else do
+ B.hPut toh b
+ hFlush toh
+ fromh *>* toh
diff --git a/src/Propellor/Ssh.hs b/src/Propellor/Ssh.hs
index a7a9452e..a8f50ed0 100644
--- a/src/Propellor/Ssh.hs
+++ b/src/Propellor/Ssh.hs
@@ -6,7 +6,7 @@ import Utility.FileSystemEncoding
import System.PosixCompat
import Data.Time.Clock.POSIX
-import qualified Data.Hash.MD5 as MD5
+import Data.Hashable
-- Parameters can be passed to both ssh and scp, to enable a ssh connection
-- caching socket.
@@ -50,24 +50,22 @@ sshCachingParams hn = do
-- 100 bytes. Try to never construct a filename longer than that.
--
-- When space allows, include the full hostname in the socket filename.
--- Otherwise, include at least a partial md5sum of it,
--- to avoid using the same socket file for multiple hosts.
+-- Otherwise, a checksum of the hostname is included in the name, to
+-- avoid using the same socket file for multiple hosts.
socketFile :: FilePath -> HostName -> FilePath
socketFile home hn = selectSocketFile
- [ sshdir </> hn ++ ".sock"
+ [ sshdir </> hn ++ ".sock"
, sshdir </> hn
- , sshdir </> take 10 hn ++ "-" ++ md5
- , sshdir </> md5
- , home </> ".propellor-" ++ md5
+ , sshdir </> take 10 hn ++ "-" ++ checksum
+ , sshdir </> checksum
]
- (".propellor-" ++ md5)
+ (home </> ".propellor-" ++ checksum)
where
sshdir = home </> ".ssh" </> "propellor"
- md5 = take 9 $ MD5.md5s $ MD5.Str hn
+ checksum = take 9 $ show $ abs $ hash hn
selectSocketFile :: [FilePath] -> FilePath -> FilePath
selectSocketFile [] d = d
-selectSocketFile [f] _ = f
selectSocketFile (f:fs) d
| valid_unix_socket_path f = f
| otherwise = selectSocketFile fs d
diff --git a/src/Propellor/Types.hs b/src/Propellor/Types.hs
index 6d6b14ea..b7c7c7f7 100644
--- a/src/Propellor/Types.hs
+++ b/src/Propellor/Types.hs
@@ -12,6 +12,7 @@ module Propellor.Types (
Host(..)
, Property(..)
, property
+ , property''
, Desc
, RevertableProperty(..)
, (<!>)
@@ -24,6 +25,7 @@ module Propellor.Types (
, DebianLike
, Debian
, Buntish
+ , ArchLinux
, FreeBSD
, HasInfo
, type (+)
@@ -35,16 +37,20 @@ module Propellor.Types (
, adjustPropertySatisfy
-- * Other included types
, module Propellor.Types.OS
+ , module Propellor.Types.ConfigurableValue
, module Propellor.Types.Dns
, module Propellor.Types.Result
, module Propellor.Types.ZFS
) where
import Data.Monoid
+import Control.Applicative
+import Prelude
import Propellor.Types.Core
import Propellor.Types.Info
import Propellor.Types.OS
+import Propellor.Types.ConfigurableValue
import Propellor.Types.Dns
import Propellor.Types.Result
import Propellor.Types.MetaTypes
@@ -53,7 +59,6 @@ import Propellor.Types.ZFS
-- | The core data type of Propellor, this represents a property
-- that the system should have, with a descrition, and an action to ensure
-- it has the property.
--- that have the property.
--
-- There are different types of properties that target different OS's,
-- and so have different metatypes.
@@ -64,7 +69,7 @@ import Propellor.Types.ZFS
--
-- There are many associated type families, which are mostly used
-- internally, so you needn't worry about them.
-data Property metatypes = Property metatypes Desc (Propellor Result) Info [ChildProperty]
+data Property metatypes = Property metatypes Desc (Maybe (Propellor Result)) Info [ChildProperty]
instance Show (Property metatypes) where
show p = "property " ++ show (getDesc p)
@@ -87,14 +92,25 @@ property
=> Desc
-> Propellor Result
-> Property (MetaTypes metatypes)
-property d a = Property sing d a mempty mempty
+property d a = Property sing d (Just a) mempty mempty
+
+property''
+ :: SingI metatypes
+ => Desc
+ -> Maybe (Propellor Result)
+ -> Property (MetaTypes metatypes)
+property'' d a = Property sing d a mempty mempty
-- | Changes the action that is performed to satisfy a property.
adjustPropertySatisfy :: Property metatypes -> (Propellor Result -> Propellor Result) -> Property metatypes
-adjustPropertySatisfy (Property t d s i c) f = Property t d (f s) i c
+adjustPropertySatisfy (Property t d s i c) f = Property t d (f <$> s) i c
-- | A property that can be reverted. The first Property is run
-- normally and the second is run when it's reverted.
+--
+-- See `Propellor.Property.Versioned.Versioned`
+-- for a way to use RevertableProperty to define different
+-- versions of a host.
data RevertableProperty setupmetatypes undometatypes = RevertableProperty
{ setupRevertableProperty :: Property setupmetatypes
, undoRevertableProperty :: Property undometatypes
@@ -145,7 +161,7 @@ type instance CombinedType (RevertableProperty (MetaTypes x) (MetaTypes x')) (Re
type instance CombinedType (RevertableProperty (MetaTypes x) (MetaTypes x')) (Property (MetaTypes y)) = Property (MetaTypes (Combine x y))
type instance CombinedType (Property (MetaTypes x)) (RevertableProperty (MetaTypes y) (MetaTypes y')) = Property (MetaTypes (Combine x y))
-type ResultCombiner = Propellor Result -> Propellor Result -> Propellor Result
+type ResultCombiner = Maybe (Propellor Result) -> Maybe (Propellor Result) -> Maybe (Propellor Result)
class Combines x y where
-- | Combines together two properties, yielding a property that
@@ -195,3 +211,35 @@ class TightenTargets p where
instance TightenTargets Property where
tightenTargets (Property _ d a i c) = Property sing d a i c
+
+-- | Any type of Property is a monoid. When properties x and y are
+-- appended together, the resulting property has a description like
+-- "x and y". Note that when x fails to be ensured, it will not
+-- try to ensure y.
+instance SingI metatypes => Monoid (Property (MetaTypes metatypes))
+ where
+ mempty = Property sing "noop property" Nothing mempty mempty
+ mappend (Property _ d1 a1 i1 c1) (Property _ d2 a2 i2 c2) =
+ Property sing d (a1 <> a2) (i1 <> i2) (c1 <> c2)
+ where
+ -- Avoid including "noop property" in description
+ -- when using eg mconcat.
+ d = case (a1, a2) of
+ (Just _, Just _) -> d1 <> " and " <> d2
+ (Just _, Nothing) -> d1
+ (Nothing, Just _) -> d2
+ (Nothing, Nothing) -> d1
+
+-- | Any type of RevertableProperty is a monoid. When revertable
+-- properties x and y are appended together, the resulting revertable
+-- property has a description like "x and y".
+-- Note that when x fails to be ensured, it will not try to ensure y.
+instance
+ ( Monoid (Property setupmetatypes)
+ , Monoid (Property undometatypes)
+ )
+ => Monoid (RevertableProperty setupmetatypes undometatypes)
+ where
+ mempty = RevertableProperty mempty mempty
+ mappend (RevertableProperty s1 u1) (RevertableProperty s2 u2) =
+ RevertableProperty (s1 <> s2) (u2 <> u1)
diff --git a/src/Propellor/Types/Bootloader.hs b/src/Propellor/Types/Bootloader.hs
new file mode 100644
index 00000000..4a75503a
--- /dev/null
+++ b/src/Propellor/Types/Bootloader.hs
@@ -0,0 +1,12 @@
+{-# LANGUAGE FlexibleInstances, DeriveDataTypeable #-}
+
+module Propellor.Types.Bootloader where
+
+import Propellor.Types.Info
+
+-- | Boot loader installed on a host.
+data BootloaderInstalled = GrubInstalled
+ deriving (Typeable, Show)
+
+instance IsInfo [BootloaderInstalled] where
+ propagateInfo _ = PropagateInfo False
diff --git a/src/Propellor/Types/Chroot.hs b/src/Propellor/Types/Chroot.hs
index fc049603..da912120 100644
--- a/src/Propellor/Types/Chroot.hs
+++ b/src/Propellor/Types/Chroot.hs
@@ -16,7 +16,7 @@ data ChrootInfo = ChrootInfo
deriving (Show, Typeable)
instance IsInfo ChrootInfo where
- propagateInfo _ = False
+ propagateInfo _ = PropagateInfo False
instance Monoid ChrootInfo where
mempty = ChrootInfo mempty mempty
diff --git a/src/Propellor/Types/CmdLine.hs b/src/Propellor/Types/CmdLine.hs
index 558c6e8b..d712a456 100644
--- a/src/Propellor/Types/CmdLine.hs
+++ b/src/Propellor/Types/CmdLine.hs
@@ -28,4 +28,5 @@ data CmdLine
| ChrootChain HostName FilePath Bool Bool
| GitPush Fd Fd
| Check
+ | Build
deriving (Read, Show, Eq)
diff --git a/src/Propellor/Types/ConfigurableValue.hs b/src/Propellor/Types/ConfigurableValue.hs
new file mode 100644
index 00000000..1414be5f
--- /dev/null
+++ b/src/Propellor/Types/ConfigurableValue.hs
@@ -0,0 +1,44 @@
+{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
+
+module Propellor.Types.ConfigurableValue where
+
+import Data.Word
+
+-- | A value that can be used in a configuration file, or otherwise used to
+-- configure a program.
+--
+-- Unlike Show, there should only be instances of this type class for
+-- values that have a standard serialization that is understood outside of
+-- Haskell code.
+--
+-- When converting a type alias such as "type Foo = String" or "type Foo = Int"
+-- to a newtype, it's unsafe to derive a Show instance, because there may
+-- be code that shows the type to configure a value. Instead, define a
+-- ConfigurableValue instance.
+class ConfigurableValue t where
+ val :: t -> String
+
+-- | val String does not do any quoting, unlike show String
+instance ConfigurableValue String where
+ val = id
+
+instance ConfigurableValue Int where
+ val = show
+
+instance ConfigurableValue Integer where
+ val = show
+
+instance ConfigurableValue Float where
+ val = show
+
+instance ConfigurableValue Double where
+ val = show
+
+instance ConfigurableValue Word8 where
+ val = show
+
+instance ConfigurableValue Word16 where
+ val = show
+
+instance ConfigurableValue Word32 where
+ val = show
diff --git a/src/Propellor/Types/Core.hs b/src/Propellor/Types/Core.hs
index 6fedc47e..a805f561 100644
--- a/src/Propellor/Types/Core.hs
+++ b/src/Propellor/Types/Core.hs
@@ -48,9 +48,10 @@ instance LiftPropellor Propellor where
instance LiftPropellor IO where
liftPropellor = liftIO
+-- | When two actions are appended together, the second action
+-- is only run if the first action does not fail.
instance Monoid (Propellor Result) where
mempty = return NoChange
- -- | The second action is only run if the first action does not fail.
mappend x y = do
rx <- x
case rx of
@@ -71,7 +72,7 @@ data Props metatypes = Props [ChildProperty]
-- | Since there are many different types of Properties, they cannot be put
-- into a list. The simplified ChildProperty can be put into a list.
-data ChildProperty = ChildProperty Desc (Propellor Result) Info [ChildProperty]
+data ChildProperty = ChildProperty Desc (Maybe (Propellor Result)) Info [ChildProperty]
instance Show ChildProperty where
show p = "property " ++ show (getDesc p)
@@ -92,7 +93,7 @@ class IsProp p where
-- | Gets the action that can be run to satisfy a Property.
-- You should never run this action directly. Use
-- 'Propellor.EnsureProperty.ensureProperty` instead.
- getSatisfy :: p -> Propellor Result
+ getSatisfy :: p -> Maybe (Propellor Result)
instance IsProp ChildProperty where
setDesc (ChildProperty _ a i c) d = ChildProperty d a i c
diff --git a/src/Propellor/Types/Dns.hs b/src/Propellor/Types/Dns.hs
index 8f15d156..87756d81 100644
--- a/src/Propellor/Types/Dns.hs
+++ b/src/Propellor/Types/Dns.hs
@@ -5,12 +5,13 @@ module Propellor.Types.Dns where
import Propellor.Types.OS (HostName)
import Propellor.Types.Empty
import Propellor.Types.Info
+import Propellor.Types.ConfigurableValue
+import Utility.Split
import Data.Word
import qualified Data.Map as M
import qualified Data.Set as S
import Data.List
-import Data.String.Utils (split, replace)
import Data.Monoid
import Prelude
@@ -19,15 +20,15 @@ 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
+instance ConfigurableValue IPAddr where
+ val (IPv4 addr) = addr
+ val (IPv6 addr) = addr
newtype AliasesInfo = AliasesInfo (S.Set HostName)
deriving (Show, Eq, Ord, Monoid, Typeable)
instance IsInfo AliasesInfo where
- propagateInfo _ = False
+ propagateInfo _ = PropagateInfo False
toAliasesInfo :: [HostName] -> AliasesInfo
toAliasesInfo l = AliasesInfo (S.fromList l)
@@ -44,7 +45,7 @@ toDnsInfo = DnsInfo
-- | DNS Info is propagated, so that eg, aliases of a container
-- are reflected in the dns for the host where it runs.
instance IsInfo DnsInfo where
- propagateInfo _ = True
+ propagateInfo _ = PropagateInfo True
-- | Represents a bind 9 named.conf file.
data NamedConf = NamedConf
@@ -101,14 +102,14 @@ data Record
type ReverseIP = String
reverseIP :: IPAddr -> ReverseIP
-reverseIP (IPv4 addr) = intercalate "." (reverse $ split "." addr) ++ ".in-addr.arpa"
-reverseIP addr@(IPv6 _) = reverse (intersperse '.' $ replace ":" "" $ fromIPAddr $ canonicalIP addr) ++ ".ip6.arpa"
+reverseIP (IPv4 addr) = intercalate "." (reverse $ splitc '.' addr) ++ ".in-addr.arpa"
+reverseIP addr@(IPv6 _) = reverse (intersperse '.' $ replace ":" "" $ val $ canonicalIP addr) ++ ".ip6.arpa"
-- | Converts an IP address (particularly IPv6) to canonical, fully
-- expanded form.
canonicalIP :: IPAddr -> IPAddr
canonicalIP (IPv4 addr) = IPv4 addr
-canonicalIP (IPv6 addr) = IPv6 $ intercalate ":" $ map canonicalGroup $ split ":" $ replaceImplicitGroups addr
+canonicalIP (IPv6 addr) = IPv6 $ intercalate ":" $ map canonicalGroup $ splitc ':' $ replaceImplicitGroups addr
where
canonicalGroup g
| l <= 4 = replicate (4 - l) '0' ++ g
@@ -116,7 +117,7 @@ canonicalIP (IPv6 addr) = IPv6 $ intercalate ":" $ map canonicalGroup $ split ":
where
l = length g
emptyGroups n = iterate (++ ":") "" !! n
- numberOfImplicitGroups a = 8 - length (split ":" $ replace "::" "" a)
+ numberOfImplicitGroups a = 8 - length (splitc ':' $ replace "::" "" a)
replaceImplicitGroups a = concat $ aux $ split "::" a
where
aux [] = []
@@ -156,7 +157,7 @@ newtype NamedConfMap = NamedConfMap (M.Map Domain NamedConf)
deriving (Eq, Ord, Show, Typeable)
instance IsInfo NamedConfMap where
- propagateInfo _ = False
+ propagateInfo _ = PropagateInfo False
-- | Adding a Master NamedConf stanza for a particulr domain always
-- overrides an existing Secondary stanza for that domain, while a
diff --git a/src/Propellor/Types/Docker.hs b/src/Propellor/Types/Docker.hs
index f3cc4a52..6ff340e5 100644
--- a/src/Propellor/Types/Docker.hs
+++ b/src/Propellor/Types/Docker.hs
@@ -16,7 +16,7 @@ data DockerInfo = DockerInfo
deriving (Show, Typeable)
instance IsInfo DockerInfo where
- propagateInfo _ = False
+ propagateInfo _ = PropagateInfo False
instance Monoid DockerInfo where
mempty = DockerInfo mempty mempty
diff --git a/src/Propellor/Types/Info.hs b/src/Propellor/Types/Info.hs
index 2e188ae5..6716c403 100644
--- a/src/Propellor/Types/Info.hs
+++ b/src/Propellor/Types/Info.hs
@@ -1,13 +1,14 @@
{-# LANGUAGE GADTs, DeriveDataTypeable, GeneralizedNewtypeDeriving #-}
module Propellor.Types.Info (
- Info,
+ Info(..),
+ InfoEntry(..),
IsInfo(..),
+ PropagateInfo(..),
addInfo,
toInfo,
fromInfo,
mapInfo,
- propagatableInfo,
InfoVal(..),
fromInfoVal,
Typeable,
@@ -16,6 +17,7 @@ module Propellor.Types.Info (
import Data.Dynamic
import Data.Maybe
import Data.Monoid
+import qualified Data.Typeable as T
import Prelude
-- | Information about a Host, which can be provided by its properties.
@@ -34,7 +36,7 @@ instance Show InfoEntry where
-- Extracts the value from an InfoEntry but only when
-- it's of the requested type.
extractInfoEntry :: Typeable v => InfoEntry -> Maybe v
-extractInfoEntry (InfoEntry v) = cast v
+extractInfoEntry (InfoEntry v) = T.cast v
-- | Values stored in Info must be members of this class.
--
@@ -44,7 +46,13 @@ extractInfoEntry (InfoEntry v) = cast v
class (Typeable v, Monoid v, Show v) => IsInfo v where
-- | Should info of this type be propagated out of a
-- container to its Host?
- propagateInfo :: v -> Bool
+ propagateInfo :: v -> PropagateInfo
+
+data PropagateInfo
+ = PropagateInfo Bool
+ | PropagatePrivData
+ -- ^ Info about PrivData generally will be propigated even in cases
+ -- where other Info is not, so it treated specially.
-- | Any value in the `IsInfo` type class can be added to an Info.
addInfo :: IsInfo v => Info -> v -> Info
@@ -68,11 +76,6 @@ mapInfo f (Info l) = Info (map go l)
Nothing -> i
Just v -> InfoEntry (f v)
--- | Filters out parts of the Info that should not propagate out of a
--- container.
-propagatableInfo :: Info -> Info
-propagatableInfo (Info l) = Info (filter (\(InfoEntry a) -> propagateInfo a) l)
-
-- | Use this to put a value in Info that is not a monoid.
-- The last value set will be used. This info does not propagate
-- out of a container.
@@ -85,7 +88,7 @@ instance Monoid (InfoVal v) where
mappend v NoInfoVal = v
instance (Typeable v, Show v) => IsInfo (InfoVal v) where
- propagateInfo _ = False
+ propagateInfo _ = PropagateInfo False
fromInfoVal :: InfoVal v -> Maybe v
fromInfoVal NoInfoVal = Nothing
diff --git a/src/Propellor/Types/MetaTypes.hs b/src/Propellor/Types/MetaTypes.hs
index e064d76f..19d1998e 100644
--- a/src/Propellor/Types/MetaTypes.hs
+++ b/src/Propellor/Types/MetaTypes.hs
@@ -7,6 +7,7 @@ module Propellor.Types.MetaTypes (
DebianLike,
Debian,
Buntish,
+ ArchLinux,
FreeBSD,
HasInfo,
MetaTypes,
@@ -35,14 +36,26 @@ data MetaType
deriving (Show, Eq, Ord)
-- | Any unix-like system
-type UnixLike = MetaTypes '[ 'Targeting 'OSDebian, 'Targeting 'OSBuntish, 'Targeting 'OSFreeBSD ]
+type UnixLike = MetaTypes
+ '[ 'Targeting 'OSDebian
+ , 'Targeting 'OSBuntish
+ , 'Targeting 'OSArchLinux
+ , 'Targeting 'OSFreeBSD
+ ]
+
-- | Any linux system
-type Linux = MetaTypes '[ 'Targeting 'OSDebian, 'Targeting 'OSBuntish ]
+type Linux = MetaTypes
+ '[ 'Targeting 'OSDebian
+ , 'Targeting 'OSBuntish
+ , 'Targeting 'OSArchLinux
+ ]
+
-- | Debian and derivatives.
type DebianLike = MetaTypes '[ 'Targeting 'OSDebian, 'Targeting 'OSBuntish ]
type Debian = MetaTypes '[ 'Targeting 'OSDebian ]
type Buntish = MetaTypes '[ 'Targeting 'OSBuntish ]
type FreeBSD = MetaTypes '[ 'Targeting 'OSFreeBSD ]
+type ArchLinux = MetaTypes '[ 'Targeting 'OSArchLinux ]
-- | Used to indicate that a Property adds Info to the Host where it's used.
type HasInfo = MetaTypes '[ 'WithInfo ]
@@ -58,16 +71,19 @@ data instance Sing (x :: MetaType) where
OSDebianS :: Sing ('Targeting 'OSDebian)
OSBuntishS :: Sing ('Targeting 'OSBuntish)
OSFreeBSDS :: Sing ('Targeting 'OSFreeBSD)
+ OSArchLinuxS :: Sing ('Targeting 'OSArchLinux)
WithInfoS :: Sing 'WithInfo
instance SingI ('Targeting 'OSDebian) where sing = OSDebianS
instance SingI ('Targeting 'OSBuntish) where sing = OSBuntishS
instance SingI ('Targeting 'OSFreeBSD) where sing = OSFreeBSDS
+instance SingI ('Targeting 'OSArchLinux) where sing = OSArchLinuxS
instance SingI 'WithInfo where sing = WithInfoS
instance SingKind ('KProxy :: KProxy MetaType) where
type DemoteRep ('KProxy :: KProxy MetaType) = MetaType
fromSing OSDebianS = Targeting OSDebian
fromSing OSBuntishS = Targeting OSBuntish
fromSing OSFreeBSDS = Targeting OSFreeBSD
+ fromSing OSArchLinuxS = Targeting OSArchLinux
fromSing WithInfoS = WithInfo
-- | Convenience type operator to combine two `MetaTypes` lists.
@@ -186,6 +202,14 @@ type instance EqT 'OSBuntish 'OSDebian = 'False
type instance EqT 'OSBuntish 'OSFreeBSD = 'False
type instance EqT 'OSFreeBSD 'OSDebian = 'False
type instance EqT 'OSFreeBSD 'OSBuntish = 'False
+type instance EqT 'OSArchLinux 'OSArchLinux = 'True
+type instance EqT 'OSArchLinux 'OSDebian = 'False
+type instance EqT 'OSArchLinux 'OSBuntish = 'False
+type instance EqT 'OSArchLinux 'OSFreeBSD = 'False
+type instance EqT 'OSDebian 'OSArchLinux = 'False
+type instance EqT 'OSBuntish 'OSArchLinux = 'False
+type instance EqT 'OSFreeBSD 'OSArchLinux = 'False
+
-- More modern version if the combinatiorial explosion gets too bad later:
--
-- type family Eq (a :: MetaType) (b :: MetaType) where
diff --git a/src/Propellor/Types/OS.hs b/src/Propellor/Types/OS.hs
index b569a6e8..01d777a4 100644
--- a/src/Propellor/Types/OS.hs
+++ b/src/Propellor/Types/OS.hs
@@ -18,10 +18,11 @@ module Propellor.Types.OS (
Group(..),
userGroup,
Port(..),
- fromPort,
systemToTargetOS,
) where
+import Propellor.Types.ConfigurableValue
+
import Network.BSD (HostName)
import Data.Typeable
import Data.String
@@ -33,6 +34,7 @@ data System = System Distribution Architecture
data Distribution
= Debian DebianKernel DebianSuite
| Buntish Release -- ^ A well-known Debian derivative founded by a space tourist. The actual name of this distribution is not used in Propellor per <http://joeyh.name/blog/entry/trademark_nonsense/>
+ | ArchLinux
| FreeBSD FreeBSDRelease
deriving (Show, Eq)
@@ -41,12 +43,14 @@ data Distribution
data TargetOS
= OSDebian
| OSBuntish
+ | OSArchLinux
| OSFreeBSD
deriving (Show, Eq, Ord)
systemToTargetOS :: System -> TargetOS
systemToTargetOS (System (Debian _ _) _) = OSDebian
systemToTargetOS (System (Buntish _) _) = OSBuntish
+systemToTargetOS (System (ArchLinux) _) = OSArchLinux
systemToTargetOS (System (FreeBSD _) _) = OSFreeBSD
-- | Most of Debian ports are based on Linux. There also exist hurd-i386,
@@ -55,7 +59,7 @@ data DebianKernel = Linux | KFreeBSD | Hurd
deriving (Show, Eq)
-- | Debian has several rolling suites, and a number of stable releases,
--- such as Stable "jessie".
+-- such as Stable "stretch".
data DebianSuite = Experimental | Unstable | Testing | Stable Release
deriving (Show, Eq)
@@ -72,10 +76,13 @@ instance IsString FBSDVersion where
fromString "9.3-RELEASE" = FBSD093
fromString _ = error "Invalid FreeBSD release"
+instance ConfigurableValue FBSDVersion where
+ val FBSD101 = "10.1-RELEASE"
+ val FBSD102 = "10.2-RELEASE"
+ val FBSD093 = "9.3-RELEASE"
+
instance Show FBSDVersion where
- show FBSD101 = "10.1-RELEASE"
- show FBSD102 = "10.2-RELEASE"
- show FBSD093 = "9.3-RELEASE"
+ show = val
isStable :: DebianSuite -> Bool
isStable (Stable _) = True
@@ -135,15 +142,21 @@ type UserName = String
newtype User = User UserName
deriving (Eq, Ord, Show)
+instance ConfigurableValue User where
+ val (User n) = n
+
newtype Group = Group String
deriving (Eq, Ord, Show)
+instance ConfigurableValue Group where
+ val (Group n) = n
+
-- | Makes a Group with the same name as the User.
userGroup :: User -> Group
userGroup (User u) = Group u
newtype Port = Port Int
- deriving (Eq, Show)
+ deriving (Eq, Ord, Show)
-fromPort :: Port -> String
-fromPort (Port p) = show p
+instance ConfigurableValue Port where
+ val (Port p) = show p
diff --git a/src/Propellor/Types/PartSpec.hs b/src/Propellor/Types/PartSpec.hs
new file mode 100644
index 00000000..2b0a8787
--- /dev/null
+++ b/src/Propellor/Types/PartSpec.hs
@@ -0,0 +1,66 @@
+-- | Partition specification combinators.
+
+module Propellor.Types.PartSpec where
+
+import Propellor.Base
+import Propellor.Property.Parted.Types
+import Propellor.Property.Mount
+import Propellor.Property.Partition
+
+-- | Specifies a mount point, mount options, and a constructor for a
+-- Partition that determines its size.
+type PartSpec t = (Maybe MountPoint, MountOpts, PartSize -> Partition, t)
+
+-- | Specifies a partition with a given filesystem.
+--
+-- The partition is not mounted anywhere by default; use the combinators
+-- below to configure it.
+partition :: Monoid t => Fs -> PartSpec t
+partition fs = (Nothing, mempty, mkPartition fs, mempty)
+
+-- | Specifies a swap partition of a given size.
+swapPartition :: Monoid t => PartSize -> PartSpec t
+swapPartition sz = (Nothing, mempty, const (mkPartition LinuxSwap sz), mempty)
+
+-- | Specifies where to mount a partition.
+mountedAt :: PartSpec t -> FilePath -> PartSpec t
+mountedAt (_, o, p, t) mp = (Just mp, o, p, t)
+
+-- | Specify a fixed size for a partition.
+setSize :: PartSpec t -> PartSize -> PartSpec t
+setSize (mp, o, p, t) sz = (mp, o, const (p sz), t)
+
+-- | Specifies a mount option, such as "noexec"
+mountOpt :: ToMountOpts o => PartSpec t -> o -> PartSpec t
+mountOpt (mp, o, p, t) o' = (mp, o <> toMountOpts o', p, t)
+
+-- | Mount option to make a partition be remounted readonly when there's an
+-- error accessing it.
+errorReadonly :: MountOpts
+errorReadonly = toMountOpts "errors=remount-ro"
+
+-- | Sets the percent of the filesystem blocks reserved for the super-user.
+--
+-- The default is 5% for ext2 and ext4. Some filesystems may not support
+-- this.
+reservedSpacePercentage :: PartSpec t -> Int -> PartSpec t
+reservedSpacePercentage s percent = adjustp s $ \p ->
+ p { partMkFsOpts = ("-m"):show percent:partMkFsOpts p }
+
+-- | Sets a flag on the partition.
+setFlag :: PartSpec t -> PartFlag -> PartSpec t
+setFlag s f = adjustp s $ \p -> p { partFlags = (f, True):partFlags p }
+
+-- | Makes a MSDOS partition be Extended, rather than Primary.
+extended :: PartSpec t -> PartSpec t
+extended s = adjustp s $ \p -> p { partType = Extended }
+
+adjustp :: PartSpec t -> (Partition -> Partition) -> PartSpec t
+adjustp (mp, o, p, t) f = (mp, o, f . p, t)
+
+adjustt :: PartSpec t -> (t -> t) -> PartSpec t
+adjustt (mp, o, p, t) f = (mp, o, p, f t)
+
+-- | Default partition size when not otherwize specified is 128 MegaBytes.
+defSz :: PartSize
+defSz = MegaBytes 128
diff --git a/src/Propellor/Types/Result.hs b/src/Propellor/Types/Result.hs
index e8510abf..5209094b 100644
--- a/src/Propellor/Types/Result.hs
+++ b/src/Propellor/Types/Result.hs
@@ -24,6 +24,9 @@ instance ToResult Bool where
toResult False = FailedChange
toResult True = MadeChange
+instance ToResult Result where
+ toResult = id
+
-- | Results of actions, with color.
class ActionResult a where
getActionResult :: a -> (String, ColorIntensity, Color)
diff --git a/src/Propellor/Types/ZFS.hs b/src/Propellor/Types/ZFS.hs
index 3ce4b22c..c68f6ba5 100644
--- a/src/Propellor/Types/ZFS.hs
+++ b/src/Propellor/Types/ZFS.hs
@@ -6,9 +6,11 @@
module Propellor.Types.ZFS where
+import Propellor.Types.ConfigurableValue
+import Utility.Split
+
import Data.String
import qualified Data.Set as Set
-import qualified Data.String.Utils as SU
import Data.List
-- | A single ZFS filesystem.
@@ -32,24 +34,27 @@ toPropertyList = Set.foldr (\p l -> l ++ [toPair p]) []
fromPropertyList :: [(String, String)] -> ZFSProperties
fromPropertyList props =
- Set.fromList $ map fromPair props
+ Set.fromList $ map fromPair props
zfsName :: ZFS -> String
zfsName (ZFS (ZPool pool) dataset) = intercalate "/" [pool, show dataset]
+instance ConfigurableValue ZDataset where
+ val (ZDataset paths) = intercalate "/" paths
+
instance Show ZDataset where
- show (ZDataset paths) = intercalate "/" paths
+ show = val
instance IsString ZDataset where
- fromString s = ZDataset $ SU.split "/" s
+ fromString s = ZDataset $ splitc '/' s
instance IsString ZPool where
- fromString p = ZPool p
+ fromString p = ZPool p
class Value a where
- toValue :: a -> String
- fromValue :: (IsString a) => String -> a
- fromValue = fromString
+ toValue :: a -> String
+ fromValue :: (IsString a) => String -> a
+ fromValue = fromString
data ZFSYesNo = ZFSYesNo Bool deriving (Show, Eq, Ord)
data ZFSOnOff = ZFSOnOff Bool deriving (Show, Eq, Ord)
@@ -57,57 +62,57 @@ data ZFSSize = ZFSSize Integer deriving (Show, Eq, Ord)
data ZFSString = ZFSString String deriving (Show, Eq, Ord)
instance Value ZFSYesNo where
- toValue (ZFSYesNo True) = "yes"
- toValue (ZFSYesNo False) = "no"
+ toValue (ZFSYesNo True) = "yes"
+ toValue (ZFSYesNo False) = "no"
instance Value ZFSOnOff where
- toValue (ZFSOnOff True) = "on"
- toValue (ZFSOnOff False) = "off"
+ toValue (ZFSOnOff True) = "on"
+ toValue (ZFSOnOff False) = "off"
instance Value ZFSSize where
- toValue (ZFSSize s) = show s
+ toValue (ZFSSize s) = show s
instance Value ZFSString where
- toValue (ZFSString s) = s
+ toValue (ZFSString s) = s
instance IsString ZFSString where
- fromString = ZFSString
+ fromString = ZFSString
instance IsString ZFSYesNo where
- fromString "yes" = ZFSYesNo True
- fromString "no" = ZFSYesNo False
- fromString _ = error "Not yes or no"
+ fromString "yes" = ZFSYesNo True
+ fromString "no" = ZFSYesNo False
+ fromString _ = error "Not yes or no"
instance IsString ZFSOnOff where
- fromString "on" = ZFSOnOff True
- fromString "off" = ZFSOnOff False
- fromString _ = error "Not on or off"
+ fromString "on" = ZFSOnOff True
+ fromString "off" = ZFSOnOff False
+ fromString _ = error "Not on or off"
data ZFSACLInherit = AIDiscard | AINoAllow | AISecure | AIPassthrough deriving (Show, Eq, Ord)
instance IsString ZFSACLInherit where
- fromString "discard" = AIDiscard
- fromString "noallow" = AINoAllow
- fromString "secure" = AISecure
- fromString "passthrough" = AIPassthrough
- fromString _ = error "Not valid aclpassthrough value"
+ fromString "discard" = AIDiscard
+ fromString "noallow" = AINoAllow
+ fromString "secure" = AISecure
+ fromString "passthrough" = AIPassthrough
+ fromString _ = error "Not valid aclpassthrough value"
instance Value ZFSACLInherit where
- toValue AIDiscard = "discard"
- toValue AINoAllow = "noallow"
- toValue AISecure = "secure"
- toValue AIPassthrough = "passthrough"
+ toValue AIDiscard = "discard"
+ toValue AINoAllow = "noallow"
+ toValue AISecure = "secure"
+ toValue AIPassthrough = "passthrough"
data ZFSACLMode = AMDiscard | AMGroupmask | AMPassthrough deriving (Show, Eq, Ord)
instance IsString ZFSACLMode where
- fromString "discard" = AMDiscard
- fromString "groupmask" = AMGroupmask
- fromString "passthrough" = AMPassthrough
- fromString _ = error "Invalid zfsaclmode"
+ fromString "discard" = AMDiscard
+ fromString "groupmask" = AMGroupmask
+ fromString "passthrough" = AMPassthrough
+ fromString _ = error "Invalid zfsaclmode"
instance Value ZFSACLMode where
- toValue AMDiscard = "discard"
- toValue AMGroupmask = "groupmask"
- toValue AMPassthrough = "passthrough"
+ toValue AMDiscard = "discard"
+ toValue AMGroupmask = "groupmask"
+ toValue AMPassthrough = "passthrough"
data ZFSProperty = Mounted ZFSYesNo
| Mountpoint ZFSString
diff --git a/src/Utility/DataUnits.hs b/src/Utility/DataUnits.hs
index 6e40932e..a6c9ffcf 100644
--- a/src/Utility/DataUnits.hs
+++ b/src/Utility/DataUnits.hs
@@ -45,6 +45,7 @@ module Utility.DataUnits (
ByteSize,
roughSize,
+ roughSize',
compareSizes,
readSize
) where
@@ -109,7 +110,10 @@ oldSchoolUnits = zipWith (curry mingle) storageUnits memoryUnits
{- approximate display of a particular number of bytes -}
roughSize :: [Unit] -> Bool -> ByteSize -> String
-roughSize units short i
+roughSize units short i = roughSize' units short 2 i
+
+roughSize' :: [Unit] -> Bool -> Int -> ByteSize -> String
+roughSize' units short precision i
| i < 0 = '-' : findUnit units' (negate i)
| otherwise = findUnit units' i
where
@@ -123,7 +127,7 @@ roughSize units short i
showUnit x (Unit size abbrev name) = s ++ " " ++ unit
where
v = (fromInteger x :: Double) / fromInteger size
- s = showImprecise 2 v
+ s = showImprecise precision v
unit
| short = abbrev
| s == "1" = name
diff --git a/src/Utility/Exception.hs b/src/Utility/Exception.hs
index f6551b45..67c2e85d 100644
--- a/src/Utility/Exception.hs
+++ b/src/Utility/Exception.hs
@@ -1,6 +1,6 @@
{- Simple IO exception handling (and some more)
-
- - Copyright 2011-2015 Joey Hess <id@joeyh.name>
+ - Copyright 2011-2016 Joey Hess <id@joeyh.name>
-
- License: BSD-2-clause
-}
@@ -10,6 +10,7 @@
module Utility.Exception (
module X,
+ giveup,
catchBoolIO,
catchMaybeIO,
catchDefaultIO,
@@ -28,9 +29,11 @@ module Utility.Exception (
import Control.Monad.Catch as X hiding (Handler)
import qualified Control.Monad.Catch as M
import Control.Exception (IOException, AsyncException)
-#if MIN_VERSION_base(4,7,0)
+#ifdef MIN_VERSION_GLASGOW_HASKELL
+#if MIN_VERSION_GLASGOW_HASKELL(7,10,0,0)
import Control.Exception (SomeAsyncException)
#endif
+#endif
import Control.Monad
import Control.Monad.IO.Class (liftIO, MonadIO)
import System.IO.Error (isDoesNotExistError, ioeGetErrorType)
@@ -38,6 +41,21 @@ import GHC.IO.Exception (IOErrorType(..))
import Utility.Data
+{- Like error, this throws an exception. Unlike error, if this exception
+ - is not caught, it won't generate a backtrace. So use this for situations
+ - where there's a problem that the user is excpected to see in some
+ - circumstances. -}
+giveup :: [Char] -> a
+#ifdef MIN_VERSION_base
+#if MIN_VERSION_base(4,9,0)
+giveup = errorWithoutStackTrace
+#else
+giveup = error
+#endif
+#else
+giveup = error
+#endif
+
{- Catches IO errors and returns a Bool -}
catchBoolIO :: MonadCatch m => m Bool -> m Bool
catchBoolIO = catchDefaultIO False
@@ -77,9 +95,11 @@ bracketIO setup cleanup = bracket (liftIO setup) (liftIO . cleanup)
catchNonAsync :: MonadCatch m => m a -> (SomeException -> m a) -> m a
catchNonAsync a onerr = a `catches`
[ M.Handler (\ (e :: AsyncException) -> throwM e)
-#if MIN_VERSION_base(4,7,0)
+#ifdef MIN_VERSION_GLASGOW_HASKELL
+#if MIN_VERSION_GLASGOW_HASKELL(7,10,0,0)
, M.Handler (\ (e :: SomeAsyncException) -> throwM e)
#endif
+#endif
, M.Handler (\ (e :: SomeException) -> onerr e)
]
diff --git a/src/Utility/FileMode.hs b/src/Utility/FileMode.hs
index bb3780c6..d9a26944 100644
--- a/src/Utility/FileMode.hs
+++ b/src/Utility/FileMode.hs
@@ -1,6 +1,6 @@
{- File mode utilities.
-
- - Copyright 2010-2012 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2017 Joey Hess <id@joeyh.name>
-
- License: BSD-2-clause
-}
@@ -130,6 +130,21 @@ withUmask umask a = bracket setup cleanup go
withUmask _ a = a
#endif
+getUmask :: IO FileMode
+#ifndef mingw32_HOST_OS
+getUmask = bracket setup cleanup return
+ where
+ setup = setFileCreationMask nullFileMode
+ cleanup = setFileCreationMask
+#else
+getUmask = return nullFileMode
+#endif
+
+defaultFileMode :: IO FileMode
+defaultFileMode = do
+ umask <- getUmask
+ return $ intersectFileModes (complement umask) stdFileMode
+
combineModes :: [FileMode] -> FileMode
combineModes [] = 0
combineModes [m] = m
@@ -162,7 +177,10 @@ writeFileProtected file content = writeFileProtected' file
(\h -> hPutStr h content)
writeFileProtected' :: FilePath -> (Handle -> IO ()) -> IO ()
-writeFileProtected' file writer = withUmask 0o0077 $
+writeFileProtected' file writer = protectedOutput $
withFile file WriteMode $ \h -> do
void $ tryIO $ modifyFileMode file $ removeModes otherGroupModes
writer h
+
+protectedOutput :: IO a -> IO a
+protectedOutput = withUmask 0o0077
diff --git a/src/Utility/FileSystemEncoding.hs b/src/Utility/FileSystemEncoding.hs
index eab98337..444dc4a9 100644
--- a/src/Utility/FileSystemEncoding.hs
+++ b/src/Utility/FileSystemEncoding.hs
@@ -1,6 +1,6 @@
{- GHC File system encoding handling.
-
- - Copyright 2012-2014 Joey Hess <id@joeyh.name>
+ - Copyright 2012-2016 Joey Hess <id@joeyh.name>
-
- License: BSD-2-clause
-}
@@ -9,9 +9,9 @@
{-# OPTIONS_GHC -fno-warn-tabs #-}
module Utility.FileSystemEncoding (
+ useFileSystemEncoding,
fileEncoding,
withFilePath,
- md5FilePath,
decodeBS,
encodeBS,
decodeW8,
@@ -19,7 +19,10 @@ module Utility.FileSystemEncoding (
encodeW8NUL,
decodeW8NUL,
truncateFilePath,
- setConsoleEncoding,
+ s2w8,
+ w82s,
+ c2w8,
+ w82c,
) where
import qualified GHC.Foreign as GHC
@@ -27,29 +30,45 @@ 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 Data.List
-import Data.List.Utils
import qualified Data.ByteString.Lazy as L
#ifdef mingw32_HOST_OS
import qualified Data.ByteString.Lazy.UTF8 as L8
#endif
import Utility.Exception
+import Utility.Split
-{- 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".
+{- Makes all subsequent Handles that are opened, as well as stdio Handles,
+ - use the filesystem encoding, instead of the encoding of the current
+ - locale.
+ -
+ - The filesystem encoding allows "arbitrary undecodable bytes to be
+ - round-tripped through it". This avoids encoded failures when data is not
+ - encoded matching the current locale.
+ -
+ - Note that code can still use hSetEncoding to change the encoding of a
+ - Handle. This only affects the default encoding.
-}
+useFileSystemEncoding :: IO ()
+useFileSystemEncoding = do
+#ifndef mingw32_HOST_OS
+ e <- Encoding.getFileSystemEncoding
+#else
+ {- The file system encoding does not work well on Windows,
+ - and Windows only has utf FilePaths anyway. -}
+ let e = Encoding.utf8
+#endif
+ hSetEncoding stdin e
+ hSetEncoding stdout e
+ hSetEncoding stderr e
+ Encoding.setLocaleEncoding e
+
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
@@ -83,10 +102,6 @@ _encodeFilePath fp = unsafePerformIO $ do
GHC.withCString enc fp (GHC.peekCString Encoding.char8)
`catchNonAsync` (\_ -> return fp)
-{- 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
@@ -127,14 +142,26 @@ decodeW8 = s2w8 . _encodeFilePath
{- Like encodeW8 and decodeW8, but NULs are passed through unchanged. -}
encodeW8NUL :: [Word8] -> FilePath
-encodeW8NUL = intercalate nul . map encodeW8 . split (s2w8 nul)
+encodeW8NUL = intercalate [nul] . map encodeW8 . splitc (c2w8 nul)
where
- nul = ['\NUL']
+ nul = '\NUL'
decodeW8NUL :: FilePath -> [Word8]
-decodeW8NUL = intercalate (s2w8 nul) . map decodeW8 . split nul
+decodeW8NUL = intercalate [c2w8 nul] . map decodeW8 . splitc nul
where
- nul = ['\NUL']
+ nul = '\NUL'
+
+c2w8 :: Char -> Word8
+c2w8 = fromIntegral . fromEnum
+
+w82c :: Word8 -> Char
+w82c = toEnum . fromIntegral
+
+s2w8 :: String -> [Word8]
+s2w8 = map c2w8
+
+w82s :: [Word8] -> String
+w82s = map w82c
{- Truncates a FilePath to the given number of bytes (or less),
- as represented on disk.
@@ -165,10 +192,3 @@ truncateFilePath n = reverse . go [] n . L8.fromString
else go (c:coll) (cnt - x') (L8.drop 1 bs)
_ -> coll
#endif
-
-{- This avoids ghc's output layer crashing on invalid encoded characters in
- - filenames when printing them out. -}
-setConsoleEncoding :: IO ()
-setConsoleEncoding = do
- fileEncoding stdout
- fileEncoding stderr
diff --git a/src/Utility/LinuxMkLibs.hs b/src/Utility/LinuxMkLibs.hs
index 122f3964..15f82fd1 100644
--- a/src/Utility/LinuxMkLibs.hs
+++ b/src/Utility/LinuxMkLibs.hs
@@ -12,10 +12,10 @@ import Utility.Directory
import Utility.Process
import Utility.Monad
import Utility.Path
+import Utility.Split
import Data.Maybe
import System.FilePath
-import Data.List.Utils
import System.Posix.Files
import Data.Char
import Control.Monad.IfElse
diff --git a/src/Utility/Misc.hs b/src/Utility/Misc.hs
index ebb42576..4498c0a0 100644
--- a/src/Utility/Misc.hs
+++ b/src/Utility/Misc.hs
@@ -10,9 +10,6 @@
module Utility.Misc where
-import Utility.FileSystemEncoding
-import Utility.Monad
-
import System.IO
import Control.Monad
import Foreign
@@ -35,20 +32,6 @@ hGetContentsStrict = hGetContents >=> \s -> length s `seq` return s
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.
-
diff --git a/src/Utility/PartialPrelude.hs b/src/Utility/PartialPrelude.hs
index 55795563..47e98318 100644
--- a/src/Utility/PartialPrelude.hs
+++ b/src/Utility/PartialPrelude.hs
@@ -2,7 +2,7 @@
- bugs.
-
- This exports functions that conflict with the prelude, which avoids
- - them being accidentially used.
+ - them being accidentally used.
-}
{-# OPTIONS_GHC -fno-warn-tabs #-}
diff --git a/src/Utility/Path.hs b/src/Utility/Path.hs
index 3ee5ff39..0779d167 100644
--- a/src/Utility/Path.hs
+++ b/src/Utility/Path.hs
@@ -10,7 +10,6 @@
module Utility.Path where
-import Data.String.Utils
import System.FilePath
import Data.List
import Data.Maybe
@@ -25,10 +24,10 @@ import System.Posix.Files
import Utility.Exception
#endif
-import qualified "MissingH" System.Path as MissingH
import Utility.Monad
import Utility.UserInfo
import Utility.Directory
+import Utility.Split
{- Simplifies a path, removing any "." component, collapsing "dir/..",
- and removing the trailing path separator.
@@ -68,18 +67,6 @@ simplifyPath path = dropTrailingPathSeparator $
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. -}
-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
-
{- takeDirectory "foo/bar/" is "foo/bar". This instead yields "foo" -}
parentDir :: FilePath -> FilePath
parentDir = takeDirectory . dropTrailingPathSeparator
@@ -89,12 +76,13 @@ parentDir = takeDirectory . dropTrailingPathSeparator
upFrom :: FilePath -> Maybe FilePath
upFrom dir
| length dirs < 2 = Nothing
- | otherwise = Just $ joinDrive drive (intercalate s $ init dirs)
+ | otherwise = Just $ joinDrive drive $ intercalate s $ init dirs
where
- -- on Unix, the drive will be "/" when the dir is absolute, otherwise ""
+ -- 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]
+ dirs = filter (not . null) $ split s path
prop_upFrom_basics :: FilePath -> Bool
prop_upFrom_basics dir
@@ -149,11 +137,11 @@ relPathDirToFile from to = relPathDirToFileAbs <$> absPath from <*> absPath to
relPathDirToFileAbs :: FilePath -> FilePath -> FilePath
relPathDirToFileAbs from to
| takeDrive from /= takeDrive to = to
- | otherwise = intercalate s $ dotdots ++ uncommon
+ | otherwise = joinPath $ dotdots ++ uncommon
where
- s = [pathSeparator]
- pfrom = split s from
- pto = split s to
+ pfrom = sp from
+ pto = sp to
+ sp = map dropTrailingPathSeparator . splitPath
common = map fst $ takeWhile same $ zip pfrom pto
same (c,d) = c == d
uncommon = drop numcommon pto
@@ -227,6 +215,8 @@ inPath command = isJust <$> searchPath command
-
- The command may be fully qualified already, in which case it will
- be returned if it exists.
+ -
+ - Note that this will find commands in PATH that are not executable.
-}
searchPath :: String -> IO (Maybe FilePath)
searchPath command
diff --git a/src/Utility/Process.hs b/src/Utility/Process.hs
index ed02f49e..6d981cb5 100644
--- a/src/Utility/Process.hs
+++ b/src/Utility/Process.hs
@@ -174,22 +174,21 @@ createBackgroundProcess p a = a =<< createProcess p
-- returns a transcript combining its stdout and stderr, and
-- whether it succeeded or failed.
processTranscript :: String -> [String] -> (Maybe String) -> IO (String, Bool)
-processTranscript = processTranscript' id
+processTranscript cmd opts = processTranscript' (proc cmd opts)
-processTranscript' :: (CreateProcess -> CreateProcess) -> String -> [String] -> Maybe String -> IO (String, Bool)
-processTranscript' modproc cmd opts input = do
+processTranscript' :: CreateProcess -> Maybe String -> IO (String, Bool)
+processTranscript' cp input = do
#ifndef mingw32_HOST_OS
{- This implementation interleves stdout and stderr in exactly the order
- the process writes them. -}
(readf, writef) <- System.Posix.IO.createPipe
readh <- System.Posix.IO.fdToHandle readf
writeh <- System.Posix.IO.fdToHandle writef
- p@(_, _, _, pid) <- createProcess $ modproc $
- (proc cmd opts)
- { std_in = if isJust input then CreatePipe else Inherit
- , std_out = UseHandle writeh
- , std_err = UseHandle writeh
- }
+ p@(_, _, _, pid) <- createProcess $ cp
+ { std_in = if isJust input then CreatePipe else Inherit
+ , std_out = UseHandle writeh
+ , std_err = UseHandle writeh
+ }
hClose writeh
get <- mkreader readh
@@ -200,12 +199,11 @@ processTranscript' modproc cmd opts input = do
return (transcript, ok)
#else
{- This implementation for Windows puts stderr after stdout. -}
- p@(_, _, _, pid) <- createProcess $ modproc $
- (proc cmd opts)
- { std_in = if isJust input then CreatePipe else Inherit
- , std_out = CreatePipe
- , std_err = CreatePipe
- }
+ p@(_, _, _, pid) <- createProcess $ cp
+ { std_in = if isJust input then CreatePipe else Inherit
+ , std_out = CreatePipe
+ , std_err = CreatePipe
+ }
getout <- mkreader (stdoutHandle p)
geterr <- mkreader (stderrHandle p)
diff --git a/src/Utility/SafeCommand.hs b/src/Utility/SafeCommand.hs
index 5ce17a84..eb34d3de 100644
--- a/src/Utility/SafeCommand.hs
+++ b/src/Utility/SafeCommand.hs
@@ -11,7 +11,7 @@ module Utility.SafeCommand where
import System.Exit
import Utility.Process
-import Data.String.Utils
+import Utility.Split
import System.FilePath
import Data.Char
import Data.List
@@ -86,7 +86,7 @@ shellEscape :: String -> String
shellEscape f = "'" ++ escaped ++ "'"
where
-- replace ' with '"'"'
- escaped = intercalate "'\"'\"'" $ split "'" f
+ escaped = intercalate "'\"'\"'" $ splitc '\'' f
-- | Unescapes a set of shellEscaped words or filenames.
shellUnEscape :: String -> [String]
diff --git a/src/Utility/Scheduled.hs b/src/Utility/Scheduled.hs
index d23aaf03..b68ff901 100644
--- a/src/Utility/Scheduled.hs
+++ b/src/Utility/Scheduled.hs
@@ -29,6 +29,7 @@ module Utility.Scheduled (
import Utility.Data
import Utility.PartialPrelude
import Utility.Misc
+import Utility.Tuple
import Data.List
import Data.Time.Clock
@@ -37,7 +38,6 @@ import Data.Time.Calendar
import Data.Time.Calendar.WeekDate
import Data.Time.Calendar.OrdinalDate
import Data.Time.Format ()
-import Data.Tuple.Utils
import Data.Char
import Control.Applicative
import Prelude
diff --git a/src/Utility/Split.hs b/src/Utility/Split.hs
new file mode 100644
index 00000000..decfe7d3
--- /dev/null
+++ b/src/Utility/Split.hs
@@ -0,0 +1,30 @@
+{- split utility functions
+ -
+ - Copyright 2017 Joey Hess <id@joeyh.name>
+ -
+ - License: BSD-2-clause
+ -}
+
+{-# OPTIONS_GHC -fno-warn-tabs #-}
+
+module Utility.Split where
+
+import Data.List (intercalate)
+import Data.List.Split (splitOn)
+
+-- | same as Data.List.Utils.split
+--
+-- intercalate x . splitOn x === id
+split :: Eq a => [a] -> [a] -> [[a]]
+split = splitOn
+
+-- | Split on a single character. This is over twice as fast as using
+-- split on a list of length 1, while producing identical results. -}
+splitc :: Eq c => c -> [c] -> [[c]]
+splitc c s = case break (== c) s of
+ (i, _c:rest) -> i : splitc c rest
+ (i, []) -> i : []
+
+-- | same as Data.List.Utils.replace
+replace :: Eq a => [a] -> [a] -> [a] -> [a]
+replace old new = intercalate new . split old
diff --git a/src/Utility/SystemDirectory.hs b/src/Utility/SystemDirectory.hs
index 3dd44d19..b9040fe1 100644
--- a/src/Utility/SystemDirectory.hs
+++ b/src/Utility/SystemDirectory.hs
@@ -13,4 +13,4 @@ module Utility.SystemDirectory (
module System.Directory
) where
-import System.Directory hiding (isSymbolicLink)
+import System.Directory hiding (isSymbolicLink, getFileSize)
diff --git a/src/Utility/Tuple.hs b/src/Utility/Tuple.hs
new file mode 100644
index 00000000..25c6e8f3
--- /dev/null
+++ b/src/Utility/Tuple.hs
@@ -0,0 +1,17 @@
+{- tuple utility functions
+ -
+ - Copyright 2017 Joey Hess <id@joeyh.name>
+ -
+ - License: BSD-2-clause
+ -}
+
+module Utility.Tuple where
+
+fst3 :: (a,b,c) -> a
+fst3 (a,_,_) = a
+
+snd3 :: (a,b,c) -> b
+snd3 (_,b,_) = b
+
+thd3 :: (a,b,c) -> c
+thd3 (_,_,c) = c
diff --git a/src/Utility/UserInfo.hs b/src/Utility/UserInfo.hs
index c6010116..dd66c331 100644
--- a/src/Utility/UserInfo.hs
+++ b/src/Utility/UserInfo.hs
@@ -15,6 +15,8 @@ module Utility.UserInfo (
) where
import Utility.Env
+import Utility.Data
+import Utility.Exception
import System.PosixCompat
import Control.Applicative
@@ -24,7 +26,7 @@ import Prelude
-
- getpwent will fail on LDAP or NIS, so use HOME if set. -}
myHomeDir :: IO FilePath
-myHomeDir = myVal env homeDirectory
+myHomeDir = either giveup return =<< myVal env homeDirectory
where
#ifndef mingw32_HOST_OS
env = ["HOME"]
@@ -33,7 +35,7 @@ myHomeDir = myVal env homeDirectory
#endif
{- Current user's user name. -}
-myUserName :: IO String
+myUserName :: IO (Either String String)
myUserName = myVal env userName
where
#ifndef mingw32_HOST_OS
@@ -47,15 +49,15 @@ myUserGecos :: IO (Maybe String)
#if defined(__ANDROID__) || defined(mingw32_HOST_OS)
myUserGecos = return Nothing
#else
-myUserGecos = Just <$> myVal [] userGecos
+myUserGecos = eitherToMaybe <$> myVal [] userGecos
#endif
-myVal :: [String] -> (UserEntry -> String) -> IO String
+myVal :: [String] -> (UserEntry -> String) -> IO (Either String String)
myVal envvars extract = go envvars
where
#ifndef mingw32_HOST_OS
- go [] = extract <$> (getUserEntryForID =<< getEffectiveUserID)
+ go [] = Right . extract <$> (getUserEntryForID =<< getEffectiveUserID)
#else
- go [] = extract <$> error ("environment not set: " ++ show envvars)
+ go [] = return $ Left ("environment not set: " ++ show envvars)
#endif
- go (v:vs) = maybe (go vs) return =<< getEnv v
+ go (v:vs) = maybe (go vs) (return . Right) =<< getEnv v
diff --git a/src/config.hs b/src/propellor-config.hs
index e3af968e..e3af968e 120000
--- a/src/config.hs
+++ b/src/propellor-config.hs
diff --git a/src/wrapper.hs b/src/wrapper.hs
index 06051500..20b4d8c6 100644
--- a/src/wrapper.hs
+++ b/src/wrapper.hs
@@ -20,6 +20,7 @@ import Utility.Directory
import Utility.FileMode
import Utility.Process
import Utility.Process.NonConcurrent
+import Utility.FileSystemEncoding
import System.Environment (getArgs)
import System.Exit
@@ -30,7 +31,9 @@ import Control.Applicative
import Prelude
main :: IO ()
-main = withConcurrentOutput $ go =<< getArgs
+main = withConcurrentOutput $ do
+ useFileSystemEncoding
+ go =<< getArgs
where
go ["--init"] = interactiveInit
go args = ifM configInCurrentWorkingDirectory
diff --git a/stack.yaml b/stack.yaml
index 2689c624..f7377cc7 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -1,4 +1,4 @@
# When updating the resolver here, also update stackResolver in Propellor.DotDir
-resolver: lts-5.10
+resolver: lts-8.22
packages:
- '.'