diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,16 @@
+propellor (5.3.2) unstable; urgency=medium
+
+  * Added Propellor.Property.Atomic, which can make a non-atomic property
+    that operates on a directory into an atomic property.
+    (Inspired by Vaibhav Sagar's talk on Functional Devops in a
+    Dysfunctional World at LCA 2018.)
+  * Added Git.pulled.
+  * Systemd.machined: Install systemd-container on Debian
+    stretch.
+    Thanks, Sean Whitton
+
+ -- Joey Hess <id@joeyh.name>  Sun, 18 Feb 2018 14:31:39 -0400
+
 propellor (5.3.1) unstable; urgency=medium
 
   * Last release mistakenly contained my personal branch not master.
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -18,12 +18,10 @@
 which includes many built-in Properties for dealing with
 [Apt](http://hackage.haskell.org/package/propellor/docs/Propellor-Property-Apt.html)
 and
-[Apache](http://hackage.haskell.org/package/propellor/docs/Propellor-Property-Apache.html)
-,
+[Apache](http://hackage.haskell.org/package/propellor/docs/Propellor-Property-Apache.html),
 [Cron](http://hackage.haskell.org/package/propellor/docs/Propellor-Property-Cron.html)
 and
-[Commands](http://hackage.haskell.org/package/propellor/docs/Propellor-Property-Cmd.html)
-,
+[Commands](http://hackage.haskell.org/package/propellor/docs/Propellor-Property-Cmd.html),
 [Dns](http://hackage.haskell.org/package/propellor/docs/Propellor-Property-Dns.html)
 and
 [Docker](http://hackage.haskell.org/package/propellor/docs/Propellor-Property-Docker.html), etc.
@@ -56,3 +54,6 @@
    each host becomes tiresome, you can
    [automate that](http://propellor.branchable.com/automated_spins/).
 7. Write some neat new properties and send patches!
+
+(Want to get your feet wet with propellor before plunging in?
+[[try this|forum/Simple_quickstart_without_git__44___SSH__44___GPG]])
diff --git a/debian/changelog b/debian/changelog
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,16 @@
+propellor (5.3.2) unstable; urgency=medium
+
+  * Added Propellor.Property.Atomic, which can make a non-atomic property
+    that operates on a directory into an atomic property.
+    (Inspired by Vaibhav Sagar's talk on Functional Devops in a
+    Dysfunctional World at LCA 2018.)
+  * Added Git.pulled.
+  * Systemd.machined: Install systemd-container on Debian
+    stretch.
+    Thanks, Sean Whitton
+
+ -- Joey Hess <id@joeyh.name>  Sun, 18 Feb 2018 14:31:39 -0400
+
 propellor (5.3.1) unstable; urgency=medium
 
   * Last release mistakenly contained my personal branch not master.
diff --git a/doc/README.mdwn b/doc/README.mdwn
--- a/doc/README.mdwn
+++ b/doc/README.mdwn
@@ -18,12 +18,10 @@
 which includes many built-in Properties for dealing with
 [Apt](http://hackage.haskell.org/package/propellor/docs/Propellor-Property-Apt.html)
 and
-[Apache](http://hackage.haskell.org/package/propellor/docs/Propellor-Property-Apache.html)
-,
+[Apache](http://hackage.haskell.org/package/propellor/docs/Propellor-Property-Apache.html),
 [Cron](http://hackage.haskell.org/package/propellor/docs/Propellor-Property-Cron.html)
 and
-[Commands](http://hackage.haskell.org/package/propellor/docs/Propellor-Property-Cmd.html)
-,
+[Commands](http://hackage.haskell.org/package/propellor/docs/Propellor-Property-Cmd.html),
 [Dns](http://hackage.haskell.org/package/propellor/docs/Propellor-Property-Dns.html)
 and
 [Docker](http://hackage.haskell.org/package/propellor/docs/Propellor-Property-Docker.html), etc.
@@ -56,3 +54,6 @@
    each host becomes tiresome, you can
    [automate that](http://propellor.branchable.com/automated_spins/).
 7. Write some neat new properties and send patches!
+
+(Want to get your feet wet with propellor before plunging in?
+[[try this|forum/Simple_quickstart_without_git__44___SSH__44___GPG]])
diff --git a/joeyconfig.hs b/joeyconfig.hs
--- a/joeyconfig.hs
+++ b/joeyconfig.hs
@@ -27,7 +27,6 @@
 import qualified Propellor.Property.LetsEncrypt as LetsEncrypt
 import qualified Propellor.Property.Locale as Locale
 import qualified Propellor.Property.Grub as Grub
-import qualified Propellor.Property.FlashKernel as FlashKernel
 import qualified Propellor.Property.Borg as Borg
 import qualified Propellor.Property.Gpg as Gpg
 import qualified Propellor.Property.Systemd as Systemd
@@ -193,9 +192,7 @@
 			`mountedAt` "/"
 			`setSize` MegaBytes 8000
 		)
-	& File.hasPrivContentExposed "/etc/flash-kernel/dtbs/sun7i-a20-cubietruck.dtb"
-		(Context "cubietruck gpio")
-		`onChange` FlashKernel.flashKernel
+	& JoeySites.cubieTruckOneWire
 	
 	& Apt.installed ["firmware-brcm80211"]
 		-- Workaround for https://bugs.debian.org/844056
diff --git a/propellor.cabal b/propellor.cabal
--- a/propellor.cabal
+++ b/propellor.cabal
@@ -1,5 +1,5 @@
 Name: propellor
-Version: 5.3.1
+Version: 5.3.2
 Cabal-Version: >= 1.20
 License: BSD2
 Maintainer: Joey Hess <id@joeyh.name>
@@ -89,6 +89,7 @@
     Propellor.Property.Apache
     Propellor.Property.Apt
     Propellor.Property.Apt.PPA
+    Propellor.Property.Atomic
     Propellor.Property.Attic
     Propellor.Property.Bootstrap
     Propellor.Property.Borg
diff --git a/src/Propellor/EnsureProperty.hs b/src/Propellor/EnsureProperty.hs
--- a/src/Propellor/EnsureProperty.hs
+++ b/src/Propellor/EnsureProperty.hs
@@ -8,6 +8,7 @@
 	( ensureProperty
 	, property'
 	, OuterMetaTypesWitness(..)
+	, Cannot_ensureProperty_WithInfo
 	) where
 
 import Propellor.Types
diff --git a/src/Propellor/Property/Atomic.hs b/src/Propellor/Property/Atomic.hs
new file mode 100644
--- /dev/null
+++ b/src/Propellor/Property/Atomic.hs
@@ -0,0 +1,161 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE TypeFamilies #-}
+
+module Propellor.Property.Atomic (
+	atomicDirUpdate,
+	atomicDirSync,
+	atomicUpdate,
+	AtomicResourcePair(..),
+	flipAtomicResourcePair,
+	SwapAtomicResourcePair,
+	CheckAtomicResourcePair,
+) where
+
+import Propellor.Base
+import Propellor.Types.Core
+import Propellor.Types.MetaTypes
+import Propellor.EnsureProperty
+import Propellor.Property.File
+import Propellor.Property.Rsync (syncDir)
+
+import System.Posix.Files
+
+-- | A pair of resources, one active and one inactive, which can swap
+-- positions atomically.
+data AtomicResourcePair a = AtomicResourcePair
+	{ activeAtomicResource :: a
+	, inactiveAtomicResource :: a
+	}
+
+flipAtomicResourcePair :: AtomicResourcePair a -> AtomicResourcePair a
+flipAtomicResourcePair a = AtomicResourcePair
+	{ activeAtomicResource = inactiveAtomicResource a
+	, inactiveAtomicResource = activeAtomicResource a
+	}
+
+-- | Action that activates the inactiveAtomicResource, and deactivates
+-- the activeAtomicResource. This action must be fully atomic.
+type SwapAtomicResourcePair a = AtomicResourcePair a -> Propellor Bool
+
+-- | Checks which of the pair of resources is currently active and
+-- which is inactive, and puts them in the correct poisition in
+-- the AtomicResourcePair.
+type CheckAtomicResourcePair a = AtomicResourcePair a -> Propellor (AtomicResourcePair a)
+
+-- | Makes a non-atomic Property be atomic, by applying it to the 
+-- inactiveAtomicResource, and if it was successful,
+-- atomically activating that resource.
+atomicUpdate
+	-- Constriaints inherited from ensureProperty.
+	:: ( Cannot_ensureProperty_WithInfo t ~ 'True
+	   , (Targets t `NotSuperset` Targets t) ~ 'CanCombine
+	   )
+	=> SingI t
+	=> AtomicResourcePair a
+	-> CheckAtomicResourcePair a
+	-> SwapAtomicResourcePair a
+	-> (a -> Property (MetaTypes t))
+	-> Property (MetaTypes t)
+atomicUpdate rbase rcheck rswap mkp = property' d $ \w -> do
+	r <- rcheck rbase
+	res <- ensureProperty w $ mkp $ inactiveAtomicResource r
+	case res of
+		FailedChange -> return FailedChange
+		NoChange -> return NoChange
+		MadeChange -> do
+			ok <- rswap r
+			if ok
+				then return res
+				else return FailedChange
+  where
+	d = getDesc $ mkp $ activeAtomicResource rbase
+
+-- | Applies a Property to a directory such that the directory is updated
+-- fully atomically; there is no point in time in which the directory will
+-- be in an inconsistent state.
+--
+-- For example, git repositories are not usually updated atomically,
+-- and so while the repository is being updated, the files in it can be a
+-- mixture of two different versions, which could cause unexpected
+-- behavior to consumers. To avoid such problems:
+--
+-- >	& atomicDirUpdate "/srv/web/example.com"
+-- >		(\d -> Git.pulled "joey" "http://.." d Nothing)
+--
+-- This operates by making a second copy of the directory, and passing it
+-- to the Property, which can make whatever changes it needs to that copy,
+-- non-atomically. After the Property successfully makes a change, the
+-- copy is swapped into place, fully atomically.
+--
+-- This necessarily uses double the disk space, since there are two copies
+-- of the directory. The parent directory will actually contain three
+-- children: a symlink with the name of the directory itself, and two copies
+-- of the directory, with names suffixed with ".1" and ".2"
+atomicDirUpdate
+	-- Constriaints inherited from ensureProperty.
+	:: ( Cannot_ensureProperty_WithInfo t ~ 'True
+	   , (Targets t `NotSuperset` Targets t) ~ 'CanCombine
+	   )
+	=> SingI t
+	=> FilePath
+	-> (FilePath -> Property (MetaTypes t))
+	-> Property (MetaTypes t)
+atomicDirUpdate d = atomicUpdate (mkDirLink d) (checkDirLink d) (swapDirLink d)
+
+mkDirLink :: FilePath -> AtomicResourcePair FilePath
+mkDirLink d = AtomicResourcePair
+	{ activeAtomicResource = addext ".1"
+	, inactiveAtomicResource = addext ".2"
+	}
+  where
+	addext = addExtension (dropTrailingPathSeparator d)
+
+inactiveLinkTarget :: AtomicResourcePair FilePath -> FilePath
+inactiveLinkTarget = takeFileName . inactiveAtomicResource
+
+swapDirLink :: FilePath -> SwapAtomicResourcePair FilePath
+swapDirLink d rp = liftIO $ do
+	v <- tryIO $ createSymbolicLink (inactiveLinkTarget rp)
+		`viaStableTmp` d
+	case v of
+		Right () -> return True
+		Left e -> do
+			warningMessage $ "Unable to update symlink at " ++ d ++ " (" ++ show e ++ ")"
+			return False
+
+checkDirLink :: FilePath -> CheckAtomicResourcePair FilePath
+checkDirLink d rp = liftIO $ do
+	v <- tryIO $ readSymbolicLink d
+	return $ case v of
+		Right t | t == inactiveLinkTarget rp ->
+			flipAtomicResourcePair rp
+		_ -> rp
+
+-- | This can optionally be used after atomicDirUpdate to rsync the changes
+-- that were made over to the other copy of the directory. It's not
+-- necessary to use this, but it can improve efficiency.
+--
+-- For example:
+--
+-- >	& atomicDirUpdate "/srv/web/example.com"
+-- >		(\d -> Git.pulled "joey" "http://.." d Nothing)
+-- >		`onChange` atomicDirSync "/srv/web/example.com"
+--
+-- Using atomicDirSync in the above example lets git only download
+-- the changes once, rather than the same changes being downloaded a second
+-- time to update the other copy of the directory the next time propellor
+-- runs.
+--
+-- Suppose that a web server program is run from the git repository,
+-- and needs to be restarted after the pull. That restart should be done
+-- after the atomicDirUpdate, but before the atomicDirSync. That way,
+-- the old web server process will not have its files changed out from
+-- under it.
+--
+-- >	& atomicDirUpdate "/srv/web/example.com"
+-- >		(\d -> Git.pulled "joey" "http://.." d Nothing)
+-- >		`onChange` (webServerRestart `before` atomicDirSync "/srv/web/example.com")
+atomicDirSync :: FilePath -> Property (DebianLike + ArchLinux)
+atomicDirSync d = syncDir (activeAtomicResource rp) (inactiveAtomicResource rp)
+  where
+	rp = mkDirLink d
diff --git a/src/Propellor/Property/Git.hs b/src/Propellor/Property/Git.hs
--- a/src/Propellor/Property/Git.hs
+++ b/src/Propellor/Property/Git.hs
@@ -61,6 +61,9 @@
 -- it will be recursively deleted first.
 --
 -- A branch can be specified, to check out.
+--
+-- Does not make subsequent changes be pulled into the repository after
+-- it's cloned.
 cloned :: User -> RepoUrl -> FilePath -> Maybe Branch -> Property DebianLike
 cloned owner url dir mbranch = check originurl go
 	`requires` installed
@@ -95,11 +98,26 @@
 		, Just "git update-server-info"
 		]
 
+-- | Specified git repository is cloned to the specified directory,
+-- and any new commits are pulled into it each time this property runs.
+pulled :: User -> RepoUrl -> FilePath -> Maybe Branch -> Property DebianLike
+pulled owner url dir mbranch = go
+	`requires` cloned owner url dir mbranch
+	`describe` desc
+  where
+	desc = "git pulled " ++ url ++ " to " ++ dir
+	go = userScriptProperty owner
+		[ "cd " ++ shellEscape dir
+		, "git pull"
+		]
+		`changesFile` (dir </> ".git" </> "FETCH_HEAD")
+
 isGitDir :: FilePath -> IO Bool
 isGitDir dir = isNothing <$> catchMaybeIO (readProcess "git" ["rev-parse", "--resolve-git-dir", dir])
 
 data GitShared = Shared Group | SharedAll | NotShared
 
+-- | Sets up a new, empty bare git repository.
 bareRepo :: FilePath -> User -> GitShared -> Property UnixLike
 bareRepo repo user gitshared = check (isRepo repo) $ propertyList ("git repo: " ++ repo) $ toProps $
 	dirExists repo : case gitshared of
diff --git a/src/Propellor/Property/SiteSpecific/JoeySites.hs b/src/Propellor/Property/SiteSpecific/JoeySites.hs
--- a/src/Propellor/Property/SiteSpecific/JoeySites.hs
+++ b/src/Propellor/Property/SiteSpecific/JoeySites.hs
@@ -541,7 +541,7 @@
 		, "smtpd_sasl_security_options = noanonymous"
 		, "smtpd_sasl_local_domain = kitenet.net"
 
-		, "# Enable postgrey."
+		, "# Enable postgrey and sasl auth and client certs."
 		, "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 is not enabled because it causes mails forwarded from eg gmail to be rejected)"
@@ -668,7 +668,6 @@
 
 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`
@@ -680,6 +679,9 @@
 		, "smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd"
 		]
 		`onChange` Postfix.reloaded
+	-- Comes after so it does not set relayhost but uses the setting 
+	-- above.
+	& Postfix.satellite
 
 hasPostfixCert :: Context -> Property (HasInfo + UnixLike)
 hasPostfixCert ctx = combineProperties "postfix tls cert installed" $ props
@@ -1048,7 +1050,7 @@
 	, "ttf-bitstream-vera"
 	, "mairix", "offlineimap", "mutt"
 	, "nmap", "whois", "wireshark", "tcpdump", "iftop"
-	, "udevil", "pmount", "tree"
+	, "udevil", "pmount", "tree", "pv"
 	, "arbtt", "hledger", "bc"
 	, "apache2", "ikiwiki", "libhighlight-perl"
 	, "pal"
@@ -1059,7 +1061,7 @@
 	, "vim-syntastic", "vim-fugitive"
 	, "adb", "gthumb"
 	, "w3m", "sm", "weechat"
-	, "borgbackup", "wipe"
+	, "borgbackup", "wipe", "smartmontools", "libgfshare-bin"
 	, "units"
 	]
 	`requires` baseSoftware
@@ -1079,3 +1081,35 @@
 	, "gdb", "dpkg-repack", "lintian"
 	, "pristine-tar", "github-backup"
 	]
+
+cubieTruckOneWire :: Property DebianLike
+cubieTruckOneWire = 
+	File.hasContent "/etc/easy-peasy-devicetree-squeezy/my.dts" mydts
+		`onChange` utilitysetup
+		`requires` utilityinstalled
+  where
+	utilityinstalled = Git.cloned (User "root") "https://git.joeyh.name/git/easy-peasy-devicetree-squeezy.git" "/usr/local/easy-peasy-devicetree-squeezy" Nothing
+		`onChange` File.isSymlinkedTo "/usr/local/bin/easy-peasy-devicetree-squeezy" (File.LinkTarget "/usr/local/easy-peasy-devicetree-squeezy/easy-peasy-devicetree-squeezy")
+	utilitysetup = cmdProperty "easy-peasy-devicetree-squeezy"
+		["--debian", "sun7i-a20-cubietruck"]
+		`assume` MadeChange
+	mydts =
+		[ "/* Device tree addition enabling onewire sensors on CubieTruck GPIO pin PG8 */"
+		, "#include <dt-bindings/gpio/gpio.h>"
+		, ""
+		, "/ {"
+		, "\tonewire_device {"
+		, "\t\tcompatible = \"w1-gpio\";"
+		, "\t\tgpios = <&pio 6 8 GPIO_ACTIVE_HIGH>; /* PG8 */"
+		, "\t\tpinctrl-names = \"default\";"
+		, "\t\tpinctrl-0 = <&my_w1_pin>;"
+		, "\t};"
+		, "};"
+		, ""
+		, "&pio {"
+		, "\tmy_w1_pin: my_w1_pin@0 {"
+		, "\t\tallwinner,pins = \"PG8\";"
+		, "\t\tallwinner,function = \"gpio_in\";"
+		, "\t};"
+		, "};"
+		]
diff --git a/src/Propellor/Property/Systemd.hs b/src/Propellor/Property/Systemd.hs
--- a/src/Propellor/Property/Systemd.hs
+++ b/src/Propellor/Property/Systemd.hs
@@ -205,8 +205,8 @@
 	case o of
 		-- Split into separate debian package since systemd 225.
 		(Just (System (Debian _ suite) _))
-			| not (isStable suite) -> ensureProperty w $
-				Apt.installed ["systemd-container"]
+			| not (isStable suite) || suite == (Stable "stretch") ->
+				ensureProperty w $ Apt.installed ["systemd-container"]
 		_ -> noChange
 
 -- | Defines a container with a given machine name,
