-- | Specific configuration for Joey Hess's sites. Probably not useful to
-- others except as an example.

{-# LANGUAGE FlexibleContexts, TypeFamilies #-}

module Propellor.Property.SiteSpecific.JoeySites where

import Propellor.Base
import qualified Propellor.Property.Apt as Apt
import qualified Propellor.Property.File as File
import qualified Propellor.Property.ConfFile as ConfFile
import qualified Propellor.Property.Gpg as Gpg
import qualified Propellor.Property.Ssh as Ssh
import qualified Propellor.Property.Git as Git
import qualified Propellor.Property.Cron as Cron
import qualified Propellor.Property.Service as Service
import qualified Propellor.Property.User as User
import qualified Propellor.Property.Group as Group
import qualified Propellor.Property.Borg as Borg
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.Split

import System.Posix.Files

kgbServer :: Property (HasInfo + DebianLike)
kgbServer = propertyList desc $ props
	& Apt.serviceInstalledRunning "kgb-bot"
	& "/etc/default/kgb-bot" `File.containsLine` "BOT_ENABLED=1"
		`describe` "kgb bot enabled"
		`onChange` Service.running "kgb-bot"
	& File.hasPrivContent "/etc/kgb-bot/kgb.conf" anyContext
		`onChange` Service.restarted "kgb-bot"
  where
	desc = "kgb.kitenet.net setup"

-- git.kitenet.net and git.joeyh.name
gitServer :: [Host] -> Property (HasInfo + DebianLike)
gitServer hosts = propertyList "git.kitenet.net setup" $ props
	& Borg.backup "/srv/git" borgrepo
		(Cron.Times "33 3 * * *")
		[]
		[Borg.KeepDays 30]
		`requires` Ssh.userKeyAt (Just sshkey)
			(User "root")
			(Context "git.kitenet.net")
			(SshEd25519, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOvgBVYP6srImGbJ+kg1K68HeUQqxHEBQswMWSqu9WOu root@kite")
		`requires` Ssh.knownHost hosts "usw-s002.rsync.net" (User "root")
	& Ssh.authorizedKeys (User "family") (Context "git.kitenet.net")
	& User.accountFor (User "family")
	& 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.daemonService "/srv/git"
	& "/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"
  where
	sshkey = "/root/.ssh/git.kitenet.net.key"
	borgrepo = rsyncNetBorgRepo "git.kitenet.net.borg" [Borg.UseSshKey sshkey]
	website hn = Apache.httpsVirtualHost' hn "/srv/web/git.kitenet.net/" letos
		[ Apache.iconDir
		, "  <Directory /srv/web/git.kitenet.net/>"
		, "    Options Indexes ExecCGI FollowSymlinks"
		, "    AllowOverride None"
		, "    AddHandler cgi-script .cgi"
		, "    DirectoryIndex index.cgi"
		,      Apache.allowAll
		, "  </Directory>"
		, ""
		, "  ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/"
		, "  <Directory /usr/lib/cgi-bin>"
		, "    SetHandler cgi-script"
		, "    Options ExecCGI"
		, "  </Directory>"
		]

type AnnexUUID = String

-- | A website, with files coming from a git-annex repository.
annexWebSite :: Git.RepoUrl -> HostName -> AnnexUUID -> [(String, Git.RepoUrl)] -> Property (HasInfo + DebianLike)
annexWebSite origin hn uuid remotes = propertyList (hn ++" website using git-annex") $ props
	& Git.cloned (User "joey") origin dir Nothing
		`onChange` setup
	& alias hn
	& postupdatehook `File.hasContent`
		[ "#!/bin/sh"
		, "exec git update-server-info"
		] `onChange`
			(postupdatehook `File.mode` (combineModes (ownerWriteMode:readModes ++ executeModes)))
	& setupapache
  where
	dir = "/srv/web/" ++ hn
	postupdatehook = dir </> ".git/hooks/post-update"
	setup = userScriptProperty (User "joey") setupscript
		`assume` MadeChange
	setupscript =
		[ "cd " ++ shellEscape dir
		, "git annex reinit " ++ shellEscape uuid
		] ++ map addremote remotes ++
		[ "git annex get"
		, "git update-server-info"
		]
	addremote (name, url) = "git remote add " ++ shellEscape name ++ " " ++ shellEscape url
	setupapache = Apache.httpsVirtualHost' hn dir letos
		[ "  ServerAlias www."++hn
		,    Apache.iconDir
		, "  <Directory "++dir++">"
		, "    Options Indexes FollowSymLinks ExecCGI"
		, "    AllowOverride None"
		, "    AddHandler cgi-script .cgi"
		, "    DirectoryIndex index.html index.cgi"
		,      Apache.allowAll
		, "  </Directory>"
		]

letos :: LetsEncrypt.AgreeTOS
letos = LetsEncrypt.AgreeTOS (Just "id@joeyh.name")

apacheSite :: HostName -> Apache.ConfigFile -> RevertableProperty DebianLike DebianLike
apacheSite hn middle = Apache.siteEnabled hn $ apachecfg hn middle

apachecfg :: HostName -> Apache.ConfigFile -> Apache.ConfigFile
apachecfg hn middle =
	[ "<VirtualHost *:" ++ val port ++ ">"
	, "  ServerAdmin grue@joeyh.name"
	, "  ServerName "++hn++":" ++ val port
	]
	++ middle ++
	[ ""
	, "  ErrorLog /var/log/apache2/error.log"
	, "  LogLevel warn"
	, "  CustomLog /var/log/apache2/access.log combined"
	, "  ServerSignature On"
	, "  "
	, Apache.iconDir
	, "</VirtualHost>"
	]
	  where
		port = Port 80

gitAnnexDistributor :: Property (HasInfo + DebianLike)
gitAnnexDistributor = combineProperties "git-annex distributor, including rsync server and signer" $ props
	& Apt.installed ["rsync"]
	& File.hasPrivContent "/etc/rsyncd.conf" (Context "git-annex distributor")
		`onChange` Service.restarted "rsync"
	& File.hasPrivContent "/etc/rsyncd.secrets" (Context "git-annex distributor")
		`onChange` Service.restarted "rsync"
	& "/etc/default/rsync" `File.containsLine` "RSYNC_ENABLE=true"
		`onChange` Service.running "rsync"
	& Systemd.enabled "rsync"
	& endpoint "/srv/web/downloads.kitenet.net/git-annex/autobuild"
	& endpoint "/srv/web/downloads.kitenet.net/git-annex/autobuild/x86_64-apple-yosemite"
	& endpoint "/srv/web/downloads.kitenet.net/git-annex/autobuild/windows"
	-- git-annex distribution signing key
	& Gpg.keyImported (Gpg.GpgKeyId "89C809CB") (User "joey")
	-- used for building rpms
	& Apt.installed ["rpm", "createrepo-c"]
  where
	endpoint d = combineProperties ("endpoint " ++ d) $ props
		& File.dirExists d
		& File.ownerGroup d (User "joey") (Group "joey")

downloads :: Property (HasInfo + DebianLike)
downloads = annexWebSite "/srv/git/downloads.git"
	"downloads.kitenet.net"
	"840760dc-08f0-11e2-8c61-576b7e66acfd"
	[]

tmp :: Property (HasInfo + DebianLike)
tmp = propertyList "tmp.joeyh.name" $ props
	& annexWebSite "/srv/git/joey/tmp.git"
		"tmp.joeyh.name"
		"26fd6e38-1226-11e2-a75f-ff007033bdba"
		[]
	& Cron.jobDropped "pump rss" (Cron.Times "15 * * * *")

ircBouncer :: Property (HasInfo + DebianLike)
ircBouncer = propertyList "IRC bouncer" $ props
	& Apt.installed ["znc"]
	& User.accountFor (User "znc")
	& File.dirExists (takeDirectory conf)
	& File.hasPrivContent conf anyContext
	& File.ownerGroup conf (User "znc") (Group "znc")
	& Cron.job "znconboot" (Cron.Times "@reboot") (User "znc") "~" "znc"
	-- ensure running if it was not already
	& userScriptProperty (User "znc") ["znc || true"]
		`assume` NoChange
		`describe` "znc running"
  where
	conf = "/home/znc/.znc/configs/znc.conf"

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"
	`requires` Ssh.knownHost hosts "usw-s002.rsync.net" (User "joey")

spamdEnabled :: Property DebianLike
spamdEnabled = Apt.serviceInstalledRunning "spamd"

spamassassinConfigured :: Property DebianLike
spamassassinConfigured = propertyList "spamassassin configured" $ props
	& spamdEnabled
	& "/etc/default/spamd" `File.containsLines`
		[ "# Propellor deployed"
		, "OPTIONS=\"--create-prefs --max-children 5 --helper-home-dir\""
		, "NICE=\"--nicelevel 15\""
		] 
		`describe` "spamd configured"
		`onChange` Service.restarted "spamd"
	& "/etc/default/spamassassin" `File.containsLines`
		[ "# Propellor deployed"
		, "CRON=1"
		]
		`describe` "spamassassin configured"
		`requires` Apt.serviceInstalledRunning "cron"
	& "/etc/spamassassin/local.cf" `File.containsLines`
		-- dnswl.org started intentionally sending false responses
		-- for spam senders when they don't like the amount of
		-- queries being made. This causes spamassassin to skip
		-- all other checks and miss flagging spam. This is
		-- irresponsible behavior, and so they should not be used.
		[ "dns_query_restriction deny dnswl.org"
		]

kiteMailServer :: Property (HasInfo + DebianLike)
kiteMailServer = propertyList "kitenet.net mail server" $ props
	& Postfix.installed
	& Apt.installed ["postfix-pcre"]
	& spamassassinConfigured
	& Apt.serviceInstalledRunning "spamass-milter"
	-- Add -m to prevent modifying messages Subject or body.
	& "/etc/default/spamass-milter" `File.containsLine`
		"OPTIONS=\"-m -u spamass-milter -i 127.0.0.1\""
		`onChange` Service.restarted "spamass-milter"
		`describe` "spamass-milter configured"

	& dkimInstalled

	& Postfix.saslAuthdInstalled
	& Fail2Ban.installed
	& Fail2Ban.jailEnabled "postfix-sasl"
	& "/etc/default/saslauthd" `File.containsLine` "MECHANISMS=sasldb"
	& Postfix.saslPasswdSet "kitenet.net" (User "errol")
	& Postfix.saslPasswdSet "kitenet.net" (User "joey")

	& Apt.installed ["maildrop"]
	& "/etc/maildroprc" `File.hasContent`
		[ "# Global maildrop filter file (deployed with propellor)"
		, "DEFAULT=\"$HOME/Maildir\""
		, "MAILBOX=\"$DEFAULT/.\""
		, "if (/^X-Spam-Status: Yes/)"
		, "{"
		, "  to ${MAILBOX}spam"
		, "}"
		]
		`describe` "maildrop configured"

	& "/etc/aliases" `File.hasPrivContentExposed` ctx
		`onChange` Postfix.newaliases
	& hasPostfixCert ctx

	& "/etc/postfix/mydomain" `File.containsLines`
		[ "/.*\\.kitenet\\.net/\tOK"
		, "/ikiwiki\\.info/\tOK"
		, "/joeyh\\.name/\tOK"
		]
		`onChange` Postfix.reloaded
		`describe` "postfix mydomain file configured"
	& "/etc/postfix/obscure_client_relay.pcre" `File.hasContent`
		-- Remove received lines for mails relayed from trusted
		-- clients. These can be a privacy violation, or trigger
		-- spam filters.
		[ "/^Received: from ([^.]+)\\.kitenet\\.net.*using TLS.*by kitenet\\.net \\(([^)]+)\\) with (E?SMTPS?A?) id ([A-F[:digit:]]+)(.*)/ IGNORE"
		-- Munge local Received line for postfix running on a
		-- trusted client that relays through. These can trigger
		-- spam filters.
		, "/^Received: by ([^.]+)\\.kitenet\\.net.*/ REPLACE X-Question: 42"
		]
		`onChange` Postfix.reloaded
		`describe` "postfix obscure_client_relay file configured"
	& Postfix.mappedFile "/etc/postfix/virtual"
		(flip File.containsLines
			[ "# *@joeyh.name to joey"
			, "@joeyh.name\tjoey"
			]
		) `describe` "postfix virtual file configured"
		`onChange` Postfix.reloaded
	& Postfix.mappedFile "/etc/postfix/relay_clientcerts"
		(flip File.hasPrivContentExposed ctx)
	& Postfix.mainCfFile `File.containsLines`
		[ "myhostname = kitenet.net"
		, "mydomain = $myhostname"
		, "append_dot_mydomain = no"
		, "myorigin = kitenet.net"
		, "mydestination = $myhostname, localhost.$mydomain, $mydomain, kite.$mydomain., localhost, regexp:$config_directory/mydomain"
		, "mailbox_command = maildrop"
		, "virtual_alias_maps = hash:/etc/postfix/virtual"

		, "# Allow clients with trusted certs to relay mail through."
		, "relay_clientcerts = hash:/etc/postfix/relay_clientcerts"
		, "smtpd_relay_restrictions = permit_mynetworks,permit_tls_clientcerts,permit_sasl_authenticated,reject_unauth_destination"

		, "# Filter out client relay lines from headers."
		, "header_checks = pcre:$config_directory/obscure_client_relay.pcre"

		, "# Password auth for relaying"
		, "smtpd_sasl_auth_enable = yes"
		, "smtpd_sasl_security_options = noanonymous"
		, "smtpd_sasl_local_domain = kitenet.net"

		, "# Enable sasl auth and client certs."
		, "smtpd_recipient_restrictions = permit_tls_clientcerts,permit_sasl_authenticated,,permit_mynetworks,reject_unauth_destination"

		, "# Enable spamass-milter (opendkim is not enabled because it causes mails forwarded from eg gmail to be rejected)"
		, "smtpd_milters = unix:/spamass/spamass.sock"
		, "# opendkim is used for outgoing mail"
		, "non_smtpd_milters = inet:localhost:8891"
		, "milter_connect_macros = j {daemon_name} v {if_name} _"
		, "# If a milter is broken, fall back to just accepting mail."
		, "milter_default_action = accept"

		, "# TLS setup -- server"
		, "smtpd_tls_CAfile = /etc/ssl/certs/joeyca.pem"
		, "smtpd_tls_cert_file = /etc/ssl/certs/postfix.pem"
		, "smtpd_tls_key_file = /etc/ssl/private/postfix.pem"
		, "smtpd_tls_loglevel = 1"
		, "smtpd_tls_received_header = yes"
		, "smtpd_tls_security_level = may"
		, "smtpd_tls_ask_ccert = yes"
		, "smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache"

		, "# TLS setup -- client"
		, "smtp_tls_CAfile = /etc/ssl/certs/joeyca.pem"
		, "smtp_tls_cert_file = /etc/ssl/certs/postfix.pem"
		, "smtp_tls_key_file = /etc/ssl/private/postfix.pem"
		, "smtp_tls_loglevel = 1"
		, "smtp_tls_security_level = may"
		, "smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache"

		, "# Allow larger attachments, up to 200 mb."
		, "# (Avoid setting too high; the postfix queue must have"
		, "# 1.5 times this much space free, or postfix will reject"
		, "# ALL mail!)"
		, "message_size_limit = 204800000"
		, "virtual_mailbox_limit = 20480000"
		]
		`onChange` Postfix.dedupMainCf
		`onChange` Postfix.reloaded
		`describe` "postfix configured"

	& Apt.serviceInstalledRunning "dovecot-imapd"
	& Apt.serviceInstalledRunning "dovecot-pop3d"
	& "/etc/dovecot/conf.d/10-mail.conf" `File.containsLine`
		"mail_location = maildir:~/Maildir"
		`onChange` Service.reloaded "dovecot"
		`describe` "dovecot mail.conf"
	& "/etc/dovecot/conf.d/10-auth.conf" `File.containsLine`
		"!include auth-passwdfile.conf.ext"
		`onChange` Service.restarted "dovecot"
		`describe` "dovecot auth.conf"
	& "/etc/dovecot/conf.d/10-ssl.conf" `File.containsLines`
		[ "ssl_cert = </etc/letsencrypt/live/kitenet.net/fullchain.pem"
		, "ssl_key = </etc/letsencrypt/live/kitenet.net/privkey.pem"
		]
		`onChange` Service.reloaded "dovecot"
		`describe` "dovecot ssl.conf using letsencrypt"
	& Cron.niceJob "dovecot restarts to update letsenctypt certificate"
		Cron.Daily (User "root") "/" "systemctl restart dovecot"
	& File.hasPrivContent dovecotusers ctx
		`onChange` (dovecotusers `File.mode`
			combineModes [ownerReadMode, groupReadMode])
	& File.ownerGroup dovecotusers (User "root") (Group "dovecot")

	& Apt.installed ["mutt", "bsd-mailx", "alpine"]

	& pinescript `File.hasContent`
		[ "#!/bin/sh"
		, "# deployed with propellor"
		, "set -e"
		, "exec alpine \"$@\""
		]
		`onChange` (pinescript `File.mode`
			combineModes (readModes ++ executeModes))
		`describe` "pine wrapper script"
	-- Make pine use dovecot pipe to read maildir.
	& "/etc/pine.conf" `File.hasContent`
		[ "# deployed with propellor"
		, "inbox-path={localhost}inbox"
		, "rsh-command=" ++ imapalpinescript
		]
		`describe` "pine configured to use local imap server"
	& imapalpinescript `File.hasContent`
		[ "#!/bin/sh"
		, "# deployed with propellor"
		, "set -e"
		, "exec /usr/lib/dovecot/imap 2>/dev/null"
		]
		`onChange` (imapalpinescript `File.mode`
			combineModes (readModes ++ executeModes))
		`describe` "imap script for pine"

	& Postfix.service ssmtp

	& Apt.installed ["fetchmail"]
  where
	ctx = Context "kitenet.net"
	pinescript = "/usr/local/bin/pine"
	imapalpinescript = "/usr/local/bin/imap-for-alpine"
	dovecotusers = "/etc/dovecot/users"

	ssmtp = Postfix.Service
		(Postfix.InetService Nothing "ssmtp")
		"smtpd" Postfix.defServiceOpts

-- Configures postfix to have the dkim milter, and no other milters.
dkimMilter :: Property (HasInfo + DebianLike)
dkimMilter = Postfix.mainCfFile `File.containsLines`
	[ "smtpd_milters = inet:localhost:8891"
	, "non_smtpd_milters = inet:localhost:8891"
	, "milter_default_action = accept"
	]
	`describe` "postfix dkim milter"
	`onChange` Postfix.dedupMainCf
	`onChange` Postfix.reloaded
	`requires` dkimInstalled
	`requires` Postfix.installed

-- This does not configure postfix to use the dkim milter,
-- nor does it set up domainkey DNS.
dkimInstalled :: Property (HasInfo + DebianLike)
dkimInstalled = go `onChange` Service.restarted "opendkim"
  where
	go = propertyList "opendkim installed" $ props
		& Apt.serviceInstalledRunning "opendkim"
		& File.dirExists "/etc/mail"
		& File.hasPrivContent "/etc/mail/dkim.key" (Context "kitenet.net")
		& File.ownerGroup "/etc/mail/dkim.key" (User "root") (Group "root")
		& "/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"
			, "Domain *"
			, "Selector mail"
			]

-- This is the dkim public key, corresponding with /etc/mail/dkim.key
-- This value can be included in a domain's additional records to make
-- it use this domainkey.
domainKey :: (BindDomain, Record)
domainKey = (RelDomain "mail._domainkey", TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCc+/rfzNdt5DseBBmfB3C6sVM7FgVvf4h1FeCfyfwPpVcmPdW6M2I+NtJsbRkNbEICxiP6QY2UM0uoo9TmPqLgiCCG2vtuiG6XMsS0Y/gGwqKM7ntg/7vT1Go9vcquOFFuLa5PnzpVf8hB9+PMFdS4NPTvWL2c5xxshl/RJzICnQIDAQAB")

postfixSaslPasswordClient :: Property (HasInfo + DebianLike)
postfixSaslPasswordClient = combineProperties "postfix uses SASL password to authenticate with smarthost" $ props
	& 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:465"
		, "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
	& "/etc/ssl/certs/postfix.pem" `File.hasPrivContentExposed` ctx
	& "/etc/ssl/private/postfix.pem" `File.hasPrivContent` ctx

-- Legacy static web sites and redirections from kitenet.net to newer
-- sites.
legacyWebSites :: Property (HasInfo + DebianLike)
legacyWebSites = propertyList "legacy web sites" $ props
	& Apt.serviceInstalledRunning "apache2"
	& Apache.modEnabled "rewrite"
	& Apache.modEnabled "cgi"
	& Apache.modEnabled "speling"
	& userDirHtml
	& Apache.httpsVirtualHost' "kitenet.net" "/var/www" letos kitenetcfg
	& alias "anna.kitenet.net"
	& apacheSite "anna.kitenet.net"
		[ "DocumentRoot /home/anna/html"
		, "<Directory /home/anna/html/>"
		, "  Options Indexes ExecCGI"
		, "  AllowOverride None"
		, Apache.allowAll
		, "</Directory>"
		]
	& alias "sows-ear.kitenet.net"
	& alias "www.sows-ear.kitenet.net"
	& apacheSite "sows-ear.kitenet.net"
		[ "ServerAlias www.sows-ear.kitenet.net"
		, "DocumentRoot /srv/web/sows-ear.kitenet.net"
		, "<Directory /srv/web/sows-ear.kitenet.net>"
		, "  Options FollowSymLinks"
		, "  AllowOverride None"
		, Apache.allowAll
		, "</Directory>"
		, "RewriteEngine On"
		, "RewriteRule .* http://www.sowsearpoetry.org/ [L]"
		]
	& alias "wortroot.kitenet.net"
	& alias "www.wortroot.kitenet.net"
	& apacheSite "wortroot.kitenet.net"
		[ "ServerAlias www.wortroot.kitenet.net"
		, "DocumentRoot /srv/web/wortroot.kitenet.net"
		, "<Directory /srv/web/wortroot.kitenet.net>"
		, "  Options FollowSymLinks"
		, "  AllowOverride None"
		, Apache.allowAll
		, "</Directory>"
		]
	& alias "creeksidepress.com"
	& apacheSite "creeksidepress.com"
		[ "ServerAlias www.creeksidepress.com"
		, "DocumentRoot /srv/web/www.creeksidepress.com"
		, "<Directory /srv/web/www.creeksidepress.com>"
		, "  Options FollowSymLinks"
		, "  AllowOverride None"
		, Apache.allowAll
		, "</Directory>"
		]
	& alias "joey.kitenet.net"
	& apacheSite "joey.kitenet.net"
		[ "DocumentRoot /var/www"
		, "<Directory /var/www/>"
		, "  Options Indexes ExecCGI"
		, "  AllowOverride None"
		, Apache.allowAll
		, "</Directory>"

		, "RewriteEngine On"

		, "# Old ikiwiki filenames for joey's wiki."
		, "rewritecond $1 !.*/index$"
		, "rewriterule (.+).html$ http://joeyh.name/$1/ [l]"

		, "rewritecond $1 !.*/index$"
		, "rewriterule (.+).rss$ http://joeyh.name/$1/index.rss [l]"

		, "# Redirect all to joeyh.name."
		, "rewriterule (.*) http://joeyh.name$1 [r]"
		]
	& alias "house.joeyh.name"
	& Apache.httpsVirtualHost' "house.joeyh.name" "/srv/web/house.joeyh.name" letos
		[ "DocumentRoot /srv/web/house.joeyh.name"
		, "<Directory /srv/web/house.joeyh.name>"
		, "  Options Indexes ExecCGI"
		, "  AllowOverride None"
		, Apache.allowAll
		, "</Directory>"
		]
  where
	kitenetcfg =
		-- /var/www is empty
		[ "DocumentRoot /var/www"
		, "<Directory /var/www>"
		, "  Options Indexes FollowSymLinks MultiViews ExecCGI Includes"
		, "  AllowOverride None"
		, Apache.allowAll
		, "</Directory>"
		, "RewriteEngine On"
		, "# Force hostname to kitenet.net"
		, "RewriteCond %{HTTP_HOST} !^kitenet\\.net [NC]"
		, "RewriteCond %{HTTP_HOST} !^$"
		, "RewriteRule ^/(.*) http://kitenet\\.net/$1 [L,R]"

		, "# Moved pages"
		, "RewriteRule /programs/debhelper http://joeyh.name/code/debhelper/ [L]"
		, "RewriteRule /programs/satutils http://joeyh.name/code/satutils/ [L]"
		, "RewriteRule /programs/filters http://joeyh.name/code/filters/ [L]"
		, "RewriteRule /programs/ticker http://joeyh.name/code/ticker/ [L]"
		, "RewriteRule /programs/pdmenu http://joeyh.name/code/pdmenu/ [L]"
		, "RewriteRule /programs/sleepd http://joeyh.name/code/sleepd/ [L]"
		, "RewriteRule /programs/Lingua::EN::Words2Nums http://joeyh.name/code/Words2Nums/ [L]"
		, "RewriteRule /programs/wmbattery http://joeyh.name/code/wmbattery/ [L]"
		, "RewriteRule /programs/dpkg-repack http://joeyh.name/code/dpkg-repack/ [L]"
		, "RewriteRule /programs/debconf http://joeyh.name/code/debconf/ [L]"
		, "RewriteRule /programs/perlmoo http://joeyh.name/code/perlmoo/ [L]"
		, "RewriteRule /programs/alien http://joeyh.name/code/alien/ [L]"
		, "RewriteRule /~joey/blog/entry/(.+)-[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9].html http://joeyh.name/blog/entry/$1/ [L]"
		, "RewriteRule /~anna/.* http://waldeneffect\\.org/ [R]"
		, "RewriteRule /~anna/.* http://waldeneffect\\.org/ [R]"
		, "RewriteRule /~anna http://waldeneffect\\.org/ [R]"
		, "RewriteRule /simpleid/ http://openid.kitenet.net:8086/simpleid/"
		, "# Even the kite home page is not here any more!"
		, "RewriteRule ^/$ http://www.kitenet.net/ [R]"
		, "RewriteRule ^/index.html http://www.kitenet.net/ [R]"
		, "RewriteRule ^/joey http://www.kitenet.net/joey/ [R]"
		, "RewriteRule ^/joey/index.html http://www.kitenet.net/joey/ [R]"
		, "RewriteRule ^/wifi http://www.kitenet.net/wifi/ [R]"
		, "RewriteRule ^/wifi/index.html http://www.kitenet.net/wifi/ [R]"

		, "# Old ikiwiki filenames for kitenet.net wiki."
		, "rewritecond $1 !^/~"
		, "rewritecond $1 !^/doc/"
		, "rewritecond $1 !^/cgi-bin/"
		, "rewritecond $1 !.*/index$"
		, "rewriterule (.+).html$ $1/ [r]"

		, "# Old ikiwiki filenames for joey's wiki."
		, "rewritecond $1 ^/~joey/"
		, "rewritecond $1 !.*/index$"
		, "rewriterule (.+).html$ http://kitenet.net/$1/ [L,R]"

		, "# ~joey to joeyh.name"
		, "rewriterule /~joey/(.*) http://joeyh.name/$1 [L]"

		, "# Old familywiki location."
		, "rewriterule /~family/(.*).html http://family.kitenet.net/$1 [L]"
		, "rewriterule /~family/(.*).rss http://family.kitenet.net/$1/index.rss [L]"
		, "rewriterule /~family(.*) http://family.kitenet.net$1 [L]"

		, "rewriterule /~kyle/bywayofscience(.*) http://bywayofscience.branchable.com$1 [L]"
		, "rewriterule /~kyle/family/wiki/(.*).html http://macleawiki.branchable.com/$1 [L]"
		, "rewriterule /~kyle/family/wiki/(.*).rss http://macleawiki.branchable.com/$1/index.rss [L]"
		, "rewriterule /~kyle/family/wiki(.*) http://macleawiki.branchable.com$1 [L]"
		]

userDirHtml :: Property DebianLike
userDirHtml = File.fileProperty "apache userdir is html" (map munge) conf
	`onChange` Apache.reloaded
	`requires` Apache.modEnabled "userdir"
  where
	munge = replace "public_html" "html"
	conf = "/etc/apache2/mods-available/userdir.conf"

-- Alarm clock: see
-- <http://joeyh.name/blog/entry/a_programmable_alarm_clock_using_systemd/>
--
-- oncalendar example value: "*-*-* 7:30"
alarmClock :: String -> User -> String -> Property Linux
alarmClock oncalendar (User user) command = combineProperties "goodmorning timer installed" $ props
	& "/etc/systemd/system/goodmorning.timer" `File.hasContent`
		[ "[Unit]"
		, "Description=good morning"
		, ""
		, "[Timer]"
		, "Unit=goodmorning.service"
		, "OnCalendar=" ++ oncalendar
		, "WakeSystem=true"
		, "Persistent=false"
		, ""
		, "[Install]"
		, "WantedBy=multi-user.target"
		]
		`onChange` (Systemd.daemonReloaded
			`before` Systemd.restarted "goodmorning.timer")
	& "/etc/systemd/system/goodmorning.service" `File.hasContent`
		[ "[Unit]"
		, "Description=good morning"
		, "RefuseManualStart=true"
		, "RefuseManualStop=true"
		, "ConditionACPower=true"
		, "StopWhenUnneeded=yes"
		, ""
		, "[Service]"
		, "Type=oneshot"
		, "ExecStart=/bin/systemd-inhibit --what=handle-lid-switch --why=goodmorning /bin/su " ++ user ++ " -c \"" ++ command ++ "\""
		]
		`onChange` Systemd.daemonReloaded
	& Systemd.enabled "goodmorning.timer"
	& Systemd.started "goodmorning.timer"
	& "/etc/systemd/logind.conf" `ConfFile.containsIniSetting`
		("Login", "LidSwitchIgnoreInhibited", "no")

epsolarUsbSetup :: Property UnixLike
epsolarUsbSetup = propertyList "epsolar usb setup" $ props
	& "/etc/udev/rules.d/99-epsolar-usb.rules" `File.hasContent`
		-- Each epever usb cable has its own serial number and gets
		-- its own stable device name.
		[ "SUBSYSTEM==\"tty\", ATTRS{idVendor}==\"04e2\", ATTRS{idProduct}==\"1411\", ATTRS{serial}==\"G9107376441\", SYMLINK+=\"epsolar1\""
		, "SUBSYSTEM==\"tty\", ATTRS{idVendor}==\"1a86\", ATTRS{idProduct}==\"55d3\", ATTRS{serial}==\"5876071260\", SYMLINK+=\"epsolar2\""
		, "SUBSYSTEM==\"tty\", ATTRS{idVendor}==\"1a86\", ATTRS{idProduct}==\"55d3\", ATTRS{serial}==\"5875017909\", SYMLINK+=\"epsolar3\""
		, "SUBSYSTEM==\"tty\", ATTRS{idVendor}==\"1a86\", ATTRS{idProduct}==\"55d3\", ATTRS{serial}==\"5876071261\", SYMLINK+=\"epsolar4\""
		, "SUBSYSTEM==\"tty\", ATTRS{idVendor}==\"1a86\", ATTRS{idProduct}==\"55d3\", ATTRS{serial}==\"5875017825\", SYMLINK+=\"epsolar5\""
		, "SUBSYSTEM==\"tty\", ATTRS{idVendor}==\"1a86\", ATTRS{idProduct}==\"55d3\", ATTRS{serial}==\"589A039325\", SYMLINK+=\"epsolar6\""
		]
	-- The xr-serial module wants to handle the epever usb cables
	-- (at least older ones, new ones are handled by ACM), but
	-- it does not work in linux 6.1. Instead I am currently installing
	-- an out of tree module, xr_usb_serial_common. In linux 6.6,
	-- the xr-serial module has been reported to work,
	-- see https://github.com/kasbert/epsolar-tracer/pull/61#issuecomment-2056442735
	& "/etc/modprobe.d/blacklist.conf" `File.containsLines`
		[ "blacklist xr-serial"
		, "blacklist xr-serial"
		]

house :: IsContext c => User -> [Host] -> c -> (SshKeyType, Ssh.PubKeyText) -> Property (HasInfo + DebianLike)
house user hosts ctx sshkey = propertyList "home automation" $ props
	& Apache.installed
	& Apt.installed ["libmodbus-dev", "rrdtool", "rsync"]
	& Git.cloned user "https://git.joeyh.name/git/joey/house.git" d Nothing
	& websitesymlink
	& build
	& epsolarUsbSetup
	& Systemd.enabled setupservicename
		`requires` setupserviceinstalled
		`onChange` Systemd.started setupservicename
	& Systemd.enabled pollerservicename
		`requires` pollerserviceinstalled
		`onChange` Systemd.started pollerservicename
	& Systemd.enabled controllerservicename
		`requires` controllerserviceinstalled
		`onChange` Systemd.started controllerservicename
	& Systemd.enabled watchdogservicename
		`requires` watchdogserviceinstalled
		`onChange` Systemd.started watchdogservicename
	& Apt.serviceInstalledRunning "watchdog"
	& "/etc/watchdog.conf" `File.containsLines`
		[ "watchdog-device = /dev/watchdog0"
		, "watchdog-timeout = 16" -- maximum supported by cubietruck
		, "interval = 1"
		]
		`onChange` Service.reloaded "watchdog"
	& Group.exists (Group "gpio") Nothing
	& User.hasGroup user (Group "gpio")
	& Apt.installed ["i2c-tools"]
	& User.hasGroup user (Group "i2c")
	& "/etc/modules-load.d/house.conf" `File.hasContent` ["i2c-dev"]
	& Cron.niceJob "house upload"
		(Cron.Times "1 * * * *") user d rsynccommand
		`requires` Ssh.userKeyAt (Just sshkeyfile) user ctx sshkey
		`requires` File.ownerGroup (takeDirectory sshkeyfile)
			user (userGroup user)
		`requires` File.dirExists (takeDirectory sshkeyfile)
		`requires` Ssh.knownHost hosts "kitenet.net" user
  where
	d = "/home/joey/house"
	sshkeyfile = d </> ".ssh/key"
	build = check (not <$> doesFileExist (d </> "controller")) $
		userScriptProperty (User "joey")
			[ "cd " ++ d
			, "cabal update"
			, "make"
			]
		`assume` MadeChange
		`requires` Apt.installed
			[ "ghc", "cabal-install", "make"
			, "libghc-http-types-dev"
			, "libghc-aeson-dev"
			, "libghc-wai-dev"
			, "libghc-warp-dev"
			, "libghc-http-client-dev"
			, "libghc-http-client-tls-dev"
			, "libghc-reactive-banana-dev"
			, "libghc-hinotify-dev"
			]
	pollerservicename = "house-poller"
	pollerservicefile = "/etc/systemd/system/" ++ pollerservicename ++ ".service"
	pollerserviceinstalled = pollerservicefile `File.hasContent`
		[ "[Unit]"
		, "Description=house poller"
		, ""
		, "[Service]"
		, "ExecStart=" ++ d ++ "/poller"
		, "WorkingDirectory=" ++ d
		, "User=joey"
		, "Group=joey"
		, "Restart=always"
		, ""
		, "[Install]"
		, "WantedBy=multi-user.target"
		, "WantedBy=house-controller.service"
		]
	controllerservicename = "house-controller"
	controllerservicefile = "/etc/systemd/system/" ++ controllerservicename ++ ".service"
	controllerserviceinstalled = controllerservicefile `File.hasContent`
		[ "[Unit]"
		, "Description=house controller"
		, ""
		, "[Service]"
		, "ExecStart=" ++ d ++ "/controller"
		, "WorkingDirectory=" ++ d
		, "User=joey"
		, "Group=joey"
		, "Restart=always"
		, ""
		, "[Install]"
		, "WantedBy=multi-user.target"
		]
	watchdogservicename = "house-watchdog"
	watchdogservicefile = "/etc/systemd/system/" ++ watchdogservicename ++ ".service"
	watchdogserviceinstalled = watchdogservicefile `File.hasContent`
		[ "[Unit]"
		, "Description=house watchdog"
		, ""
		, "[Service]"
		, "ExecStart=" ++ d ++ "/watchdog"
		, "WorkingDirectory=" ++ d
		, "User=root"
		, "Group=root"
		, "Restart=always"
		, ""
		, "[Install]"
		, "WantedBy=multi-user.target"
		]
	setupservicename = "house-setup"
	setupservicefile = "/etc/systemd/system/" ++ setupservicename ++ ".service"
	setupserviceinstalled = setupservicefile `File.hasContent`
		[ "[Unit]"
		, "Description=house setup"
		, ""
		, "[Service]"
		, "ExecStart=" ++ d ++ "/setup"
		, "WorkingDirectory=" ++ d
		, "User=root"
		, "Group=root"
		, "Type=oneshot"
		, ""
		, "[Install]"
		, "WantedBy=multi-user.target"
		, "WantedBy=house-poller.service"
		, "WantedBy=house-controller.service"
		, "WantedBy=house-watchdog.service"
		]
	-- Any changes to the rsync command will need my .authorized_keys
	-- rsync server command to be updated too.
	rsynccommand = "rsync -e 'ssh -i" ++ sshkeyfile ++ "' -avz rrds/ joey@kitenet.net:/srv/web/house.joeyh.name/rrds/ >/dev/null 2>&1"

	websitesymlink :: Property UnixLike
	websitesymlink = check (not . isSymbolicLink <$> getSymbolicLinkStatus "/var/www/html")
		(property "website symlink" $ makeChange $ do
			removeDirectoryRecursive "/var/www/html"
			createSymbolicLink d "/var/www/html"
		)

data Interfaces = Interfaces
	{ ethernetInterface :: String
	, wifiInterface :: String
	, wifiInterfaceOld :: String
	}

-- Connect to the starlink dish directly (no starlink router)
connectStarlinkDish :: Interfaces -> Property DebianLike
connectStarlinkDish ifs = propertyList "connected via starlink dish" $ props
	-- Use dhcpcd for ipv6 prefix delegation to the wifi interface.
	& Apt.installed ["dhcpcd-base"]
	& Apt.removed ["isc-dhcp-client"]
	& "/etc/dhcpcd.conf" `File.containsLine`
		("ia_pd 1 " ++ wifiInterface ifs)
	& "/etc/dhcpcd.conf" `File.lacksLine`
		("ia_pd 1 " ++ wifiInterfaceOld ifs)
	-- Avoid using dns servers sent by starlink.
	& "/etc/dhcpcd.conf" `File.containsLine`
		"nohook resolv.conf"
	& Network.dhcp (ethernetInterface ifs)
		`requires` Network.cleanInterfacesFile

-- Connect to the starlink router with its ethernet adapter.
--
-- Static route because with dhcp it sometimes fails to get an address from
-- starlink.
connectStarlinkRouter :: Interfaces -> Property DebianLike
connectStarlinkRouter ifs = propertyList "connected via starlink router" $ props
	& Network.static (ethernetInterface ifs) (IPv4 "192.168.1.62")
		(Just (Network.Gateway (IPv4 "192.168.1.1")))
		`requires` Network.cleanInterfacesFile

-- My home router, running hostapd and dnsmasq.
homeRouter :: String -> Interfaces -> String -> HostapdConfig -> Property DebianLike
homeRouter hn ifs wifinetworkname (HostapdConfig hostapdconfig) = propertyList "home router" $ props
	& File.notPresent (Network.interfaceDFile (wifiInterfaceOld ifs))
	& Network.static (wifiInterface ifs) (IPv4 "10.1.1.1") Nothing
		`requires` Network.cleanInterfacesFile
	& Apt.installed ["hostapd"]
	& File.hasContent "/etc/hostapd/hostapd.conf"
			([ "interface=" ++ wifiInterface ifs
			, "ssid=" ++ wifinetworkname
			] ++ hostapdconfig)
		`requires` File.dirExists "/etc/hostapd"
		`requires` File.hasContent "/etc/default/hostapd"
			[ "DAEMON_CONF=/etc/hostapd/hostapd.conf" ]
		`onChange` Service.running "hostapd"
	! Systemd.masked "hostapd"
	& Systemd.enabled "hostapd"
	& File.hasContent "/etc/resolv.conf.head"
		[ "domain kitenet.net"
		, "search kitenet.net"
		, "nameserver 8.8.8.8"
		, "nameserver 8.8.4.4"
		]
	& Apt.installed ["dnsmasq"]
	& File.hasContent "/etc/dnsmasq.conf"
		[ "domain-needed"
		, "bogus-priv"
		, "interface=" ++ wifiInterface ifs
		, "domain=lan"
		-- lease time is short because the house
		-- controller wants to know when clients disconnect
		, "dhcp-range=10.1.1.100,10.1.1.150,10m"
		, "no-hosts"
		, "address=/" ++ hn ++ "/10.1.1.1"
		, "address=/house.lan/10.1.1.2"
		-- allow accessing starlink dish when it's not online yet
		, "address=/dishy.starlink.com/192.168.100.1"
		]
		`onChange` Service.restarted "dnsmasq"
	-- Avoid DHCPNAK of lease obtained at boot, after NTP slews clock
	-- forward too far, causing that lease to not be valid.
	& "/etc/default/dnsmasq" `File.containsLine` "DNSMASQ_OPTS=\"--dhcp-authoritative\""
		`onChange` Service.restarted "dnsmasq"
	& ipmasq (wifiInterface ifs)
	& Apt.installed ["radvd"]
	-- This needs ipv6 prefix delegation to the wifi interface to be
	-- enabled.
	& File.hasContent "/etc/radvd.conf"
		[ "interface " ++ wifiInterface ifs ++ " {"
		, "  AdvSendAdvert on;"
		, "  MinRtrAdvInterval 3;"
		, "  MaxRtrAdvInterval 10;"
		, "  prefix ::/64 {"
		, "    AdvOnLink on;"
		, "    AdvAutonomous on;"
		, "    AdvRouterAddr on;"
		, "  };"
		, "};"
		]
		`onChange` Service.restarted "radvd"
	& "/etc/sysctl.d/ip-forwarding.conf" 
		`File.containsLine` "net.ipv6.conf.all.forwarding=1"
	-- Programs that I find useful on a router.
	& Apt.installed ["net-tools", "wireless-tools"]
	& Apt.installed ["mtr-tiny", "iftop", "screen", "nmap"]

-- | Enable IP masqerading, on whatever other interfaces come up, besides 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
	`requires` Apt.installed ["iptables"]
  where
	ifupscript = "/etc/network/if-up.d/ipmasq"
	scriptmode f = f `File.mode` combineModes (readModes ++ executeModes)

laptopSoftware :: Property DebianLike
laptopSoftware = Apt.installed
	[ "intel-microcode", "acpi"
	, "procmeter3", "xfce4", "procmeter3", "unclutter-xfixes"
	, "vlc", "mpv", "mplayer", "fbreader", "foliate", "firefox", "chromium"
	, "yt-dlp"
	, "libdatetime-event-sunrise-perl", "libtime-duration-perl"
	, "network-manager", "network-manager-openvpn-gnome", "openvpn"
	, "powertop"
	, "gimp", "gthumb", "inkscape", "xzgv", "hugin"
	, "mpc", "mpd", "ncmpc", "sonata", "mpdtoys"
	, "bsdgames", "nethack-console"
	, "xmonad", "libghc-xmonad-dev", "libghc-xmonad-contrib-dev"
	, "ttf-bitstream-vera", "fonts-symbola", "fonts-noto-color-emoji"
	, "mairix", "offlineimap", "mutt", "slrn"
	, "mtr", "nmap", "whois", "wireshark", "tcpdump", "iftop"
	, "pmount", "tree", "pv"
	, "hledger", "bc"
	, "apache2", "ikiwiki", "libhighlight-perl"
	, "avahi-daemon", "avahi-discover"
	, "pal", "ncal"
	, "yeahconsole", "xkbset", "xinput"
	, "assword", "pumpa"
	, "vorbis-tools", "audacity"
	, "bluez-firmware", "blueman", "pulseaudio-module-bluetooth" 
	, "pulseaudio-utils"
	, "fwupd"
	, "webext-ublock-origin-firefox", "webext-ublock-origin-chromium"
	, "vim-syntastic", "vim-fugitive"
	, "adb", "gthumb"
	, "w3m", "sm", "weechat"
	, "borgbackup", "wipe", "smartmontools", "libgfshare-bin"
	, "units"
	, "libnotify-bin"
	, "libmodbus-dev"
	, "qemu-kvm"
	]
	`requires` baseSoftware
	`requires` devSoftware

baseSoftware :: Property DebianLike
baseSoftware = Apt.installed
	[ "bash", "bash-completion", "vim", "screen", "less", "moreutils"
	, "git", "mr", "etckeeper", "git-annex", "ssh" --, "vim-vimoutliner"
	]

devSoftware :: Property DebianLike
devSoftware = Apt.installed
	[ "build-essential", "debhelper", "devscripts"
	, "ghc", "cabal-install", "haskell-stack"
	, "hasktags", "hlint"
	, "gdb", "time"
	, "dpkg-repack", "lintian"
	, "pristine-tar"
	]

cubieTruckOneWire :: Property DebianLike
cubieTruckOneWire = utilitysetup
	`requires` dtsinstalled
	`requires` utilityinstalled
  where
	dtsinstalled = File.hasContent "/etc/easy-peasy-devicetree-squeezy/my.dts" mydts
		`requires` File.dirExists "/etc/easy-peasy-devicetree-squeezy"
	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/sbin/easy-peasy-devicetree-squeezy" (File.LinkTarget "/usr/local/easy-peasy-devicetree-squeezy/easy-peasy-devicetree-squeezy")
		`requires` Apt.installed ["pv", "device-tree-compiler", "cpp", "linux-source"]
	utilitysetup = check (not <$> doesFileExist dtb) $ 
		cmdProperty "easy-peasy-devicetree-squeezy"
			["--debian", "sun7i-a20-cubietruck"]
			`assume` MadeChange
	dtb = "/etc/flash-kernel/dtbs/sun7i-a20-cubietruck.dtb"
	mydts =
		[ "/* Device tree addition enabling onewire sensors on CubieTruck GPIO pin PC21 */"
		, "#include <dt-bindings/gpio/gpio.h>"
		, ""
		, "/ {"
		, "\tonewire_device {"
		, "\t\tcompatible = \"w1-gpio\";"
		, "\t\tgpios = <&pio 2 21 GPIO_ACTIVE_HIGH>; /* PC21 */"
		, "\t\tpinctrl-names = \"default\";"
		, "\t\tpinctrl-0 = <&my_w1_pin>;"
		, "\t};"
		, "};"
		, ""
		, "&pio {"
		, "\tmy_w1_pin: my_w1_pin@0 {"
		, "\t\tallwinner,pins = \"PC21\";"
		, "\t\tallwinner,function = \"gpio_in\";"
		, "\t};"
		, "};"
		]

rsyncNetBorgRepo :: String -> [Borg.BorgRepoOpt] -> Borg.BorgRepo
rsyncNetBorgRepo d os = Borg.BorgRepoUsing os' ("2318@usw-s002.rsync.net:" ++ d)
  where
	-- rsync.net has a newer borg here
	os' = Borg.UsesEnvVar ("BORG_REMOTE_PATH", "borg1") : os

noExim :: Property DebianLike
noExim = Apt.removed ["exim4", "exim4-base", "exim4-daemon-light"]
	`onChange` Apt.autoRemove

data HostapdConfig = HostapdConfig [String]

hostapd2GhzConfig :: HostapdConfig
hostapd2GhzConfig = HostapdConfig
	[ "hw_mode=g"
	, "channel=5"
	, "country_code=US"
	, "ieee80211d=1"
	, "ieee80211n=1"
	, "wmm_enabled=1"
	]

-- For wifi adapters such as the Alfa AWUS036ACHM
--
-- Note that for maximum speed, this needs channel 5 or 6.
-- This should make it be capable of 300 Mb/s.
hostapd2GhzConfig_mt76 :: HostapdConfig
hostapd2GhzConfig_mt76 = HostapdConfig $ c ++ 
	[ "ht_capab=[HT40+][HT40-][GF][SHORT-GI-20][SHORT-GI-40]"
	]
  where
	HostapdConfig c = hostapd2GhzConfig

hostapd5GhzConfig :: HostapdConfig
hostapd5GhzConfig = HostapdConfig
	[ "hw_mode=a"
	, "channel=36"
	, "country_code=US"
	, "ieee80211d=1"
	, "ieee80211n=1"
	, "ieee80211ac=1"
	, "wmm_enabled=1"
	]

thinkPadLedsOff :: Property Linux
thinkPadLedsOff =
	"/etc/systemd/system/thinkpad_leds_off.service" `File.hasContent`
		[ "[Unit]"
		, "After=suspend.target"
		, "[Service]"
		, "Type=simple"
		, "ExecStart=sh -c 'echo 0 > /sys/devices/platform/thinkpad_acpi/leds/tpacpi::lid_logo_dot/brightness; echo 0 > /sys/devices/platform/thinkpad_acpi/leds/tpacpi::power/brightness'"
		, "[Install]"
		, "WantedBy=suspend.target"
		]
		`onChange` (Systemd.daemonReloaded
			`before` Systemd.enabled "thinkpad_leds_off")

