Initial version.
authorMatt Birkholz <matt@birchwood-abbey.net>
Sun, 17 Dec 2023 23:24:06 +0000 (16:24 -0700)
committerMatt Birkholz <matt@birchwood-abbey.net>
Sun, 17 Dec 2023 23:24:06 +0000 (16:24 -0700)
44 files changed:
.gitignore [new file with mode: 0644]
.gitmodules [new file with mode: 0644]
Institute [new submodule]
README.html [new file with mode: 0644]
README.org [new file with mode: 0644]
abbey [new file with mode: 0755]
ansible.cfg [new file with mode: 0644]
hosts [new file with mode: 0644]
jquery.js [new symlink]
org.css [new symlink]
org.js [new symlink]
playbooks/check-inst-vars.yml [new file with mode: 0644]
playbooks/reboots.yml [new file with mode: 0644]
playbooks/site.yml [new file with mode: 0644]
playbooks/timezone.yml [new file with mode: 0644]
playbooks/upgrade.yml [new file with mode: 0644]
playbooks/versarch.yml [new file with mode: 0644]
private_ex/vars-abbey.yml [new file with mode: 0644]
public/vars.yml [new file with mode: 0644]
publish [new file with mode: 0755]
publish.el [new file with mode: 0644]
roles_t/abbey-cloister/handlers/main.yml [new file with mode: 0644]
roles_t/abbey-cloister/tasks/main.yml [new file with mode: 0644]
roles_t/abbey-core/files/abbey_pisensors [new file with mode: 0644]
roles_t/abbey-core/handlers/main.yml [new file with mode: 0644]
roles_t/abbey-core/tasks/main.yml [new file with mode: 0644]
roles_t/abbey-core/templates/nagios-devaron.cfg [new file with mode: 0644]
roles_t/abbey-core/templates/nagios-kamino.cfg [new file with mode: 0644]
roles_t/abbey-core/templates/nagios-kessel.cfg [new file with mode: 0644]
roles_t/abbey-dvr/handlers/main.yml [new file with mode: 0644]
roles_t/abbey-dvr/tasks/main.yml [new file with mode: 0644]
roles_t/abbey-front/files/certbot_logrotate [new file with mode: 0644]
roles_t/abbey-front/files/cron.daily_letsencrypt [new file with mode: 0644]
roles_t/abbey-front/files/logrotate-mailer [new file with mode: 0644]
roles_t/abbey-front/files/logrotate-mailer.conf [new file with mode: 0644]
roles_t/abbey-front/handlers/main.yml [new file with mode: 0644]
roles_t/abbey-front/tasks/main.yml [new file with mode: 0644]
roles_t/abbey-tvr/handlers/main.yml [new file with mode: 0644]
roles_t/abbey-tvr/tasks/main.yml [new file with mode: 0644]
roles_t/abbey-tvr/templates/mythweb.conf.j2 [new file with mode: 0644]
roles_t/abbey-weather/files/daemon-anoat [new file with mode: 0644]
roles_t/abbey-weather/handlers/main.yml [new file with mode: 0644]
roles_t/abbey-weather/tasks/main.yml [new file with mode: 0644]
roles_t/abbey-weather/templates/weather-daemon.j2 [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..52df594
--- /dev/null
@@ -0,0 +1,4 @@
+/private
+/roles/
+/Secret*
+/mythtv-ansible/
diff --git a/.gitmodules b/.gitmodules
new file mode 100644 (file)
index 0000000..2c3ed3c
--- /dev/null
@@ -0,0 +1,3 @@
+[submodule "Institute"]
+       path = Institute
+       url = ./Institute/
diff --git a/Institute b/Institute
new file mode 160000 (submodule)
index 0000000..e23b88a
--- /dev/null
+++ b/Institute
@@ -0,0 +1 @@
+Subproject commit e23b88ab267abf73db7fcc5d678ac26e4829eb26
diff --git a/README.html b/README.html
new file mode 100644 (file)
index 0000000..1015cd3
--- /dev/null
@@ -0,0 +1,5047 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+<head>
+<!-- 2023-12-17 Sun 16:05 -->
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1" />
+<title>Birchwood Abbey Networks</title>
+<meta name="author" content="Matt Birkholz" />
+<meta name="generator" content="Org Mode" />
+<link rel="stylesheet" href="org.css">
+<script language="javascript" type="text/javascript" src="jquery.js"></script>
+<script language="javascript" type="text/javascript" src="org.js"></script>
+</head>
+<body>
+<div id="content" class="content">
+<h1 class="title">Birchwood Abbey Networks</h1>
+<p>
+The abbey's network services are configured by Ansible scripts based
+on <a href="Institute/README.html">A Small Institute</a>.  The institutional roles like <code>core</code>, <code>gate</code> and
+<code>front</code> are intended for general use and so are kept free of abbey
+idiosyncrasies.  The roles herein are abbey specific, emphasized by
+the <code>abbey-</code> prefix on their names.  These roles are applied <i>after</i>
+the generic institutional roles (again, documented <a href="Institute/README.html">here</a>).
+</p>
+<div id="outline-container-orgc282e28" class="outline-2">
+<h2 id="orgc282e28"><span class="section-number-2">1.</span> Overview</h2>
+<div class="outline-text-2" id="text-1">
+<p>
+A Small Institute makes security and privacy top priorities but
+Birchwood Abbey approaches these from a particularly Elvish viewpoint.
+Elves depend for survival on speed, agility, and concealment.  Working
+toward those ends (esp. the last) Birchwood Abbey's network topology
+was designed to look like that of an average Amerikan household.
+Korporate Amerika expects our ISP to provide us with a
+Wi-Fi/router/modem that all of our appliances can use to communicate
+amongst themselves in a cliquey, New World Order IoT kumbaya.  We dare
+not disappoint.
+</p>
+
+<p>
+Thus Samsung (our refrigerator) is able to browse for our printer or
+connect to Kroger (our grocer) or Kaiser (our health care provider)
+for whatever reason (presumably to report on our eating habits).  The
+only suspicious character in this Amerikan household will be Gate, a
+Raspberry Pi passing many encrypted packets.  Thus when the New World
+Police come a-knock'n (i.e. after they kick the door and kill the dog)
+we might still hold onto some plausible deniability.
+</p>
+
+<p>
+To most look like our neighbors we sit between our smart TVs and our
+smart refrigerators and <i>consciously</i> play the flaccid consumer
+streaming Amazon and watching Blu-ray discs.  This works because we
+have preserved a means of escape.  We may not be able to hide our
+entertainment choices nor even eating habits anymore, but we can
+still just turn it all off and retreat into private correspondence
+between Inner Citadels.
+</p>
+
+<p>
+The small institute tries to look "normal" too so the abbey's network
+map is very similar, with differences mainly in terminology,
+philosophy, attitude.
+</p>
+
+<pre class="example" id="orga98e579">
+                |                                                   
+                =                                                   
+              _|||_                                                 
+      ----- The Temple-----                                         
+          =   =   =   =                                             
+          =   =   =   =                                             
+        =====-Front-=====                                           
+                |                                                   
+        -----------------                                           
+      (                   )                                         
+     (   The Internet(s)   )----(Hotel Wi-Fi)                       
+      (                   )         |                               
+        -----------------           |                               
+                |                   +----Monk's notebook abroad     
+                |                                                   
+=============== | ==================================================
+                |                                           Premises
+           (House ISP)                                              
+                |                                                   
+                |            +----Monk's notebook in the house      
+                |            +----Samsung refrigerator              
+                |            +----Sony Bluray                       
+                |            +----Lexmark printer                   
+                |            |                                      
+                | +----(House Wi-Fi)                                
+                | |                                  Game of Thrones
+============== Gate ================================================
+                |                                           Cloister
+                +----Ethernet switch                                
+                        |                                           
+                        +----Core                                   
+                        +----Security DVR                           
+                        +----IP camera(s)                           
+                        +----HDTV TVR                               
+                        +----WebTV                                  
+</pre>
+</div>
+</div>
+<div id="outline-container-orgbd0fb96" class="outline-2">
+<h2 id="orgbd0fb96"><span class="section-number-2">2.</span> The Abbey Particulars</h2>
+<div class="outline-text-2" id="text-2">
+<p>
+The abbey's public particulars are included below.  They are the
+public particulars of a small institute, nothing more.  As for the
+abbey's private data, examples (only! ;-) are included in the
+following chapters.
+</p>
+
+<div class="org-src-container">
+<q>public/vars.yml</q><pre class="src src-conf">---
+domain_name: birchwood-abbey.net
+domain_priv: birchwood.private
+
+full_name: Birchwood Abbey
+
+front_addr: 159.65.75.60
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-orgf21aecc" class="outline-2">
+<h2 id="orgf21aecc"><span class="section-number-2">3.</span> The Abbey Front Role</h2>
+<div class="outline-text-2" id="text-3">
+<p>
+Birchwood Abbey's front door is a Digital Ocean Droplet configured as
+A Small Institute Front.  Thus it is already serving a public web site
+with Apache2, spooling email with Postfix and serving it with
+Dovecot-IMAPd, and hosting a VPN with OpenVPN.
+</p>
+</div>
+<div id="outline-container-org0d60f6a" class="outline-3">
+<h3 id="org0d60f6a"><span class="section-number-3">3.1.</span> Install Emacs</h3>
+<div class="outline-text-3" id="text-3-1">
+<p>
+The monks of the abbey are masters of the staff (bo) and Emacs.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-front/tasks/main.yml</q><pre class="src src-conf">---
+- name: Install Emacs.
+  become: yes
+  <span class="org-variable-name">apt: pkg</span>=emacs
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org0d682e6" class="outline-3">
+<h3 id="org0d682e6"><span class="section-number-3">3.2.</span> Configure Public Email Aliases</h3>
+<div class="outline-text-3" id="text-3-2">
+<p>
+The abbey uses several additional email aliases.  These are the public
+mailboxes <code>@birchwood-abbey.net</code>.  The institute already funnels the
+common mailboxes like <code>postmaster</code> and <code>admin</code> into <code>root</code> and <code>root</code>
+to the machine's privileged account (<code>sysadm</code>).  The abbey takes it
+from there, forwarding <code>sysadm</code> to a real person.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-front/tasks/main.yml</q><pre class="src src-conf">
+- name: Install abbey email aliases.
+  become: yes
+  blockinfile:
+    block: |
+        sysadm:         matt
+        keymaster:      root
+        codemaster:     matt
+        all:            matt, lori, erica
+        elders:         matt, lori
+        rents:          elders
+        puck:           matt
+        abbess:         lori
+    dest: /etc/aliases
+    marker: <span class="org-string">"# {mark} ABBEY MANAGED BLOCK"</span>
+  notify: New aliases.
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>roles_t/abbey-front/handlers/main.yml</q><pre class="src src-conf">---
+- name: New aliases.
+  become: yes
+  command: newaliases
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org31d6bbb" class="outline-3">
+<h3 id="org31d6bbb"><span class="section-number-3">3.3.</span> Configure Git Daemon on Front</h3>
+<div class="outline-text-3" id="text-3-3">
+<p>
+The abbey publishes member Git repositories with <code>git-daemon</code>.  If
+Dick (a member of A Small Institute) builds a Foo project Git
+repository in <q>~/foo/</q>, he can publish it to the campus by
+symbolically linking its <q>.git/</q> into <q>~/Public/Git/</q> on Core.  If the
+repository is world readable and contains a <q>git-daemon-export-ok</q>
+file, it will be served at <q>git://www/~dick/foo</q>.
+</p>
+
+<pre class="example">
+touch ~/foo/.git/git-daemon-export-ok
+ln -s ~/foo/.git ~/Public/Git/foo
+chmod -R o+r ~/foo/.git
+find ~/foo/.git -type d -print0 | xargs -0 chmod o+rx
+</pre>
+
+
+<p>
+User repositories can be made available to the public at a URL like
+<code>git://small.example.org/~dick/foo</code> by copying it to the same path on
+Front (<q>~dick/Public/Git/foo/</q>).  The following <code>rsync</code> command
+creates or updates such a copy.
+</p>
+
+<pre class="example">
+rsync -av ~/foo/.git/ small.example.org:Public/Git/foo/
+</pre>
+
+
+<p>
+Note that Dick's Git repository, mirrored to Front (or Core), does not
+need to be backed up, assuming Dick's home directory (including
+<q>~/foo/</q>) <i>is</i>.  If updates are git-pushed to a repository on Front,
+regular backups should be made, but this is Dick's responsibility.
+There are no regular, system backups on Front.
+</p>
+
+<pre class="example">
+rsync -av --del small.institute.org:Public/foo/ ~/Public/foo/
+</pre>
+
+
+<p>
+With SystemD and the <code>git-daemon-sysvinit</code> package installed, SystemD
+supervises a <code>git-daemon</code> service unit launched with
+<code>/etc/init.d/git-daemon</code>.  The old SysV <code>init</code> script gets its
+configuration from the customary <q>/etc/default/git-daemon</q> file.  The
+script then constructs the appropriate <code>git-daemon</code> command.  The
+<code>git-daemon(1)</code> manual page explains the command options in detail.
+As explained in <q>/usr/share/doc/git-daemon-sysvinit/README.Debian</q>,
+the service must be enabled by setting <code>GIT_DAEMON_ENABLE</code> to <code>true</code>.
+The base path is also changed to agree with <q>gitweb.cgi</q>.
+</p>
+
+<p>
+User repositories are enabled by adding a <code>user-path</code> option <i>and</i>
+disabling the default whitelist.  To specify an empty whitelist, the
+default (a list of one directory: <q>/var/lib/git</q>) must be avoided by
+setting <code>GIT_DAEMON_DIRECTORY</code> to a blank (not empty) string.
+</p>
+
+<p>
+The code below is included in both Front and Core configurations,
+which should be nearly identical for testing purposes.  Rather than
+factor out small roles like <code>abbey-git-server</code>, Emacs Org Mode's Noweb
+support does the duplication, by multiple references to code blocks
+like <code>git-tasks</code> and <code>git-handlers</code>.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-front/tasks/main.yml</q><pre class="src src-conf">
+- name: Install git daemon.
+  become: yes
+  <span class="org-variable-name">apt: pkg</span>=git-daemon-sysvinit
+
+- name: Configure git daemon.
+  become: yes
+  lineinfile:
+    path: /etc/default/git-daemon
+    regexp: <span class="org-string">"{{ item.patt }}"</span>
+    line: <span class="org-string">"{{ item.line }}"</span>
+  loop:
+  - patt: <span class="org-string">'^GIT_DAEMON_ENABLE *='</span>
+    line: <span class="org-string">'GIT_DAEMON_ENABLE=true'</span>
+  - patt: <span class="org-string">'^GIT_DAEMON_OPTIONS *='</span>
+    line: <span class="org-string">'GIT_DAEMON_OPTIONS="--user-path=Public/Git"'</span>
+  - patt: <span class="org-string">'^GIT_DAEMON_BASE_PATH *='</span>
+    line: <span class="org-string">'GIT_DAEMON_BASE_PATH="/var/www/git"'</span>
+  - patt: <span class="org-string">'^GIT_DAEMON_DIRECTORY *='</span>
+    line: <span class="org-string">'GIT_DAEMON_DIRECTORY=" "'</span>
+  notify: Restart git daemon.
+
+- name: Create /var/www/git/.
+  become: yes
+  file:
+    path: /var/www/git
+    state: directory
+    group: staff
+    <span class="org-variable-name">mode: u</span>=rwx,g=srwx,o=rx
+</pre>
+</div>
+
+<div class="org-src-container">
+<code>git-tasks</code><pre class="src src-conf" id="orgdf3814b">- name: Install git daemon.
+  become: yes
+  <span class="org-variable-name">apt: pkg</span>=git-daemon-sysvinit
+
+- name: Configure git daemon.
+  become: yes
+  lineinfile:
+    path: /etc/default/git-daemon
+    regexp: <span class="org-string">"{{ item.patt }}"</span>
+    line: <span class="org-string">"{{ item.line }}"</span>
+  loop:
+  - patt: <span class="org-string">'^GIT_DAEMON_ENABLE *='</span>
+    line: <span class="org-string">'GIT_DAEMON_ENABLE=true'</span>
+  - patt: <span class="org-string">'^GIT_DAEMON_OPTIONS *='</span>
+    line: <span class="org-string">'GIT_DAEMON_OPTIONS="--user-path=Public/Git"'</span>
+  - patt: <span class="org-string">'^GIT_DAEMON_BASE_PATH *='</span>
+    line: <span class="org-string">'GIT_DAEMON_BASE_PATH="/var/www/git"'</span>
+  - patt: <span class="org-string">'^GIT_DAEMON_DIRECTORY *='</span>
+    line: <span class="org-string">'GIT_DAEMON_DIRECTORY=" "'</span>
+  notify: Restart git daemon.
+
+- name: Create /var/www/git/.
+  become: yes
+  file:
+    path: /var/www/git
+    state: directory
+    group: staff
+    <span class="org-variable-name">mode: u</span>=rwx,g=srwx,o=rx
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>roles_t/abbey-front/handlers/main.yml</q><pre class="src src-conf">
+
+- name: Restart git daemon.
+  become: yes
+  command: systemctl restart git-daemon
+</pre>
+</div>
+
+<div class="org-src-container">
+<code>git-handlers</code><pre class="src src-conf" id="org76516ba">
+- name: Restart git daemon.
+  become: yes
+  command: systemctl restart git-daemon
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-orgdf93aec" class="outline-3">
+<h3 id="orgdf93aec"><span class="section-number-3">3.4.</span> Configure Gitweb on Front</h3>
+<div class="outline-text-3" id="text-3-4">
+<p>
+The abbey provides an HTML interface to members' public Git
+repositories using <code>gitweb.cgi</code>, one of the few CGI scripts allowed on
+Front.  Unlike the Git daemon, the Gitweb interface does <i>not</i> care if
+the repository contains a <q>git-daemon-export-ok</q> file.
+</p>
+
+<p>
+Again Front and Core need to be configured congruently, so the
+necessary Apache directives are given here and referenced in the
+Apache configurations.
+</p>
+
+<p>
+Like the suggested per-user rewrite rule in the <code>gitweb(1)</code> manual
+page, the second <code>RewriteRule</code> specifies the root directory of the
+user's public Git repositories via the <code>GITWEB_PROJECTROOT</code>
+environment variable.  It makes <code>http://www/~dick/gitweb.cgi</code> run
+Gitweb with the project root <q>~dick/Public/Git/</q>, the same directory
+the <code>git-daemon</code> makes available.  The first <code>RewriteRule</code> directs
+URLs with no user name to the default.  Thus <code>http://www/gitweb.cgi</code>
+lists the repositories found in <q>/var/www/git/</q>.  The match patterns
+of both rules recognize <q>/gitweb</q> as well as <q>/gitweb.cgi</q>.
+</p>
+
+<div class="org-src-container">
+<code>apache-gitweb</code><pre class="src src-conf" id="org119fc95">
+Alias /gitweb-static/ /usr/share/gitweb/static/
+&lt;Directory <span class="org-string">"/usr/share/gitweb/static/"</span>&gt;
+    Options MultiViews
+&lt;/Directory&gt;
+RewriteEngine on
+RewriteRule ^/gitweb(\.cgi)?(/.*)?$ \
+            /cgi-bin/gitweb.cgi$2 [QSA,L,PT]
+RewriteRule ^/\~([^\/]+)/gitweb(\.cgi)?(/.*)?$ \
+            /cgi-bin/gitweb.cgi$3 \
+            [<span class="org-type">QSA,E=GITWEB_PROJECTROOT:/home/$1/Public/Git/,L,PT</span>]
+</pre>
+</div>
+
+<p>
+The <code>RewriteRule</code> flags used here are:
+</p>
+
+<dl class="org-dl">
+<dt>QSA | qsappend </dt><dd>Append the request's query string.</dd>
+<dt>E= | env</dt><dd>Set or unset an environment variable.</dd>
+<dt>L | last</dt><dd>Stop with this Last rule.</dd>
+<dt>PT | passthrugh</dt><dd>Treat the result as a URI, not a file path.</dd>
+</dl>
+
+<p>
+The <code>RewriteEngine on</code> directive must be included in the virtual host
+or no rewriting will take place.
+</p>
+
+<p>
+The CGI script and <code>RewriteRule</code> require Apache's <code>cgi</code> and <code>rewrite</code>
+modules, which are not normally enabled on a small institute's public
+server.  Thus they need to be enabled here.  Note that Debian and
+-Ubuntu install different Apache MPMs (multi-processing modules)
+-requiring different CGI modules, turning two tasks into three.
+</p>
+
+<p>
+The script uses the <code>CGI</code> Perl module, which must be installed.
+</p>
+
+<p>
+The rewrite rule maps to the URL <q>/cgi-bin/gitweb.cgi</q>, which is
+mapped by default to <q>/usr/lib/cgi-bin/gitweb.cgi</q>.  The <code>git</code> package
+installs <q>gitweb.cgi</q> in <q>/usr/share/gitweb/</q>, so it and its related
+<q>index.cgi</q> script are linked into <q>/usr/lib/cgi-bin/</q>.
+</p>
+
+<p>
+The <q>static/</q> directory, also installed in <q>/usr/share/gitweb/</q>, is
+made available as <code>http://www/gitweb-static/</code> via an <code>Alias</code>
+directive.  The global Perl configuration file, <q>/etc/gitweb.conf</q>,
+overrides the relative URLs Gitweb normally generates, and uses the
+web site <q>/favicon.ico</q>.
+</p>
+
+<div class="org-src-container">
+<code>apache-gitweb-tasks</code><pre class="src src-conf" id="orgd2fd2f5">- name: Enable Apache2 rewrite module for Gitweb.
+  become: yes
+  <span class="org-variable-name">apache2_module: name</span>=rewrite
+  notify: Restart Apache2.
+
+- name: Enable Apache2 cgid module for Gitweb (Ubuntu).
+  become: yes
+  <span class="org-variable-name">apache2_module: name</span>=cgid
+  <span class="org-variable-name">when: ansible_distribution</span> == <span class="org-string">'Ubuntu'</span>
+  notify: Restart Apache2.
+
+- name: Enable Apache2 cgi module for Gitweb (Debian).
+  become: yes
+  <span class="org-variable-name">apache2_module: name</span>=cgi
+  <span class="org-variable-name">when: ansible_distribution</span> == <span class="org-string">'Debian'</span>
+  notify: Restart Apache2.
+
+- name: Install libcgi-pm-perl for Gitweb.
+  become: yes
+  <span class="org-variable-name">apt: pkg</span>=libcgi-pm-perl
+
+- name: Link Gitweb into /cgi-bin/.
+  become: yes
+  file:
+    state: link
+    path: /usr/lib/cgi-bin/{{ item }}
+    src: /usr/share/gitweb/{{ item }}
+  loop: [ gitweb.cgi, index.cgi ]
+
+- name: Override Gitweb assets location.
+  become: yes
+  copy:
+    content: |
+      <span class="org-variable-name">$projectroot</span> = $ENV{<span class="org-string">'GITWEB_PROJECTROOT'</span>} || <span class="org-string">"/var/www/git"</span>;
+      <span class="org-variable-name">@stylesheets</span> = (<span class="org-string">"/gitweb-static/gitweb.css"</span>);
+      <span class="org-variable-name">$logo</span> = <span class="org-string">"/gitweb-static/git-logo.png"</span>;
+      <span class="org-variable-name">$favicon</span> = <span class="org-string">"/favicon.ico"</span>;
+      <span class="org-variable-name">$javascript</span> = <span class="org-string">"/gitweb-static/gitweb.js"</span>;
+    dest: /etc/gitweb.conf
+    <span class="org-variable-name">mode: u</span>=rw,g=r,o=r
+</pre>
+</div>
+
+<div class="org-src-container">
+<code>apache-gitweb-handlers</code><pre class="src src-conf" id="org7715d62">- name: Restart Apache2.
+  become: yes
+  systemd:
+    service: apache2
+    state: restarted
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-orga0317b1" class="outline-3">
+<h3 id="orga0317b1"><span class="section-number-3">3.5.</span> Configure CGit on Front</h3>
+<div class="outline-text-3" id="text-3-5">
+<p>
+CGit is handled similarly, modifying <q>/etc/cgitrc</q> to reference a
+<code>CGIT_SCANPATH</code> environment variable set by Apache re-write rules.
+The resulting Apache directives are given in <code>apache-cgit</code> and the
+Ansible tasks in <code>apache-cgit-tasks</code>, for both Front and Core.
+</p>
+
+<div class="org-src-container">
+<code>apache-cgit</code><pre class="src src-conf" id="org9ae034e">
+ScriptAlias /cgit/ /usr/lib/cgit/cgit.cgi/
+Alias /cgit-css/ /usr/share/cgit/
+&lt;Directory <span class="org-string">"/usr/lib/cgit/"</span>&gt;
+   AllowOverride None
+   Options ExecCGI FollowSymlinks
+   Require all granted
+&lt;/Directory&gt;
+RewriteRule ^/cgit?(/.*)$ \
+            <span class="org-variable-name">/cgit$1 [QSA,E</span>=CGIT_SCANPATH:/var/www/git/,L,PT]
+RewriteRule ^/\~([^\/]+)/cgit(/.*)?$ \
+            <span class="org-variable-name">/cgit$2 [QSA,E</span>=CGIT_SCANPATH:/home/$1/Public/Git/,L,PT]
+</pre>
+</div>
+
+<div class="org-src-container">
+<code>apache-cgi-tasks</code><pre class="src src-conf" id="org50bf153">
+- name: Install CGit.
+  become: yes
+  <span class="org-variable-name">apt: pkg</span>=cgit
+
+- name: Disable CGit default configuration.
+  become: yes
+  command:
+    cmd: a2disconf -q cgit
+    removes: /etc/apache2/conf-enabled/cgit.conf
+
+- name: Override CGit scan path.
+  become: yes
+  lineinfile:
+    path: /etc/cgitrc
+    regexp: <span class="org-string">"^scan-path *="</span>
+    line: <span class="org-string">"scan-path=$CGIT_SCANPATH"</span>
+  notify: Reload Apache2.
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-orge8ea40f" class="outline-3">
+<h3 id="orge8ea40f"><span class="section-number-3">3.6.</span> Configure Apache for Abbey Documentation</h3>
+<div class="outline-text-3" id="text-3-6">
+<p>
+Some of the directives added to the <q>-vhost.conf</q> file are needed by
+the abbey's documentation, published at
+<a href="https://birchwood-abbey.net/Abbey/">https://birchwood-abbey.net/Abbey/</a>.  The following template uses a
+<code>docroot</code> variable for the actual path to the HTML.  On Front this
+variable is set to <q>/home/www</q>.  The same template is used on Core, to
+ensure matching configurations for accurate previews and tests.
+</p>
+
+<p>
+The abbey's network documentation currently uses automatic directory
+indexes, and declares the types of files with several additional
+filename suffixes.
+</p>
+
+<div class="org-src-container">
+<code>apache-abbey</code><pre class="src src-conf" id="org158c789">&lt;Directory {{ docroot }}/Abbey/&gt;
+    AllowOverride Indexes FileInfo
+    Options +Indexes +FollowSymLinks
+&lt;/Directory&gt;
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org1a605db" class="outline-3">
+<h3 id="org1a605db"><span class="section-number-3">3.7.</span> Configure Photos URLs on Front</h3>
+<div class="outline-text-3" id="text-3-7">
+<p>
+Some of the directives added to the <q>-vhost.conf</q> file map the abbey's
+abstract photo URLs, e.g. <q>/Photos/2022/08/06/</q>, into actual file
+paths.  The following template uses the <code>docroot</code> variable introduced
+in the previous section.  On Front this variable is set to
+<q>/home/www</q>.  The same template is used on Core, to ensure
+matching configurations for accurate previews and tests.
+</p>
+
+<div class="org-src-container">
+<code>apache-photos</code><pre class="src src-conf" id="org5e1b247">
+RedirectMatch /Photos$ /Photos/
+RedirectMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])$ \
+              /Photos/$1_$2_$3/
+AliasMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])/(.+)$ \
+           {{ docroot }}/Photos/$1/$2/$3/$4
+AliasMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])/$ \
+           {{ docroot }}/Photos/$1/$2/$3/index.html
+AliasMatch /Photos/$ {{ docroot }}/Photos/index.html
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org2ab9cdd" class="outline-3">
+<h3 id="org2ab9cdd"><span class="section-number-3">3.8.</span> Configure Apache on Front</h3>
+<div class="outline-text-3" id="text-3-8">
+<p>
+The abbey needs to add some Apache2 configuration directives to the
+virtual host listening for HTTPS requests to <q>birchwood-abbey.net</q>.
+Luckily there is support for this in the institutional configuration.
+The abbey simply creates a <q>birchwood-abbey.net-vhost.conf</q> file in
+<q>/etc/apache2/sites-available/</q>.
+</p>
+
+<p>
+The following task adds the <a href="#org158c789"><code>apache-abbey</code></a>, <a href="#org5e1b247"><code>apache-photos</code></a>,
+<a href="#org119fc95"><code>apache-gitweb</code></a>, and <a href="#org9ae034e"><code>apache-cgit</code></a> directives described above to the
+<q>-vhost.conf</q> file, and includes <q>options-ssl-apache.conf</q> from
+<q>/etc/letsencrypt/</q>.  The rest of the Let's Encrypt configuration is
+discussed in the following <a href="#orgdc68478">Install Let's Encrypt</a> section.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-front/tasks/main.yml</q><pre class="src src-conf">
+- name: Configure Apache.
+  become: yes
+  vars:
+    docroot: /home/www
+  copy:
+    content: |
+        &lt;Directory {{ docroot }}/Abbey/&gt;
+            AllowOverride Indexes FileInfo
+            Options +Indexes +FollowSymLinks
+        &lt;/Directory&gt;
+
+        RedirectMatch /Photos$ /Photos/
+        RedirectMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])$ \
+                      /Photos/$1_$2_$3/
+        AliasMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])/(.+)$ \
+                   {{ docroot }}/Photos/$1/$2/$3/$4
+        AliasMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])/$ \
+                   {{ docroot }}/Photos/$1/$2/$3/index.html
+        AliasMatch /Photos/$ {{ docroot }}/Photos/index.html
+
+        Alias /gitweb-static/ /usr/share/gitweb/static/
+        &lt;Directory <span class="org-string">"/usr/share/gitweb/static/"</span>&gt;
+            Options MultiViews
+        &lt;/Directory&gt;
+        RewriteEngine on
+        RewriteRule ^/gitweb(\.cgi)?(/.*)?$ \
+                    /cgi-bin/gitweb.cgi$2 [QSA,L,PT]
+        RewriteRule ^/\~([^\/]+)/gitweb(\.cgi)?(/.*)?$ \
+                    /cgi-bin/gitweb.cgi$3 \
+                    [<span class="org-type">QSA,E=GITWEB_PROJECTROOT:/home/$1/Public/Git/,L,PT</span>]
+
+        ScriptAlias /cgit/ /usr/lib/cgit/cgit.cgi/
+        Alias /cgit-css/ /usr/share/cgit/
+        &lt;Directory <span class="org-string">"/usr/lib/cgit/"</span>&gt;
+           AllowOverride None
+           Options ExecCGI FollowSymlinks
+           Require all granted
+        &lt;/Directory&gt;
+        RewriteRule ^/cgit?(/.*)$ \
+                    <span class="org-variable-name">/cgit$1 [QSA,E</span>=CGIT_SCANPATH:/var/www/git/,L,PT]
+        RewriteRule ^/\~([^\/]+)/cgit(/.*)?$ \
+                    <span class="org-variable-name">/cgit$2 [QSA,E</span>=CGIT_SCANPATH:/home/$1/Public/Git/,L,PT]
+        IncludeOptional /etc/letsencrypt/options-ssl-apache.conf
+    dest: /etc/apache2/sites-available/{{ domain_name }}-vhost.conf
+  notify: Restart Apache2.
+
+- name: Enable Apache2 rewrite module for Gitweb.
+  become: yes
+  <span class="org-variable-name">apache2_module: name</span>=rewrite
+  notify: Restart Apache2.
+
+- name: Enable Apache2 cgid module for Gitweb (Ubuntu).
+  become: yes
+  <span class="org-variable-name">apache2_module: name</span>=cgid
+  <span class="org-variable-name">when: ansible_distribution</span> == <span class="org-string">'Ubuntu'</span>
+  notify: Restart Apache2.
+
+- name: Enable Apache2 cgi module for Gitweb (Debian).
+  become: yes
+  <span class="org-variable-name">apache2_module: name</span>=cgi
+  <span class="org-variable-name">when: ansible_distribution</span> == <span class="org-string">'Debian'</span>
+  notify: Restart Apache2.
+
+- name: Install libcgi-pm-perl for Gitweb.
+  become: yes
+  <span class="org-variable-name">apt: pkg</span>=libcgi-pm-perl
+
+- name: Link Gitweb into /cgi-bin/.
+  become: yes
+  file:
+    state: link
+    path: /usr/lib/cgi-bin/{{ item }}
+    src: /usr/share/gitweb/{{ item }}
+  loop: [ gitweb.cgi, index.cgi ]
+
+- name: Override Gitweb assets location.
+  become: yes
+  copy:
+    content: |
+      <span class="org-variable-name">$projectroot</span> = $ENV{<span class="org-string">'GITWEB_PROJECTROOT'</span>} || <span class="org-string">"/var/www/git"</span>;
+      <span class="org-variable-name">@stylesheets</span> = (<span class="org-string">"/gitweb-static/gitweb.css"</span>);
+      <span class="org-variable-name">$logo</span> = <span class="org-string">"/gitweb-static/git-logo.png"</span>;
+      <span class="org-variable-name">$favicon</span> = <span class="org-string">"/favicon.ico"</span>;
+      <span class="org-variable-name">$javascript</span> = <span class="org-string">"/gitweb-static/gitweb.js"</span>;
+    dest: /etc/gitweb.conf
+    <span class="org-variable-name">mode: u</span>=rw,g=r,o=r
+
+- name: Install CGit.
+  become: yes
+  <span class="org-variable-name">apt: pkg</span>=cgit
+
+- name: Disable CGit default configuration.
+  become: yes
+  command:
+    cmd: a2disconf -q cgit
+    removes: /etc/apache2/conf-enabled/cgit.conf
+
+- name: Override CGit scan path.
+  become: yes
+  lineinfile:
+    path: /etc/cgitrc
+    regexp: <span class="org-string">"^scan-path *="</span>
+    line: <span class="org-string">"scan-path=$CGIT_SCANPATH"</span>
+  notify: Reload Apache2.
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>roles_t/abbey-front/handlers/main.yml</q><pre class="src src-conf">
+- name: Restart Apache2.
+  become: yes
+  systemd:
+    service: apache2
+    state: restarted
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org70e49eb" class="outline-3">
+<h3 id="org70e49eb"><span class="section-number-3">3.9.</span> Configure Apache Log Archival</h3>
+<div class="outline-text-3" id="text-3-9">
+<p>
+These tasks hack Apache's <code>logrotate(8)</code> configuration to rotate
+weekly, keep the last 12 weeks, and email each week's log to <code>root</code>.
+The <code>logrotate(8)</code> manual page explains the configuration options.
+</p>
+
+<p>
+The Systemd configuration drop tells <code>logrotate</code> to use a special
+script for its mail program.  Postfix's <code>mail</code> work-alike did not take
+the subject as a command line argument as provided by <code>logrotate</code>.
+The replacement <q>logrotate-mailer</q> does, and includes it in a
+<code>Subject</code> header prepended to <code>logrotate</code>'s message.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-front/tasks/main.yml</q><pre class="src src-conf">
+- name: Configure Apache log archival.
+  become: yes
+  lineinfile:
+    path: /etc/logrotate.d/apache2
+    regexp: <span class="org-string">"{{ item.regexp }}"</span>
+    line: <span class="org-string">"{{ item.line }}"</span>
+  loop:
+  - { regexp: <span class="org-string">'^ *daily'</span>, line: <span class="org-string">"\tweekly"</span> }
+  - { regexp: <span class="org-string">'^ *rotate'</span>, line: <span class="org-string">"\trotate 12"</span> }
+
+- name: Configure Apache log email.
+  become: yes
+  lineinfile:
+    path: /etc/logrotate.d/apache2
+    regexp: <span class="org-string">"{{ item.regexp }}"</span>
+    line: <span class="org-string">"{{ item.line }}"</span>
+    insertbefore: <span class="org-string">" *}"</span>
+    firstmatch: yes
+  loop:
+  - { regexp: <span class="org-string">"^\tmail "</span>, line: <span class="org-string">"\tmail webmaster"</span> }
+  - { regexp: <span class="org-string">"^\tmailfirst"</span>, line: <span class="org-string">"\tmailfirst"</span> }
+
+- name: Configure logrotate.
+  become: yes
+  copy:
+    src: logrotate-mailer.conf
+    dest: /etc/systemd/system/logrotate.service.d/mailer.conf
+  notify: Reload systemd.
+
+- name: Install logrotate mailer.
+  become: yes
+  copy:
+    src: logrotate-mailer
+    dest: /usr/local/sbin/logrotate-mailer
+    <span class="org-variable-name">mode: u</span>=rwx,g=rx,o=rx
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>roles_t/abbey-front/handlers/main.yml</q><pre class="src src-conf">
+- name: Reload systemd.
+  become: yes
+  systemd:
+    daemon_reload: yes
+</pre>
+</div>
+
+<p>
+Note that the first setting for <code>ExecStart</code> is intended to clear the
+system's <code>ExecStart</code> in <q>/lib/systemd/system/logrotate.service</q>.  (A
+<code>oneshot</code> service like this can have multiple <code>ExecStart</code> settings.
+See the description of <code>ExecStart</code> in the <code>systemd.service(5)</code> manual
+page.)
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-front/files/logrotate-mailer.conf</q><pre class="src src-conf">[<span class="org-type">Service</span>]
+<span class="org-variable-name">ExecStart</span>=
+<span class="org-variable-name">ExecStart</span>=/usr/sbin/logrotate \
+                --mail /usr/local/sbin/logrotate-mailer \
+                /etc/logrotate.conf
+</pre>
+</div>
+
+<p>
+The <q>/usr/local/sbin/logrotate-mailer</q> script (below) was originally
+needed because Postfix does not provide an emulation of <code>mail(1)</code> and
+some translation to <code>sendmail(1)</code> was required.  Since then the script
+has learned to compute the date-dependent file name, compress the log,
+convert it to base64, and encapsulate it in MIME format, before
+sending it on to <code>sendmail</code>.  Note that there is no encryption (yet).
+This is a low priority because much of the data is available to
+Droplet's ISP's Mom, the NSA/CIA/NWO.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-front/files/logrotate-mailer</q><pre class="src src-sh"><span class="org-comment-delimiter">#</span><span class="org-comment">!/bin/</span><span class="org-keyword">bash</span><span class="org-comment"> -e</span>
+
+<span class="org-keyword">if</span> [ <span class="org-string">"$#"</span> != 3 -o <span class="org-string">"$1"</span> != <span class="org-string">"-s"</span> ]; <span class="org-keyword">then</span>
+    <span class="org-builtin">echo</span> <span class="org-string">"usage: $0 -s subject recipient"</span> 1&gt;&amp;2
+    <span class="org-keyword">exit</span> 1
+<span class="org-keyword">fi</span>
+
+<span class="org-variable-name">D</span>=<span class="org-sh-quoted-exec">`date -d yesterday "+%Y%m%d"`</span>
+<span class="org-keyword">if</span> [[ <span class="org-string">"$2"</span> == *error.log* ]]; <span class="org-keyword">then</span>
+    <span class="org-variable-name">F</span>=<span class="org-string">"$D-error.log.gz"</span>
+<span class="org-keyword">else</span>
+    <span class="org-variable-name">F</span>=<span class="org-string">"$D.log.gz"</span>
+<span class="org-keyword">fi</span>
+
+( <span class="org-builtin">echo</span> <span class="org-string">"Subject: $2"</span>
+  <span class="org-builtin">echo</span> <span class="org-string">"Content-Type: multipart/mixed; boundary=\"boundary\""</span>
+  <span class="org-builtin">echo</span> <span class="org-string">"MIME-Version: 1.0"</span>
+  <span class="org-builtin">echo</span> <span class="org-string">""</span>
+  <span class="org-builtin">echo</span> <span class="org-string">"--boundary"</span>
+  <span class="org-builtin">echo</span> <span class="org-string">"Content-Type: text/plain"</span>
+  <span class="org-builtin">echo</span> <span class="org-string">"Content-Transfer-Encoding: 8bit"</span>
+  <span class="org-builtin">echo</span> <span class="org-string">""</span>
+  <span class="org-builtin">echo</span> <span class="org-string">"$F"</span>
+  <span class="org-builtin">echo</span> <span class="org-string">"--boundary"</span>
+  <span class="org-builtin">echo</span> <span class="org-string">"Content-Type: application/gzip; name=\"$F\""</span>
+  <span class="org-builtin">echo</span> <span class="org-string">"Content-Disposition: attachment; filename=\"$F\""</span>
+  <span class="org-builtin">echo</span> <span class="org-string">"Content-Transfer-Encoding: base64"</span>
+  <span class="org-builtin">echo</span> <span class="org-string">""</span>
+  gzip | base64
+  <span class="org-builtin">echo</span> <span class="org-string">""</span>
+  <span class="org-builtin">echo</span> <span class="org-string">"--boundary--"</span> ) | sendmail <span class="org-string">"$3"</span>
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-orgdc68478" class="outline-3">
+<h3 id="orgdc68478"><span class="section-number-3">3.10.</span> Install Let's Encrypt</h3>
+<div class="outline-text-3" id="text-3-10">
+<p>
+The abbey uses a Let's Encrypt certificate to authenticate its public
+web site and email services.  Initial installation of a Let's Encrypt
+certificate is a terminal session affair (with prompts and lines
+entered as shown below).
+</p>
+
+<pre class="example" id="org489c6c5">
+$ sudo apt install python3-certbot-apache
+$ sudo certbot --apache -d birchwood-abbey.net
+...
+Enter email address (...) (Enter 'c' to cancel): webmaster@birchwood-a
+bbey.net
+...
+Please read the Terms of Service at
+...
+(A)gree/(C)ancel: A
+...
+Would you be willing to share your email address...
+...
+(Y)es/(N)o: Y
+...
+Deploying Certificate to VirtualHost /etc/apache2/sites-enabled/birchw
+ood-abbey.net.conf
+
+Please choose whether or not to redirect HTTP traffic to HTTPS, removi
+ng HTTP access.
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+1: No redirect - Make no further changes to the webserver configuratio
+n.
+...
+Select the appropriate number [1-2] then [enter] (press 'c' to cancel)
+: 1
+
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+Congratulations! You have successfully enabled https://birchwood-abbey
+.net
+
+You should test your configuration at:
+https://www.ssllabs.com/ssltest/analyze.html?d=birchwood-abbey.net
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+
+IMPORTANT NOTES:
+ - Your account credentials have been saved in your Certbot
+   configuration directory at /etc/letsencrypt. You should make a
+   secure backup of this folder now. This configuration directory will
+   also contain certificates and private keys obtained by Certbot so
+   making regular backups of this folder is ideal.
+...
+ - Congratulations! Your certificate and chain have been saved at:
+   /etc/letsencrypt/live/birchwood-abbey.net/fullchain.pem
+   Your key file has been saved at:
+   /etc/letsencrypt/live/birchwood-abbey.net/privkey.pem
+   Your cert will expire on 2019-01-13. To obtain a new or tweaked
+   version of this certificate in the future, simply run certbot again
+   with the "certonly" option. To non-interactively renew *all* of
+   your certificates, run "certbot renew"
+</pre>
+
+<p>
+When the <q>/etc/letsencrypt/</q> directory is restored from a backup copy,
+and the following tasks performed, the web server will be prepared to
+do ACME (the certificate protocol) when next Let's Encrypt calls
+(quarterly).  The following tasks ensure the <code>python3-cerbot-apache</code>
+package is installed and its <q>live/</q> subdirectory is world readable.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-front/tasks/main.yml</q><pre class="src src-conf">
+- name: Install Certbot for Apache.
+  become: yes
+  <span class="org-variable-name">apt: pkg</span>=python3-certbot-apache
+
+- name: Ensure Let<span class="org-string">'s Encrypt certificate is readable.</span>
+<span class="org-string">  become: yes</span>
+<span class="org-string">  file:</span>
+<span class="org-string">    mode: u=rwx,g=rx,o=rx</span>
+<span class="org-string">    path: /etc/letsencrypt/live</span>
+</pre>
+</div>
+
+<p>
+Front's Dovecot (and Postfix) certificate and key are in separate
+files despite their warning about a race condition (when updating the
+pair of files) mainly because that is how they are provided (and
+updated) by Let's Encrypt, but also because Let's Encrypt's symbolic
+links keep the window for a mismatch extremely small.
+</p>
+
+<p>
+With the institutional configuration, Postfix, Dovecot and Apache
+servers get their certificate&amp;key from <q>/etc/server.crt&amp;.key</q>.  The
+institutional roles check that they exist, but will not create them.
+In this abbey specific role, <q>/etc/server.crt&amp;key</q> are ours to frob.
+The following tasks ensure they are symbolic links to
+<q>/etc/letsencrypt/live/birchwood-abbey.net/fullchain&amp;privkey.pem</q>.  If
+<q>/etc/letsencrypt/</q> was restored from a backup, the servers should be
+restarted manually.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-front/tasks/main.yml</q><pre class="src src-conf">
+- name: Use Let<span class="org-string">'s Encrypt certificate&amp;key.</span>
+<span class="org-string">  file:</span>
+<span class="org-string">    state: link</span>
+<span class="org-string">    src: "{{ item.target }}"</span>
+<span class="org-string">    path: "{{ item.link }}"</span>
+<span class="org-string">    force: yes</span>
+<span class="org-string">  loop:</span>
+<span class="org-string">  - target: /etc/letsencrypt/live/birchwood-abbey.net/fullchain.pem</span>
+<span class="org-string">    link: /etc/server.crt</span>
+<span class="org-string">  - target: /etc/letsencrypt/live/birchwood-abbey.net/privkey.pem</span>
+<span class="org-string">    link: /etc/server.key</span>
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org043445c" class="outline-3">
+<h3 id="org043445c"><span class="section-number-3">3.11.</span> Rotate Let's Encrypt Log</h3>
+<div class="outline-text-3" id="text-3-11">
+<p>
+The following task arranges to rotate Certbot's logs files.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-front/tasks/main.yml</q><pre class="src src-conf">
+- name: Install Certbot logrotate configuration.
+  become: yes
+  copy:
+    src: certbot_logrotate
+    dest: /etc/logrotate.d/certbot
+    <span class="org-variable-name">mode: u</span>=rw,g=r,o=r
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>roles_t/abbey-front/files/certbot_logrotate</q><pre class="src src-conf"><span class="org-type">/var/log/letsencrypt/*.log</span> {
+    rotate 12
+    weekly
+    compress
+    missingok
+}
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org0a13320" class="outline-3">
+<h3 id="org0a13320"><span class="section-number-3">3.12.</span> Archive Let's Encrypt Data</h3>
+<div class="outline-text-3" id="text-3-12">
+<p>
+A backup copy of Let's Encrypt's data (<q>/etc/letsencrypt/</q>) is sent to
+<code>root@core</code> in S/MIME encrypted email every time it changes.  Changes
+are detected by keeping a copy in <q>/etc/letsencrypt~/</q> for comparison.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-front/tasks/main.yml</q><pre class="src src-conf">
+- name: Install Let<span class="org-string">'s Encrypt archive script.</span>
+<span class="org-string">  become: yes</span>
+<span class="org-string">  copy:</span>
+<span class="org-string">    src: cron.daily_letsencrypt</span>
+<span class="org-string">    dest: /etc/cron.daily/letsencrypt</span>
+<span class="org-string">    mode: u=rwx,g=rx,o=rx</span>
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>roles_t/abbey-front/files/cron.daily_letsencrypt</q><pre class="src src-sh"><span class="org-comment-delimiter">#</span><span class="org-comment">!/bin/</span><span class="org-keyword">bash</span><span class="org-comment"> -e</span>
+
+<span class="org-builtin">cd</span> /etc/
+
+[ -d letsencrypt~ ] <span class="org-sh-escaped-newline">\</span>
+&amp;&amp; diff -rq letsencrypt/ letsencrypt~/ <span class="org-sh-escaped-newline">\</span>
+&amp;&amp; <span class="org-keyword">exit</span> 0
+
+( <span class="org-builtin">echo</span> <span class="org-string">"Subject: New /etc/letsencrypt/ on Droplet."</span>
+  <span class="org-builtin">echo</span> <span class="org-string">""</span>
+  tar czf - letsencrypt/ <span class="org-sh-escaped-newline">\</span>
+  | gpg --encrypt --armor <span class="org-sh-escaped-newline">\</span>
+        --trust-model always --recipient root@core ) <span class="org-sh-escaped-newline">\</span>
+| sendmail root <span class="org-sh-escaped-newline">\</span>
+|| <span class="org-keyword">exit</span> $<span class="org-variable-name">?</span>
+
+rm -rf letsencrypt~
+cp -a letsencrypt letsencrypt~
+</pre>
+</div>
+
+<p>
+The message is encrypted with <code>root@core</code>'s public key, which is
+imported into <code>root@front</code>'s GnuPG key file.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-front/tasks/main.yml</q><pre class="src src-conf">
+- name: Copy root@core<span class="org-string">'s public key.</span>
+<span class="org-string">  become: yes</span>
+<span class="org-string">  copy:</span>
+<span class="org-string">    src: ../Secret/root-pub.pem</span>
+<span class="org-string">    dest: /root/.gnupg-root-pub.pem</span>
+<span class="org-string">    mode: u=r,g=r,o=r</span>
+<span class="org-string">  notify: Import root@core'</span>s public key.
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>roles_t/abbey-front/handlers/main.yml</q><pre class="src src-conf">
+- name: Import root@core<span class="org-string">'s public key.</span>
+<span class="org-string">  become: yes</span>
+<span class="org-string">  command: gpg --import ~/.gnupg-root-pub.pem</span>
+</pre>
+</div>
+</div>
+</div>
+</div>
+<div id="outline-container-org62c2afa" class="outline-2">
+<h2 id="org62c2afa"><span class="section-number-2">4.</span> The Abbey Core Role</h2>
+<div class="outline-text-2" id="text-4">
+<p>
+Birchwood Abbey's core is a mini-PC (System76 Meerkat) configured as A
+Small Institute Core.  Thus it is already serving a local web site
+with Apache2, hosting a private cloud with Nextcloud, handling email
+with Postfix and Dovecot, and providing essential localnet services:
+NTP, DNS and DHCP.
+</p>
+</div>
+<div id="outline-container-org001474c" class="outline-3">
+<h3 id="org001474c"><span class="section-number-3">4.1.</span> Install Additional Packages</h3>
+<div class="outline-text-3" id="text-4-1">
+<p>
+The scripts that maintain the abbey's web site and run the Weather
+project use a number of additional software packages.  The
+<q>/WWW/live/Private/make-top-index</q> script uses <code>HTML::TreeBuilder</code> in
+the <code>libhtml-tree-perl</code> package.  The house task list uses JQuery.
+Weather scripts use <code>mit-scheme</code> and <code>gnuplot</code> (in pseudonymous
+packages).
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-core/tasks/main.yml</q><pre class="src src-conf">---
+- name: Install additional packages.
+  apt:
+    pkg: [ libhtml-tree-perl, libjs-jquery, mit-scheme, gnuplot ]
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-orgd7a5da4" class="outline-3">
+<h3 id="orgd7a5da4"><span class="section-number-3">4.2.</span> Configure Private Email Aliases</h3>
+<div class="outline-text-3" id="text-4-2">
+<p>
+The abbey uses several additional email aliases.  These are the campus
+mailboxes <code>@*.birchwood-abbey.net</code>.  The institute already includes
+some standard system aliases, as well as mailboxes for accounts
+running services: <code>www-data</code> and <code>monkey</code>.  The institute funnels
+these to <code>root</code> and forwards <code>root</code> to <code>sysadm</code> (as on Front).  The
+abbey takes it from there, forwarding <code>sysadm</code> to a real person and
+including mailboxes for all accounts running services on any campus
+machine.  (They should all be relaying to <code>smtp.birchwood-abbey.net</code>
+which delivers any <code>.birchwood-abbey.net</code> email,
+e.g. <code>mythtv@mythtv.birchwood-abbey.net</code>, locally.)
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-core/tasks/main.yml</q><pre class="src src-conf">
+- name: Install abbey email aliases.
+  become: yes
+  blockinfile:
+    block: |
+        sysadm:         matt
+        house:          sysadm
+        mythtv:         sysadm
+        scanner:        sysadm
+    dest: /etc/aliases
+    marker: <span class="org-string">"# {mark} ABBEY MANAGED BLOCK"</span>
+  notify: New aliases.
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>roles_t/abbey-core/handlers/main.yml</q><pre class="src src-conf">---
+- name: New aliases.
+  become: yes
+  command: newaliases
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org6cfc8e7" class="outline-3">
+<h3 id="org6cfc8e7"><span class="section-number-3">4.3.</span> Configure Git Daemon on Core</h3>
+<div class="outline-text-3" id="text-4-3">
+<p>
+These tasks are identical to those executed on Front, for similar Git
+services on Front and Core.  See <a href="#org31d6bbb">3.3</a> and
+<a href="#orgdf93aec">Configure Gitweb on Front</a> for more information.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-core/tasks/main.yml</q><pre class="src src-conf">
+- name: Install git daemon.
+  become: yes
+  <span class="org-variable-name">apt: pkg</span>=git-daemon-sysvinit
+
+- name: Configure git daemon.
+  become: yes
+  lineinfile:
+    path: /etc/default/git-daemon
+    regexp: <span class="org-string">"{{ item.patt }}"</span>
+    line: <span class="org-string">"{{ item.line }}"</span>
+  loop:
+  - patt: <span class="org-string">'^GIT_DAEMON_ENABLE *='</span>
+    line: <span class="org-string">'GIT_DAEMON_ENABLE=true'</span>
+  - patt: <span class="org-string">'^GIT_DAEMON_OPTIONS *='</span>
+    line: <span class="org-string">'GIT_DAEMON_OPTIONS="--user-path=Public/Git"'</span>
+  - patt: <span class="org-string">'^GIT_DAEMON_BASE_PATH *='</span>
+    line: <span class="org-string">'GIT_DAEMON_BASE_PATH="/var/www/git"'</span>
+  - patt: <span class="org-string">'^GIT_DAEMON_DIRECTORY *='</span>
+    line: <span class="org-string">'GIT_DAEMON_DIRECTORY=" "'</span>
+  notify: Restart git daemon.
+
+- name: Create /var/www/git/.
+  become: yes
+  file:
+    path: /var/www/git
+    state: directory
+    group: staff
+    <span class="org-variable-name">mode: u</span>=rwx,g=srwx,o=rx
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>roles_t/abbey-core/handlers/main.yml</q><pre class="src src-conf">
+
+- name: Restart git daemon.
+  become: yes
+  command: systemctl restart git-daemon
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-orged71937" class="outline-3">
+<h3 id="orged71937"><span class="section-number-3">4.4.</span> Configure Apache on Core</h3>
+<div class="outline-text-3" id="text-4-4">
+<p>
+The Apache2 configuration on Core specifies three web sites (live,
+test, and campus).  The live and test sites must operate just like the
+site on Front.  Their configurations include the same <a href="#org158c789"><code>apache-abbey</code></a>,
+<a href="#org5e1b247"><code>apache-photos</code></a>, <a href="#org119fc95"><code>apache-gitweb</code></a>, and <a href="#org9ae034e"><code>apache-cgit</code></a> used on Front.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-core/tasks/main.yml</q><pre class="src src-conf">
+- name: Configure live website.
+  become: yes
+  vars:
+    docroot: /WWW/live
+  copy:
+    content: |
+        &lt;Directory {{ docroot }}/Abbey/&gt;
+            AllowOverride Indexes FileInfo
+            Options +Indexes +FollowSymLinks
+        &lt;/Directory&gt;
+
+        RedirectMatch /Photos$ /Photos/
+        RedirectMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])$ \
+                      /Photos/$1_$2_$3/
+        AliasMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])/(.+)$ \
+                   {{ docroot }}/Photos/$1/$2/$3/$4
+        AliasMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])/$ \
+                   {{ docroot }}/Photos/$1/$2/$3/index.html
+        AliasMatch /Photos/$ {{ docroot }}/Photos/index.html
+
+        Alias /gitweb-static/ /usr/share/gitweb/static/
+        &lt;Directory <span class="org-string">"/usr/share/gitweb/static/"</span>&gt;
+            Options MultiViews
+        &lt;/Directory&gt;
+        RewriteEngine on
+        RewriteRule ^/gitweb(\.cgi)?(/.*)?$ \
+                    /cgi-bin/gitweb.cgi$2 [QSA,L,PT]
+        RewriteRule ^/\~([^\/]+)/gitweb(\.cgi)?(/.*)?$ \
+                    /cgi-bin/gitweb.cgi$3 \
+                    [<span class="org-type">QSA,E=GITWEB_PROJECTROOT:/home/$1/Public/Git/,L,PT</span>]
+
+        ScriptAlias /cgit/ /usr/lib/cgit/cgit.cgi/
+        Alias /cgit-css/ /usr/share/cgit/
+        &lt;Directory <span class="org-string">"/usr/lib/cgit/"</span>&gt;
+           AllowOverride None
+           Options ExecCGI FollowSymlinks
+           Require all granted
+        &lt;/Directory&gt;
+        RewriteRule ^/cgit?(/.*)$ \
+                    <span class="org-variable-name">/cgit$1 [QSA,E</span>=CGIT_SCANPATH:/var/www/git/,L,PT]
+        RewriteRule ^/\~([^\/]+)/cgit(/.*)?$ \
+                    <span class="org-variable-name">/cgit$2 [QSA,E</span>=CGIT_SCANPATH:/home/$1/Public/Git/,L,PT]
+    dest: /etc/apache2/sites-available/live-vhost.conf
+    <span class="org-variable-name">mode: u</span>=rw,g=r,o=r
+  notify: Restart Apache2.
+
+- name: Configure test website.
+  become: yes
+  vars:
+    docroot: /WWW/test
+  copy:
+    content: |
+        &lt;Directory {{ docroot }}/Abbey/&gt;
+            AllowOverride Indexes FileInfo
+            Options +Indexes +FollowSymLinks
+        &lt;/Directory&gt;
+
+        RedirectMatch /Photos$ /Photos/
+        RedirectMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])$ \
+                      /Photos/$1_$2_$3/
+        AliasMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])/(.+)$ \
+                   {{ docroot }}/Photos/$1/$2/$3/$4
+        AliasMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])/$ \
+                   {{ docroot }}/Photos/$1/$2/$3/index.html
+        AliasMatch /Photos/$ {{ docroot }}/Photos/index.html
+
+        Alias /gitweb-static/ /usr/share/gitweb/static/
+        &lt;Directory <span class="org-string">"/usr/share/gitweb/static/"</span>&gt;
+            Options MultiViews
+        &lt;/Directory&gt;
+        RewriteEngine on
+        RewriteRule ^/gitweb(\.cgi)?(/.*)?$ \
+                    /cgi-bin/gitweb.cgi$2 [QSA,L,PT]
+        RewriteRule ^/\~([^\/]+)/gitweb(\.cgi)?(/.*)?$ \
+                    /cgi-bin/gitweb.cgi$3 \
+                    [<span class="org-type">QSA,E=GITWEB_PROJECTROOT:/home/$1/Public/Git/,L,PT</span>]
+
+        ScriptAlias /cgit/ /usr/lib/cgit/cgit.cgi/
+        Alias /cgit-css/ /usr/share/cgit/
+        &lt;Directory <span class="org-string">"/usr/lib/cgit/"</span>&gt;
+           AllowOverride None
+           Options ExecCGI FollowSymlinks
+           Require all granted
+        &lt;/Directory&gt;
+        RewriteRule ^/cgit?(/.*)$ \
+                    <span class="org-variable-name">/cgit$1 [QSA,E</span>=CGIT_SCANPATH:/var/www/git/,L,PT]
+        RewriteRule ^/\~([^\/]+)/cgit(/.*)?$ \
+                    <span class="org-variable-name">/cgit$2 [QSA,E</span>=CGIT_SCANPATH:/home/$1/Public/Git/,L,PT]
+    dest: /etc/apache2/sites-available/test-vhost.conf
+    <span class="org-variable-name">mode: u</span>=rw,g=r,o=r
+  notify: Restart Apache2.
+
+- name: Enable Apache2 rewrite module for Gitweb.
+  become: yes
+  <span class="org-variable-name">apache2_module: name</span>=rewrite
+  notify: Restart Apache2.
+
+- name: Enable Apache2 cgid module for Gitweb (Ubuntu).
+  become: yes
+  <span class="org-variable-name">apache2_module: name</span>=cgid
+  <span class="org-variable-name">when: ansible_distribution</span> == <span class="org-string">'Ubuntu'</span>
+  notify: Restart Apache2.
+
+- name: Enable Apache2 cgi module for Gitweb (Debian).
+  become: yes
+  <span class="org-variable-name">apache2_module: name</span>=cgi
+  <span class="org-variable-name">when: ansible_distribution</span> == <span class="org-string">'Debian'</span>
+  notify: Restart Apache2.
+
+- name: Install libcgi-pm-perl for Gitweb.
+  become: yes
+  <span class="org-variable-name">apt: pkg</span>=libcgi-pm-perl
+
+- name: Link Gitweb into /cgi-bin/.
+  become: yes
+  file:
+    state: link
+    path: /usr/lib/cgi-bin/{{ item }}
+    src: /usr/share/gitweb/{{ item }}
+  loop: [ gitweb.cgi, index.cgi ]
+
+- name: Override Gitweb assets location.
+  become: yes
+  copy:
+    content: |
+      <span class="org-variable-name">$projectroot</span> = $ENV{<span class="org-string">'GITWEB_PROJECTROOT'</span>} || <span class="org-string">"/var/www/git"</span>;
+      <span class="org-variable-name">@stylesheets</span> = (<span class="org-string">"/gitweb-static/gitweb.css"</span>);
+      <span class="org-variable-name">$logo</span> = <span class="org-string">"/gitweb-static/git-logo.png"</span>;
+      <span class="org-variable-name">$favicon</span> = <span class="org-string">"/favicon.ico"</span>;
+      <span class="org-variable-name">$javascript</span> = <span class="org-string">"/gitweb-static/gitweb.js"</span>;
+    dest: /etc/gitweb.conf
+    <span class="org-variable-name">mode: u</span>=rw,g=r,o=r
+
+- name: Install CGit.
+  become: yes
+  <span class="org-variable-name">apt: pkg</span>=cgit
+
+- name: Disable CGit default configuration.
+  become: yes
+  command:
+    cmd: a2disconf -q cgit
+    removes: /etc/apache2/conf-enabled/cgit.conf
+
+- name: Override CGit scan path.
+  become: yes
+  lineinfile:
+    path: /etc/cgitrc
+    regexp: <span class="org-string">"^scan-path *="</span>
+    line: <span class="org-string">"scan-path=$CGIT_SCANPATH"</span>
+  notify: Reload Apache2.
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>roles_t/abbey-core/handlers/main.yml</q><pre class="src src-conf">
+- name: Restart Apache2.
+  become: yes
+  systemd:
+    service: apache2
+    state: restarted
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org7cfc2f1" class="outline-3">
+<h3 id="org7cfc2f1"><span class="section-number-3">4.5.</span> Configure Documentation URLs</h3>
+<div class="outline-text-3" id="text-4-5">
+<p>
+The institute serves its <q>/usr/share/doc/</q> on the house (campus) web
+site.  This is a debugging convenience, making some HTML documentation
+more accessible, especially the documentation of software installed on
+Core and not on typical desktop clients.  Also included: the Apache2
+directives that enable user Git publishing with Gitweb and CGit
+(defined <a href="#org119fc95">here</a> and <a href="#org9ae034e">here</a> respectively).
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-core/tasks/main.yml</q><pre class="src src-conf">
+- name: Configure house website.
+  become: yes
+  copy:
+    content: |
+      Alias /doc /usr/share/doc
+      &lt;Directory /usr/share/doc/&gt;
+          Options Indexes
+      &lt;/Directory&gt;
+
+      Alias /gitweb-static/ /usr/share/gitweb/static/
+      &lt;Directory <span class="org-string">"/usr/share/gitweb/static/"</span>&gt;
+          Options MultiViews
+      &lt;/Directory&gt;
+      RewriteEngine on
+      RewriteRule ^/gitweb(\.cgi)?(/.*)?$ \
+                  /cgi-bin/gitweb.cgi$2 [QSA,L,PT]
+      RewriteRule ^/\~([^\/]+)/gitweb(\.cgi)?(/.*)?$ \
+                  /cgi-bin/gitweb.cgi$3 \
+                  [<span class="org-type">QSA,E=GITWEB_PROJECTROOT:/home/$1/Public/Git/,L,PT</span>]
+
+      ScriptAlias /cgit/ /usr/lib/cgit/cgit.cgi/
+      Alias /cgit-css/ /usr/share/cgit/
+      &lt;Directory <span class="org-string">"/usr/lib/cgit/"</span>&gt;
+         AllowOverride None
+         Options ExecCGI FollowSymlinks
+         Require all granted
+      &lt;/Directory&gt;
+      RewriteRule ^/cgit?(/.*)$ \
+                  <span class="org-variable-name">/cgit$1 [QSA,E</span>=CGIT_SCANPATH:/var/www/git/,L,PT]
+      RewriteRule ^/\~([^\/]+)/cgit(/.*)?$ \
+                  <span class="org-variable-name">/cgit$2 [QSA,E</span>=CGIT_SCANPATH:/home/$1/Public/Git/,L,PT]
+    dest: /etc/apache2/sites-available/www-vhost.conf
+    <span class="org-variable-name">mode: u</span>=rw,g=r,o=r
+  notify: Restart Apache2.
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org1ad313a" class="outline-3">
+<h3 id="org1ad313a"><span class="section-number-3">4.6.</span> Install Apt Cacher</h3>
+<div class="outline-text-3" id="text-4-6">
+<p>
+The abbey uses the Apt-Cacher:TNG package cache on Core.  The
+<code>apt-cacher</code> domain name is defined in <q>private/db.domain</q>.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-core/tasks/main.yml</q><pre class="src src-conf">
+- name: Install Apt-Cacher:TNG.
+  become: yes
+  <span class="org-variable-name">apt: pkg</span>=apt-cacher-ng
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-orge2a23bc" class="outline-3">
+<h3 id="orge2a23bc"><span class="section-number-3">4.7.</span> Use Cloister Apt Cache</h3>
+<div class="outline-text-3" id="text-4-7">
+<p>
+Core itself will benefit from using the package cache.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-core/tasks/main.yml</q><pre class="src src-conf">
+- name: Use the local Apt package cache.
+  become: yes
+  copy:
+    content: |
+     Acquire::http::Proxy <span class="org-string">"http://apt-cacher.{{ domain_priv }}.:3142"</span>;
+    dest: /etc/apt/apt.conf.d/01proxy
+    <span class="org-variable-name">mode: u</span>=rw,g=r,o=r
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org30c2703" class="outline-3">
+<h3 id="org30c2703"><span class="section-number-3">4.8.</span> Configure NAGIOS</h3>
+<div class="outline-text-3" id="text-4-8">
+<p>
+A small institute uses <code>nagios4</code> to monitor the health of its network,
+with an initial smattering of monitors adopted from the Debian
+<code>monitoring-plugins</code> package.  Thus a NAGIOS4 server on the abbey's
+Core monitors core network services, and uses <code>nagios-nrpe-server</code> to
+monitor Gate.  The abbey adds several more monitors, installing
+additional configuration files in <q>/etc/nagios4/conf.d/</q>, and another
+customized <code>check_sensors</code> plugin (<code>abbey_pisensors</code>) in
+<q>/usr/local/sbin/</q> on the Raspberry Pis.
+</p>
+</div>
+</div>
+<div id="outline-container-org5b67d8f" class="outline-3">
+<h3 id="org5b67d8f"><span class="section-number-3">4.9.</span> Monitoring The Home Disk</h3>
+<div class="outline-text-3" id="text-4-9">
+<p>
+The abbey adds monitoring of the space remaining on the volume at
+<q>/home/</q> on Core.  (The small institute only monitors the space
+remaining on roots.)
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-core/tasks/main.yml</q><pre class="src src-conf">
+- name: Configure NAGIOS monitoring for Core /home/.
+  become: yes
+  copy:
+    content: |
+      <span class="org-type">define service</span> {
+          use                     local-service
+          host_name               core
+          service_description     Home Partition
+          check_command           check_local_disk!20%!10%!/home
+      }
+    dest: /etc/nagios4/conf.d/abbey.cfg
+  notify: Reload NAGIOS4.
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>roles_t/abbey-core/handlers/main.yml</q><pre class="src src-conf">
+- name: Reload NAGIOS4.
+  become: yes
+  systemd:
+    service: nagios4
+    state: reloaded
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org154a00c" class="outline-3">
+<h3 id="org154a00c"><span class="section-number-3">4.10.</span> Custom NAGIOS Monitor <code>abbey_pisensors</code></h3>
+<div class="outline-text-3" id="text-4-10">
+<p>
+The <code>check_sensors</code> plugin is included in the package
+<code>monitoring-plugins-basic</code>, but it does not report any readings.  The
+small institute substitutes a Custom NAGIOS Monitor <code>inst_sensors</code>
+that reports core CPU temperatures, but the <code>sensors</code> command on a
+Raspberry Pi does not reveal core CPU temperatures, so the abbey
+includes yet another version, <code>abbey_pisensors</code>, that reports any
+recognizable temperature in the <code>sensors</code> output.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-core/files/abbey_pisensors</q><pre class="src src-sh"><span class="org-comment-delimiter">#</span><span class="org-comment">!/bin/</span><span class="org-keyword">sh</span>
+
+<span class="org-variable-name">PATH</span>=<span class="org-string">"/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin"</span>
+<span class="org-builtin">export</span> PATH
+<span class="org-variable-name">PROGNAME</span>=<span class="org-sh-quoted-exec">`basename $0`</span>
+<span class="org-variable-name">REVISION</span>=<span class="org-string">"2.3.1"</span>
+
+<span class="org-builtin">.</span> /usr/lib/nagios/plugins/utils.sh
+
+<span class="org-function-name">print_usage</span>() {
+        <span class="org-builtin">echo</span> <span class="org-string">"Usage: $PROGNAME"</span> [--ignore-fault]
+}
+
+<span class="org-function-name">print_help</span>() {
+        print_revision $<span class="org-variable-name">PROGNAME</span> $<span class="org-variable-name">REVISION</span>
+        <span class="org-builtin">echo</span> <span class="org-string">""</span>
+        print_usage
+        <span class="org-builtin">echo</span> <span class="org-string">""</span>
+        <span class="org-builtin">echo</span> <span class="org-string">"This plugin checks hardware status using the lm_sensors package."</span>
+        <span class="org-builtin">echo</span> <span class="org-string">""</span>
+        support
+        <span class="org-keyword">exit</span> $<span class="org-variable-name">STATE_OK</span>
+}
+
+<span class="org-function-name">brief_data</span>() {
+    <span class="org-builtin">echo</span> <span class="org-string">"$1"</span> | sed -n -E -e <span class="org-string">'</span>
+<span class="org-string">  /^temp[0-9]+: +[-+][0-9.]+&#176;C/ { s/^temp[0-9]+: +([-+][0-9.]+)&#176;C.*/ \1/; H }</span>
+<span class="org-string">  $ { x; s/\n//g; p }'</span>
+}
+
+<span class="org-keyword">case</span> <span class="org-string">"$1"</span><span class="org-keyword"> in</span>
+        --help)
+                print_help
+                <span class="org-keyword">exit</span> $<span class="org-variable-name">STATE_OK</span>
+                ;;
+        -h)
+                print_help
+                <span class="org-keyword">exit</span> $<span class="org-variable-name">STATE_OK</span>
+                ;;
+        --version)
+                print_revision $<span class="org-variable-name">PROGNAME</span> $<span class="org-variable-name">REVISION</span>
+                <span class="org-keyword">exit</span> $<span class="org-variable-name">STATE_OK</span>
+                ;;
+        -V)
+                print_revision $<span class="org-variable-name">PROGNAME</span> $<span class="org-variable-name">REVISION</span>
+                <span class="org-keyword">exit</span> $<span class="org-variable-name">STATE_OK</span>
+                ;;
+        *)
+                <span class="org-variable-name">sensordata</span>=<span class="org-sh-quoted-exec">`sensors 2&gt;&amp;1`</span>
+                <span class="org-variable-name">status</span>=$<span class="org-variable-name">?</span>
+                <span class="org-keyword">if </span><span class="org-builtin">test</span> ${<span class="org-variable-name">status</span>} -eq 127; <span class="org-keyword">then</span>
+                        <span class="org-variable-name">text</span>=<span class="org-string">"SENSORS UNKNOWN - command not found"</span>
+                        <span class="org-variable-name">text</span>=<span class="org-string">"$text (did you install lmsensors?)"</span>
+                        <span class="org-variable-name">exit</span>=$<span class="org-variable-name">STATE_UNKNOWN</span>
+                <span class="org-keyword">elif </span><span class="org-builtin">test</span> ${<span class="org-variable-name">status</span>} -ne 0; <span class="org-keyword">then</span>
+                        <span class="org-variable-name">text</span>=<span class="org-string">"WARNING - sensors returned state $status"</span>
+                        <span class="org-variable-name">exit</span>=$<span class="org-variable-name">STATE_WARNING</span>
+                <span class="org-keyword">elif </span><span class="org-builtin">echo</span> ${<span class="org-variable-name">sensordata</span>} | egrep ALARM &gt; /dev/null; <span class="org-keyword">then</span>
+                        <span class="org-variable-name">text</span>=<span class="org-string">"SENSOR CRITICAL -`brief_data "${sensordata}"`"</span>
+                        <span class="org-variable-name">exit</span>=$<span class="org-variable-name">STATE_CRITICAL</span>
+                <span class="org-keyword">elif </span><span class="org-builtin">echo</span> ${<span class="org-variable-name">sensordata</span>} | egrep FAULT &gt; /dev/null <span class="org-sh-escaped-newline">\</span>
+                    &amp;&amp; <span class="org-builtin">test</span> <span class="org-string">"$1"</span> != <span class="org-string">"-i"</span> -a <span class="org-string">"$1"</span> != <span class="org-string">"--ignore-fault"</span>; <span class="org-keyword">then</span>
+                        <span class="org-variable-name">text</span>=<span class="org-string">"SENSOR UNKNOWN - Sensor reported fault"</span>
+                        <span class="org-variable-name">exit</span>=$<span class="org-variable-name">STATE_UNKNOWN</span>
+                <span class="org-keyword">else</span>
+                        <span class="org-variable-name">text</span>=<span class="org-string">"SENSORS OK -`brief_data "${sensordata}"`"</span>
+                        <span class="org-variable-name">exit</span>=$<span class="org-variable-name">STATE_OK</span>
+                <span class="org-keyword">fi</span>
+
+                <span class="org-builtin">echo</span> <span class="org-string">"$text"</span>
+                <span class="org-keyword">if </span><span class="org-builtin">test</span> <span class="org-string">"$1"</span> = <span class="org-string">"-v"</span> -o <span class="org-string">"$1"</span> = <span class="org-string">"--verbose"</span>; <span class="org-keyword">then</span>
+                        <span class="org-builtin">echo</span> ${<span class="org-variable-name">sensordata</span>}
+                <span class="org-keyword">fi</span>
+                <span class="org-keyword">exit</span> $<span class="org-variable-name">exit</span>
+                ;;
+<span class="org-keyword">esac</span>
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org362dff5" class="outline-3">
+<h3 id="org362dff5"><span class="section-number-3">4.11.</span> Monitoring The Cloister</h3>
+<div class="outline-text-3" id="text-4-11">
+<p>
+The abbey adds monitoring for more servers: Kamino, Kessel and
+Devaron.  They are <code>abbey-cloister</code> servers, so they are configured as
+small institute <code>campus</code> servers, like Gate, with an NRPE (a NAGIOS
+Remote Plugin Executor) server and an <code>inst_sensors</code> command.
+</p>
+
+<p>
+The configurations for the servers are very similar to Gate's, but are
+idiosyncratically in flux.  In particular, Kamino does not irritate
+<code>check_total_procs</code>, yet Kessel does.  Both are Pop!<sub>OS</sub> 22.04, but
+Kessel is a wireless host while Kamino is wired.  Devaron, the
+Raspberry Pi OS (ARM64) machine, uses the <code>abbey_pisensors</code> monitor.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-core/tasks/main.yml</q><pre class="src src-conf">
+- name: Configure cloister NAGIOS monitoring.
+  become: yes
+  template:
+    src: nagios-{{ item }}.cfg
+    dest: /etc/nagios4/conf.d/{{ item }}.cfg
+  loop: [ devaron, kamino, kessel ]
+  notify: Reload NAGIOS4.
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>roles_t/abbey-core/templates/nagios-devaron.cfg</q><pre class="src src-conf"><span class="org-type">define host</span> {
+    use                     linux-server
+    host_name               devaron
+    address                 {{ devaron_addr }}
+}
+
+<span class="org-type">define service</span> {
+    use                     generic-service
+    host_name               devaron
+    service_description     Root Partition
+    check_command           check_nrpe!inst_root
+}
+
+<span class="org-comment-delimiter"># </span><span class="org-comment">define service {</span>
+<span class="org-comment-delimiter">#     </span><span class="org-comment">use                     generic-service</span>
+<span class="org-comment-delimiter">#     </span><span class="org-comment">host_name               devaron</span>
+<span class="org-comment-delimiter">#     </span><span class="org-comment">service_description     Current Load</span>
+<span class="org-comment-delimiter">#     </span><span class="org-comment">check_command           check_nrpe!check_load</span>
+<span class="org-comment-delimiter"># </span><span class="org-comment">}</span>
+
+<span class="org-type">define service</span> {
+    use                     generic-service
+    host_name               devaron
+    service_description     Zombie Processes
+    check_command           check_nrpe!check_zombie_procs
+}
+
+<span class="org-comment-delimiter"># </span><span class="org-comment">define service {</span>
+<span class="org-comment-delimiter">#     </span><span class="org-comment">use                     generic-service</span>
+<span class="org-comment-delimiter">#     </span><span class="org-comment">host_name               devaron</span>
+<span class="org-comment-delimiter">#     </span><span class="org-comment">service_description     Total Processes</span>
+<span class="org-comment-delimiter">#     </span><span class="org-comment">check_command           check_nrpe!check_total_procs</span>
+<span class="org-comment-delimiter"># </span><span class="org-comment">}</span>
+
+<span class="org-type">define service</span> {
+    use                     generic-service
+    host_name               devaron
+    service_description     Swap Usage
+    check_command           check_nrpe!inst_swap
+}
+
+<span class="org-type">define service</span> {
+    use                     generic-service
+    host_name               devaron
+    service_description     Temperature Sensors
+    check_command           check_nrpe!abbey_pisensors
+}
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>roles_t/abbey-core/templates/nagios-kamino.cfg</q><pre class="src src-conf"><span class="org-type">define host</span> {
+    use                     linux-server
+    host_name               kamino
+    address                 {{ kamino_addr }}
+}
+
+<span class="org-type">define service</span> {
+    use                     generic-service
+    host_name               kamino
+    service_description     Root Partition
+    check_command           check_nrpe!inst_root
+}
+
+<span class="org-type">define service</span> {
+    use                     generic-service
+    host_name               kamino
+    service_description     Current Load
+    check_command           check_nrpe!check_load
+}
+
+<span class="org-type">define service</span> {
+    use                     generic-service
+    host_name               kamino
+    service_description     Zombie Processes
+    check_command           check_nrpe!check_zombie_procs
+}
+
+<span class="org-comment-delimiter"># </span><span class="org-comment">define service {</span>
+<span class="org-comment-delimiter">#     </span><span class="org-comment">use                     generic-service</span>
+<span class="org-comment-delimiter">#     </span><span class="org-comment">host_name               kamino</span>
+<span class="org-comment-delimiter">#     </span><span class="org-comment">service_description     Total Processes</span>
+<span class="org-comment-delimiter">#     </span><span class="org-comment">check_command           check_nrpe!check_total_procs</span>
+<span class="org-comment-delimiter"># </span><span class="org-comment">}</span>
+
+<span class="org-type">define service</span> {
+    use                     generic-service
+    host_name               kamino
+    service_description     Swap Usage
+    check_command           check_nrpe!inst_swap
+}
+
+<span class="org-type">define service</span> {
+    use                     generic-service
+    host_name               kamino
+    service_description     Temperature Sensors
+    check_command           check_nrpe!inst_sensors
+}
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>roles_t/abbey-core/templates/nagios-kessel.cfg</q><pre class="src src-conf"><span class="org-type">define host</span> {
+    use                     linux-server
+    host_name               kessel
+    address                 {{ kessel_addr }}
+}
+
+<span class="org-type">define service</span> {
+    use                     generic-service
+    host_name               kessel
+    service_description     Root Partition
+    check_command           check_nrpe!inst_root
+}
+
+<span class="org-comment-delimiter"># </span><span class="org-comment">define service {</span>
+<span class="org-comment-delimiter">#     </span><span class="org-comment">use                     generic-service</span>
+<span class="org-comment-delimiter">#     </span><span class="org-comment">host_name               kessel</span>
+<span class="org-comment-delimiter">#     </span><span class="org-comment">service_description     Current Load</span>
+<span class="org-comment-delimiter">#     </span><span class="org-comment">check_command           check_nrpe!check_load</span>
+<span class="org-comment-delimiter"># </span><span class="org-comment">}</span>
+
+<span class="org-type">define service</span> {
+    use                     generic-service
+    host_name               kessel
+    service_description     Zombie Processes
+    check_command           check_nrpe!check_zombie_procs
+}
+
+<span class="org-comment-delimiter"># </span><span class="org-comment">define service {</span>
+<span class="org-comment-delimiter">#     </span><span class="org-comment">use                     generic-service</span>
+<span class="org-comment-delimiter">#     </span><span class="org-comment">host_name               kessel</span>
+<span class="org-comment-delimiter">#     </span><span class="org-comment">service_description     Total Processes</span>
+<span class="org-comment-delimiter">#     </span><span class="org-comment">check_command           check_nrpe!check_total_procs</span>
+<span class="org-comment-delimiter"># </span><span class="org-comment">}</span>
+
+<span class="org-type">define service</span> {
+    use                     generic-service
+    host_name               kessel
+    service_description     Swap Usage
+    check_command           check_nrpe!inst_swap
+}
+
+<span class="org-type">define service</span> {
+    use                     generic-service
+    host_name               kessel
+    service_description     Temperature Sensors
+    check_command           check_nrpe!inst_sensors
+}
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-orga9351cb" class="outline-3">
+<h3 id="orga9351cb"><span class="section-number-3">4.12.</span> Install Analog</h3>
+<div class="outline-text-3" id="text-4-12">
+<p>
+The abbey's public web site's access and error logs are emailed
+regularly to <code>webmaster</code>, who saves them in <q>/Logs/apache2-public/</q>
+and runs <code>analog</code> to generate <q>/WWW/campus/analog.html</q>, available to
+the campus as <code>http://www/analog.html</code>.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-core/tasks/main.yml</q><pre class="src src-conf">
+- name: Install Analog.
+  become: yes
+  <span class="org-variable-name">apt: pkg</span>=analog
+
+- name: Configure Analog (removing old /var/log/apache/ LOGFILEs).
+  become: yes
+  lineinfile:
+    path: /etc/analog.cfg
+    regexp: <span class="org-string">'^LOGFILE /var/log/apache/'</span>
+    state: absent
+
+- name: Configure Analog (adding new configuration lines).
+  become: yes
+  lineinfile:
+    path: /etc/analog.cfg
+    line: <span class="org-string">"{{ item }}"</span>
+    insertafter: EOF
+  loop:
+  - <span class="org-string">"LOGFILE /Logs/apache2-public/*-access.log.gz"</span>
+  - <span class="org-string">"ALLCHART OFF"</span>
+  - <span class="org-string">"DNS WRITE"</span>
+  - <span class="org-string">"HOSTNAME \"{{ full_name }}\""</span>
+  - <span class="org-string">"OUTFILE /WWW/campus/analog.html"</span>
+
+- name: Create /Logs/.
+  become: yes
+  file:
+    path: /Logs
+    state: directory
+    <span class="org-variable-name">mode: u</span>=rwx,g=rx,o=rx
+
+- name: Create /Logs/apache2-public/.
+  become: yes
+  file:
+    path: /Logs/apache2-public
+    state: directory
+    owner: monkey
+    group: staff
+    <span class="org-variable-name">mode: u</span>=rwx,g=srwx,o=rx
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org4cc42f5" class="outline-3">
+<h3 id="org4cc42f5"><span class="section-number-3">4.13.</span> Add Monkey to Web Server Group</h3>
+<div class="outline-text-3" id="text-4-13">
+<p>
+Monkey needs to be in <code>www-data</code> so that it can run
+<q>/WWW/live/Photos/Private/cronjob</q> to publish photos from multiple
+user cloud accounts, found in files owned by <code>www-data</code>, files like
+<q>InstantUpload/Camera/2021/01/IMG_20210115_092838.jpg</q> in
+<q>/var/www/nextcloud/data/$USER/files/</q>.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-core/tasks/main.yml</q><pre class="src src-conf">
+- name: Add Monkey to Nextcloud group.
+  become: yes
+  user:
+    name: monkey
+    append: yes
+    groups: www-data
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-orgb69761e" class="outline-3">
+<h3 id="orgb69761e"><span class="section-number-3">4.14.</span> Install netpbm For Photo Processing</h3>
+<div class="outline-text-3" id="text-4-14">
+<p>
+Monkey's photo processing scripts use <code>netpbm</code> commands like
+<code>jpegtopnm</code>.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-core/tasks/main.yml</q><pre class="src src-conf">
+- name: Install netpbm.
+  become: yes
+  <span class="org-variable-name">apt: pkg</span>=netpbm
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org9a9dc68" class="outline-3">
+<h3 id="org9a9dc68"><span class="section-number-3">4.15.</span> Configure Weather Updates</h3>
+<div class="outline-text-3" id="text-4-15">
+<p>
+Monkey on Core runs <q>/WWW/campus/Weather/Private/cronjob</q> every 5
+minutes and <q>cronjob-midnight</q> at midnight.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-core/tasks/main.yml</q><pre class="src src-:tangle">
+- name: Create Monkey's weather job.
+  become: yes
+  cron:
+    name: weather
+    hour: "*"
+    minute: "*/5"
+    job: "[ -d /WWW/house ] &amp;&amp; /WWW/house/Weather/Private/cronjob"
+    user: monkey
+</pre>
+</div>
+</div>
+</div>
+</div>
+<div id="outline-container-org3960a98" class="outline-2">
+<h2 id="org3960a98"><span class="section-number-2">5.</span> The Abbey Gate Role</h2>
+<div class="outline-text-2" id="text-5">
+<p>
+Birchwood Abbey's gate is a $110 ÂµPC configured as A Small Institute
+Gate, thus providing a campus VPN on a campus Wi-Fi access point.  It
+routes network traffic from its <code>wifi</code> and <code>lan</code> interfaces to its
+<code>isp</code> interface (and back) with NAT.  That is all the abbey requires
+of its gate, so there is no additional Ansible configuration in this
+chapter (yet).
+</p>
+</div>
+<div id="outline-container-org743d9d2" class="outline-3">
+<h3 id="org743d9d2"><span class="section-number-3">5.1.</span> The Abbey Gate's Network Interfaces</h3>
+<div class="outline-text-3" id="text-5-1">
+<p>
+The abbey gate's <code>lan</code> interface is the PC's built-in Ethernet
+interface, connected to the cloister Ethernet, a Gigabit Ethernet
+switch.  Its <code>wifi</code> interface is a USB3.0 Ethernet adapter connected
+with a cross-over cable to the WAN interface of a Think Penguin
+TPE-R1300 (and at one time a Linksys WRT1900AC).  The <code>isp</code> interface
+is another USB3.0 Ethernet adapter connected with a cross-over cable
+to the Ethernet interface of a "cable modem" (a Starlink terminal).
+</p>
+
+<p>
+The MAC address of each interface is set in <q>private/vars.yml</q>, the
+values of the <code>gate_lan_mac</code>, <code>gate_wifi_mac</code> and <code>gate_isp_mac</code>
+variables.
+</p>
+</div>
+</div>
+<div id="outline-container-orgd8fd372" class="outline-3">
+<h3 id="orgd8fd372"><span class="section-number-3">5.2.</span> The Abbey's Starlink Configuration</h3>
+<div class="outline-text-3" id="text-5-2">
+<p>
+The abbey connects to Starlink via Ethernet, and disables Starlink's
+Wi-Fi access point.  An Ethernet adapter add-on (ordered separately)
+was installed on the Starlink cable, and a second USB-Ethernet dongle
+on Gate.  The adapters were then connected with a cross-over cable.
+</p>
+
+<p>
+The abbey could have avoided buying a separate campus Wi-Fi access
+point, and used Starlink's Wi-Fi instead, with or without its add-on
+Ethernet interface.  Instead, the abbey invested in a 2.4GHz-only
+Think Penguin access point, and connected it to a third Ethernet
+interface on Gate.
+</p>
+
+<p>
+This was preferred for a number of reasons.  Using the add-on Ethernet
+interface allowed Starlink's Wi-Fi to be disabled, reducing the Wi-Fi
+clutter in the campground ether.  Starlink is not always available.
+(It does not work well under trees.)  A dedicated campus Wi-Fi is
+always available.  The password to the campus Wi-Fi is long and
+complex and has been laboriously entered into several household IoT
+devices.  The Think Penguin access point is transparent, trustworthy
+hardware that has earned a Respects Your Freedom certification (see
+<a href="https://ryf.fsf.org/">https://ryf.fsf.org/</a>).  And most importantly, a campus Wi-Fi keeps
+campus network traffic out of the hands of the abbey's ISPs.
+</p>
+</div>
+</div>
+<div id="outline-container-orgbc20e11" class="outline-3">
+<h3 id="orgbc20e11"><span class="section-number-3">5.3.</span> Alternate ISPs</h3>
+<div class="outline-text-3" id="text-5-3">
+<p>
+The abbey used to use a cell phone on a USB tether to get Internet
+service.  At that time, Gate's <q>/etc/netplan/60-isp.yaml</q> file was the
+following.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-conf">network:
+  ethernets:
+    tether:
+      match:
+        name: usb0
+      set-name: isp
+      dhcp4: true
+      dhcp4-overrides:
+        use-dns: false
+</pre>
+</div>
+
+<p>
+The abbey has occasionally used a campground Wi-Fi for Internet
+service, using a <q>60-isp.yaml</q> file similar to the lines below.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-conf">network:
+  wifis:
+    tether:
+      match:
+        name: wlan0
+      set-name: isp
+      dhcp4: true
+      dhcp4-overrides:
+        use-dns: false
+      access-points:
+        <span class="org-string">"AP with password"</span>:
+          password: <span class="org-string">"password"</span>
+        <span class="org-string">"AP with no password"</span>: {}
+</pre>
+</div>
+</div>
+</div>
+</div>
+<div id="outline-container-orgb1328d7" class="outline-2">
+<h2 id="orgb1328d7"><span class="section-number-2">6.</span> The Abbey Cloister Role</h2>
+<div class="outline-text-2" id="text-6">
+<p>
+Birchwood Abbey's cloister is a small institute campus.  The <code>campus</code>
+role configures all campus machines to trust the institute's CA, sync
+with the campus time server, and forward email to Core.  The
+<code>cloister</code> role additionally configures cloistered machines to use the
+cloister Apt cache, respond to Core's NAGIOS network monitor, and to
+install Emacs.  There are also a few OS specific tasks, namely
+configuration required on Raspberry Pi OS machines.
+</p>
+
+<p>
+Wireless clients are issued keys for the cloister VPN by the <code>./abbey
+client</code> command.  This command includes the institutional process
+described in <a href="Institute/README.html#org0ad53cf">The Client Command</a>.  The process handles three types of
+clients: Android, Debian and Campus.  The last type never roams, and
+is not associated with a member of the small institute.
+</p>
+</div>
+<div id="outline-container-org5715c44" class="outline-3">
+<h3 id="org5715c44"><span class="section-number-3">6.1.</span> Use Cloister Apt Cache</h3>
+<div class="outline-text-3" id="text-6-1">
+<p>
+The Apt-Cacher:TNG program does not work well on the frontier, so is
+not a common part of a small institute.  But it is helpful even for a
+cloister with less than a dozen hosts (especially to a homogeneous
+cloister using many of the same packages), so it is tolerable to the
+abbey's monks.  Monks are patient enough to re-run failed scans
+repeatedly until few or no incomplete or damaged files are found.
+Depending on the quality of the Internet connection, this may take a
+while.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-cloister/tasks/main.yml</q><pre class="src src-conf">---
+- name: Use the local Apt package cache.
+  become: yes
+  copy:
+    content: |
+     Acquire::http::Proxy <span class="org-string">"http://apt-cacher.{{ domain_priv }}.:3142"</span>;
+    dest: /etc/apt/apt.conf.d/01proxy
+    <span class="org-variable-name">mode: u</span>=rw,g=r,o=r
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-orgc08e9b0" class="outline-3">
+<h3 id="orgc08e9b0"><span class="section-number-3">6.2.</span> Configure Cloister NRPE</h3>
+<div class="outline-text-3" id="text-6-2">
+<p>
+Each cloistered host is a small institute campus host and thus is
+already running an NRPE server (a NAGIOS Remote Plugin Executor
+server) with a custom <code>inst_sensors</code> monitor (described in <a href="Institute/README.html#orgbd0ce38">Configure
+NRPE</a> of <a href="Institute/README.html">A Small Institute</a>).  The abbey adds one complication: yet
+another <code>check_sensors</code> variant, <code>abbey_pisensors</code>, installed on
+Raspberry Pis (architecture <code>aarch64</code>) only.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-cloister/tasks/main.yml</q><pre class="src src-conf">
+- name: Install abbey_pisensors NAGIOS plugin.
+  become: yes
+  copy:
+    src: ../abbey-core/files/abbey_pisensors
+    dest: /usr/local/sbin/abbey_pisensors
+    <span class="org-variable-name">mode: u</span>=rwx,g=rx,o=rx
+  <span class="org-variable-name">when: ansible_architecture</span> == <span class="org-string">'aarch64'</span>
+
+- name: Configure NAGIOS command.
+  become: yes
+  copy:
+    content: |
+      <span class="org-variable-name">command</span>[<span class="org-constant">abbey_pisensors</span>]=/usr/local/sbin/abbey_pisensors
+    dest: /etc/nagios/nrpe.d/abbey.cfg
+  <span class="org-variable-name">when: ansible_architecture</span> == <span class="org-string">'aarch64'</span>
+  notify: Reload NRPE server.
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>roles_t/abbey-cloister/handlers/main.yml</q><pre class="src src-conf">
+- name: Reload NRPE server.
+  become: yes
+  systemd:
+    service: nagios-nrpe-server
+    state: reloaded
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org4b97d6a" class="outline-3">
+<h3 id="org4b97d6a"><span class="section-number-3">6.3.</span> Install Emacs</h3>
+<div class="outline-text-3" id="text-6-3">
+<p>
+The monks of the abbey are masters of the staff and Emacs.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-cloister/tasks/main.yml</q><pre class="src src-conf">
+- name: Install monastic software.
+  become: yes
+  <span class="org-variable-name">apt: pkg</span>=emacs
+</pre>
+</div>
+</div>
+</div>
+</div>
+<div id="outline-container-org7341dda" class="outline-2">
+<h2 id="org7341dda"><span class="section-number-2">7.</span> The Abbey Weather Role</h2>
+<div class="outline-text-2" id="text-7">
+<p>
+Birchwood Abbey's weather hosts use the 1-Wire server (from the
+<code>owserver</code> package) and a 1-Wire USB adapter.  They use an
+unprivileged account (<code>monkey</code>) to run a SystemD service named
+<code>weatherd</code> (aka "the daemon").  The daemon is a Perl script that runs
+<code>owread</code> and logs the new measurements once per minute.
+</p>
+
+<p>
+The log files are collected by Monkey on Core (via <code>rsync</code>), then
+processed and published in campus web pages by The Weather Project's
+code (old, using <code>gnuplot(1)</code>, and so&#x2026; unpublished).
+</p>
+</div>
+<div id="outline-container-org6925511" class="outline-3">
+<h3 id="org6925511"><span class="section-number-3">7.1.</span> The Abbey Weather Hardware</h3>
+<div class="outline-text-3" id="text-7-1">
+<p>
+The abbey currently has one weather host, Gate, and a couple 1-Wire
+sensor modules.  The modules measure inside and outside temperature
+and humidity.  Their desired locations are 7-8m from the core servers
+so they are plugged into a custom Y cable, with the inside sensor
+cable spliced into the middle of the outside/main cable.  The proximal
+end's RJ11 plugs into a 1-Wire USB adapter (a DS9490R) plugged into
+Gate.  The outside end goes out the window with the Starlink cable.
+</p>
+</div>
+</div>
+<div id="outline-container-org0b52261" class="outline-3">
+<h3 id="org0b52261"><span class="section-number-3">7.2.</span> The Abbey Weather Host Setup</h3>
+<div class="outline-text-3" id="text-7-2">
+<p>
+The Ansible code in the <code>abbey-weather</code> role assumes it is working
+with a cloistered host (as described in <a href="#org110d7b3">Cloistering</a>) and proceeds in
+two phases.  The first installs the <code>ow-server</code> package and configures
+it to use a DS9490 (USB adapter) rather than a debugging fake.  After
+the first <code>./abbey config new</code>, the new weather host seems to need a
+reboot before the 1-Wire bus becomes visible via <code>owdir</code>.
+</p>
+
+<p>
+After a reboot <code>owdir</code> should list one or more type 26 device IDs.
+Listing them (e.g. running <code>owdir /26.nnnnnnnn</code> or <code>owdir
+/26.nnnnnnnn/HIH</code>) should reveal "files" named <q>temperature</q> and
+<q>HIH/humidity</q>.  These pseudo-file paths are used in the daemon script
+below.  A test session is shown below.
+</p>
+
+<pre class="example" id="org58a9437">
+monkey@new$ owdir
+...
+    /26.2153B6000000/
+...
+monkey@new$ owdir /26.2153B6000000
+...
+    /26.2153B6000000/temperature
+...
+monkey@new$ owread /26.2153B6000000/temperature; echo
+26.125
+monkey@new$ 
+</pre>
+
+<p>
+The second phase of weather host configuration waits for the host-
+specific weather daemon script to appear in the role's <q>files/</q>.
+</p>
+</div>
+</div>
+<div id="outline-container-org6ea9cdd" class="outline-3">
+<h3 id="org6ea9cdd"><span class="section-number-3">7.3.</span> The Abbey Weather Daemons</h3>
+<div class="outline-text-3" id="text-7-3">
+<p>
+Different weather hosts, with different 1-Wire devices, need different
+daemon scripts, to call <code>owread</code> with different paths (containing the
+IDs of each host's devices).  At the moment there is just the
+one weather host, <code>anoat</code>.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-weather/files/daemon-anoat</q><pre class="src src-perl"><span class="org-comment-delimiter">#</span><span class="org-comment">!/usr/bin/perl -w</span>
+<span class="org-comment-delimiter"># </span><span class="org-comment">-*- CPerl -*-</span>
+<span class="org-comment-delimiter">#</span>
+<span class="org-comment-delimiter"># </span><span class="org-comment">Weather/daemon</span>
+<span class="org-comment-delimiter">#</span>
+<span class="org-comment-delimiter"># </span><span class="org-comment">Fetches data from the local owserver once per minute.  Appends to</span>
+<span class="org-comment-delimiter"># </span><span class="org-comment">Log/{In,Out}side/YEAR/MONTH/DAY.txt.</span>
+
+<span class="org-constant">use</span> strict;
+<span class="org-constant">use</span> IO::File;
+<span class="org-constant">use</span> Date::Format;
+
+<span class="org-type">my</span> $<span class="org-variable-name">ILOG</span>;
+<span class="org-type">my</span> $<span class="org-variable-name">OLOG</span>;
+<span class="org-type">my</span> $<span class="org-variable-name">ymd</span> = <span class="org-string">""</span>;
+<span class="org-keyword">sub</span> <span class="org-function-name">mymkdir</span> ($);
+<span class="org-keyword">sub</span> <span class="org-function-name">reopen_logs</span> ()
+{
+  <span class="org-type">my</span> $<span class="org-variable-name">time</span> = time;
+  <span class="org-type">my</span> $<span class="org-variable-name">datime</span> = time2str (<span class="org-string">"%Y-%m-%d %H:%M:%S"</span>, $<span class="org-variable-name">time</span>, <span class="org-string">"UTC"</span>);
+  <span class="org-type">my</span> ($<span class="org-variable-name">year</span>, $<span class="org-variable-name">month</span>, $<span class="org-variable-name">day</span>) = $<span class="org-variable-name">datime</span> =~ <span class="org-string">/^(\d{4})-(\d\d)-(\d\d) /</span>;
+  <span class="org-type">my</span> $<span class="org-variable-name">new_ymd</span> = <span class="org-string">"$year/$month/$day"</span>;
+  <span class="org-keyword">return</span> <span class="org-keyword">if</span> $<span class="org-variable-name">new_ymd</span> eq $<span class="org-variable-name">ymd</span>;
+  close $<span class="org-variable-name">ILOG</span> <span class="org-keyword">if</span> defined $<span class="org-variable-name">ILOG</span>;
+  close $<span class="org-variable-name">OLOG</span> <span class="org-keyword">if</span> defined $<span class="org-variable-name">OLOG</span>;
+  umask 07;
+  mymkdir <span class="org-string">"Inside/$year/$month"</span>;
+  mymkdir <span class="org-string">"Outside/$year/$month"</span>;
+  umask 027;
+  <span class="org-type">my</span> $<span class="org-variable-name">filename</span> = <span class="org-string">"Inside/$new_ymd.txt"</span>;
+  $<span class="org-variable-name">ILOG</span> = new IO::File;
+  open $<span class="org-variable-name">ILOG</span>, <span class="org-string">"&gt;&gt;$filename"</span> or <span class="org-keyword">die</span> <span class="org-string">"Could not open $filename: $!\n"</span>;
+  $<span class="org-variable-name">filename</span> = <span class="org-string">"Outside/$new_ymd.txt"</span>;
+  $<span class="org-variable-name">OLOG</span> = new IO::File;
+  open $<span class="org-variable-name">OLOG</span>, <span class="org-string">"&gt;&gt;$filename"</span> or <span class="org-keyword">die</span> <span class="org-string">"Could not open $filename: $!\n"</span>;
+  $<span class="org-variable-name">ymd</span> = $<span class="org-variable-name">new_ymd</span>;
+}
+
+<span class="org-keyword">sub</span> <span class="org-function-name">logit</span> ($$$);
+<span class="org-keyword">sub</span> <span class="org-function-name">main</span> () {
+  <span class="org-keyword">die</span> <span class="org-string">"usage: $0\n"</span> <span class="org-keyword">if</span> @<span class="org-underline"><span class="org-variable-name">ARGV</span></span> != 0;
+  $<span class="org-variable-name">0</span> = <span class="org-string">"weatherd"</span>;
+  chdir <span class="org-string">"/home/monkey/Weather/Log"</span> or <span class="org-keyword">die</span>;
+  umask 027;
+  <span class="org-type">my</span> $<span class="org-variable-name">start</span> = time;
+  {
+    <span class="org-type">my</span> $<span class="org-variable-name">secs</span> = 60 - $<span class="org-variable-name">start</span> % 60;
+    $<span class="org-variable-name">start</span> += $<span class="org-variable-name">secs</span>;
+    sleep ($<span class="org-variable-name">secs</span>);
+  }
+  <span class="org-keyword">while</span> (1) {
+    reopen_logs;
+    logit $<span class="org-variable-name">OLOG</span>, <span class="org-string">"T"</span>, <span class="org-string">"/26.2153B6000000/temperature"</span>;
+    logit $<span class="org-variable-name">OLOG</span>, <span class="org-string">"H"</span>, <span class="org-string">"/26.2153B6000000/HIH4000/humidity"</span>;
+    logit $<span class="org-variable-name">ILOG</span>, <span class="org-string">"T"</span>, <span class="org-string">"/26.8859B6000000/temperature"</span>;
+    logit $<span class="org-variable-name">ILOG</span>, <span class="org-string">"H"</span>, <span class="org-string">"/26.8859B6000000/HIH4000/humidity"</span>;
+    $<span class="org-variable-name">start</span> += 60;
+    <span class="org-type">my</span> $<span class="org-variable-name">now</span> = time;
+    <span class="org-keyword">while</span> ($<span class="org-variable-name">start</span> &lt; $<span class="org-variable-name">now</span>) { $<span class="org-variable-name">start</span> += 60; }
+    <span class="org-type">my</span> $<span class="org-variable-name">secs</span> = $<span class="org-variable-name">start</span> - $<span class="org-variable-name">now</span>;
+    sleep  ($<span class="org-variable-name">secs</span>);
+  }
+}
+
+<span class="org-keyword">sub</span> <span class="org-function-name">logit</span> ($$$)
+{
+  <span class="org-type">my</span> ($<span class="org-variable-name">log</span>, $<span class="org-variable-name">name</span>, $<span class="org-variable-name">query</span>) = @<span class="org-underline"><span class="org-variable-name">_</span></span>;
+
+  <span class="org-type">my</span> $<span class="org-variable-name">tries</span> = 0;
+  <span class="org-keyword">while</span> ($<span class="org-variable-name">tries</span> &lt; 3) {
+    <span class="org-type">my</span> $<span class="org-variable-name">time</span> = time;
+    <span class="org-type">my</span> $<span class="org-variable-name">datime</span> = time2str (<span class="org-string">"%Y-%m-%d %H:%M:%S"</span>, $<span class="org-variable-name">time</span>, <span class="org-string">"UTC"</span>);
+    $<span class="org-variable-name">tries</span> += 1;
+    <span class="org-type">my</span> @<span class="org-underline"><span class="org-variable-name">lines</span></span> = <span class="org-string">`/usr/bin/owread $query`</span>;
+    chomp @<span class="org-underline"><span class="org-variable-name">lines</span></span>;
+    <span class="org-type">my</span> $<span class="org-variable-name">status</span> = $?;
+    <span class="org-type">my</span> $<span class="org-variable-name">sig</span> = $<span class="org-variable-name">status</span> &amp; 127;
+    $<span class="org-variable-name">status</span> &gt;&gt;= 8;
+    <span class="org-keyword">if</span> ($<span class="org-variable-name">status</span> != 0) {
+      <span class="org-type">my</span> $<span class="org-variable-name">L</span> = join <span class="org-string">"\\n"</span>, @<span class="org-underline"><span class="org-variable-name">lines</span></span>;
+      print $<span class="org-variable-name">log</span> <span class="org-string">"$datime\t$name\terror: status $status: $L\n"</span>;
+      $<span class="org-variable-name">log</span>-&gt;flush;
+    } <span class="org-keyword">elsif</span> (@<span class="org-underline"><span class="org-variable-name">lines</span></span> != 1) {
+      <span class="org-type">my</span> $<span class="org-variable-name">L</span> = join <span class="org-string">"\\n"</span>, @<span class="org-underline"><span class="org-variable-name">lines</span></span>;
+      print $<span class="org-variable-name">log</span> <span class="org-string">"$datime\t$name\terror: multiple lines: $L\n"</span>;
+      $<span class="org-variable-name">log</span>-&gt;flush;
+    } <span class="org-keyword">elsif</span> ($<span class="org-variable-name">lines</span>[0] !~ <span class="org-string">/^ *(-?\d+(\.\d+)?)$/</span>) {
+      <span class="org-type">my</span> $<span class="org-variable-name">L</span> = $<span class="org-variable-name">lines</span>[0];
+      print $<span class="org-variable-name">log</span> <span class="org-string">"$datime\t$name\terror: bogus line: $L\n"</span>;
+      $<span class="org-variable-name">log</span>-&gt;flush;
+    } <span class="org-keyword">else</span> {
+      <span class="org-type">my</span> $<span class="org-variable-name">datum</span> = $<span class="org-variable-name">1</span>;
+      print $<span class="org-variable-name">log</span> <span class="org-string">"$datime\t$name\t$datum\n"</span>;
+      $<span class="org-variable-name">log</span>-&gt;flush;
+      <span class="org-keyword">return</span>;
+    }
+  }
+}
+
+<span class="org-keyword">sub</span> <span class="org-function-name">mymkdir</span> ($)
+{
+  <span class="org-type">my</span> ($<span class="org-variable-name">dirpath</span>) = @<span class="org-underline"><span class="org-variable-name">_</span></span>;
+
+  <span class="org-type">my</span> @<span class="org-underline"><span class="org-variable-name">path_names</span></span> = split <span class="org-string">/\//</span>, $<span class="org-variable-name">dirpath</span>;
+  <span class="org-type">my</span> $<span class="org-variable-name">path</span>;
+  <span class="org-keyword">if</span> (!$<span class="org-variable-name">path_names</span>[0]) {
+    $<span class="org-variable-name">path</span> = <span class="org-string">"/"</span>;
+    shift @<span class="org-underline"><span class="org-variable-name">path_names</span></span>;
+  } <span class="org-keyword">else</span> {
+    $<span class="org-variable-name">path</span> = <span class="org-string">"."</span>;
+  }
+  <span class="org-type">my</span> @<span class="org-underline"><span class="org-variable-name">created</span></span>;
+  <span class="org-keyword">while</span> (@<span class="org-underline"><span class="org-variable-name">path_names</span></span>) {
+    $<span class="org-variable-name">path</span> .= <span class="org-string">"/"</span> . shift @<span class="org-underline"><span class="org-variable-name">path_names</span></span>;
+    <span class="org-keyword">if</span> (! -d $<span class="org-variable-name">path</span>) {
+      <span class="org-keyword">if</span> (-e $<span class="org-variable-name">path</span>) {
+        <span class="org-keyword">die</span> <span class="org-string">"mkdir $dirpath: already exists; not a directory!\n"</span>;
+      }
+      <span class="org-keyword">if</span> (! mkdir $<span class="org-variable-name">path</span>) {
+        <span class="org-keyword">die</span> <span class="org-string">"mkdir $path: $!\n"</span>;
+      } <span class="org-keyword">else</span> {
+        chmod 02775, $<span class="org-variable-name">path</span>;
+        push @<span class="org-underline"><span class="org-variable-name">created</span></span>, $<span class="org-variable-name">path</span>;
+      }
+    }
+  }
+  <span class="org-keyword">return</span> @<span class="org-underline"><span class="org-variable-name">created</span></span>;
+}
+
+main;
+</pre>
+</div>
+
+<p>
+The above Perl script uses the <code>Date::Format</code> module, which is
+installed by the following task.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-weather/tasks/main.yml</q><pre class="src src-conf">---
+- name: Install weather daemon packages.
+  become: yes
+  <span class="org-variable-name">apt: pkg</span>=libtimedate-perl
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-orgfe87cd1" class="outline-3">
+<h3 id="orgfe87cd1"><span class="section-number-3">7.4.</span> Install 1-Wire Server</h3>
+<div class="outline-text-3" id="text-7-4">
+<p>
+The following task installs the 1-Wire server and shell commands.  The
+abbey uses the Dallas Semiconductor DS9490R, a USB to 1-Wire adapter,
+on all its weather hosts, so it also configures the server to use the
+USB adapter (rather than a test "fake" adapter).
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-weather/tasks/main.yml</q><pre class="src src-conf">
+- name: Install 1-Wire server.
+  become: yes
+  apt:
+    pkg: [ owserver, ow-shell ]
+
+- name: Configure 1-Wire server.
+  become: yes
+  lineinfile:
+    path: /etc/owfs.conf
+    regexp: <span class="org-string">"{{ item.regexp }}"</span>
+    line: <span class="org-string">"{{ item.line }}"</span>
+    backrefs: yes
+  loop:
+  - { regexp: <span class="org-string">'^[# ]*server: *FAKE(.*)$'</span>, line: <span class="org-string">'#server: FAKE\1'</span> }
+  - { regexp: <span class="org-string">'^[# ]*server: *usb(.*)$'</span>, line: <span class="org-string">'server: usb\1'</span> }
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org816673b" class="outline-3">
+<h3 id="org816673b"><span class="section-number-3">7.5.</span> Install Rsync</h3>
+<div class="outline-text-3" id="text-7-5">
+<p>
+Monkey on Core will want to download log records (files) using
+<code>rsync(1)</code>.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-weather/tasks/main.yml</q><pre class="src src-conf">
+- name: Install Rsync.
+  become: yes
+  <span class="org-variable-name">apt: pkg</span>=rsync
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org13dabcd" class="outline-3">
+<h3 id="org13dabcd"><span class="section-number-3">7.6.</span> Create Monkey</h3>
+<div class="outline-text-3" id="text-7-6">
+<p>
+The weather daemon is run by an unprivileged <code>monkey</code> account (<i>not</i>
+<code>sysadm</code>) which allows <code>monkey</code> on Core shell access.  This is also
+executed during the initial phase of configuration, allowing the
+administrator to login on the new weather host as <code>monkey</code> and thus to
+test access to the 1-Wire adapter and devices.  To facilitate
+debugging the <code>sysadm</code> account is included in the <code>monkey</code> group.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-weather/tasks/main.yml</q><pre class="src src-conf">
+- name: Create monkey.
+  become: yes
+  user:
+    name: monkey
+    system: yes
+
+- name: Authorize monkey@core.
+  become: yes
+  vars:
+    pubkeyfile: ../Secret/ssh_monkey/id_rsa.pub
+  authorized_key:
+    user: monkey
+    key: <span class="org-string">"{{ lookup('file', pubkeyfile) }}"</span>
+    manage_dir: yes
+
+- name: Add {{ ansible_user }} to monkey group.
+  become: yes
+  user:
+    name: <span class="org-string">"{{ ansible_user }}"</span>
+    append: yes
+    groups: monkey
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org225880e" class="outline-3">
+<h3 id="org225880e"><span class="section-number-3">7.7.</span> Install Weather Daemon</h3>
+<div class="outline-text-3" id="text-7-7">
+<p>
+The weather daemon is kept alive as a Systemd service unit.  This task
+creates and starts that service <i>after</i> the host-specific
+<q>files/daemon-HOST</q> file becomes available.
+</p>
+
+<p>
+The <code>ExecStartPre=/bin/sleep 30</code> is intended to avoid recent hangs in
+<code>owread</code>.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-weather/tasks/main.yml</q><pre class="src src-conf">
+- name: Install weather directory.
+  become: yes
+  file:
+    path: /home/monkey/Weather/Log
+    state: directory
+    owner: monkey
+    group: monkey
+    <span class="org-variable-name">mode: u</span>=rwx,g=rx,o=rx
+
+- name: Test for weather daemon script.
+  vars:
+    dir: ../roles/abbey-weather/files
+    file: <span class="org-string">"{{ dir }}/daemon-{{ inventory_hostname }}"</span>
+  <span class="org-variable-name">stat: path</span>=<span class="org-string">"{{ file }}"</span>
+  delegate_to: localhost
+  register: weather
+
+- name: Note missing weather daemon script.
+  vars:
+    dir: ../roles/abbey-weather/files
+    script: <span class="org-string">"{{ dir }}/daemon-{{ inventory_hostname }}"</span>
+  debug:
+    msg: <span class="org-string">"{{ script }}: not found"</span>
+  when: not weather.stat.exists
+
+- name: Install weather daemon.
+  become: yes
+  vars:
+    dir: ../roles/abbey-weather/files
+    script: <span class="org-string">"{{ dir }}/daemon-{{ inventory_hostname }}"</span>
+  copy:
+    src: <span class="org-string">"{{ script }}"</span>
+    dest: /home/monkey/Weather/daemon
+    owner: monkey
+    group: monkey
+    <span class="org-variable-name">mode: u</span>=rwx,g=rx,o=
+  when: weather.stat.exists
+
+- name: Install weatherd service.
+  become: yes
+  copy:
+    content: |
+      [<span class="org-type">Unit</span>]
+      <span class="org-variable-name">Description</span>=Weather Logger
+      <span class="org-variable-name">After</span>=owserver.service
+
+      [<span class="org-type">Service</span>]
+      <span class="org-variable-name">User</span>=monkey
+      <span class="org-variable-name">ExecStartPre</span>=/bin/sleep 30
+      <span class="org-variable-name">ExecStart</span>=/home/monkey/Weather/daemon
+      <span class="org-variable-name">Restart</span>=always
+
+      [<span class="org-type">Install</span>]
+      <span class="org-variable-name">WantedBy</span>=multi-user.target
+    dest: /etc/systemd/system/weatherd.service
+  when: weather.stat.exists
+  notify:
+  - Reload Systemd.
+  - Restart weather daemon.
+
+- name: Enable/Start weather daemon.
+  become: yes
+  systemd:
+    service: weatherd
+    enabled: yes
+    state: started
+  when: weather.stat.exists
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>roles_t/abbey-weather/handlers/main.yml</q><pre class="src src-conf">---
+- name: Reload Systemd.
+  become: yes
+  command: systemctl daemon-reload
+
+- name: Restart weather daemon.
+  become: yes
+  systemd:
+    service: weatherd
+    state: restarted
+</pre>
+</div>
+</div>
+</div>
+</div>
+<div id="outline-container-org2c65dbc" class="outline-2">
+<h2 id="org2c65dbc"><span class="section-number-2">8.</span> The Abbey DVR Role</h2>
+<div class="outline-text-2" id="text-8">
+<p>
+The abbey uses Zoneminder to record video from PoE IP HD security
+cameras.  The Abbey DVR Role installs Zoneminder and configures it to
+record to <q>/Zoneminder/</q>, the mount point for a separate, large
+storage volume.  It follows the instructions in
+<q>/usr/share/doc/zoneminder/README.Debian</q> to create the <code>zm</code> database
+and configuring Apache.
+</p>
+</div>
+<div id="outline-container-org4fe0a29" class="outline-3">
+<h3 id="org4fe0a29"><span class="section-number-3">8.1.</span> DVR Machine Setup</h3>
+<div class="outline-text-3" id="text-8-1">
+<p>
+The installation process involves some manual intervention.  The first
+time a host is enrolled, Ansible will install the necessary packages,
+but it cannot create the database, nor the database user (yet, in the
+first pass).  After adding the new machine to the <code>dvrs</code> group in
+<a href="#orgd0676df">10.2</a>, run Ansible to get the Zoneminder software installed.
+</p>
+
+<pre class="example">
+./abbey config HOST
+</pre>
+
+
+<p>
+Several configuration steps will be skipped because <q>/Zoneminder/</q> has
+not been created yet.  To proceed, first create the database and
+database user manually, as described in section <a href="#org6c0f481">Manually Create
+Zoneminder DB and User</a>.
+</p>
+</div>
+</div>
+<div id="outline-container-org65ae5a4" class="outline-3">
+<h3 id="org65ae5a4"><span class="section-number-3">8.2.</span> Create <q>/Zoneminder/</q></h3>
+<div class="outline-text-3" id="text-8-2">
+<p>
+<q>/Zoneminder/</q> should be a separate, large volume lest Zoneminder fill
+the root file system.  For acceptable performance, <q>/Zoneminder/</q>
+should also be the mount point of a solid-state disk (SSD).  A
+symbolic link at <q>/var/cache/zoneminder/events</q> targets <q>/Zoneminder</q>
+to make it Zoneminder's "default" storage area.  (The <code>PurgeWhenFull</code>
+filter only works with the default storage area in v1.34.)
+</p>
+</div>
+</div>
+<div id="outline-container-orgd30766b" class="outline-3">
+<h3 id="orgd30766b"><span class="section-number-3">8.3.</span> Continue Zoneminder Configuration</h3>
+<div class="outline-text-3" id="text-8-3">
+<p>
+Once the <code>zm</code> database (and <code>zmuser</code> database user) are created, and a
+large volume mounted at <q>/Zoneminder/</q>, Ansible can continue with the
+Zoneminder configuration.
+</p>
+
+<pre class="example">
+./abbey configure HOST
+</pre>
+
+
+<p>
+Configuring Zoneminder's cameras is still a manual process as
+described in the final section, <a href="#org1115114">Configure Cameras</a>, below.
+</p>
+</div>
+</div>
+<div id="outline-container-org2556ab2" class="outline-3">
+<h3 id="org2556ab2"><span class="section-number-3">8.4.</span> Include Abbey Variables</h3>
+<div class="outline-text-3" id="text-8-4">
+<p>
+In this abbey specific document, most abbey particulars are not
+replaced with variables, but specified in-line.  Some, however, are
+not published (e.g. database passwords).  The variables that replace
+them are included from <q>private/vars-abbey.yml</q>.  Example values are
+given in this document.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-dvr/tasks/main.yml</q><pre class="src src-conf">---
+- name: Include private abbey variables.
+  include_vars: ../private/vars-abbey.yml
+</pre>
+</div>
+
+<p>
+The relative filename should be found only in the playbook's
+directory, <q>playbooks/</q>.
+</p>
+</div>
+</div>
+<div id="outline-container-orgccc0d2c" class="outline-3">
+<h3 id="orgccc0d2c"><span class="section-number-3">8.5.</span> Install Zoneminder v1.34</h3>
+<div class="outline-text-3" id="text-8-5">
+<p>
+The latest version of Zoneminder (1.36) was manually downloaded, built
+and installed, but it immediately had problems, randomly producing
+short events, dropping "problem" cameras entirely, etc.  Version 1.34
+did not have those problems, but could still melt down (thrash?) when
+<q>/Zoneminder/</q> was a Seagate Barracuda in a USB3.1gen2 external drive
+enclosure.  A Western Digital Passport Ultra seemed to work much
+better, for a short while.  Ultimately a solid-state drive (a 2TB
+USB3.2 Gen2 Samsung T7 Shield) mounted at <q>/Zoneminder/</q> got
+Zoneminder 1.34 to work reliably.
+</p>
+
+<p>
+After uninstalling 1.36, the Debian 11 package (1.34) was installed
+and configured per the instructions in sections "Web server set-up"
+and "Time Zone" in <q>/usr/share/doc/zoneminder/README.Debian.gz</q>.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-dvr/tasks/main.yml</q><pre class="src src-conf">
+- name: Install Zoneminder.
+  become: yes
+  <span class="org-variable-name">apt: pkg</span>=zoneminder
+
+- name: Enable Apache modules for Zoneminder.
+  become: yes
+  apache2_module:
+    name: <span class="org-string">"{{ item }}"</span>
+  loop: [ cgi, rewrite, expires, headers ]
+  notify: Restart Apache2.
+
+- name: Enable Zoneminder Apache configuration.
+  become: yes
+  command:
+    cmd: a2enconf zoneminder
+    creates: /etc/apache2/conf-enabled/zoneminder.conf
+  notify: Restart Apache2.
+
+- name: Configure MySQL for Zoneminder.
+  become: yes
+  copy:
+    content: |
+      [<span class="org-type">mysqld</span>]
+      <span class="org-variable-name">sql_mode</span> = NO_ENGINE_SUBSTITUTION
+    dest: /etc/mysql/conf.d/zoneminder.cnf
+  notify: Restart MySQL.
+
+- name: Configure PHP date.timezone.
+  become: yes
+  lineinfile:
+    <span class="org-variable-name">regexp: date.timezone ?</span>=
+    <span class="org-variable-name">line: date.timezone</span> = {{ lookup(<span class="org-string">'file'</span>, <span class="org-string">'/etc/timezone'</span>) }}
+    path: <span class="org-string">"{{ item }}"</span>
+  loop:
+  - /etc/php/7.4/cli/php.ini
+  - /etc/php/7.4/apache2/php.ini
+  notify: Restart Apache2.
+
+- name: Enable/Start Apache2.
+  become: yes
+  systemd:
+    service: apache2
+    enabled: yes
+    state: started
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>roles_t/abbey-dvr/handlers/main.yml</q><pre class="src src-conf">---
+- name: Restart MySQL.
+  become: yes
+  systemd:
+    service: mysql
+    state: restarted
+
+- name: Restart Apache2.
+  become: yes
+  systemd:
+    service: apache2
+    state: restarted
+</pre>
+</div>
+
+<p>
+The following Rsyslog configuration drop-in gets Zoneminder's natter
+out of <q>/var/log/syslog</q>.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-dvr/tasks/main.yml</q><pre class="src src-conf">
+- name: Use /var/log/zoneminder.log
+  become: yes
+  copy:
+    content: |
+      :programname,startswith,<span class="org-string">"zm"</span> -/var/log/zoneminder.log
+      &amp; stop
+    dest: /etc/rsyslog.d/40-zoneminder.conf
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org6940ae7" class="outline-3">
+<h3 id="org6940ae7"><span class="section-number-3">8.6.</span> Create Zoneminder Database</h3>
+<div class="outline-text-3" id="text-8-6">
+<p>
+Zoneminder's MariaDB database is created by the following task, when
+the <code>mysql_db</code> Ansible module supports <code>check_implicit_admin</code>.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-conf">
+- name: Create Zoneminder DB.
+  become: yes
+  mysql_db:
+    check_implicit_admin: yes
+    name: zm
+    collation: utf8mb4_general_ci
+    encoding: utf8mb4
+</pre>
+</div>
+
+<p>
+Unfortunately it does not currently, yet the institute prefers the
+more secure Unix socket authentication method.  Rather than create a
+privileged DB user, the <code>zm</code> database is created manually (below).
+</p>
+</div>
+</div>
+<div id="outline-container-org7213248" class="outline-3">
+<h3 id="org7213248"><span class="section-number-3">8.7.</span> Create Zoneminder DB User</h3>
+<div class="outline-text-3" id="text-8-7">
+<p>
+The following task would create the DB user (<code>mysql_user</code> supports
+<code>check_implicit_admin</code>) <i>but</i> the <code>zm</code> database was not created above.
+</p>
+
+<p>
+The DB user's password is taken from the <code>zoneminder_dbpass</code>
+variable, kept in <q>private/vars-abbey.yml</q>, and generated e.g. with
+the <code>apg -n 1 -x 12 -m 12</code> command.
+</p>
+
+<div class="org-src-container">
+<q>private/vars-abbey.yml</q><pre class="src src-conf">---
+zoneminder_dbpass:           gakJopbikJadsEdd
+</pre>
+</div>
+
+<div class="org-src-container">
+<pre class="src src-conf">
+- name: Create Zoneminder DB user.
+  become: yes
+  mysql_user:
+    check_implicit_admin: yes
+    name: zmuser
+    password: <span class="org-string">"{{ zoneminder_dbpass }}"</span>
+    priv: &gt;-
+      zm.*:
+      lock tables,alter,create,index,select,insert,update,delete
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org6c0f481" class="outline-3">
+<h3 id="org6c0f481"><span class="section-number-3">8.8.</span> Manually Create Zoneminder DB and User</h3>
+<div class="outline-text-3" id="text-8-8">
+<p>
+The Zoneminder database and database user are created manually with
+the following SQL (with the <code>zoneminder_dbpass</code> spliced in).  The SQL
+commands are entered at the SQL prompt of the <code>sudo mysql</code> command, or
+perhaps piped into the command.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sql"><span class="org-keyword">create</span> database zm
+    <span class="org-type">character</span> <span class="org-keyword">set</span> utf8mb4
+    <span class="org-keyword">collate</span> utf8mb4_general_ci;
+<span class="org-keyword">grant</span> lock tables,<span class="org-keyword">alter</span>,<span class="org-keyword">create</span>,index,<span class="org-keyword">select</span>,<span class="org-keyword">insert</span>,<span class="org-keyword">update</span>,<span class="org-keyword">delete</span>
+    <span class="org-keyword">on</span> zm.*
+    <span class="org-keyword">to</span> <span class="org-string">'zmuser'</span>@<span class="org-string">'localhost'</span>
+    identified <span class="org-keyword">by</span> <span class="org-string">'{{ zoneminder_dbpass }}'</span>;
+flush <span class="org-keyword">privileges</span>;
+exit;
+</pre>
+</div>
+
+<p>
+Finally, <code>zm</code>'s tables are created, completing the database setup,
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">sudo mysql &lt; /usr/share/zoneminder/db/zm_create.sql
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-orge311581" class="outline-3">
+<h3 id="orge311581"><span class="section-number-3">8.9.</span> Use <q>/Zoneminder/</q></h3>
+<div class="outline-text-3" id="text-8-9">
+<p>
+The following tasks start with a test for the existence of
+<q>/Zoneminder</q>.  Configuration tasks that require <q>/Zoneminder/</q> or the
+<code>zm</code> database are executed only when <code>zoneminder.stat.exists</code>.  The
+last "Link&#x2026;" task below "forces" the link, whether the target exists
+or not (yet).
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-dvr/tasks/main.yml</q><pre class="src src-conf">
+- name: Test for /Zoneminder/.
+  stat:
+    path: /Zoneminder
+  register: zoneminder
+- debug:
+    msg: <span class="org-string">"/Zoneminder/ does not yet exist"</span>
+  when: not zoneminder.stat.exists
+
+- name: Check /Zoneminder/.
+  become: yes
+  file:
+    state: directory
+    path: /Zoneminder
+    owner: www-data
+    group: www-data
+    <span class="org-variable-name">mode: u</span>=rwx,g=rx,o=rx
+  when: zoneminder.stat.exists
+
+- name: Link to /Zoneminder/.
+  become: yes
+  file:
+    state: link
+    src: /Zoneminder
+    path: /var/cache/zoneminder/events
+    force: yes
+    follow: no
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-orgd750b3c" class="outline-3">
+<h3 id="orgd750b3c"><span class="section-number-3">8.10.</span> Configure Zoneminder</h3>
+<div class="outline-text-3" id="text-8-10">
+<p>
+The remaining tasks ensure that the <q>/etc/zm/zm.conf</q> file has the
+proper permissions and contains the correct password.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-dvr/tasks/main.yml</q><pre class="src src-conf">
+- name: Set /etc/zm/zm.conf permissions.
+  become: yes
+  file:
+    path: /etc/zm/zm.conf
+    owner: root
+    group: www-data
+    <span class="org-variable-name">mode: u</span>=rw,g=r,o=
+
+- name: Set Zoneminder passphrase.
+  become: yes
+  lineinfile:
+    regexp: <span class="org-string">'^ *ZM_DB_PASS *='</span>
+    <span class="org-variable-name">line: ZM_DB_PASS</span>={{ zoneminder_dbpass }}
+    path: /etc/zm/zm.conf
+</pre>
+</div>
+
+<p>
+Finally, Zoneminder's service unit can be enabled (and started) <i>if</i>
+<q>/Zoneminder/</q> exists.  It is assumed that, if <q>/Zoneminder/</q> exists,
+the <code>zm</code> database has also been created, and the service is ready to
+run.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-dvr/tasks/main.yml</q><pre class="src src-conf">
+- name: Enable/Start Zoneminder.
+  become: yes
+  systemd:
+    service: zoneminder
+    enabled: yes
+    state: started
+  when: zoneminder.stat.exists
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org1115114" class="outline-3">
+<h3 id="org1115114"><span class="section-number-3">8.11.</span> Configure Cameras</h3>
+<div class="outline-text-3" id="text-8-11">
+<p>
+A new security camera is setup as described in <a href="#org110d7b3">Cloistering</a>, after
+which the camera should be accessible by name on the abbey networks.
+Assuming <code>ping -c1 new</code> works, the camera's web interface will be
+accessible at <code>http://new/</code>.
+</p>
+
+<p>
+The abbey's administrator logs into <code>http://new/</code> and turns off any
+OSD (on-screen display).  Zoneminder will add its own timestamp, for
+the best accuracy and reliability.  The administrator also turns down
+the frame rate to 5fps.  The abbey prefers HD resolution (e.g. 1080p)
+and long duration logs, thus fewer frames per second.  The
+administrator also creates an unprivileged user with a short password
+e.g. <code>user:gobbledygook</code>.
+</p>
+
+<p>
+After Ansible has configured and started Zoneminder, a camera can be
+created by clicking on "Add" in the Zoneminder console.  (If the
+Zoneminder host was named "security", the Zoneminder console can be
+found at <code>http://security/zm/</code>.)  In the Add dialog, the following
+settings should be changed.  (The parenthesized settings are default
+settings that should be checked but are probably already correctly
+set.)
+</p>
+
+<ul class="org-ul">
+<li>In the "General" tab, specify:
+<ul class="org-ul">
+<li>Name: Front</li>
+<li>(Server: None)</li>
+<li>(Source type: Ffmpeg)</li>
+<li>Function: Record</li>
+<li>Enabled: yes</li>
+<li>(Analysis FPS: &lt;blank&gt;)</li>
+<li>(Maximum FPS: &lt;blank&gt;)</li>
+<li>(Alarm Maximum FPS: &lt;blank&gt;)</li>
+</ul></li>
+<li>In the "Source" tab, specify:
+<ul class="org-ul">
+<li>Src path: rtsp://user:gobbledygook@new.small.private.:554/11</li>
+<li>(Method: TCP)</li>
+<li>(Target colorspace: 32 bit colour)</li>
+<li>Capture Resolution: 1920x1080 1080p</li>
+</ul></li>
+<li>In the "Timestamp" tab, specify:
+<ul class="org-ul">
+<li>Timestamp Label X: 10</li>
+<li>Timestamp Label Y: 10</li>
+<li>Font Size: Large</li>
+</ul></li>
+<li>In the "Buffers" tab, specify:
+<ul class="org-ul">
+<li>Image Buffer Size (frames): 40</li>
+</ul></li>
+</ul>
+</div>
+</div>
+</div>
+<div id="outline-container-orgbe3cdfe" class="outline-2">
+<h2 id="orgbe3cdfe"><span class="section-number-2">9.</span> The Abbey TVR Role</h2>
+<div class="outline-text-2" id="text-9">
+<p>
+The abbey has a few TV tuners and a subscription to <a href="https://schedulesdirect.org/">Schedules Direct</a>
+for North American TV broadcast schedules.  It uses one (master)
+MythTV server and its MythWeb interface to make and serve recordings
+of area broadcasts.
+</p>
+
+<p>
+The Abbey TVR Role installs the MythTV backend and the MythWeb web
+interface on the master server.  It configures the Apache web server
+to serve MythWeb pages at e.g. <code>http://NEW/mythweb/</code>.
+</p>
+</div>
+<div id="outline-container-orge6089d9" class="outline-3">
+<h3 id="orge6089d9"><span class="section-number-3">9.1.</span> Building MythTV and MythWeb</h3>
+<div class="outline-text-3" id="text-9-1">
+<p>
+Neither Debian nor the MythTV project provide binary packages of
+MythTV and MythWeb.  The project recommends building from source
+according to their <a href="https://www.mythtv.org/wiki/Build_from_Source">Build from Source</a> wiki page.  To do this, the
+target host will need several dozen "developer" packages installed.
+Thus the abbey's TVR role proceeds in two phases.
+</p>
+
+<p>
+In the first phase, the MythTV project's Ansible code, in
+<q>mythtv-ansible/</q>, is used to assemble a list of packages needed
+during the build.  The packages are installed and the rest of the
+role's tasks are skipped.  This allows the administrator to manually
+build and install MythTV, creating <q>/usr/local/bin/mythtv-setup</q>.
+The administrator will also download and install MythWeb before
+running the TVR role again for its second phase.  The administrator
+will <i>not</i> be able to run <code>mythtv-setup</code> before completing the second
+phase.
+</p>
+
+<p>
+In the second phase, the role finds <q>mythtv-setup</q> has been installed
+on the target host and so proceeds with the "Post-installation tasks"
+section of the wiki page.  This still leaves a number of manual steps
+to be performed with the <code>mythtv-setup</code> program, e.g. configuring a
+video source and capture card, after which the backend can be started.
+</p>
+</div>
+</div>
+<div id="outline-container-orgd36bd18" class="outline-3">
+<h3 id="orgd36bd18"><span class="section-number-3">9.2.</span> TVR Machine Setup</h3>
+<div class="outline-text-3" id="text-9-2">
+<p>
+A new TVR machine needs only <a href="#org110d7b3">Cloistering</a> to prepare it for
+Ansible.  As part of that process, it should be added to the <code>tvrs</code>
+group in the <q>hosts</q> file.  An existing server can become a TVR
+machine simply by adding it to the <code>tvrs</code> group.
+</p>
+</div>
+</div>
+<div id="outline-container-orgf7717ca" class="outline-3">
+<h3 id="orgf7717ca"><span class="section-number-3">9.3.</span> Include Abbey Variables</h3>
+<div class="outline-text-3" id="text-9-3">
+<p>
+In this abbey specific document, most abbey particulars are not
+replaced with variables, but specified in-line.  Some, however, are
+not published (e.g. database passwords).  The variables that replace
+them are included from <q>private/vars-abbey.yml</q>.  Example values are
+given in this document.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-tvr/tasks/main.yml</q><pre class="src src-conf">---
+- name: Include private abbey variables.
+  include_vars: ../private/vars-abbey.yml
+</pre>
+</div>
+
+<p>
+The relative filename should be found only in the playbook's
+directory, <q>playbooks/</q>.
+</p>
+</div>
+</div>
+<div id="outline-container-org004060a" class="outline-3">
+<h3 id="org004060a"><span class="section-number-3">9.4.</span> Install MythTV Build Requisites</h3>
+<div class="outline-text-3" id="text-9-4">
+<p>
+A number of developer packages are needed to build MythTV.  The wiki
+page recommends Ansible playbooks to assemble the appropriate list of
+package names (several dozen count) depending on the target OS
+version.  The playbooks are in <a href="https://github.com/MythTV/ansible">https://github.com/MythTV/ansible</a> which
+contains a <q>README.md</q>.
+</p>
+
+<p>
+The instructions in the <q>README.md</q> are to clone the repository and
+run <code>sudo ansible-playbook -i hosts qt5.yml</code> on the build machine.
+However the abbey prefers to keep the Ansible code on an
+administrator's machine with the rest of the abbey's roles.  The
+following commands were used to create a <q>mythtv-ansible/</q>
+subdirectory.  (A <code>git pull origin</code> command in this subdirectory might
+be appropriate to download updates.)
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">git clone https://github.com/MythTV/ansible mythtv-ansible
+<span class="org-builtin">cd</span> mythtv-ansible
+git checkout fixes/32
+</pre>
+</div>
+
+<p>
+The <code>abbey-tvr</code> role uses a couple tasks files in <q>mythtv-ansible/</q>
+directly, bypassing the inventories, playbooks and roles, <i>after</i>
+"fixing" the final <code>apt</code> tasks by adding <code>become: yes</code>.  After making
+these edits, the <code>git diff</code> command should produce something like the
+following.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-diff"><span class="org-diff-header">diff --git a/roles/mythtv-deb/tasks/main.yml b/roles/mythtv-deb/tasks/main.yml</span>
+<span class="org-diff-header">index 868c9b7..3dcf115 100644</span>
+<span class="org-diff-header">--- </span><span class="org-diff-header"><span class="org-diff-file-header">a/roles/mythtv-deb/tasks/main.yml</span></span>
+<span class="org-diff-header">+++ </span><span class="org-diff-header"><span class="org-diff-file-header">b/roles/mythtv-deb/tasks/main.yml</span></span>
+<span class="org-diff-hunk-header">@@ -366,6 +366,7 @@</span>
+<span class="org-diff-context">       '{{ lookup("flattened", deb_pkg_lst) }}'</span>
+
+<span class="org-diff-context"> - name: install packages</span>
+<span class="org-diff-indicator-added">+</span><span class="org-diff-added">  become: yes</span>
+<span class="org-diff-context">   apt:</span>
+<span class="org-diff-context">     name:</span>
+<span class="org-diff-context">       '{{ lookup("flattened", deb_pkg_lst ) }}'</span>
+<span class="org-diff-header">diff --git a/roles/qt5/tasks/qt5-deb.yml b/roles/qt5/tasks/qt5-deb.yml</span>
+<span class="org-diff-header">index 7a1a0bc..26ba782 100644</span>
+<span class="org-diff-header">--- </span><span class="org-diff-header"><span class="org-diff-file-header">a/roles/qt5/tasks/qt5-deb.yml</span></span>
+<span class="org-diff-header">+++ </span><span class="org-diff-header"><span class="org-diff-file-header">b/roles/qt5/tasks/qt5-deb.yml</span></span>
+<span class="org-diff-hunk-header">@@ -25,6 +25,7 @@</span>
+<span class="org-diff-context">       '{{ lookup("flattened", deb_pkg_lst) }}'</span>
+
+<span class="org-diff-context"> - name: install deb qt5 packages</span>
+<span class="org-diff-indicator-added">+</span><span class="org-diff-added">  become: yes</span>
+<span class="org-diff-context">   apt:</span>
+<span class="org-diff-context">     name:</span>
+       '{{ lookup("flattened", deb_pkg_lst ) }}'
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>roles_t/abbey-tvr/tasks/mains.yml</q><pre class="src src-conf">
+- name: Install MythTV runtime requisites.
+  become: yes
+  apt:
+    pkg: [ mariadb-server, xmltv ]
+
+- name: Install MythTV build requisites.
+  include_tasks: <span class="org-string">"{{ item }}"</span>
+  loop:
+  - ../mythtv-ansible/roles/mythtv-deb/tasks/main.yml
+  - ../mythtv-ansible/roles/qt5/tasks/qt5-deb.yml
+</pre>
+</div>
+
+<p>
+The tasks above install runtime and compile-time requisites during the
+"first" run of e.g. <code>./abbey config NEW</code>.  The "first" run can be
+repeated until successful.  The remaining tasks are skipped until
+MythTV is built and installed.
+</p>
+</div>
+</div>
+<div id="outline-container-org64aabf5" class="outline-3">
+<h3 id="org64aabf5"><span class="section-number-3">9.5.</span> Build and Install MythTV</h3>
+<div class="outline-text-3" id="text-9-5">
+<p>
+After a successful "first" run of e.g. <code>./abbey config NEW</code>, the
+target machine is prepared to build (and install) MythTV.  The
+following commands are used.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh"><span class="org-builtin">cd</span> /usr/local/src/
+git clone https://github.com/MythTV/mythtv
+<span class="org-builtin">cd</span> mythtv/
+git checkout fixes/32
+<span class="org-builtin">cd</span> mythtv/
+./configure
+make
+sudo make install
+</pre>
+</div>
+
+<p>
+The <code>make install</code> command does not need to be run as <code>root</code> if
+<q>bin/</q>, <q>lib/</q>, <q>include/</q>, <q>share/</q> in <q>/usr/local/</q> and
+<q>dist-packages/</q> in <q>/usr/local/lib/python3.9/</q> on the target machine
+are writable by the builder.
+</p>
+
+<p>
+The following task probes for the <q>mythtv-setup</q> program, installed in
+<q>/usr/local/bin/</q>, to detect that the build/install process has
+completed.  It registers the results in the <code>mythtv</code> variable.
+Several of the remaining installation steps are skipped unless
+<code>mythtv.stat.exists</code>.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-tvr/tasks/main.yml</q><pre class="src src-conf">
+- name: Test for MythTV binary packages.
+  stat:
+    path: /usr/local/bin/mythtv-setup
+  register: mythtv
+- debug:
+    msg: <span class="org-string">"/usr/local/bin/mythtv-setup does not yet exist"</span>
+  when: not mythtv.stat.exists
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org0918ba8" class="outline-3">
+<h3 id="org0918ba8"><span class="section-number-3">9.6.</span> Create MythTV User</h3>
+<div class="outline-text-3" id="text-9-6">
+<p>
+MythTV Backend needs to run as its own user: <code>mythtv</code>.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-tvr/tasks/main.yml</q><pre class="src src-conf">
+- name: Create mythtv.
+  become: yes
+  user:
+    name: mythtv
+    system: yes
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-orga439b85" class="outline-3">
+<h3 id="orga439b85"><span class="section-number-3">9.7.</span> Create MythTV DB</h3>
+<div class="outline-text-3" id="text-9-7">
+<p>
+MythTV's MariaDB database is created by the following task, when the
+<code>mysql_db</code> Ansible module supports <code>check_implicit_admin</code>.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-conf">
+- name: Create MythTV DB.
+  become: yes
+  mysql_db:
+    check_implicit_admin: yes
+    name: mythconverg
+    collation: utf8mb4_general_ci
+    encoding: utf8mb4
+</pre>
+</div>
+
+<p>
+Unfortunately it does not currently, yet the institute prefers the
+more secure Unix socket authentication method.  Rather than create a
+privileged DB user, the <code>mythconverg</code> database is created manually
+(below).
+</p>
+</div>
+</div>
+<div id="outline-container-org6b043d2" class="outline-3">
+<h3 id="org6b043d2"><span class="section-number-3">9.8.</span> Create MythTV DB User</h3>
+<div class="outline-text-3" id="text-9-8">
+<p>
+The DB user's password is taken from the <code>mythtv_dbpass</code> variable,
+kept in <q>private/vars-abbey.yml</q>, and generated e.g. with the <code>apg -n
+1 -x 12 -m 12</code> command.
+</p>
+
+<div class="org-src-container">
+<q>private/vars-abbey.yml</q><pre class="src src-conf">mythtv_dbpass:           daJkibpoJkag
+</pre>
+</div>
+
+<p>
+The following task would create the DB user (<code>mysql_user</code> supports
+<code>check_implicit_admin</code>) <i>but</i> the <code>mythconverg</code> database was not
+created above.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-conf">
+- name: Create MythTV DB user.
+  become: yes
+  mysql_user:
+    check_implicit_admin: yes
+    name: mythtv
+    password: <span class="org-string">"{{ mythtv_dbpass }}"</span>
+    priv: <span class="org-string">"mythconverg.*:all"</span>
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-orgf23c6ec" class="outline-3">
+<h3 id="orgf23c6ec"><span class="section-number-3">9.9.</span> Manually Create MythTV DB and DB User</h3>
+<div class="outline-text-3" id="text-9-9">
+<p>
+The MythTV database and database user are created manually with the
+following SQL (with the <code>mythtv_dbpass</code> spliced in).  The SQL commands
+are entered at the SQL prompt of the <code>sudo mysql</code> command, or perhaps
+piped into the command.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sql"><span class="org-keyword">create</span> database mythconverg
+    <span class="org-type">character</span> <span class="org-keyword">set</span> utf8mb4
+    <span class="org-keyword">collate</span> utf8mb4_general_ci;
+<span class="org-keyword">create</span> <span class="org-builtin">user</span> <span class="org-string">'mythtv'</span>@<span class="org-string">'%'</span> identified <span class="org-keyword">by</span> <span class="org-string">'{{ mythtv_dbpass }}'</span>;
+<span class="org-keyword">create</span> <span class="org-builtin">user</span> <span class="org-string">'mythtv'</span>@<span class="org-string">'localhost'</span> identified <span class="org-keyword">by</span> <span class="org-string">'{{ mythtv_dbpass }}'</span>;
+<span class="org-keyword">grant</span> <span class="org-keyword">all</span> <span class="org-keyword">privileges</span> <span class="org-keyword">on</span> mythconverg.*
+    <span class="org-keyword">to</span> <span class="org-string">'mythtv'</span>@<span class="org-string">'%'</span> <span class="org-keyword">with</span> <span class="org-keyword">grant</span> <span class="org-keyword">option</span>;
+<span class="org-keyword">grant</span> <span class="org-keyword">all</span> <span class="org-keyword">privileges</span> <span class="org-keyword">on</span> mythconverg.*
+    <span class="org-keyword">to</span> <span class="org-string">'mythtv'</span>@<span class="org-string">'localhost'</span> <span class="org-keyword">with</span> <span class="org-keyword">grant</span> <span class="org-keyword">option</span>;
+flush <span class="org-keyword">privileges</span>;
+exit;
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org0e830e0" class="outline-3">
+<h3 id="org0e830e0"><span class="section-number-3">9.10.</span> Load DB Timezone Info</h3>
+<div class="outline-text-3" id="text-9-10">
+<p>
+Starting with MythTV version 0.26, the time zone tables must be loaded
+into MySQL.  The MariaDB installed by Debian 11 seems to need this
+too.  The test SQL produced <code>NULL</code>.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sql"><span class="org-keyword">SELECT</span> CONVERT_TZ(NOW(), <span class="org-string">'SYSTEM'</span>, <span class="org-string">'Etc/UTC'</span>);
+</pre>
+</div>
+
+<p>
+After running the following command line, the test SQL produced
+e.g. <code>2022-09-13 20:15:41</code>.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">mysql_tzinfo_to_sql /usr/share/zoneinfo | sudo mysql mysql
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-orge16b82b" class="outline-3">
+<h3 id="orge16b82b"><span class="section-number-3">9.11.</span> Create MythTV Backend Service</h3>
+<div class="outline-text-3" id="text-9-11">
+<p>
+This task installs the <q>mythtv-backend.service</q> file.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-tvr/tasks/mains.yml</q><pre class="src src-conf">
+- name: Create mythtv-backend service.
+  become: yes
+  copy:
+    content: |
+      [<span class="org-type">Unit</span>]
+      <span class="org-variable-name">Description</span>=MythTV Backend
+      <span class="org-variable-name">Documentation</span>=https://www.mythtv.org/wiki/Mythbackend
+      <span class="org-variable-name">After</span>=mysql.service network.target
+
+      [<span class="org-type">Service</span>]
+      <span class="org-variable-name">User</span>=mythtv
+      <span class="org-variable-name">ExecStartPre</span>=/bin/sleep 30
+      <span class="org-comment-delimiter">#</span><span class="org-comment">TimeoutStartSec=infinity</span>
+      <span class="org-variable-name">ExecStart</span>=/usr/local/bin/mythbackend --quiet --syslog local7
+      <span class="org-variable-name">StartLimitBurst</span>=10
+      <span class="org-variable-name">StartLimitInterval</span>=10m
+      <span class="org-variable-name">Restart</span>=on-failure
+      <span class="org-variable-name">RestartSec</span>=1
+
+      [<span class="org-type">Install</span>]
+      <span class="org-variable-name">WantedBy</span>=multi-user.target
+    dest: /etc/systemd/system/mythtv-backend.service
+  when: mythtv.stat.exists
+  notify: Reload Systemd.
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>roles_t/abbey-tvr/handlers/main.yml</q><pre class="src src-conf">---
+- name: Reload Systemd.
+  become: yes
+  command: systemctl daemon-reload
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org330aa60" class="outline-3">
+<h3 id="org330aa60"><span class="section-number-3">9.12.</span> Set PHP Timezone</h3>
+<div class="outline-text-3" id="text-9-12">
+<p>
+This task checks PHP's timezone.  If unset, MythTV's backend logs
+bitter complaints.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-tvr/tasks/main.yml</q><pre class="src src-conf">
+- name: Configure PHP date.timezone.
+  become: yes
+  lineinfile:
+    <span class="org-variable-name">regexp: date.timezone ?</span>=
+    <span class="org-variable-name">line: date.timezone</span> = {{ lookup(<span class="org-string">'file'</span>, <span class="org-string">'/etc/timezone'</span>) }}
+    path: <span class="org-string">"{{ item }}"</span>
+  loop:
+  - /etc/php/7.4/cli/php.ini
+  - /etc/php/7.4/apache2/php.ini
+  when: mythtv.stat.exists
+  notify: Restart Apache2.
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>roles_t/abbey-tvr/handlers/main.yml</q><pre class="src src-conf">
+- name: Restart Apache2.
+  become: yes
+  systemd:
+    service: apache2
+    state: restarted
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org85e76a2" class="outline-3">
+<h3 id="org85e76a2"><span class="section-number-3">9.13.</span> Create MythTV Storage Area</h3>
+<div class="outline-text-3" id="text-9-13">
+<p>
+The backend does not have a default storage area for its recordings.
+A path to an appropriate directory must be set with the <code>mythtv-setup</code>
+program (as described below).  The abbey uses
+<q>/home/mythtv/Recordings/</q> for MythTV's default storage.  This task
+creates that directory and ensures it has appropriate permissions.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-tvr/tasks/main.yml</q><pre class="src src-conf">
+- name: Create MythTV storage area.
+  become: yes
+  file:
+    state: directory
+    dest: /home/mythtv/Recordings
+    owner: mythtv
+    group: mythtv
+    <span class="org-variable-name">mode: u</span>=rwx,g+rwx,o=rx
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org5913169" class="outline-3">
+<h3 id="org5913169"><span class="section-number-3">9.14.</span> Configure MythTV Backend</h3>
+<div class="outline-text-3" id="text-9-14">
+<p>
+With MythTV built and installed, and the post-installation tasks
+addressed, MythTV Setup (the <code>mythtv-setup</code> program) can be run.  It
+must be run by the <code>mythtv</code> user, whose home directory will contain
+the MythTV (and XMLTV) configuration files.  The program is best run
+remotely (unless there is a graphical desktop on the server) by a
+command like <code>ssh -X mythtv@NEW mythtv-setup</code>.
+</p>
+
+<p>
+Patience is required.  The <code>mythtv-setup</code> program was not written for
+X11 and the X11 adapter has a difficult job.  It is often hard to
+determine what button is selected or how to proceed (sometimes simply
+with <code>ESC</code>!).  Sticking to the arrow, enter and escape keys best
+emulates a TV remote (for which the interface was designed).
+</p>
+
+<p>
+In MythTV Setup:
+</p>
+
+<ul class="org-ul">
+<li>In the initial MythTV Startup Status ("Unable to connect to
+Database."), use the "Setup" button to get to "Database
+Configuration".  Leave the default hostname (<code>localhost</code>), port
+(<code>3306</code>), database name (<code>mythconverg</code>) and user (<code>mythtv</code>).  Enter
+the value of <code>mythtv_dbpass</code> (in <q>private/vars-abbey.yml</q>) for the
+password.  Leave the rest of the settings at their default values.
+Leave "Database Configuration" by pressing Escape and confirming
+"Save and Exit".</li>
+
+<li>Once in MythTV Setup proper, you will see the main menu.  Scroll
+down and choose "Storage Directories".  In the Local Storage Groups
+dialog, add to the "Local 'Default' Storage Group Directories" a new
+directory: <q>/home/mythtv/Recordings</q>.</li>
+</ul>
+</div>
+</div>
+<div id="outline-container-org9451aba" class="outline-3">
+<h3 id="org9451aba"><span class="section-number-3">9.15.</span> Configure Tuner</h3>
+<div class="outline-text-3" id="text-9-15">
+<p>
+The abbey has a Silicon Dust Homerun HDTV Duo (with two tuners).  It
+is setup as described in <a href="#org110d7b3">Cloistering</a>, after which the tuner is
+accessible by name (e.g. <code>new</code>) on the cloister network.  Assuming
+<code>ping -c1 new</code> works, the tuner should be accessible via the
+<code>hdhomerun_config_gui</code> command, a graphical interface contributed to
+Debian by Silicon Dust and found in the <code>hdhomerun-config-gui</code>
+package.  The program, run with the command <code>hdhomerun_config_gui</code>,
+will broadcast on the localnet to find any Homeruns there, but the new
+tuner's domain name or IP address can also be entered.
+</p>
+</div>
+</div>
+<div id="outline-container-orgaf6031c" class="outline-3">
+<h3 id="orgaf6031c"><span class="section-number-3">9.16.</span> Add HDHomerun and Mr.Antenna</h3>
+<div class="outline-text-3" id="text-9-16">
+<p>
+In MythTV Setup:
+</p>
+<ul class="org-ul">
+<li>Choose "Capture cards".
+<ul class="org-ul">
+<li>Choose "(Add Capture Card)", then the "New Capture Card".</li>
+<li>Choose Card Type and select "HDHomeRun Networked Tuner".</li>
+<li>Press the right arrow key to see card type parameters.  Choose the
+tuner's address, which should be listed assuming the tuner and TVR
+are on the same subnet (e.g. the private Ethernet).</li>
+<li>Save and Exit (via Escape key).</li>
+</ul></li>
+<li>Choose "Video sources".
+<ul class="org-ul">
+<li>Choose "(New Video Source)", then the "New Video Source".</li>
+<li>Enter video source name "Mr.Antenna".</li>
+<li>Choose listings grabber "Schedules Direct JSON API (xmltv)".</li>
+<li>Save and Exit.</li>
+</ul></li>
+<li>Choose "Input Connections".
+<ul class="org-ul">
+<li>Choose the HDHomeRun.</li>
+<li>Choose video source "Mr.Antenna".</li>
+<li>Save and Exit.</li>
+</ul></li>
+<li>Choose "Capture cards".
+<ul class="org-ul">
+<li>Add a second HDHomeRun as above.</li>
+<li>Save and Exit.</li>
+</ul></li>
+<li>Choose "Input connections".
+<ul class="org-ul">
+<li>Connect the second HDHomeRun to Mr.Antenna as above.</li>
+<li>Save and Exit.</li>
+</ul></li>
+<li>Exit MythTV Setup or continue directly to Scan for New Channels.  In
+any case, do <i>not</i> run <code>mythfilldatabase</code>.</li>
+</ul>
+</div>
+</div>
+<div id="outline-container-orgdd4b427" class="outline-3">
+<h3 id="orgdd4b427"><span class="section-number-3">9.17.</span> Scan for New Channels</h3>
+<div class="outline-text-3" id="text-9-17">
+<p>
+In MythTV Setup:
+</p>
+<ul class="org-ul">
+<li>Choose "Channel Editor".
+<ul class="org-ul">
+<li>Navigate to the "Delete" button, leaving Video Source All (right
+and down and down, or left six times, or sump'n).  Confirm
+deletion of all channels.</li>
+<li>Choose video source Mr.Antenna, then Channel Scan.  Scroll down to
+the "scan" button and choose it (select and Enter).</li>
+<li>Choose "Insert All" when the scan is complete and the count of
+channels is presented.  Delete All unused transports.</li>
+<li>Save and Exit from the scan.  Exit from the channel editor.</li>
+</ul></li>
+<li>Exit MythTV Setup.  Do <i>not</i> run <code>mythfilldatabase</code>.</li>
+</ul>
+</div>
+</div>
+<div id="outline-container-org720bce1" class="outline-3">
+<h3 id="org720bce1"><span class="section-number-3">9.18.</span> Configure XMLTV</h3>
+<div class="outline-text-3" id="text-9-18">
+<p>
+The <code>xmltv</code> package, specifically its <code>tv_grab_zz_sdjson</code> program, is
+used to download broadcast listings from Schedules Direct.  The
+program is run by the <code>mythtv</code> user (like <code>mythtv-setup</code>) and is
+initially configured (the <i>first</i> time) using its <code>--configure</code>
+option.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">tv_grab_zz_sdjson --configure
+cp ~/.xmltv/tv_grab_zz_sdjson.conf ~/.mythtv/Mr.Antenna.xmltv
+</pre>
+</div>
+
+<p>
+The <code>--configure</code> command above prompts with many questions and
+creates <q>~/.xmltv/tv_grab_zz_sdjson.conf</q>, which is copied to
+<q>~/.mythtv/Mr.Antenna.xmltv</q> where <code>mythfilldatabase</code> will find it.
+Afterwards any re-configuration should use the following command.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">tv_grab_zz_sdjson --configure --config-file ~/.mythtv/Mr.Antenna.xmltv
+</pre>
+</div>
+
+<p>
+Here is a transcript of a session with <code>tv_grab_zz_sdjson</code>.  Note that
+the list of "inputs" available in a postal code typically ends with
+the OTA (over the air) broadcasts.
+</p>
+
+<pre class="example" id="orgb68ecfe">
+$ tv_grab_zz_sdjson --configure --config-file .mythtv/Mr.Antenna.xmltv
+Cache file for lineups, schedules and programs.
+Cache file: [/home/mythtv/.xmltv/tv_grab_zz_sdjson.cache]
+If you are migrating from a different grabber selecting an alternate
+ channel ID format can make the migration easier.
+Select channel ID format:
+0: Default Format (eg: I12345.json.schedulesdirect.org)
+1: tv_grab_na_dd Format (eg: I12345.labs.zap2it.com)
+2: MythTV Internal DD Grabber Format (eg: 12345)
+Select one: [0,1,2 (default=0)] 
+As the JSON data only includes the previously shown date normally the
+ XML output should only have the date. However some programs such as
+ older versions of MythTV also need a time.
+Select previously shown format:
+0: Date Only
+1: Date And Time
+Select one: [0,1 (default=0)] 
+Schedules Direct username.
+Username: USERNAME
+Schedules Direct password.
+Password: PASSWORD
+** POST https://json.schedulesdirect.org/20141201/token ==&gt; 200 OK
+** GET https://json.schedulesdirect.org/20141201/status ==&gt; 200 OK (1s)
+** GET https://json.schedulesdirect.org/20141201/lineups ==&gt; 200 OK
+This step configures the lineups enabled for your Schedules Direct
+ account. It impacts all other configurations and programs using the
+ JSON API with your account. A maximum of 4 lineups can by added to
+ your account. In a later step you will choose which lineups or
+ channels to actually use for this configuration.
+Current lineups enabled for your Schedules Direct account:
+#. Lineup ID | Name | Location | Transport
+1. USA-OTA-57719 | Local Over the Air Broadcast | 57719 | Antenna
+Edit account lineups: [continue,add,delete (default=continue)] 
+Choose whether you want to include complete lineups or individual
+ channels for this configuration.
+Select mode: [lineups,channels (default=lineups)] 
+** GET https://json.schedulesdirect.org/20141201/lineups ==&gt; 200 OK
+Choose lineups to use for this configuration.
+USA-OTA-57719 [yes,no,all,none (default=no)] all
+</pre>
+
+<p>
+Once configured, the <code>mythfilldatabase</code> program should be able to use
+<code>tv_grab_zz_sdjson</code> to connect to Schedules Direct and download the
+chosen line-up.  However <code>mythfilldatabase</code> is happiest when the
+backend is running, so it is not run until then.
+</p>
+</div>
+</div>
+<div id="outline-container-org94330c2" class="outline-3">
+<h3 id="org94330c2"><span class="section-number-3">9.19.</span> Debug XMLTV</h3>
+<div class="outline-text-3" id="text-9-19">
+<p>
+If the <code>mythfilldatabase</code> command fails or expected listings do not
+appear, more information is available by adding the <code>--verbose</code>
+option.  The <code>--help</code> option also reveals much, including a <code>--manual</code>
+option for "interactive configuration".
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">sudo -H -u mythtv mythfilldatabase --verbose
+</pre>
+</div>
+
+<p>
+The command might, for example, show that it is failing to run a
+<code>tv_grab_zz_sdjson</code> command like the following.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">nice tv_grab_zz_sdjson <span class="org-sh-escaped-newline">\</span>
+        --config-file <span class="org-string">'/home/mythtv/.mythtv/Mr.Antenna.xmltv'</span> <span class="org-sh-escaped-newline">\</span>
+        --output /tmp/myths5Sq35 --quiet
+</pre>
+</div>
+
+<p>
+Running a similar command (without <code>--quiet</code>) might be more revealing.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">sudo -H -u mythtv <span class="org-sh-escaped-newline">\</span>
+    tv_grab_zz_sdjson <span class="org-sh-escaped-newline">\</span>
+        --config-file <span class="org-string">'/home/mythtv/.mythtv/Mr.Antenna.xmltv'</span> <span class="org-sh-escaped-newline">\</span>
+        --output /tmp/mythFUBAR
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-orgb3acb2a" class="outline-3">
+<h3 id="orgb3acb2a"><span class="section-number-3">9.20.</span> Configure MythTV Backend Logging</h3>
+<div class="outline-text-3" id="text-9-20">
+<p>
+The abbey directs MythTV log messages to <q>/var/log/mythtv.log</q> (and
+away from <q>/var/log/syslog</q>) and rotates the log file.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-tvr/tasks/main.yml</q><pre class="src src-conf">
+<span class="org-variable-name">- name: Install</span> =/etc/rsyslog.d/40-mythtv.conf.
+  become: yes
+  copy:
+    content: |
+      :msg,startswith,<span class="org-string">" myth"</span> -/var/log/mythtv.log
+      &amp; stop
+    dest: /etc/rsyslog.d/40-mythtv.conf
+
+<span class="org-variable-name">- name: Install</span> =/etc/logrotate.d/mythtv=.
+  become: yes
+  copy:
+    content: |
+      <span class="org-type">/var/log/mythtv.log</span> {
+          daily
+          <span class="org-variable-name">size</span>=10M
+          rotate 7
+          notifempty
+          copytruncate
+          missingok
+          postrotate
+              reload rsyslog &gt;/dev/null 2&gt;&amp;1 || true
+          endscript
+      }
+    dest: /etc/logrotate.d/mythtv
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org053357f" class="outline-3">
+<h3 id="org053357f"><span class="section-number-3">9.21.</span> Start MythTV Backend</h3>
+<div class="outline-text-3" id="text-9-21">
+<p>
+After configuring with <code>mythtv-setup</code> as discussed above, start and
+enable (at boot time) the <code>mythtv-backend</code> service.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">sudo systemctl enable mythtv-backend
+sudo systemctl start mythtv-backend
+systemctl status -l mythtv-backend
+sudo -u mythtv mythfilldatabase
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org4113415" class="outline-3">
+<h3 id="org4113415"><span class="section-number-3">9.22.</span> Install MythWeb</h3>
+<div class="outline-text-3" id="text-9-22">
+<p>
+MythWeb, like MythTV, is installed from a Git repository.  The
+following commands create <q>/usr/local/share/mythtv/mythweb/</q> by
+cloning the MythWeb repository in <q>/usr/local/src/mythweb/</q>, checking
+out the appropriate branch, and copying the appropriate portion.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh"><span class="org-builtin">cd</span> /usr/local/src/
+git clone https://github.com/MythTV/mythweb
+( <span class="org-builtin">cd</span> mythweb/; git checkout fixes/32 )
+rsync -C mythweb /usr/local/share/mythtv/
+</pre>
+</div>
+
+<p>
+The following tasks take care of the rest of the installation.
+</p>
+
+<div class="org-src-container">
+<q>roles_t/abbey-tvr/tasks/main.yml</q><pre class="src src-conf">
+- name: Install MythWeb requisites.
+  become: yes
+  apt:
+    pkg: [ apache2, php, php-mysql ]
+
+- name: Install MythWeb in web server DocumentRoot.
+  file:
+    state: link
+    src: /usr/local/share/mythtv/mythweb
+    dest: /var/www/html/mythweb
+
+- name: Configure MythWeb data directory.
+  file:
+    state: directory
+    dest: /var/www/html/mythweb/data
+    group: www-data
+    <span class="org-variable-name">mode: u</span>=rwx,g+rwx,o=rx
+
+- name: Install MythWeb configuration.
+  become: yes
+  template:
+    src: mythweb.conf.j2
+    dest: /etc/apache2/sites-available/mythweb.conf
+  notify: Restart Apache2.
+
+- name: Enable MythWeb configuration.
+  become: yes
+  command:
+    cmd: a2ensite -q mythweb
+    creates: /etc/apache2/sites-enabled/mythweb.conf
+  notify: Restart Apache2.
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>roles_t/abbey-tvr/templates/mythweb.conf.j2</q><pre class="src src-conf"><span class="org-comment-delimiter">#</span>
+<span class="org-comment-delimiter"># </span><span class="org-comment">Apache configuration directives for MythWeb.</span>
+<span class="org-comment-delimiter">#</span>
+<span class="org-comment-delimiter"># </span><span class="org-comment">Note that this file is maintained by the network administration.</span>
+&lt;Directory <span class="org-string">"/var/www/html/mythweb/data"</span>&gt;
+    <span class="org-comment-delimiter"># </span><span class="org-comment">For Apache 2.2</span>
+    <span class="org-comment-delimiter">#</span><span class="org-comment">Options -All +FollowSymLinks +IncludesNoExec</span>
+    <span class="org-comment-delimiter"># </span><span class="org-comment">For Apache 2.4+</span>
+    Options +FollowSymLinks +IncludesNoExec
+&lt;/Directory&gt;
+&lt;Directory <span class="org-string">"/var/www/html/mythweb"</span> &gt;
+    &lt;Files mythweb.*&gt;
+    setenv db_server <span class="org-string">"127.0.0.1"</span>
+    setenv db_name <span class="org-string">"mythconverg"</span>
+    setenv db_login <span class="org-string">"mythtv"</span>
+    setenv db_password <span class="org-string">"{{ mythtv_dbpass }}"</span>
+    &lt;/Files&gt;
+    &lt;Files *.php&gt;
+        php_value file_uploads                  0
+        php_value allow_url_fopen               On
+        php_value zlib.output_handler           Off
+        php_value memory_limit                  64M
+        php_value max_execution_time 30
+        php_value display_startup_errors        On
+        php_value display_errors                On
+    &lt;/Files&gt;
+    RewriteEngine  on
+    RewriteRule \
+^(css|data|images|js|themes|skins|README|INSTALL|[a-z_]+\.(php|pl))(/|$)\
+        - [L]
+    RewriteRule ^(pl(/.*)?)$            mythweb.pl/$1  [QSA,L]
+    RewriteRule ^(.+)$                  mythweb.php/$1 [QSA,L]
+    RewriteRule ^(.*)$                  mythweb.php    [QSA,L]
+    AllowOverride All
+    Options         FollowSymLinks
+    AddType video/nuppelvideo   .nuv
+    AddType image/x-icon        .ico
+    &lt;IfModule deflate_module&gt;
+        BrowserMatch ^Mozilla/4 gzip-only-text/html
+        BrowserMatch ^Mozilla/4\.0[678] no-gzip
+        BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
+        AddOutputFilterByType DEFLATE text/html
+        AddOutputFilterByType DEFLATE text/css
+        AddOutputFilterByType DEFLATE application/x-javascript
+    &lt;/IfModule&gt;
+    &lt;IfModule headers_module&gt;
+        <span class="org-variable-name">Header append Vary User-Agent env</span>=!dont-vary
+    &lt;/IfModule&gt;
+    &lt;Files *.pl&gt;
+        SetHandler cgi-script
+        Options +ExecCGI
+    &lt;/Files&gt;
+
+&lt;/Directory&gt;
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org6a7f10b" class="outline-3">
+<h3 id="org6a7f10b"><span class="section-number-3">9.23.</span> Change Broadcast Area</h3>
+<div class="outline-text-3" id="text-9-23">
+<p>
+The abbey changes location almost weekly, so its HDTV broadcast area
+changes frequently.  At the start of a long stay the administrator
+uses the MythTV Setup program to scan for the new area's channels, as
+described in <a href="#orgdd4b427">Scan for New Channels</a>.
+</p>
+
+<p>
+To change MythTV's "listings", the administrator needs the new area's
+postal code and the username and password of the abbey's Schedules
+Direct account.  The administrator then runs the <code>tv_grab_zz_sdjson</code>
+program as user <code>mythtv</code>.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">tv_grab_zz_sdjson --configure --config-file ~/.mythtv/Mr.Antenna.xmltv
+</pre>
+</div>
+
+<p>
+The program will prompt for the zip code and offer a list of "inputs"
+available in that area, as described in <a href="#org720bce1">Configure XMLTV</a>.
+</p>
+</div>
+</div>
+</div>
+<div id="outline-container-org965be21" class="outline-2">
+<h2 id="org965be21"><span class="section-number-2">10.</span> The Ansible Configuration</h2>
+<div class="outline-text-2" id="text-10">
+<p>
+The abbey's Ansible configuration, like that of <a href="Institute/README.html">A Small Institute</a>, is
+kept on an administrator's notebook.  The private SSH key that allows
+remote access to privileged accounts on all abbey servers is kept on
+an encrypted, off-line volume plugged into the administrator's
+notebook only when running <code>./abbey</code> commands.
+</p>
+
+<p>
+The small institute provided examples of both public and private
+variables.  This document includes the abbey's actual public
+variables, and examples of the private variables.  As in A Small
+Institute, this document's roles tangle into <q>roles_t/</q>, separate from
+the running (and perhaps recently debugged!) code in <q>roles/</q>.
+</p>
+
+<p>
+The configuration of a small institute is included as a git sub-module
+in <q>Institute/</q>.  Its roles are included in the <code>roles_path</code> setting
+in <q>ansible.cfg</q>.  Its example <q>hosts</q> inventory, and <q>public/</q> and
+<q>private/</q> directories are <i>not</i> included, and are replaced by abbey
+specific versions.
+</p>
+
+<p>
+NOTE: if you have not read at least the <a href="Institute/README.html#org56d00a8">Overview</a> of <a href="Institute/README.html">A Small Institute</a>
+you are lost.
+</p>
+
+<p>
+The Ansible configuration:
+</p>
+
+<dl class="org-dl">
+<dt><q>ansible.cfg</q></dt><dd>The Ansible configuration file.</dd>
+<dt><q>hosts</q></dt><dd>The inventory of hosts.</dd>
+<dt><q>playbooks/site.yml</q></dt><dd>The play that assigns roles to hosts.</dd>
+<dt><q>public/</q></dt><dd>Variables, certificates.</dd>
+<dt><q>public/vars.yml</q></dt><dd>The institutional variables.</dd>
+<dt><q>private/</q></dt><dd>Sensitive variables, files, templates.</dd>
+<dt><q>private/vars.yml</q></dt><dd>Sensitive institutional variables.</dd>
+<dt><q>private/vars-abbey.yml</q></dt><dd>Sensitive liturgical variables.</dd>
+<dt><q>roles/</q></dt><dd>The running copy of <q>roles_t/</q>.</dd>
+<dt><q>roles_t/</q></dt><dd>The liturgical roles as tangled from this document.</dd>
+<dt><q>Institute/roles/</q></dt><dd>The running copy of <q>Institute/roles_t/</q>.</dd>
+<dt><q>Institute/roles_t/</q></dt><dd>The institutional roles as tangled from
+<q>Institute/README.org</q>.</dd>
+</dl>
+
+<p>
+The first three files in the list are included in this chapter.  The
+rest are built up piecemeal by (tangled from) this document,
+<q>README.org</q>, and <a href="Institute/README.html"><q>Institute/README.org</q></a>.
+</p>
+</div>
+<div id="outline-container-org3a422c6" class="outline-3">
+<h3 id="org3a422c6"><span class="section-number-3">10.1.</span> <q>ansible.cfg</q></h3>
+<div class="outline-text-3" id="text-10-1">
+<p>
+This is much like the example (test) institutional configuration file,
+except the roles are found in <q>Institute/roles/</q> as well as <q>roles/</q>.
+</p>
+
+<div class="org-src-container">
+<q>ansible.cfg</q><pre class="src src-conf">[<span class="org-type">defaults</span>]
+<span class="org-variable-name">interpreter_python</span>=/usr/bin/python3
+<span class="org-variable-name">vault_password_file</span>=Secret/vault-password
+<span class="org-variable-name">inventory</span>=hosts
+<span class="org-variable-name">roles_path</span>=roles:Institute/roles
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-orgd0676df" class="outline-3">
+<h3 id="orgd0676df"><span class="section-number-3">10.2.</span> <q>hosts</q></h3>
+<div class="outline-text-3" id="text-10-2">
+<div class="org-src-container">
+<q>hosts</q><pre class="src src-conf" id="orgc3f7e11">all:
+  vars:
+    ansible_user: sysadm
+    ansible_ssh_extra_args: -i Secret/ssh_admin/id_rsa
+  hosts:
+    <span class="org-comment-delimiter"># </span><span class="org-comment">The Main Servers: Front, Gate and Core.</span>
+    droplet:
+      ansible_host: 159.65.75.60
+      ansible_become_password: <span class="org-string">"{{ become_droplet }}"</span>
+    anoat:
+      ansible_host: {{ gate_addr }}
+      ansible_become_password: <span class="org-string">"{{ become_anoat }}"</span>
+    dantooine:
+      ansible_host: {{ core_addr }}
+      ansible_become_password: <span class="org-string">"{{ become_dantooine }}"</span>
+    <span class="org-comment-delimiter"># </span><span class="org-comment">WebTVs (Desktops)</span>
+    devaron:
+    kamino:
+      ansible_become_password: <span class="org-string">"{{ become_kamino }}"</span>
+    kessel:
+      ansible_become_password: <span class="org-string">"{{ become_kessel }}"</span>
+    <span class="org-comment-delimiter"># </span><span class="org-comment">Notebooks</span>
+    endor:
+      ansible_become_password: <span class="org-string">"{{ become_endor }}"</span>
+    geonosis:
+      ansible_host: 127.0.0.1
+      ansible_user: matt
+      ansible_become_password: <span class="org-string">"{{ become_geonosis }}"</span>
+      postfix_mydestination: &gt;-
+        geonosis.{{ domain_priv }}
+        geonosis
+        geonosis.localdomain
+        localhost.localdomain
+        localhost
+  children:
+    front:
+      hosts:
+        droplet:
+    gate:
+      hosts:
+        anoat:
+    core:
+      hosts:
+        dantooine:
+    campus:
+      hosts:
+        anoat:
+        devaron:
+        kamino:
+        kessel:
+    weather:
+      hosts:
+        anoat:
+    dvrs:
+      hosts:
+        dantooine:
+    tvrs:
+      hosts:
+        dantooine:
+    notebooks:
+      hosts:
+        endor:
+        geonosis:
+    builders:
+      hosts:
+        devaron:
+        geonosis:
+        kamino:
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org850f665" class="outline-3">
+<h3 id="org850f665"><span class="section-number-3">10.3.</span> <q>playbooks/site.yml</q></h3>
+<div class="outline-text-3" id="text-10-3">
+<p>
+This playbook provisions the entire network by applying first the
+institutional roles, then the liturgical roles.
+</p>
+
+<div class="org-src-container">
+<q>playbooks/site.yml</q><pre class="src src-conf">---
+- name: Configure Front
+  hosts: front
+  roles: [ front, abbey-front ]
+
+- name: Configure Core
+  hosts: core
+  roles: [ core, abbey-core ]
+
+- name: Configure Gate
+  hosts: gate
+  roles: [ gate ]
+
+- name: Configure Campus
+  hosts: campus
+  roles: [ campus, abbey-cloister ]
+
+- name: Configure Weather
+  hosts: weather
+  roles: [ abbey-weather ]
+
+- name: Configure DVRs
+  hosts: dvrs
+  roles: [ abbey-dvr ]
+
+- name: Configure TVRs
+  hosts: tvrs
+  roles: [ abbey-tvr ]
+</pre>
+</div>
+</div>
+</div>
+</div>
+<div id="outline-container-org4fe467e" class="outline-2">
+<h2 id="org4fe467e"><span class="section-number-2">11.</span> The Abbey Commands</h2>
+<div class="outline-text-2" id="text-11">
+<p>
+The <code>./abbey</code> script encodes the abbey's canonical procedures.  It
+includes <a href="Institute/README.html#org1c6f4a8">The Institute Commands</a> and adds a few abbey-specific
+sub-commands.
+</p>
+</div>
+<div id="outline-container-org3733a87" class="outline-3">
+<h3 id="org3733a87"><span class="section-number-3">11.1.</span> Abbey Command Overview</h3>
+<div class="outline-text-3" id="text-11-1">
+<p>
+Institutional sub-commands:
+</p>
+
+<dl class="org-dl">
+<dt>config</dt><dd>Check/Set the configuration of one or all hosts.</dd>
+<dt>new</dt><dd>Create system accounts for a new member.</dd>
+<dt>old</dt><dd>Disable system accounts for a former member.</dd>
+<dt>pass</dt><dd>Set the password of a current member.</dd>
+<dt>client</dt><dd>Produce an OpenVPN configuration (<q>.ovpn</q>) file for a
+member's device.</dd>
+</dl>
+
+<p>
+Liturgical sub-commands:
+</p>
+
+<dl class="org-dl">
+<dt>tz</dt><dd>Run <code>timedatectl set-timezone</code> on cloister servers.</dd>
+<dt>upgrade</dt><dd>Run <code>apt update; apt full-upgrade --autoremove</code> on all
+hosts.</dd>
+<dt>reboots</dt><dd>Look for <q>/run/reboot*</q> on all hosts.</dd>
+<dt>versions</dt><dd>Report <code>ansible_distribution</code>, <code>_distribution_version</code>,
+and <code>_architecture</code> for all hosts.</dd>
+</dl>
+</div>
+</div>
+<div id="outline-container-org2775f4a" class="outline-3">
+<h3 id="org2775f4a"><span class="section-number-3">11.2.</span> Abbey Command Script</h3>
+<div class="outline-text-3" id="text-11-2">
+<p>
+The script begins with the following prefix and trampolines.
+</p>
+
+<div class="org-src-container">
+<q>abbey</q><pre class="src src-perl"><span class="org-comment-delimiter">#</span><span class="org-comment">!/usr/bin/perl -w</span>
+<span class="org-comment-delimiter">#</span>
+<span class="org-comment-delimiter"># </span><span class="org-comment">DO NOT EDIT.  This file was tangled from README.org.</span>
+
+<span class="org-constant">use</span> strict;
+
+<span class="org-keyword">if</span> ($<span class="org-variable-name">ARGV</span>[0] eq <span class="org-string">"config"</span>) {
+  <span class="org-keyword">exec</span> <span class="org-string">"./Institute/inst"</span>, @<span class="org-underline"><span class="org-variable-name">ARGV</span></span>;
+}
+<span class="org-keyword">if</span> ($<span class="org-variable-name">ARGV</span>[0] eq <span class="org-string">"new"</span>) {
+  <span class="org-keyword">exec</span> <span class="org-string">"./Institute/inst"</span>, @<span class="org-underline"><span class="org-variable-name">ARGV</span></span>;
+}
+<span class="org-keyword">if</span> ($<span class="org-variable-name">ARGV</span>[0] eq <span class="org-string">"old"</span>) {
+  <span class="org-keyword">exec</span> <span class="org-string">"./Institute/inst"</span>, @<span class="org-underline"><span class="org-variable-name">ARGV</span></span>;
+}
+<span class="org-keyword">if</span> ($<span class="org-variable-name">ARGV</span>[0] eq <span class="org-string">"pass"</span>) {
+  <span class="org-keyword">exec</span> <span class="org-string">"./Institute/inst"</span>, @<span class="org-underline"><span class="org-variable-name">ARGV</span></span>;
+}
+<span class="org-keyword">if</span> ($<span class="org-variable-name">ARGV</span>[0] eq <span class="org-string">"client"</span>) {
+  <span class="org-keyword">exec</span> <span class="org-string">"./Institute/inst"</span>, @<span class="org-underline"><span class="org-variable-name">ARGV</span></span>;
+}
+</pre>
+</div>
+
+<p>
+The small institute's <code>./inst</code> command expects to be running in
+<q>Institute/</q>, not <q>./</q>, but it only references <q>public/</q>, <q>private/</q>,
+<q>Secret/</q> and <q>playbooks/check-inst-vars.yml</q>, and will find the abbey
+specific versions of these.  The <code>roles_path</code> setting in <a href="#org3a422c6"><q>ansible.cfg</q></a>
+effectively merges the institutional roles into the distinctly named
+abbey specific roles.  The roles likewise reference files with
+relative names, and will find the abbey specific <q>private/</q>
+directory (named <q>../private/</q> relative to <q>playbooks/</q>).
+</p>
+
+<p>
+Ansible does not implement a <code>playbooks_path</code> key, so the following
+code block "duplicates" the action of the institute's
+<q>check-inst-vars.yml</q>.
+</p>
+
+<div class="org-src-container">
+<q>playbooks/check-inst-vars.yml</q><pre class="src src-conf">- import_playbook: ../Institute/playbooks/check-inst-vars.yml
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org39d7904" class="outline-3">
+<h3 id="org39d7904"><span class="section-number-3">11.3.</span> The Upgrade Command</h3>
+<div class="outline-text-3" id="text-11-3">
+<p>
+The script implements an <code>upgrade</code> sub-command that runs <code>apt update</code>
+and <code>apt full-upgrade --autoremove</code> on all abbey managed machines.  It
+recognizes an optional <code>-n</code> flag indicating that the upgrade tasks
+should only be checked.  Any other (single, optional) argument must be
+a limit pattern.  For example:
+</p>
+
+<pre class="example">
+./abbey upgrade
+./abbey upgrade -n
+./abbey upgrade core
+./abbey upgrade -n core
+./abbey upgrade '!front'
+</pre>
+
+
+<div class="org-src-container">
+<q>abbey</q><pre class="src src-perl">
+<span class="org-keyword">if</span> ($<span class="org-variable-name">ARGV</span>[0] eq <span class="org-string">"upgrade"</span>) {
+  shift;
+  <span class="org-type">my</span> @<span class="org-underline"><span class="org-variable-name">args</span></span> = ( <span class="org-string">"-e"</span>, <span class="org-string">"\@Secret/become.yml"</span> );
+  <span class="org-keyword">if</span> (defined $<span class="org-variable-name">ARGV</span>[0] &amp;&amp; $<span class="org-variable-name">ARGV</span>[0] eq <span class="org-string">"-n"</span>) {
+    shift;
+    push @<span class="org-underline"><span class="org-variable-name">args</span></span>, <span class="org-string">"--check"</span>, <span class="org-string">"--diff"</span>;
+  }
+  <span class="org-keyword">if</span> (defined $<span class="org-variable-name">ARGV</span>[0]) {
+    <span class="org-type">my</span> $<span class="org-variable-name">limit</span> = $<span class="org-variable-name">ARGV</span>[0];
+    shift;
+    <span class="org-keyword">die</span> <span class="org-string">"illegal characters: $limit"</span>
+      <span class="org-keyword">if</span> $<span class="org-variable-name">limit</span> !~ <span class="org-string">/^!?[a-z][-a-z0-9,!]+$/</span>;
+    push @<span class="org-underline"><span class="org-variable-name">args</span></span>, <span class="org-string">"-l"</span>, $<span class="org-variable-name">limit</span>;
+  }
+  <span class="org-keyword">exec</span> (<span class="org-string">"ansible-playbook"</span>, @<span class="org-underline"><span class="org-variable-name">args</span></span>, <span class="org-string">"playbooks/upgrade.yml"</span>);
+}
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>playbooks/upgrade.yml</q><pre class="src src-conf">- hosts: all
+  tasks:
+
+  - name: Upgrade packages.
+    become: yes
+    apt:
+      update_cache: yes
+      upgrade: full
+      autoremove: yes
+      purge: yes
+      autoclean: yes
+
+  - name: Check for /run/reboot-required.
+    stat:
+      path: /run/reboot-required
+    no_log: true
+    register: st
+
+  - debug:
+      msg: Reboot required.
+    when: st.stat.exists
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-orgdc160ec" class="outline-3">
+<h3 id="orgdc160ec"><span class="section-number-3">11.4.</span> The Reboots Command</h3>
+<div class="outline-text-3" id="text-11-4">
+<p>
+The script implements a <code>reboots</code> sub-command that looks for
+<q>/run/reboot-required</q> on all abbey managed machines.
+</p>
+
+<div class="org-src-container">
+<q>abbey</q><pre class="src src-perl"><span class="org-keyword">if</span> ($<span class="org-variable-name">ARGV</span>[0] eq <span class="org-string">"reboots"</span>) {
+  <span class="org-keyword">exec</span> (<span class="org-string">"ansible-playbook"</span>, <span class="org-string">"-e"</span>, <span class="org-string">"\@Secret/become.yml"</span>,
+        <span class="org-string">"playbooks/reboots.yml"</span>);
+}
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>playbooks/reboots.yml</q><pre class="src src-conf">---
+- hosts: all
+  tasks:
+
+  - stat:
+      path: /run/reboot-required
+    register: st
+
+  - debug:
+      msg: Reboot required.
+    when: st.stat.exists
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-orgcf4c1b6" class="outline-3">
+<h3 id="orgcf4c1b6"><span class="section-number-3">11.5.</span> The Versions Command</h3>
+<div class="outline-text-3" id="text-11-5">
+<p>
+The script implements a <code>versions</code> sub-command that reports the
+operating system version of all abbey managed machines.
+</p>
+
+<div class="org-src-container">
+<q>abbey</q><pre class="src src-perl"><span class="org-keyword">if</span> ($<span class="org-variable-name">ARGV</span>[0] eq <span class="org-string">"versions"</span>) {
+  <span class="org-keyword">exec</span> (<span class="org-string">"ansible-playbook"</span>, <span class="org-string">"-e"</span>, <span class="org-string">"\@Secret/become.yml"</span>,
+        <span class="org-string">"playbooks/versarch.yml"</span>);
+}
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>playbooks/versarch.yml</q><pre class="src src-conf">- hosts: all
+  tasks:
+  - debug:
+      msg: &gt;-
+        {{ ansible_distribution }}
+        {{ ansible_distribution_version }}
+        {{ ansible_architecture }}
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org1fd86c1" class="outline-3">
+<h3 id="org1fd86c1"><span class="section-number-3">11.6.</span> The TZ Command</h3>
+<div class="outline-text-3" id="text-11-6">
+<p>
+The abbey changes location almost weekly, so its timezone changes
+occasionally.  Droplet does not move.  Gate and other simple servers
+(the weather monitors) are kept in UTC.  Core, the DVRs, TVRs, and the
+desktops all want updating to the current local timezone.  The
+desktops are managed maually, but the rest can all be updated using
+Ansible.
+</p>
+
+<p>
+The <code>tz</code> sub-command runs the <q>timezone.yml</q> playbook, which uses the
+current timezone/city on the administrator's notebook and updates
+Core, the DVRs and TVRs.  Each runs <code>timedatectl set-timezone</code> and
+restarts the affected services.
+</p>
+
+<p>
+This is an experimental playbook until it is used/tested with separate
+machines hosting the DVR and TVR services.  It assumes each host sees
+the <code>new_tz</code> result registered by it in a previous play and not by the
+last host in the previous play.
+</p>
+
+<div class="org-src-container">
+<q>abbey</q><pre class="src src-perl"><span class="org-keyword">if</span> ($<span class="org-variable-name">ARGV</span>[0] eq <span class="org-string">"tz"</span>) {
+  <span class="org-type">my</span> $<span class="org-variable-name">city</span> = <span class="org-string">`cat /etc/timezone`</span>; chomp $<span class="org-variable-name">city</span>;
+  <span class="org-type">my</span> $<span class="org-variable-name">zone</span> = <span class="org-string">`date +%Z`</span>; chomp $<span class="org-variable-name">zone</span>;
+  print <span class="org-string">"Setting timezones to $city.\n"</span>;
+  <span class="org-keyword">exec</span> (<span class="org-string">"ansible-playbook"</span>, <span class="org-string">"-e"</span>, <span class="org-string">"\@Secret/become.yml"</span>,
+        <span class="org-string">"-e"</span>, <span class="org-string">"zone=$zone"</span>, <span class="org-string">"-e"</span>, <span class="org-string">"city=$city"</span>,
+        <span class="org-string">"playbooks/timezone.yml"</span>);
+}
+</pre>
+</div>
+
+<div class="org-src-container">
+<q>playbooks/timezone.yml</q><pre class="src src-conf">---
+- hosts: core, dvrs, tvrs
+  tasks:
+  - name: Update timezone.
+    become: yes
+    command: timedatectl set-timezone {{ city }}
+    <span class="org-variable-name">when: ansible_date_time.tz !</span>= zone
+    register: new_tz
+  <span class="org-variable-name">- debug: msg</span>={{ new_tz }}
+
+- hosts: dvrs
+  tasks:
+  - name: Restart Zoneminder.
+    become: yes
+    systemd:
+      service: <span class="org-string">"{{ item }}"</span>
+      restarted: yes
+    loop: [ mysql, zoneminder ]
+    when: new_tz.changed
+
+- hosts: tvrs
+  tasks:
+  - name: Restart MythTV.
+    become: yes
+    systemd:
+      service: <span class="org-string">"{{ item }}"</span>
+      restarted: yes
+    loop: [ mysql, mythtv-backend ]
+    when: new_tz.changed
+
+- hosts: core
+  tasks:
+  - name: Update PHP date.timezone.
+    become: yes
+    lineinfile:
+      <span class="org-variable-name">regexp: date.timezone ?</span>=
+      <span class="org-variable-name">line: date.timezone</span> = {{ city }}
+      path: <span class="org-string">"{{ item }}"</span>
+    loop:
+    - /etc/php/7.4/cli/php.ini
+    - /etc/php/7.4/apache2/php.ini
+    notify: Restart Apache2.
+  handlers:
+  - name: Restart Apache2.
+    become: yes
+    systemd:
+      service: apache2
+      state: restarted
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org773e94c" class="outline-3">
+<h3 id="org773e94c"><span class="section-number-3">11.7.</span> Abbey Command Help</h3>
+<div class="outline-text-3" id="text-11-7">
+<div class="org-src-container">
+<q>abbey</q><pre class="src src-perl"><span class="org-keyword">die</span>
+  <span class="org-string">"usage: $0 [config,new,old,pass,client,upgrade,reboots,versions]\n"</span>;
+</pre>
+</div>
+</div>
+</div>
+</div>
+<div id="outline-container-org110d7b3" class="outline-2">
+<h2 id="org110d7b3"><span class="section-number-2">12.</span> Cloistering</h2>
+<div class="outline-text-2" id="text-12">
+<p>
+This is how a new machine is brought into the cloister.  The process
+is initially quite different depending on the device type but then
+narrows down to the common preparation of all machines administered by
+Ansible.
+</p>
+</div>
+<div id="outline-container-orgd05b52b" class="outline-3">
+<h3 id="orgd05b52b"><span class="section-number-3">12.1.</span> IoT Devices</h3>
+<div class="outline-text-3" id="text-12-1">
+<p>
+A wireless IoT device (smart TV, Blu-ray deck, etc.) cannot install
+Debian nor even an OpenVPN app from F-Droid.  And it shouldn't.  As an
+untrustworthy bit of kit, it should have no access to the cloister,
+merely the Internet.  It need not appear in the Ansible inventory.
+</p>
+
+<p>
+IoT devices trusted enough to be patched to the cloister Ethernet (IP
+cameras, TV Tuners, etc.) are added to <q>/etc/dhcp/dhcpd.conf</q> and
+given a private domain name as described in the following steps.
+</p>
+
+<ul class="org-ul">
+<li><a href="#org9f0e885">Add to Core DHCP</a></li>
+<li><a href="#org15590d4">Create Wired Domain Name</a></li>
+</ul>
+
+<p>
+Wireless IoT devices are manually configured with the cloister Wi-Fi
+password and may be given a private domain name as described here.
+</p>
+
+<ul class="org-ul">
+<li><a href="#org68a65ec">Create Wireless Domain Name</a></li>
+</ul>
+</div>
+</div>
+<div id="outline-container-org390d48b" class="outline-3">
+<h3 id="org390d48b"><span class="section-number-3">12.2.</span> Raspberry Pis</h3>
+<div class="outline-text-3" id="text-12-2">
+<p>
+The abbey's Raspberry Pis run Raspberry Pi OS, either the desktop
+(PIXEL) or the Lite version (for headless servers).  The following was
+the installation process with a wireless desktop Raspberry Pi OS
+Bookworm (12) machine.
+</p>
+
+<ul class="org-ul">
+<li>Write the disk image, <q>2023-10-10-raspios-bookworm-arm64.img.xz</q>, to
+a fast (U3 and/or A1) ÂµSD card and insert it in the Pi.</li>
+<li>Attach an HDMI monitor, a USB keyboard/mouse, and the cloister
+Ethernet, and power up.</li>
+<li>Answer first-boot installation questions:
+<ul class="org-ul">
+<li>Language: English (USA)</li>
+<li>Keyboard: English (USA)</li>
+<li>new username: sysadm</li>
+<li>new password: fubar</li>
+</ul></li>
+<li><a href="#org9f0e885">Add to Core DHCP</a></li>
+<li><a href="#org15590d4">Create Wired Domain Name</a></li>
+<li>Log in as <code>sysadm</code> on the console.</li>
+<li>Run <code>sudo raspi-config</code> and use the following menu items.
+<ul class="org-ul">
+<li>S4 Hostname (Set name for this computer on a network): new</li>
+<li>I1 SSH (Enable/disable remote command line access using SSH): enable</li>
+<li>A1 Expand Filesystem (Ensures that all of the SD card is available)</li>
+</ul></li>
+<li><a href="#org2846f52">Update From Cloister Apt Cache</a></li>
+<li><a href="#orgb8a472a">Authorize Remote Administration</a></li>
+<li><a href="#org9216df5">Configure with Ansible</a></li>
+</ul>
+
+<p>
+If the Pi is going to operate wirelessly, the following additional
+steps are taken.
+</p>
+
+<ul class="org-ul">
+<li><a href="#org4f5e619">Connect to Cloister Wi-Fi</a></li>
+<li><a href="#org0929940">Connect to Cloister VPN</a></li>
+<li><a href="#org68a65ec">Create Wireless Domain Name</a></li>
+</ul>
+</div>
+</div>
+<div id="outline-container-org527e70f" class="outline-3">
+<h3 id="org527e70f"><span class="section-number-3">12.3.</span> PCs</h3>
+<div class="outline-text-3" id="text-12-3">
+<p>
+Most of the abbey's machines, like Core and Gate, are general-purpose
+PCs running Debian.  The process of cloistering these machines
+follows.
+</p>
+
+<ul class="org-ul">
+<li>Write the disk image, e.g. <q>debian-12.2.0-amd64-netinst.iso</q>, to a
+USB drive and insert it in the PC.</li>
+<li>Attach an HDMI monitor, a USB keyboard/mouse, and the cloister
+Ethernet, and power up.  Choose to boot from the USB drive.</li>
+<li>Answer first-boot installation questions:
+<ul class="org-ul">
+<li>Language: English (USA)</li>
+<li>Keyboard: English (USA)</li>
+<li>new username: sysadm</li>
+<li>new password: fubar</li>
+</ul></li>
+<li><a href="#org9f0e885">Add to Core DHCP</a></li>
+<li><a href="#org15590d4">Create Wired Domain Name</a></li>
+<li>Log in as <code>sysadm</code> on the console.</li>
+<li><a href="#org2846f52">Update From Cloister Apt Cache</a></li>
+<li><p>
+Install OpenSSH.  Plain Debian does not come with OpenSSH installed.
+</p>
+<pre class="example">
+sudo apt install openssh-server
+</pre></li>
+<li><a href="#orgb8a472a">Authorize Remote Administration</a></li>
+<li><a href="#org9216df5">Configure with Ansible</a></li>
+</ul>
+
+<p>
+If the PC is going to operate wirelessly, the following additional
+steps are taken.
+</p>
+
+<ul class="org-ul">
+<li><a href="#org4f5e619">Connect to Cloister Wi-Fi</a></li>
+<li><a href="#org0929940">Connect to Cloister VPN</a></li>
+<li><a href="#org68a65ec">Create Wireless Domain Name</a></li>
+</ul>
+</div>
+</div>
+<div id="outline-container-org9f0e885" class="outline-3">
+<h3 id="org9f0e885"><span class="section-number-3">12.4.</span> Add to Core DHCP</h3>
+<div class="outline-text-3" id="text-12-4">
+<p>
+When a new machine is connected to the cloister Ethernet, its MAC
+address must be added to Core's DHCP configuration.  Core does not
+provide network addresses to new devices automatically.
+</p>
+
+<p>
+IoT devices (IP cameras, HDTV tuners, etc.) often have their MAC
+address printed on their case or mentioned in a configuration page.
+The MAC address <i>must</i> also appear in the device's DHCP Discover
+broadcasts, which are logged to <q>/var/log/daemon.log</q> on Core.  As a
+last (or first!) resort, the following command line should reveal the
+new device's MAC.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">tail -100 /var/log/daemon.log | grep DISCOVER
+</pre>
+</div>
+
+<p>
+With the new device's Ethernet MAC in hand, a stanza like the
+following is added to the bottom of <q>private/core-dhcpd.conf</q>.  The IP
+address must be unique.  Typically the next host number after the
+last entry is chosen.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-conf"><span class="org-type">host new</span> {
+  hardware ethernet 08:00:27:f3:41:66; fixed-address 192.168.56.4; }
+</pre>
+</div>
+
+<p>
+The DHCP service is then <i>restarted</i>.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">sudo systemctl restart isc-dhcp-server
+</pre>
+</div>
+
+<p>
+Soon after this the device should be offered a lease for its IP
+address, <code>192.168.56.4</code>.  It might be power cycled to speed up the
+process.
+</p>
+
+<p>
+When successful, the following command shows the device is accessible,
+reporting <code>1 packets transmitted, 1 received, 0% packet loss...</code>.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">ping -c1 192.168.56.4
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org15590d4" class="outline-3">
+<h3 id="org15590d4"><span class="section-number-3">12.5.</span> Create Wired Domain Name</h3>
+<div class="outline-text-3" id="text-12-5">
+<p>
+A wired device is assigned an IP address when it is added to Core's
+DHCP configuration (as in <a href="#org9f0e885">Add to Core DHCP</a>).  A private domain name is
+then associated with this address.  If the device is intended to
+operate wirelessly, the name for its address is modified with a <code>-w</code>
+suffix.  Thus <code>new-w.birchwood.private</code> would be the name of the new
+device while it is temporarily connected to the cloister Ethernet, and
+<code>new.birchwood.private</code> would be its "normal" name used when it is on
+the cloister Wi-Fi.
+</p>
+
+<p>
+The private domain name is created by adding a line like the following
+to <q>private/db.domain</q> and incrementing the serial number at the top
+of the file.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-conf">new-w   IN      A       192.168.56.4
+</pre>
+</div>
+
+<p>
+The reverse mapping is also created by adding a line like the
+following to <q>private/db.private</q> and incrementing the serial number
+at the top of that file.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-conf">4       IN      PTR     new-w.birchwood.private.
+</pre>
+</div>
+
+<p>
+After <code>./abbey config core</code> updates Core, resolution of the <code>new-w</code>
+name can be tested.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">resolvectl query new-w.birchwood.private.
+resolvectl query 192.168.56.4
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org2846f52" class="outline-3">
+<h3 id="org2846f52"><span class="section-number-3">12.6.</span> Update From Cloister Apt Cache</h3>
+<div class="outline-text-3" id="text-12-6">
+<ul class="org-ul">
+<li>Log in as <code>sysadm</code> on the console.</li>
+<li><p>
+Create <q>/etc/apt/apt.conf.d/01proxy</q>.
+</p>
+<pre class="example">
+D=apt-cacher.birchwood.private.
+echo "Acquire::http::Proxy \"http://$D:3142\";" \
+&gt; | sudo tee /etc/apt/apt.conf.d/01proxy
+</pre></li>
+<li><p>
+Update the system and reboot.
+</p>
+<pre class="example">
+sudo apt update
+sudo apt full-upgrade --autoremove
+sudo reboot
+</pre></li>
+</ul>
+</div>
+</div>
+<div id="outline-container-orgb8a472a" class="outline-3">
+<h3 id="orgb8a472a"><span class="section-number-3">12.7.</span> Authorize Remote Administration</h3>
+<div class="outline-text-3" id="text-12-7">
+<p>
+To remotely administer <code>new-w</code>, Ansible must be authorized to login as
+<code>sysadm@new-w</code> without a login password, using an SSH key pair.  This is
+accomplished by copying Ansible's SSH public key to <code>new-w</code>.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">scp Secret/ssh_admin/id_rsa.pub sysadm@new-w:admin_key
+</pre>
+</div>
+
+<p>
+Then on <code>new-w</code> (logged in as <code>sysadm</code>) the public key is installed in
+<q>~sysadm/.ssh/authorized_keys</q>.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">( <span class="org-builtin">cd</span>; <span class="org-builtin">umask</span> 077; mkdir .ssh; cp admin_key .ssh/authorized_keys )
+</pre>
+</div>
+
+<p>
+Now the administrator can test access to <code>new-w</code> using Ansible's SSH
+key.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">ssh -i Secret/ssh_admin/id_rsa sysadm@new-w
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org9216df5" class="outline-3">
+<h3 id="org9216df5"><span class="section-number-3">12.8.</span> Configure with Ansible</h3>
+<div class="outline-text-3" id="text-12-8">
+<p>
+With remote administration authorized and tested (as in <a href="#orgb8a472a">Authorize
+Remote Administration</a>), and the machine connected to the cloister
+Ethernet, the configuration of <code>new-w</code> can be completed by Ansible.
+Note that if the machine is staying on the cloister Ethernet, its
+domain name will be <code>new</code> (having had no <code>-w</code> suffix added).
+</p>
+
+<p>
+First <code>new-w</code> is added to Ansible's inventory in <a href="#orgd0676df"><q>hosts</q></a>.  A <code>new-w</code>
+section is added to the list of all hosts, and an empty section of the
+same name is added to the list of <code>campus</code> hosts.  If the machine uses
+the usual privileged account name, <code>sysadm</code>, the <code>ansible_user</code> key in
+not needed.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-conf">hosts:
+  ...
+  new-w:
+    ansible_user: pi
+    ansible_become_password: <span class="org-string">"{{ become_new }}"</span>
+  ...
+children:
+  ...
+  campus:
+    hosts:
+      ...
+      new-w:
+</pre>
+</div>
+
+<p>
+If the <code>sudo</code> command on <code>new-w</code> never prompts <code>sysadm</code> for a
+password, then the <code>ansible_become_password</code> setting is also not
+needed.  Otherwise, the password is added to <q>Secret/become.yml</q> as
+shown below.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh"><span class="org-builtin">echo</span> -n <span class="org-string">"become_new: "</span> &gt;&gt;Secret/become.yml
+ansible-vault encrypt_string PASSWORD &gt;&gt;Secret/become.yml
+</pre>
+</div>
+
+<p>
+Finally the <code>./abbey config new-w</code> command is run.  It will install
+several additional software packages and change several more
+configuration files.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">./abbey config new-w
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org4f5e619" class="outline-3">
+<h3 id="org4f5e619"><span class="section-number-3">12.9.</span> Connect to Cloister Wi-Fi</h3>
+<div class="outline-text-3" id="text-12-9">
+<p>
+On an IoT device, or a Debian or Android "desktop", the cloister Wi-Fi
+name and password are entered manually.  Once the device is connected,
+its Wi-Fi IP address may be discovered in its network settings, and
+perhaps via the access point's local domain, e.g. as <code>new.lan</code> on a
+desktop connected to the cloister Wi-Fi.
+</p>
+
+<p>
+Wireless Debian machines use <code>ifupdown</code> configured with a short
+<q>/etc/network/interfaces.d/wifi</q> drop-in.  In this example, the Wi-Fi
+interface on <code>new</code> is named <code>wlan0</code>.
+</p>
+
+<div class="org-src-container">
+=/etc/network/interfaces.d/wifi<pre class="src src-conf">auto wlan0
+iface wlan0 inet dhcp
+    wpa-ssid <span class="org-string">"Birchwood Abbey"</span>
+    wpa-psk <span class="org-string">"PASSWORD"</span>
+</pre>
+</div>
+
+<p>
+Once the <code>sudo ifup wlan0</code> command is successful, the machine will get
+an IP address on the access point's local network (revealed by the
+command <code>ip addr show dev wlan0</code>).
+</p>
+
+<p>
+The new Wi-Fi IP address, e.g. <code>192.168.10.225</code>, should be tested on a
+desktop connected to the Wi-Fi using the following <code>ping</code> command.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">ping -c1 192.168.10.225
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org0929940" class="outline-3">
+<h3 id="org0929940"><span class="section-number-3">12.10.</span> Connect to Cloister VPN</h3>
+<div class="outline-text-3" id="text-12-10">
+<p>
+Wireless devices connected to the cloister Wi-Fi will get an IP
+address on the access point's local network and a default route to the
+Internet, per the default configuration of a commodity cable modem
+with Wi-Fi access point included.  Access to further abbey resources,
+however, is possible only via the cloister VPN.
+</p>
+
+<p>
+Connections to the cloister VPN are authorized by OpenVPN
+configuration (<q>.ovpn</q>) files generated by the <code>./abbey client...</code>
+command (aka <a href="Institute/README.html#org0ad53cf">The Client Command</a>).  These are secret files, kept
+readable only by their owners and are deleted after use.  They are
+copied to new OpenVPN clients using secure (<code>ssh</code>) connections.
+</p>
+</div>
+<div id="outline-container-orgc2c05c2" class="outline-4">
+<h4 id="orgc2c05c2"><span class="section-number-4">12.10.1.</span> Debian Servers</h4>
+<div class="outline-text-4" id="text-12-10-1">
+<p>
+Wireless Debian servers (without NetworkManager) are connected to the
+cloister VPN via the following process.
+</p>
+
+<ul class="org-ul">
+<li>Create a new client certificate and OpenVPN configuration for the
+new campus server.</li>
+<li>Copy the <q>campus.ovpn</q> file to <q>/etc/openvpn/cloister.conf</q>.</li>
+<li>In a secure shell session on the new machine as <code>sysadm</code>:</li>
+<li>Install the <code>openvpn</code> and <code>openvpn-systemd-resolved</code> software
+packages.</li>
+<li>Start the SystemD service unit.</li>
+<li>Test the connection (and name resolution).</li>
+<li>Enable the SystemD service unit.</li>
+<li>Clean up secrets on the new machine.</li>
+<li>Clean up secrets on the administrator's machine.</li>
+</ul>
+
+<p>
+And these are the commands.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">./abbey client campus new
+scp campus.ovpn sysadm@new-w:
+ssh sysadm@new-w
+sudo apt install openvpn openvpn-systemd-resolved
+( <span class="org-builtin">cd</span>; <span class="org-builtin">umask</span> 077; sudo cp campus.ovpn /etc/openvpn/cloister.conf )
+sudo systemctl start openvpn@cloister
+ping -c1 core
+sudo systemctl enable openvpn@cloister
+rm campus.ovpn
+<span class="org-keyword">logout</span>
+rm campus.ovpn
+</pre>
+</div>
+</div>
+</div>
+<div id="outline-container-org110b3d7" class="outline-4">
+<h4 id="org110b3d7"><span class="section-number-4">12.10.2.</span> Debian Desktops</h4>
+<div class="outline-text-4" id="text-12-10-2">
+<p>
+Wireless Debian desktop machines (both PCs and Pis, running
+NetworkManager) and are connected to the cloister VPN via the
+following process.  Note that they do not appear in the set of
+<code>campus</code> hosts and are not configured by Ansible.  They do not appear
+in Ansible's host inventory at all unless the desktop owner is willing
+to provide the password to a privileged account on their machine.
+</p>
+
+<ul class="org-ul">
+<li>Create a new client certificate and campus/public OpenVPN
+configurations for the new abbey desktop.</li>
+<li>Copy the <q>campus.ovpn</q> and <q>public.ovpn</q> files to the new desktop.</li>
+<li>Install the <code>openvpn</code>, <code>openvpn-systemd-resolved</code> and
+<code>network-manager-openvpn-gnome</code> packages on the new desktop.</li>
+<li>Open the desktop Settings &gt; Network &gt; VPN + &gt; Import from
+file&#x2026; and choose <q>~/campus.ovpn</q>.</li>
+<li>Open the Routes dialogues for both IPv4 and IPv6 and choose
+"Use this connection only for resources on its network.".</li>
+<li>Save the new VPN.</li>
+<li>Do the same with the <q>~/public.ovpn</q> file.</li>
+<li>Connected the cloister VPN and test it with <code>ping -c1 core</code>.</li>
+<li>Expunge the <q>~/campus.ovpn</q> and <q>~/public.ovpn</q> just as the system
+administrator will have already done.</li>
+</ul>
+
+<p>
+And these are the commands, assuming there is a privileged <code>sysadm</code>
+account available on the new desktop machine.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">./abbey client debian dicks-notebook dick
+scp campus.ovpn public.ovpn sysadm@dicks-notebook.lan:
+rm campus.ovpn public.ovpn
+ssh sysadm@dicks-notebook.lan
+sudo apt install openvpn openvpn-systemd-resolved <span class="org-sh-escaped-newline">\</span>
+                 network-manager-openvpn-gnome
+ping -c1 core.birchwood.private.
+</pre>
+</div>
+
+<p>
+Note that Dick's notebook does not need to connect to the cloister
+Ethernet.  It is authorized simply by copying the <q>.ovpn</q> files
+securely (e.g. using <code>ssh</code>) to a local domain name provided by the
+Wi-Fi AP (<code>dicks-notebook.lan</code>).  If the AP does not provide a local
+domain name, the machine's Wi-Fi IP address,
+e.g. <code>sysadm@192.168.10.225</code>, can be used instead.  (This IP address
+is often revealed in the desktop network settings.)
+</p>
+</div>
+</div>
+<div id="outline-container-org4faba4c" class="outline-4">
+<h4 id="org4faba4c"><span class="section-number-4">12.10.3.</span> Android</h4>
+<div class="outline-text-4" id="text-12-10-3">
+<p>
+Android phones and tablets are connected to the cloister VPN via the
+following process.  Note that they do not appear in the set of
+<code>campus</code> hosts, are not configured by Ansible, and do not appear in
+the host inventory.
+</p>
+
+<ul class="org-ul">
+<li>Create a new client certificate and campus/public OpenVPN
+configurations for the new abbey Android.</li>
+<li>Copy the <q>campus.ovpn</q> and <q>public.ovpn</q> files to a USB drive.</li>
+<li>On the Android machine:</li>
+<li>Connect to the cloister Wi-Fi.</li>
+<li>Install <a href="https://f-droid.org">F-Droid</a> and use it to install OpenVPN.</li>
+<li>Connect the USB drive, perhaps with an OTG (On The Go) adapter,
+and open the <q>campus.ovpn</q> file.  The file should be opened with
+the OpenVPN app, which will appear to ask for confirmation before
+creating the new VPN.</li>
+<li>Open the <q>public.ovpn</q> file and create a second VPN.</li>
+</ul>
+
+<p>
+The <q>.ovpn</q> files must be transferred to the Android via a secure
+medium: the <code>scp</code> command, a USB drive, a cloud download, or perhaps
+an encrypted email.  In the following commands, the files are copied
+to a USB drive labeled <code>Transfers</code>.  After insertion into the Android,
+its "storage" is viewed with the Files app, which should launch
+OpenVPN when a <q>.ovpn</q> file is opened.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">./abbey client android dicks-tablet dick
+cp campus.ovpn public.ovpn /media/sysadm/Transfers/
+rm campus.ovpn public.ovpn
+</pre>
+</div>
+</div>
+</div>
+</div>
+<div id="outline-container-org68a65ec" class="outline-3">
+<h3 id="org68a65ec"><span class="section-number-3">12.11.</span> Create Wireless Domain Name</h3>
+<div class="outline-text-3" id="text-12-11">
+<p>
+A wireless machine is assigned a Wi-Fi address when it connects to the
+cloister Wi-Fi, and a "VPN address" when it connects to Gate's OpenVPN
+server.  The VPN address can be discovered by running <code>ip addr show
+dev ovpn</code> on the machine, or inspecting <q>/etc/openvpn/ipp.txt</q> on
+Gate.  Once discovered, a private domain name,
+e.g. <code>new.birchwood.private</code>, can be associated with the VPN address,
+e.g <code>10.84.138.7</code>.  The administrator adds a line like the following
+to <q>private/db.domain</q> and increments the serial number at the top of
+the file.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-conf">new     IN      A       10.84.138.7
+</pre>
+</div>
+
+<p>
+The administrator also creates the reverse mapping by adding a line
+like the following to <q>private/db.campus_vpn</q> and incrementing the
+serial number at the top of that file.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-conf">7       IN      PTR     new.birchwood.private.
+</pre>
+</div>
+
+<p>
+After <code>./abbey config core</code> updates Core, the administrator can test
+resolution of the new name.
+</p>
+
+<div class="org-src-container">
+<pre class="src src-sh">resolvectl query new.birchwood.private.
+resolvectl query 10.84.138.7
+</pre>
+</div>
+
+<p>
+A wireless device with no Ethernet interface and unable to run OpenVPN
+gets just a Wi-Fi address.  It can be given a private domain name
+(e.g. <code>new.birchwood.private</code>) associated with the Wi-Fi address
+(e.g. <code>192.168.10.225</code>), but a reverse lookup on a machine connected
+to the Wi-Fi may yield a name like <code>new.lan</code> (provided by the access
+point) while elsewhere (e.g. on the cloister Ethernet) the IP address
+will not resolve at all.  (There is no "reverse mapping" to be added
+to <q>private/db.campus_vpn</q>.)
+</p>
+</div>
+</div>
+</div>
+</div>
+<div id="postamble" class="status">
+<p class="author">Author: Matt Birkholz</p>
+<p class="date">Created: 2023-12-17 Sun 16:05</p>
+<p class="validation"><a href="https://validator.w3.org/check?uri=referer">Validate</a></p>
+</div>
+</body>
+</html>
diff --git a/README.org b/README.org
new file mode 100644 (file)
index 0000000..e66ae48
--- /dev/null
@@ -0,0 +1,3963 @@
+#+TITLE: Birchwood Abbey Networks
+
+The abbey's network services are configured by Ansible scripts based
+on [[file:Institute/README.org][A Small Institute]].  The institutional roles like ~core~, ~gate~ and
+~front~ are intended for general use and so are kept free of abbey
+idiosyncrasies.  The roles herein are abbey specific, emphasized by
+the ~abbey-~ prefix on their names.  These roles are applied /after/
+the generic institutional roles (again, documented [[file:Institute/README.org][here]]).
+
+* Overview
+
+A Small Institute makes security and privacy top priorities but
+Birchwood Abbey approaches these from a particularly Elvish viewpoint.
+Elves depend for survival on speed, agility, and concealment.  Working
+toward those ends (esp. the last) Birchwood Abbey's network topology
+was designed to look like that of an average Amerikan household.
+Korporate Amerika expects our ISP to provide us with a
+Wi-Fi/router/modem that all of our appliances can use to communicate
+amongst themselves in a cliquey, New World Order IoT kumbaya.  We dare
+not disappoint.
+
+Thus Samsung (our refrigerator) is able to browse for our printer or
+connect to Kroger (our grocer) or Kaiser (our health care provider)
+for whatever reason (presumably to report on our eating habits).  The
+only suspicious character in this Amerikan household will be Gate, a
+Raspberry Pi passing many encrypted packets.  Thus when the New World
+Police come a-knock'n (i.e. after they kick the door and kill the dog)
+we might still hold onto some plausible deniability.
+
+To most look like our neighbors we sit between our smart TVs and our
+smart refrigerators and /consciously/ play the flaccid consumer
+streaming Amazon and watching Blu-ray discs.  This works because we
+have preserved a means of escape.  We may not be able to hide our
+entertainment choices nor even eating habits anymore, but we can
+still just turn it all off and retreat into private correspondence
+between Inner Citadels.
+
+The small institute tries to look "normal" too so the abbey's network
+map is very similar, with differences mainly in terminology,
+philosophy, attitude.
+
+#+BEGIN_EXAMPLE
+                  |                                                   
+                  =                                                   
+                _|||_                                                 
+        ----- The Temple-----                                         
+            =   =   =   =                                             
+            =   =   =   =                                             
+          =====-Front-=====                                           
+                  |                                                   
+          -----------------                                           
+        (                   )                                         
+       (   The Internet(s)   )----(Hotel Wi-Fi)                       
+        (                   )         |                               
+          -----------------           |                               
+                  |                   +----Monk's notebook abroad     
+                  |                                                   
+  =============== | ==================================================
+                  |                                           Premises
+             (House ISP)                                              
+                  |                                                   
+                  |            +----Monk's notebook in the house      
+                  |            +----Samsung refrigerator              
+                  |            +----Sony Bluray                       
+                  |            +----Lexmark printer                   
+                  |            |                                      
+                  | +----(House Wi-Fi)                                
+                  | |                                  Game of Thrones
+  ============== Gate ================================================
+                  |                                           Cloister
+                  +----Ethernet switch                                
+                          |                                           
+                          +----Core                                   
+                          +----Security DVR                           
+                          +----IP camera(s)                           
+                          +----HDTV TVR                               
+                          +----WebTV                                  
+#+END_EXAMPLE
+
+
+* The Abbey Particulars
+
+The abbey's public particulars are included below.  They are the
+public particulars of a small institute, nothing more.  As for the
+abbey's private data, examples (only! ;-) are included in the
+following chapters.
+
+#+CAPTION: =public/vars.yml=
+#+BEGIN_SRC conf :tangle public/vars.yml :mkdirp yes
+---
+domain_name: birchwood-abbey.net
+domain_priv: birchwood.private
+
+full_name: Birchwood Abbey
+
+front_addr: 159.65.75.60
+#+END_SRC
+
+
+* The Abbey Front Role
+
+Birchwood Abbey's front door is a Digital Ocean Droplet configured as
+A Small Institute Front.  Thus it is already serving a public web site
+with Apache2, spooling email with Postfix and serving it with
+Dovecot-IMAPd, and hosting a VPN with OpenVPN.
+
+** Install Emacs
+
+The monks of the abbey are masters of the staff (bo) and Emacs.
+
+#+CAPTION: =roles_t/abbey-front/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-front/tasks/main.yml :mkdirp yes
+---
+- name: Install Emacs.
+  become: yes
+  apt: pkg=emacs
+#+END_SRC
+
+** Configure Public Email Aliases
+
+The abbey uses several additional email aliases.  These are the public
+mailboxes ~@birchwood-abbey.net~.  The institute already funnels the
+common mailboxes like ~postmaster~ and ~admin~ into ~root~ and ~root~
+to the machine's privileged account (~sysadm~).  The abbey takes it
+from there, forwarding ~sysadm~ to a real person.
+
+#+CAPTION: =roles_t/abbey-front/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-front/tasks/main.yml :noweb yes
+
+- name: Install abbey email aliases.
+  become: yes
+  blockinfile:
+    block: |
+        sysadm:                matt
+        keymaster:     root
+        codemaster:    matt
+        all:           matt, lori, erica
+        elders:                matt, lori
+        rents:         elders
+        puck:          matt
+        abbess:                lori
+    dest: /etc/aliases
+    marker: "# {mark} ABBEY MANAGED BLOCK"
+  notify: New aliases.
+#+END_SRC
+
+#+CAPTION: =roles_t/abbey-front/handlers/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-front/handlers/main.yml :mkdirp yes
+---
+- name: New aliases.
+  become: yes
+  command: newaliases
+#+END_SRC
+
+** Configure Git Daemon on Front
+
+The abbey publishes member Git repositories with ~git-daemon~.  If
+Dick (a member of A Small Institute) builds a Foo project Git
+repository in =~/foo/=, he can publish it to the campus by
+symbolically linking its =.git/= into =~/Public/Git/= on Core.  If the
+repository is world readable and contains a =git-daemon-export-ok=
+file, it will be served at =git://www/~dick/foo=.
+
+: touch ~/foo/.git/git-daemon-export-ok
+: ln -s ~/foo/.git ~/Public/Git/foo
+: chmod -R o+r ~/foo/.git
+: find ~/foo/.git -type d -print0 | xargs -0 chmod o+rx
+
+User repositories can be made available to the public at a URL like
+~git://small.example.org/~dick/foo~ by copying it to the same path on
+Front (=~dick/Public/Git/foo/=).  The following ~rsync~ command
+creates or updates such a copy.
+
+: rsync -av ~/foo/.git/ small.example.org:Public/Git/foo/
+
+Note that Dick's Git repository, mirrored to Front (or Core), does not
+need to be backed up, assuming Dick's home directory (including
+=~/foo/=) /is/.  If updates are git-pushed to a repository on Front,
+regular backups should be made, but this is Dick's responsibility.
+There are no regular, system backups on Front.
+
+: rsync -av --del small.institute.org:Public/foo/ ~/Public/foo/
+
+With SystemD and the ~git-daemon-sysvinit~ package installed, SystemD
+supervises a ~git-daemon~ service unit launched with
+~/etc/init.d/git-daemon~.  The old SysV ~init~ script gets its
+configuration from the customary =/etc/default/git-daemon= file.  The
+script then constructs the appropriate ~git-daemon~ command.  The
+~git-daemon(1)~ manual page explains the command options in detail.
+As explained in =/usr/share/doc/git-daemon-sysvinit/README.Debian=,
+the service must be enabled by setting ~GIT_DAEMON_ENABLE~ to ~true~.
+The base path is also changed to agree with =gitweb.cgi=.
+
+User repositories are enabled by adding a ~user-path~ option /and/
+disabling the default whitelist.  To specify an empty whitelist, the
+default (a list of one directory: =/var/lib/git=) must be avoided by
+setting ~GIT_DAEMON_DIRECTORY~ to a blank (not empty) string.
+
+The code below is included in both Front and Core configurations,
+which should be nearly identical for testing purposes.  Rather than
+factor out small roles like ~abbey-git-server~, Emacs Org Mode's Noweb
+support does the duplication, by multiple references to code blocks
+like ~git-tasks~ and ~git-handlers~.
+
+#+CAPTION: =roles_t/abbey-front/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-front/tasks/main.yml :noweb yes
+
+<<git-tasks>>
+#+END_SRC
+
+#+NAME: git-tasks
+#+CAPTION: ~git-tasks~
+#+BEGIN_SRC conf
+- name: Install git daemon.
+  become: yes
+  apt: pkg=git-daemon-sysvinit
+
+- name: Configure git daemon.
+  become: yes
+  lineinfile:
+    path: /etc/default/git-daemon
+    regexp: "{{ item.patt }}"
+    line: "{{ item.line }}"
+  loop:
+  - patt: '^GIT_DAEMON_ENABLE *='
+    line: 'GIT_DAEMON_ENABLE=true'
+  - patt: '^GIT_DAEMON_OPTIONS *='
+    line: 'GIT_DAEMON_OPTIONS="--user-path=Public/Git"'
+  - patt: '^GIT_DAEMON_BASE_PATH *='
+    line: 'GIT_DAEMON_BASE_PATH="/var/www/git"'
+  - patt: '^GIT_DAEMON_DIRECTORY *='
+    line: 'GIT_DAEMON_DIRECTORY=" "'
+  notify: Restart git daemon.
+
+- name: Create /var/www/git/.
+  become: yes
+  file:
+    path: /var/www/git
+    state: directory
+    group: staff
+    mode: u=rwx,g=srwx,o=rx
+#+END_SRC
+
+#+CAPTION: =roles_t/abbey-front/handlers/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-front/handlers/main.yml :noweb yes
+
+<<git-handlers>>
+#+END_SRC
+
+#+NAME: git-handlers
+#+CAPTION: ~git-handlers~
+#+BEGIN_SRC conf
+
+- name: Restart git daemon.
+  become: yes
+  command: systemctl restart git-daemon
+#+END_SRC
+
+** Configure Gitweb on Front
+
+The abbey provides an HTML interface to members' public Git
+repositories using ~gitweb.cgi~, one of the few CGI scripts allowed on
+Front.  Unlike the Git daemon, the Gitweb interface does /not/ care if
+the repository contains a =git-daemon-export-ok= file.
+
+Again Front and Core need to be configured congruently, so the
+necessary Apache directives are given here and referenced in the
+Apache configurations.
+
+Like the suggested per-user rewrite rule in the ~gitweb(1)~ manual
+page, the second ~RewriteRule~ specifies the root directory of the
+user's public Git repositories via the ~GITWEB_PROJECTROOT~
+environment variable.  It makes ~http://www/~dick/gitweb.cgi~ run
+Gitweb with the project root =~dick/Public/Git/=, the same directory
+the ~git-daemon~ makes available.  The first ~RewriteRule~ directs
+URLs with no user name to the default.  Thus ~http://www/gitweb.cgi~
+lists the repositories found in =/var/www/git/=.  The match patterns
+of both rules recognize =/gitweb= as well as =/gitweb.cgi=.
+
+#+NAME: apache-gitweb
+#+CAPTION: ~apache-gitweb~
+#+BEGIN_SRC conf
+
+Alias /gitweb-static/ /usr/share/gitweb/static/
+<Directory "/usr/share/gitweb/static/">
+    Options MultiViews
+</Directory>
+RewriteEngine on
+RewriteRule ^/gitweb(\.cgi)?(/.*)?$ \
+            /cgi-bin/gitweb.cgi$2 [QSA,L,PT]
+RewriteRule ^/\~([^\/]+)/gitweb(\.cgi)?(/.*)?$ \
+            /cgi-bin/gitweb.cgi$3 \
+            [QSA,E=GITWEB_PROJECTROOT:/home/$1/Public/Git/,L,PT]
+#+END_SRC
+
+The ~RewriteRule~ flags used here are:
+
+- QSA | qsappend  :: Append the request's query string.
+- E= | env :: Set or unset an environment variable.
+- L | last :: Stop with this Last rule.
+- PT | passthrugh :: Treat the result as a URI, not a file path.
+
+The ~RewriteEngine on~ directive must be included in the virtual host
+or no rewriting will take place.
+
+The CGI script and ~RewriteRule~ require Apache's ~cgi~ and ~rewrite~
+modules, which are not normally enabled on a small institute's public
+server.  Thus they need to be enabled here.  Note that Debian and
+-Ubuntu install different Apache MPMs (multi-processing modules)
+-requiring different CGI modules, turning two tasks into three.
+
+The script uses the ~CGI~ Perl module, which must be installed.
+
+The rewrite rule maps to the URL =/cgi-bin/gitweb.cgi=, which is
+mapped by default to =/usr/lib/cgi-bin/gitweb.cgi=.  The ~git~ package
+installs =gitweb.cgi= in =/usr/share/gitweb/=, so it and its related
+=index.cgi= script are linked into =/usr/lib/cgi-bin/=.
+
+The =static/= directory, also installed in =/usr/share/gitweb/=, is
+made available as ~http://www/gitweb-static/~ via an ~Alias~
+directive.  The global Perl configuration file, =/etc/gitweb.conf=,
+overrides the relative URLs Gitweb normally generates, and uses the
+web site =/favicon.ico=.
+
+#+NAME: apache-gitweb-tasks
+#+CAPTION: ~apache-gitweb-tasks~
+#+BEGIN_SRC conf
+- name: Enable Apache2 rewrite module for Gitweb.
+  become: yes
+  apache2_module: name=rewrite
+  notify: Restart Apache2.
+
+- name: Enable Apache2 cgid module for Gitweb (Ubuntu).
+  become: yes
+  apache2_module: name=cgid
+  when: ansible_distribution == 'Ubuntu'
+  notify: Restart Apache2.
+
+- name: Enable Apache2 cgi module for Gitweb (Debian).
+  become: yes
+  apache2_module: name=cgi
+  when: ansible_distribution == 'Debian'
+  notify: Restart Apache2.
+
+- name: Install libcgi-pm-perl for Gitweb.
+  become: yes
+  apt: pkg=libcgi-pm-perl
+
+- name: Link Gitweb into /cgi-bin/.
+  become: yes
+  file:
+    state: link
+    path: /usr/lib/cgi-bin/{{ item }}
+    src: /usr/share/gitweb/{{ item }}
+  loop: [ gitweb.cgi, index.cgi ]
+
+- name: Override Gitweb assets location.
+  become: yes
+  copy:
+    content: |
+      $projectroot = $ENV{'GITWEB_PROJECTROOT'} || "/var/www/git";
+      @stylesheets = ("/gitweb-static/gitweb.css");
+      $logo = "/gitweb-static/git-logo.png";
+      $favicon = "/favicon.ico";
+      $javascript = "/gitweb-static/gitweb.js";
+    dest: /etc/gitweb.conf
+    mode: u=rw,g=r,o=r
+#+END_SRC
+
+#+NAME: apache-gitweb-handlers
+#+CAPTION: ~apache-gitweb-handlers~
+#+BEGIN_SRC conf
+- name: Restart Apache2.
+  become: yes
+  systemd:
+    service: apache2
+    state: restarted
+#+END_SRC
+
+** Configure CGit on Front
+
+CGit is handled similarly, modifying =/etc/cgitrc= to reference a
+~CGIT_SCANPATH~ environment variable set by Apache re-write rules.
+The resulting Apache directives are given in ~apache-cgit~ and the
+Ansible tasks in ~apache-cgit-tasks~, for both Front and Core.
+
+#+NAME: apache-cgit
+#+CAPTION: ~apache-cgit~
+#+BEGIN_SRC conf
+
+ScriptAlias /cgit/ /usr/lib/cgit/cgit.cgi/
+Alias /cgit-css/ /usr/share/cgit/
+<Directory "/usr/lib/cgit/">
+   AllowOverride None
+   Options ExecCGI FollowSymlinks
+   Require all granted
+</Directory>
+RewriteRule ^/cgit?(/.*)$ \
+            /cgit$1 [QSA,E=CGIT_SCANPATH:/var/www/git/,L,PT]
+RewriteRule ^/\~([^\/]+)/cgit(/.*)?$ \
+            /cgit$2 [QSA,E=CGIT_SCANPATH:/home/$1/Public/Git/,L,PT]
+#+END_SRC
+
+#+NAME: apache-cgit-tasks
+#+CAPTION: ~apache-cgi-tasks~
+#+BEGIN_SRC conf
+
+- name: Install CGit.
+  become: yes
+  apt: pkg=cgit
+
+- name: Disable CGit default configuration.
+  become: yes
+  command:
+    cmd: a2disconf -q cgit
+    removes: /etc/apache2/conf-enabled/cgit.conf
+
+- name: Override CGit scan path.
+  become: yes
+  lineinfile:
+    path: /etc/cgitrc
+    regexp: "^scan-path *="
+    line: "scan-path=$CGIT_SCANPATH"
+  notify: Reload Apache2.
+#+END_SRC
+
+** Configure Apache for Abbey Documentation
+
+Some of the directives added to the =-vhost.conf= file are needed by
+the abbey's documentation, published at
+[[https://birchwood-abbey.net/Abbey/]].  The following template uses a
+~docroot~ variable for the actual path to the HTML.  On Front this
+variable is set to =/home/www=.  The same template is used on Core, to
+ensure matching configurations for accurate previews and tests.
+
+The abbey's network documentation currently uses automatic directory
+indexes, and declares the types of files with several additional
+filename suffixes.
+
+#+NAME: apache-abbey
+#+CAPTION: ~apache-abbey~
+#+BEGIN_SRC conf
+<Directory {{ docroot }}/Abbey/>
+    AllowOverride Indexes FileInfo
+    Options +Indexes +FollowSymLinks
+</Directory>
+#+END_SRC
+
+** Configure Photos URLs on Front
+
+Some of the directives added to the =-vhost.conf= file map the abbey's
+abstract photo URLs, e.g. =/Photos/2022/08/06/=, into actual file
+paths.  The following template uses the ~docroot~ variable introduced
+in the previous section.  On Front this variable is set to
+=/home/www=.  The same template is used on Core, to ensure
+matching configurations for accurate previews and tests.
+
+#+NAME: apache-photos
+#+CAPTION: ~apache-photos~
+#+BEGIN_SRC conf
+
+RedirectMatch /Photos$ /Photos/
+RedirectMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])$ \
+              /Photos/$1_$2_$3/
+AliasMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])/(.+)$ \
+           {{ docroot }}/Photos/$1/$2/$3/$4
+AliasMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])/$ \
+           {{ docroot }}/Photos/$1/$2/$3/index.html
+AliasMatch /Photos/$ {{ docroot }}/Photos/index.html
+#+END_SRC
+
+** Configure Apache on Front
+
+The abbey needs to add some Apache2 configuration directives to the
+virtual host listening for HTTPS requests to =birchwood-abbey.net=.
+Luckily there is support for this in the institutional configuration.
+The abbey simply creates a =birchwood-abbey.net-vhost.conf= file in
+=/etc/apache2/sites-available/=.
+
+The following task adds the [[apache-abbey][~apache-abbey~]], [[apache-photos][~apache-photos~]],
+[[apache-gitweb][~apache-gitweb~]], and [[apache-cgit][~apache-cgit~]] directives described above to the
+=-vhost.conf= file, and includes =options-ssl-apache.conf= from
+=/etc/letsencrypt/=.  The rest of the Let's Encrypt configuration is
+discussed in the following [[*Install Let's Encrypt][Install Let's Encrypt]] section.
+
+#+CAPTION: =roles_t/abbey-front/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-front/tasks/main.yml :noweb yes
+
+- name: Configure Apache.
+  become: yes
+  vars:
+    docroot: /home/www
+  copy:
+    content: |
+        <<apache-abbey>>
+        <<apache-photos>>
+        <<apache-gitweb>>
+        <<apache-cgit>>
+        IncludeOptional /etc/letsencrypt/options-ssl-apache.conf
+    dest: /etc/apache2/sites-available/{{ domain_name }}-vhost.conf
+  notify: Restart Apache2.
+
+<<apache-gitweb-tasks>>
+<<apache-cgit-tasks>>
+#+END_SRC
+
+#+CAPTION: =roles_t/abbey-front/handlers/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-front/handlers/main.yml :noweb yes
+
+<<apache-gitweb-handlers>>
+#+END_SRC
+
+** Configure Apache Log Archival
+
+These tasks hack Apache's ~logrotate(8)~ configuration to rotate
+weekly, keep the last 12 weeks, and email each week's log to ~root~.
+The ~logrotate(8)~ manual page explains the configuration options.
+
+The Systemd configuration drop tells ~logrotate~ to use a special
+script for its mail program.  Postfix's ~mail~ work-alike did not take
+the subject as a command line argument as provided by ~logrotate~.
+The replacement =logrotate-mailer= does, and includes it in a
+~Subject~ header prepended to ~logrotate~'s message.
+
+#+CAPTION: =roles_t/abbey-front/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-front/tasks/main.yml
+
+- name: Configure Apache log archival.
+  become: yes
+  lineinfile:
+    path: /etc/logrotate.d/apache2
+    regexp: "{{ item.regexp }}"
+    line: "{{ item.line }}"
+  loop:
+  - { regexp: '^ *daily', line: "\tweekly" }
+  - { regexp: '^ *rotate', line: "\trotate 12" }
+
+- name: Configure Apache log email.
+  become: yes
+  lineinfile:
+    path: /etc/logrotate.d/apache2
+    regexp: "{{ item.regexp }}"
+    line: "{{ item.line }}"
+    insertbefore: " *}"
+    firstmatch: yes
+  loop:
+  - { regexp: "^\tmail ", line: "\tmail webmaster" }
+  - { regexp: "^\tmailfirst", line: "\tmailfirst" }
+
+- name: Configure logrotate.
+  become: yes
+  copy:
+    src: logrotate-mailer.conf
+    dest: /etc/systemd/system/logrotate.service.d/mailer.conf
+  notify: Reload systemd.
+
+- name: Install logrotate mailer.
+  become: yes
+  copy:
+    src: logrotate-mailer
+    dest: /usr/local/sbin/logrotate-mailer
+    mode: u=rwx,g=rx,o=rx
+#+END_SRC
+
+#+CAPTION: =roles_t/abbey-front/handlers/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-front/handlers/main.yml
+
+- name: Reload systemd.
+  become: yes
+  systemd:
+    daemon_reload: yes
+#+END_SRC
+
+Note that the first setting for ~ExecStart~ is intended to clear the
+system's ~ExecStart~ in =/lib/systemd/system/logrotate.service=.  (A
+~oneshot~ service like this can have multiple ~ExecStart~ settings.
+See the description of ~ExecStart~ in the ~systemd.service(5)~ manual
+page.)
+
+#+CAPTION: =roles_t/abbey-front/files/logrotate-mailer.conf=
+#+BEGIN_SRC conf :tangle roles_t/abbey-front/files/logrotate-mailer.conf :mkdirp yes
+[Service]
+ExecStart=
+ExecStart=/usr/sbin/logrotate \
+               --mail /usr/local/sbin/logrotate-mailer \
+               /etc/logrotate.conf
+#+END_SRC
+
+The =/usr/local/sbin/logrotate-mailer= script (below) was originally
+needed because Postfix does not provide an emulation of ~mail(1)~ and
+some translation to ~sendmail(1)~ was required.  Since then the script
+has learned to compute the date-dependent file name, compress the log,
+convert it to base64, and encapsulate it in MIME format, before
+sending it on to ~sendmail~.  Note that there is no encryption (yet).
+This is a low priority because much of the data is available to
+Droplet's ISP's Mom, the NSA/CIA/NWO.
+
+#+CAPTION: =roles_t/abbey-front/files/logrotate-mailer=
+#+BEGIN_SRC sh :tangle roles_t/abbey-front/files/logrotate-mailer
+#!/bin/bash -e
+
+if [ "$#" != 3 -o "$1" != "-s" ]; then
+    echo "usage: $0 -s subject recipient" 1>&2
+    exit 1
+fi
+
+D=`date -d yesterday "+%Y%m%d"`
+if [[ "$2" == *error.log* ]]; then
+    F="$D-error.log.gz"
+else
+    F="$D.log.gz"
+fi
+
+( echo "Subject: $2"
+  echo "Content-Type: multipart/mixed; boundary=\"boundary\""
+  echo "MIME-Version: 1.0"
+  echo ""
+  echo "--boundary"
+  echo "Content-Type: text/plain"
+  echo "Content-Transfer-Encoding: 8bit"
+  echo ""
+  echo "$F"
+  echo "--boundary"
+  echo "Content-Type: application/gzip; name=\"$F\""
+  echo "Content-Disposition: attachment; filename=\"$F\""
+  echo "Content-Transfer-Encoding: base64"
+  echo ""
+  gzip | base64
+  echo ""
+  echo "--boundary--" ) | sendmail "$3"
+#+END_SRC
+
+** Install Let's Encrypt
+
+The abbey uses a Let's Encrypt certificate to authenticate its public
+web site and email services.  Initial installation of a Let's Encrypt
+certificate is a terminal session affair (with prompts and lines
+entered as shown below).
+
+#+BEGIN_EXAMPLE
+$ sudo apt install python3-certbot-apache
+$ sudo certbot --apache -d birchwood-abbey.net
+...
+Enter email address (...) (Enter 'c' to cancel): webmaster@birchwood-a
+bbey.net
+...
+Please read the Terms of Service at
+...
+(A)gree/(C)ancel: A
+...
+Would you be willing to share your email address...
+...
+(Y)es/(N)o: Y
+...
+Deploying Certificate to VirtualHost /etc/apache2/sites-enabled/birchw
+ood-abbey.net.conf
+
+Please choose whether or not to redirect HTTP traffic to HTTPS, removi
+ng HTTP access.
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+1: No redirect - Make no further changes to the webserver configuratio
+n.
+...
+Select the appropriate number [1-2] then [enter] (press 'c' to cancel)
+: 1
+
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+Congratulations! You have successfully enabled https://birchwood-abbey
+.net
+
+You should test your configuration at:
+https://www.ssllabs.com/ssltest/analyze.html?d=birchwood-abbey.net
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
+
+IMPORTANT NOTES:
+ - Your account credentials have been saved in your Certbot
+   configuration directory at /etc/letsencrypt. You should make a
+   secure backup of this folder now. This configuration directory will
+   also contain certificates and private keys obtained by Certbot so
+   making regular backups of this folder is ideal.
+...
+ - Congratulations! Your certificate and chain have been saved at:
+   /etc/letsencrypt/live/birchwood-abbey.net/fullchain.pem
+   Your key file has been saved at:
+   /etc/letsencrypt/live/birchwood-abbey.net/privkey.pem
+   Your cert will expire on 2019-01-13. To obtain a new or tweaked
+   version of this certificate in the future, simply run certbot again
+   with the "certonly" option. To non-interactively renew *all* of
+   your certificates, run "certbot renew"
+#+END_EXAMPLE
+
+When the =/etc/letsencrypt/= directory is restored from a backup copy,
+and the following tasks performed, the web server will be prepared to
+do ACME (the certificate protocol) when next Let's Encrypt calls
+(quarterly).  The following tasks ensure the ~python3-cerbot-apache~
+package is installed and its =live/= subdirectory is world readable.
+
+#+CAPTION: =roles_t/abbey-front/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-front/tasks/main.yml
+
+- name: Install Certbot for Apache.
+  become: yes
+  apt: pkg=python3-certbot-apache
+
+- name: Ensure Let's Encrypt certificate is readable.
+  become: yes
+  file:
+    mode: u=rwx,g=rx,o=rx
+    path: /etc/letsencrypt/live
+#+END_SRC
+
+Front's Dovecot (and Postfix) certificate and key are in separate
+files despite their warning about a race condition (when updating the
+pair of files) mainly because that is how they are provided (and
+updated) by Let's Encrypt, but also because Let's Encrypt's symbolic
+links keep the window for a mismatch extremely small.
+
+With the institutional configuration, Postfix, Dovecot and Apache
+servers get their certificate&key from =/etc/server.crt&.key=.  The
+institutional roles check that they exist, but will not create them.
+In this abbey specific role, =/etc/server.crt&key= are ours to frob.
+The following tasks ensure they are symbolic links to
+=/etc/letsencrypt/live/birchwood-abbey.net/fullchain&privkey.pem=.  If
+=/etc/letsencrypt/= was restored from a backup, the servers should be
+restarted manually.
+
+#+CAPTION: =roles_t/abbey-front/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-front/tasks/main.yml
+
+- name: Use Let's Encrypt certificate&key.
+  file:
+    state: link
+    src: "{{ item.target }}"
+    path: "{{ item.link }}"
+    force: yes
+  loop:
+  - target: /etc/letsencrypt/live/birchwood-abbey.net/fullchain.pem
+    link: /etc/server.crt
+  - target: /etc/letsencrypt/live/birchwood-abbey.net/privkey.pem
+    link: /etc/server.key
+#+END_SRC
+
+** Rotate Let's Encrypt Log
+
+The following task arranges to rotate Certbot's logs files.
+
+#+CAPTION: =roles_t/abbey-front/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-front/tasks/main.yml
+
+- name: Install Certbot logrotate configuration.
+  become: yes
+  copy:
+    src: certbot_logrotate
+    dest: /etc/logrotate.d/certbot
+    mode: u=rw,g=r,o=r
+#+END_SRC
+
+#+CAPTION: =roles_t/abbey-front/files/certbot_logrotate=
+#+BEGIN_SRC conf :tangle roles_t/abbey-front/files/certbot_logrotate
+/var/log/letsencrypt/*.log {
+    rotate 12
+    weekly
+    compress
+    missingok
+}
+#+END_SRC
+
+** Archive Let's Encrypt Data
+
+A backup copy of Let's Encrypt's data (=/etc/letsencrypt/=) is sent to
+~root@core~ in S/MIME encrypted email every time it changes.  Changes
+are detected by keeping a copy in =/etc/letsencrypt~/= for comparison.
+
+#+CAPTION: =roles_t/abbey-front/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-front/tasks/main.yml
+
+- name: Install Let's Encrypt archive script.
+  become: yes
+  copy:
+    src: cron.daily_letsencrypt
+    dest: /etc/cron.daily/letsencrypt
+    mode: u=rwx,g=rx,o=rx
+#+END_SRC
+
+#+CAPTION: =roles_t/abbey-front/files/cron.daily_letsencrypt=
+#+BEGIN_SRC sh :tangle roles_t/abbey-front/files/cron.daily_letsencrypt
+#!/bin/bash -e
+
+cd /etc/
+
+[ -d letsencrypt~ ] \
+&& diff -rq letsencrypt/ letsencrypt~/ \
+&& exit 0
+
+( echo "Subject: New /etc/letsencrypt/ on Droplet."
+  echo ""
+  tar czf - letsencrypt/ \
+  | gpg --encrypt --armor \
+       --trust-model always --recipient root@core ) \
+| sendmail root \
+|| exit $?
+
+rm -rf letsencrypt~
+cp -a letsencrypt letsencrypt~
+#+END_SRC
+
+The message is encrypted with ~root@core~'s public key, which is
+imported into ~root@front~'s GnuPG key file.
+
+#+CAPTION: =roles_t/abbey-front/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-front/tasks/main.yml
+
+- name: Copy root@core's public key.
+  become: yes
+  copy:
+    src: ../Secret/root-pub.pem
+    dest: /root/.gnupg-root-pub.pem
+    mode: u=r,g=r,o=r
+  notify: Import root@core's public key.
+#+END_SRC
+
+#+CAPTION: =roles_t/abbey-front/handlers/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-front/handlers/main.yml
+
+- name: Import root@core's public key.
+  become: yes
+  command: gpg --import ~/.gnupg-root-pub.pem
+#+END_SRC
+
+
+* The Abbey Core Role
+
+Birchwood Abbey's core is a mini-PC (System76 Meerkat) configured as A
+Small Institute Core.  Thus it is already serving a local web site
+with Apache2, hosting a private cloud with Nextcloud, handling email
+with Postfix and Dovecot, and providing essential localnet services:
+NTP, DNS and DHCP.
+
+** Install Additional Packages
+
+The scripts that maintain the abbey's web site and run the Weather
+project use a number of additional software packages.  The
+=/WWW/live/Private/make-top-index= script uses ~HTML::TreeBuilder~ in
+the ~libhtml-tree-perl~ package.  The house task list uses JQuery.
+Weather scripts use ~mit-scheme~ and ~gnuplot~ (in pseudonymous
+packages).
+
+#+CAPTION: =roles_t/abbey-core/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-core/tasks/main.yml :mkdirp yes
+---
+- name: Install additional packages.
+  apt:
+    pkg: [ libhtml-tree-perl, libjs-jquery, mit-scheme, gnuplot ]
+#+END_SRC
+
+** Configure Private Email Aliases
+
+The abbey uses several additional email aliases.  These are the campus
+mailboxes ~@*.birchwood-abbey.net~.  The institute already includes
+some standard system aliases, as well as mailboxes for accounts
+running services: ~www-data~ and ~monkey~.  The institute funnels
+these to ~root~ and forwards ~root~ to ~sysadm~ (as on Front).  The
+abbey takes it from there, forwarding ~sysadm~ to a real person and
+including mailboxes for all accounts running services on any campus
+machine.  (They should all be relaying to ~smtp.birchwood-abbey.net~
+which delivers any ~.birchwood-abbey.net~ email,
+e.g. ~mythtv@mythtv.birchwood-abbey.net~, locally.)
+
+#+CAPTION: =roles_t/abbey-core/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-core/tasks/main.yml :noweb yes
+
+- name: Install abbey email aliases.
+  become: yes
+  blockinfile:
+    block: |
+        sysadm:                matt
+        house:         sysadm
+        mythtv:                sysadm
+        scanner:       sysadm
+    dest: /etc/aliases
+    marker: "# {mark} ABBEY MANAGED BLOCK"
+  notify: New aliases.
+#+END_SRC
+
+#+CAPTION: =roles_t/abbey-core/handlers/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-core/handlers/main.yml :mkdirp yes
+---
+- name: New aliases.
+  become: yes
+  command: newaliases
+#+END_SRC
+
+** Configure Git Daemon on Core
+
+These tasks are identical to those executed on Front, for similar Git
+services on Front and Core.  See [[Configure Git Daemon on Front]] and
+[[*Configure Gitweb on Front][Configure Gitweb on Front]] for more information.
+
+#+CAPTION: =roles_t/abbey-core/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-core/tasks/main.yml :noweb yes
+
+<<git-tasks>>
+#+END_SRC
+
+#+CAPTION: =roles_t/abbey-core/handlers/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-core/handlers/main.yml :noweb yes
+
+<<git-handlers>>
+#+END_SRC
+
+** Configure Apache on Core
+
+The Apache2 configuration on Core specifies three web sites (live,
+test, and campus).  The live and test sites must operate just like the
+site on Front.  Their configurations include the same [[apache-abbey][~apache-abbey~]],
+[[apache-photos][~apache-photos~]], [[apache-gitweb][~apache-gitweb~]], and [[apache-cgit][~apache-cgit~]] used on Front.
+
+#+CAPTION: =roles_t/abbey-core/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-core/tasks/main.yml :noweb yes
+
+- name: Configure live website.
+  become: yes
+  vars:
+    docroot: /WWW/live
+  copy:
+    content: |
+        <<apache-abbey>>
+        <<apache-photos>>
+        <<apache-gitweb>>
+        <<apache-cgit>>
+    dest: /etc/apache2/sites-available/live-vhost.conf
+    mode: u=rw,g=r,o=r
+  notify: Restart Apache2.
+
+- name: Configure test website.
+  become: yes
+  vars:
+    docroot: /WWW/test
+  copy:
+    content: |
+        <<apache-abbey>>
+        <<apache-photos>>
+        <<apache-gitweb>>
+        <<apache-cgit>>
+    dest: /etc/apache2/sites-available/test-vhost.conf
+    mode: u=rw,g=r,o=r
+  notify: Restart Apache2.
+
+<<apache-gitweb-tasks>>
+<<apache-cgit-tasks>>
+#+END_SRC
+
+#+CAPTION: =roles_t/abbey-core/handlers/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-core/handlers/main.yml :noweb yes
+
+<<apache-gitweb-handlers>>
+#+END_SRC
+
+** Configure Documentation URLs
+
+The institute serves its =/usr/share/doc/= on the house (campus) web
+site.  This is a debugging convenience, making some HTML documentation
+more accessible, especially the documentation of software installed on
+Core and not on typical desktop clients.  Also included: the Apache2
+directives that enable user Git publishing with Gitweb and CGit
+(defined [[apache-gitweb][here]] and [[apache-cgit][here]] respectively).
+
+#+CAPTION: =roles_t/abbey-core/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-core/tasks/main.yml :noweb yes
+
+- name: Configure house website.
+  become: yes
+  copy:
+    content: |
+      Alias /doc /usr/share/doc
+      <Directory /usr/share/doc/>
+          Options Indexes
+      </Directory>
+      <<apache-gitweb>>
+      <<apache-cgit>>
+    dest: /etc/apache2/sites-available/www-vhost.conf
+    mode: u=rw,g=r,o=r
+  notify: Restart Apache2.
+#+END_SRC
+
+** Install Apt Cacher
+
+The abbey uses the Apt-Cacher:TNG package cache on Core.  The
+~apt-cacher~ domain name is defined in =private/db.domain=.
+
+#+CAPTION: =roles_t/abbey-core/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-core/tasks/main.yml
+
+- name: Install Apt-Cacher:TNG.
+  become: yes
+  apt: pkg=apt-cacher-ng
+#+END_SRC
+
+** Use Cloister Apt Cache
+
+Core itself will benefit from using the package cache.
+
+#+CAPTION: =roles_t/abbey-core/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-core/tasks/main.yml
+
+- name: Use the local Apt package cache.
+  become: yes
+  copy:
+    content: |
+     Acquire::http::Proxy "http://apt-cacher.{{ domain_priv }}.:3142";
+    dest: /etc/apt/apt.conf.d/01proxy
+    mode: u=rw,g=r,o=r
+#+END_SRC
+
+** Configure NAGIOS
+
+A small institute uses ~nagios4~ to monitor the health of its network,
+with an initial smattering of monitors adopted from the Debian
+~monitoring-plugins~ package.  Thus a NAGIOS4 server on the abbey's
+Core monitors core network services, and uses ~nagios-nrpe-server~ to
+monitor Gate.  The abbey adds several more monitors, installing
+additional configuration files in =/etc/nagios4/conf.d/=, and another
+customized ~check_sensors~ plugin (~abbey_pisensors~) in
+=/usr/local/sbin/= on the Raspberry Pis.
+
+** Monitoring The Home Disk
+
+The abbey adds monitoring of the space remaining on the volume at
+=/home/= on Core.  (The small institute only monitors the space
+remaining on roots.)
+
+#+CAPTION: =roles_t/abbey-core/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-core/tasks/main.yml
+
+- name: Configure NAGIOS monitoring for Core /home/.
+  become: yes
+  copy:
+    content: |
+      define service {
+          use                     local-service
+          host_name               core
+          service_description     Home Partition
+          check_command           check_local_disk!20%!10%!/home
+      }
+    dest: /etc/nagios4/conf.d/abbey.cfg
+  notify: Reload NAGIOS4.
+#+END_SRC
+
+#+CAPTION: =roles_t/abbey-core/handlers/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-core/handlers/main.yml
+
+- name: Reload NAGIOS4.
+  become: yes
+  systemd:
+    service: nagios4
+    state: reloaded
+#+END_SRC
+
+** Custom NAGIOS Monitor ~abbey_pisensors~
+
+The ~check_sensors~ plugin is included in the package
+~monitoring-plugins-basic~, but it does not report any readings.  The
+small institute substitutes a Custom NAGIOS Monitor ~inst_sensors~
+that reports core CPU temperatures, but the ~sensors~ command on a
+Raspberry Pi does not reveal core CPU temperatures, so the abbey
+includes yet another version, ~abbey_pisensors~, that reports any
+recognizable temperature in the ~sensors~ output.
+
+#+CAPTION: =roles_t/abbey-core/files/abbey_pisensors=
+#+BEGIN_SRC sh :tangle roles_t/abbey-core/files/abbey_pisensors
+#!/bin/sh
+
+PATH="/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin"
+export PATH
+PROGNAME=`basename $0`
+REVISION="2.3.1"
+
+. /usr/lib/nagios/plugins/utils.sh
+
+print_usage() {
+       echo "Usage: $PROGNAME" [--ignore-fault]
+}
+
+print_help() {
+       print_revision $PROGNAME $REVISION
+       echo ""
+       print_usage
+       echo ""
+       echo "This plugin checks hardware status using the lm_sensors package."
+       echo ""
+       support
+       exit $STATE_OK
+}
+
+brief_data() {
+    echo "$1" | sed -n -E -e '
+  /^temp[0-9]+: +[-+][0-9.]+°C/ { s/^temp[0-9]+: +([-+][0-9.]+)°C.*/ \1/; H }
+  $ { x; s/\n//g; p }'
+}
+
+case "$1" in
+       --help)
+               print_help
+               exit $STATE_OK
+               ;;
+       -h)
+               print_help
+               exit $STATE_OK
+               ;;
+       --version)
+               print_revision $PROGNAME $REVISION
+               exit $STATE_OK
+               ;;
+       -V)
+               print_revision $PROGNAME $REVISION
+               exit $STATE_OK
+               ;;
+       *)
+               sensordata=`sensors 2>&1`
+               status=$?
+               if test ${status} -eq 127; then
+                       text="SENSORS UNKNOWN - command not found"
+                       text="$text (did you install lmsensors?)"
+                       exit=$STATE_UNKNOWN
+               elif test ${status} -ne 0; then
+                       text="WARNING - sensors returned state $status"
+                       exit=$STATE_WARNING
+               elif echo ${sensordata} | egrep ALARM > /dev/null; then
+                       text="SENSOR CRITICAL -`brief_data "${sensordata}"`"
+                       exit=$STATE_CRITICAL
+               elif echo ${sensordata} | egrep FAULT > /dev/null \
+                   && test "$1" != "-i" -a "$1" != "--ignore-fault"; then
+                       text="SENSOR UNKNOWN - Sensor reported fault"
+                       exit=$STATE_UNKNOWN
+               else
+                       text="SENSORS OK -`brief_data "${sensordata}"`"
+                       exit=$STATE_OK
+               fi
+
+               echo "$text"
+               if test "$1" = "-v" -o "$1" = "--verbose"; then
+                       echo ${sensordata}
+               fi
+               exit $exit
+               ;;
+esac
+#+END_SRC
+
+** Monitoring The Cloister
+
+The abbey adds monitoring for more servers: Kamino, Kessel and
+Devaron.  They are ~abbey-cloister~ servers, so they are configured as
+small institute ~campus~ servers, like Gate, with an NRPE (a NAGIOS
+Remote Plugin Executor) server and an ~inst_sensors~ command.
+
+The configurations for the servers are very similar to Gate's, but are
+idiosyncratically in flux.  In particular, Kamino does not irritate
+~check_total_procs~, yet Kessel does.  Both are Pop!_OS 22.04, but
+Kessel is a wireless host while Kamino is wired.  Devaron, the
+Raspberry Pi OS (ARM64) machine, uses the ~abbey_pisensors~ monitor.
+
+#+CAPTION: =roles_t/abbey-core/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-core/tasks/main.yml
+
+- name: Configure cloister NAGIOS monitoring.
+  become: yes
+  template:
+    src: nagios-{{ item }}.cfg
+    dest: /etc/nagios4/conf.d/{{ item }}.cfg
+  loop: [ devaron, kamino, kessel ]
+  notify: Reload NAGIOS4.
+#+END_SRC
+
+#+CAPTION: =roles_t/abbey-core/templates/nagios-devaron.cfg=
+#+BEGIN_SRC conf :tangle roles_t/abbey-core/templates/nagios-devaron.cfg :mkdirp yes
+define host {
+    use                     linux-server
+    host_name               devaron
+    address                 {{ devaron_addr }}
+}
+
+define service {
+    use                     generic-service
+    host_name               devaron
+    service_description     Root Partition
+    check_command           check_nrpe!inst_root
+}
+
+# define service {
+#     use                     generic-service
+#     host_name               devaron
+#     service_description     Current Load
+#     check_command           check_nrpe!check_load
+# }
+
+define service {
+    use                     generic-service
+    host_name               devaron
+    service_description     Zombie Processes
+    check_command           check_nrpe!check_zombie_procs
+}
+
+# define service {
+#     use                     generic-service
+#     host_name               devaron
+#     service_description     Total Processes
+#     check_command           check_nrpe!check_total_procs
+# }
+
+define service {
+    use                     generic-service
+    host_name               devaron
+    service_description     Swap Usage
+    check_command           check_nrpe!inst_swap
+}
+
+define service {
+    use                     generic-service
+    host_name               devaron
+    service_description     Temperature Sensors
+    check_command           check_nrpe!abbey_pisensors
+}
+#+END_SRC
+
+#+CAPTION: =roles_t/abbey-core/templates/nagios-kamino.cfg=
+#+BEGIN_SRC conf :tangle roles_t/abbey-core/templates/nagios-kamino.cfg
+define host {
+    use                     linux-server
+    host_name               kamino
+    address                 {{ kamino_addr }}
+}
+
+define service {
+    use                     generic-service
+    host_name               kamino
+    service_description     Root Partition
+    check_command           check_nrpe!inst_root
+}
+
+define service {
+    use                     generic-service
+    host_name               kamino
+    service_description     Current Load
+    check_command           check_nrpe!check_load
+}
+
+define service {
+    use                     generic-service
+    host_name               kamino
+    service_description     Zombie Processes
+    check_command           check_nrpe!check_zombie_procs
+}
+
+# define service {
+#     use                     generic-service
+#     host_name               kamino
+#     service_description     Total Processes
+#     check_command           check_nrpe!check_total_procs
+# }
+
+define service {
+    use                     generic-service
+    host_name               kamino
+    service_description     Swap Usage
+    check_command           check_nrpe!inst_swap
+}
+
+define service {
+    use                     generic-service
+    host_name               kamino
+    service_description     Temperature Sensors
+    check_command           check_nrpe!inst_sensors
+}
+#+END_SRC
+
+#+CAPTION: =roles_t/abbey-core/templates/nagios-kessel.cfg=
+#+BEGIN_SRC conf :tangle roles_t/abbey-core/templates/nagios-kessel.cfg
+define host {
+    use                     linux-server
+    host_name               kessel
+    address                 {{ kessel_addr }}
+}
+
+define service {
+    use                     generic-service
+    host_name               kessel
+    service_description     Root Partition
+    check_command           check_nrpe!inst_root
+}
+
+# define service {
+#     use                     generic-service
+#     host_name               kessel
+#     service_description     Current Load
+#     check_command           check_nrpe!check_load
+# }
+
+define service {
+    use                     generic-service
+    host_name               kessel
+    service_description     Zombie Processes
+    check_command           check_nrpe!check_zombie_procs
+}
+
+# define service {
+#     use                     generic-service
+#     host_name               kessel
+#     service_description     Total Processes
+#     check_command           check_nrpe!check_total_procs
+# }
+
+define service {
+    use                     generic-service
+    host_name               kessel
+    service_description     Swap Usage
+    check_command           check_nrpe!inst_swap
+}
+
+define service {
+    use                     generic-service
+    host_name               kessel
+    service_description     Temperature Sensors
+    check_command           check_nrpe!inst_sensors
+}
+#+END_SRC
+
+** Install Analog
+
+The abbey's public web site's access and error logs are emailed
+regularly to ~webmaster~, who saves them in =/Logs/apache2-public/=
+and runs ~analog~ to generate =/WWW/campus/analog.html=, available to
+the campus as ~http://www/analog.html~.
+
+#+CAPTION: =roles_t/abbey-core/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-core/tasks/main.yml
+
+- name: Install Analog.
+  become: yes
+  apt: pkg=analog
+
+- name: Configure Analog (removing old /var/log/apache/ LOGFILEs).
+  become: yes
+  lineinfile:
+    path: /etc/analog.cfg
+    regexp: '^LOGFILE /var/log/apache/'
+    state: absent
+
+- name: Configure Analog (adding new configuration lines).
+  become: yes
+  lineinfile:
+    path: /etc/analog.cfg
+    line: "{{ item }}"
+    insertafter: EOF
+  loop:
+  - "LOGFILE /Logs/apache2-public/*-access.log.gz"
+  - "ALLCHART OFF"
+  - "DNS WRITE"
+  - "HOSTNAME \"{{ full_name }}\""
+  - "OUTFILE /WWW/campus/analog.html"
+
+- name: Create /Logs/.
+  become: yes
+  file:
+    path: /Logs
+    state: directory
+    mode: u=rwx,g=rx,o=rx
+
+- name: Create /Logs/apache2-public/.
+  become: yes
+  file:
+    path: /Logs/apache2-public
+    state: directory
+    owner: monkey
+    group: staff
+    mode: u=rwx,g=srwx,o=rx
+#+END_SRC
+
+** Add Monkey to Web Server Group
+
+Monkey needs to be in ~www-data~ so that it can run
+=/WWW/live/Photos/Private/cronjob= to publish photos from multiple
+user cloud accounts, found in files owned by ~www-data~, files like
+=InstantUpload/Camera/2021/01/IMG_20210115_092838.jpg= in
+=/var/www/nextcloud/data/$USER/files/=.
+
+#+CAPTION: =roles_t/abbey-core/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-core/tasks/main.yml
+
+- name: Add Monkey to Nextcloud group.
+  become: yes
+  user:
+    name: monkey
+    append: yes
+    groups: www-data
+#+END_SRC
+
+** Install netpbm For Photo Processing
+
+Monkey's photo processing scripts use ~netpbm~ commands like
+~jpegtopnm~.
+
+#+CAPTION: =roles_t/abbey-core/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-core/tasks/main.yml
+
+- name: Install netpbm.
+  become: yes
+  apt: pkg=netpbm
+#+END_SRC
+
+** Configure Weather Updates
+
+Monkey on Core runs =/WWW/campus/Weather/Private/cronjob= every 5
+minutes and =cronjob-midnight= at midnight.
+
+#+CAPTION: =roles_t/abbey-core/tasks/main.yml=
+#+BEGIN_SRC :tangle roles_t/abbey-core/tasks/main.yml
+
+- name: Create Monkey's weather job.
+  become: yes
+  cron:
+    name: weather
+    hour: "*"
+    minute: "*/5"
+    job: "[ -d /WWW/house ] && /WWW/house/Weather/Private/cronjob"
+    user: monkey
+#+END_SRC
+
+
+* The Abbey Gate Role
+
+Birchwood Abbey's gate is a $110 ÂµPC configured as A Small Institute
+Gate, thus providing a campus VPN on a campus Wi-Fi access point.  It
+routes network traffic from its ~wifi~ and ~lan~ interfaces to its
+~isp~ interface (and back) with NAT.  That is all the abbey requires
+of its gate, so there is no additional Ansible configuration in this
+chapter (yet).
+
+** The Abbey Gate's Network Interfaces
+
+The abbey gate's ~lan~ interface is the PC's built-in Ethernet
+interface, connected to the cloister Ethernet, a Gigabit Ethernet
+switch.  Its ~wifi~ interface is a USB3.0 Ethernet adapter connected
+with a cross-over cable to the WAN interface of a Think Penguin
+TPE-R1300 (and at one time a Linksys WRT1900AC).  The ~isp~ interface
+is another USB3.0 Ethernet adapter connected with a cross-over cable
+to the Ethernet interface of a "cable modem" (a Starlink terminal).
+
+The MAC address of each interface is set in =private/vars.yml=, the
+values of the ~gate_lan_mac~, ~gate_wifi_mac~ and ~gate_isp_mac~
+variables.
+
+** The Abbey's Starlink Configuration
+
+The abbey connects to Starlink via Ethernet, and disables Starlink's
+Wi-Fi access point.  An Ethernet adapter add-on (ordered separately)
+was installed on the Starlink cable, and a second USB-Ethernet dongle
+on Gate.  The adapters were then connected with a cross-over cable.
+
+The abbey could have avoided buying a separate campus Wi-Fi access
+point, and used Starlink's Wi-Fi instead, with or without its add-on
+Ethernet interface.  Instead, the abbey invested in a 2.4GHz-only
+Think Penguin access point, and connected it to a third Ethernet
+interface on Gate.
+
+This was preferred for a number of reasons.  Using the add-on Ethernet
+interface allowed Starlink's Wi-Fi to be disabled, reducing the Wi-Fi
+clutter in the campground ether.  Starlink is not always available.
+(It does not work well under trees.)  A dedicated campus Wi-Fi is
+always available.  The password to the campus Wi-Fi is long and
+complex and has been laboriously entered into several household IoT
+devices.  The Think Penguin access point is transparent, trustworthy
+hardware that has earned a Respects Your Freedom certification (see
+[[https://ryf.fsf.org/]]).  And most importantly, a campus Wi-Fi keeps
+campus network traffic out of the hands of the abbey's ISPs.
+
+** Alternate ISPs
+
+The abbey used to use a cell phone on a USB tether to get Internet
+service.  At that time, Gate's =/etc/netplan/60-isp.yaml= file was the
+following.
+
+#+BEGIN_SRC conf
+network:
+  ethernets:
+    tether:
+      match:
+        name: usb0
+      set-name: isp
+      dhcp4: true
+      dhcp4-overrides:
+        use-dns: false
+#+END_SRC
+
+The abbey has occasionally used a campground Wi-Fi for Internet
+service, using a =60-isp.yaml= file similar to the lines below.
+
+#+BEGIN_SRC conf
+network:
+  wifis:
+    tether:
+      match:
+        name: wlan0
+      set-name: isp
+      dhcp4: true
+      dhcp4-overrides:
+        use-dns: false
+      access-points:
+        "AP with password":
+          password: "password"
+        "AP with no password": {}
+#+END_SRC
+
+
+* The Abbey Cloister Role
+
+Birchwood Abbey's cloister is a small institute campus.  The ~campus~
+role configures all campus machines to trust the institute's CA, sync
+with the campus time server, and forward email to Core.  The
+~cloister~ role additionally configures cloistered machines to use the
+cloister Apt cache, respond to Core's NAGIOS network monitor, and to
+install Emacs.  There are also a few OS specific tasks, namely
+configuration required on Raspberry Pi OS machines.
+
+Wireless clients are issued keys for the cloister VPN by the ~./abbey
+client~ command.  This command includes the institutional process
+described in [[file:Institute/README.org::*The Client Command][The Client Command]].  The process handles three types of
+clients: Android, Debian and Campus.  The last type never roams, and
+is not associated with a member of the small institute.
+
+** Use Cloister Apt Cache
+
+The Apt-Cacher:TNG program does not work well on the frontier, so is
+not a common part of a small institute.  But it is helpful even for a
+cloister with less than a dozen hosts (especially to a homogeneous
+cloister using many of the same packages), so it is tolerable to the
+abbey's monks.  Monks are patient enough to re-run failed scans
+repeatedly until few or no incomplete or damaged files are found.
+Depending on the quality of the Internet connection, this may take a
+while.
+
+#+CAPTION: =roles_t/abbey-cloister/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-cloister/tasks/main.yml :mkdirp yes
+---
+- name: Use the local Apt package cache.
+  become: yes
+  copy:
+    content: |
+     Acquire::http::Proxy "http://apt-cacher.{{ domain_priv }}.:3142";
+    dest: /etc/apt/apt.conf.d/01proxy
+    mode: u=rw,g=r,o=r
+#+END_SRC
+
+** Configure Cloister NRPE
+
+Each cloistered host is a small institute campus host and thus is
+already running an NRPE server (a NAGIOS Remote Plugin Executor
+server) with a custom ~inst_sensors~ monitor (described in [[file:Institute/README.org::*Configure NRPE][Configure
+NRPE]] of [[file:Institute/README.org][A Small Institute]]).  The abbey adds one complication: yet
+another ~check_sensors~ variant, ~abbey_pisensors~, installed on
+Raspberry Pis (architecture ~aarch64~) only.
+
+#+CAPTION: =roles_t/abbey-cloister/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-cloister/tasks/main.yml
+
+- name: Install abbey_pisensors NAGIOS plugin.
+  become: yes
+  copy:
+    src: ../abbey-core/files/abbey_pisensors
+    dest: /usr/local/sbin/abbey_pisensors
+    mode: u=rwx,g=rx,o=rx
+  when: ansible_architecture == 'aarch64'
+
+- name: Configure NAGIOS command.
+  become: yes
+  copy:
+    content: |
+      command[abbey_pisensors]=/usr/local/sbin/abbey_pisensors
+    dest: /etc/nagios/nrpe.d/abbey.cfg
+  when: ansible_architecture == 'aarch64'
+  notify: Reload NRPE server.
+#+END_SRC
+
+#+CAPTION: =roles_t/abbey-cloister/handlers/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-cloister/handlers/main.yml :mkdirp yes
+
+- name: Reload NRPE server.
+  become: yes
+  systemd:
+    service: nagios-nrpe-server
+    state: reloaded
+#+END_SRC
+
+** Install Emacs
+
+The monks of the abbey are masters of the staff and Emacs.
+
+#+CAPTION: =roles_t/abbey-cloister/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-cloister/tasks/main.yml
+
+- name: Install monastic software.
+  become: yes
+  apt: pkg=emacs
+#+END_SRC
+
+
+* The Abbey Weather Role
+
+Birchwood Abbey's weather hosts use the 1-Wire server (from the
+~owserver~ package) and a 1-Wire USB adapter.  They use an
+unprivileged account (~monkey~) to run a SystemD service named
+~weatherd~ (aka "the daemon").  The daemon is a Perl script that runs
+~owread~ and logs the new measurements once per minute.
+
+The log files are collected by Monkey on Core (via ~rsync~), then
+processed and published in campus web pages by The Weather Project's
+code (old, using ~gnuplot(1)~, and so... unpublished).
+
+** The Abbey Weather Hardware
+
+The abbey currently has one weather host, Gate, and a couple 1-Wire
+sensor modules.  The modules measure inside and outside temperature
+and humidity.  Their desired locations are 7-8m from the core servers
+so they are plugged into a custom Y cable, with the inside sensor
+cable spliced into the middle of the outside/main cable.  The proximal
+end's RJ11 plugs into a 1-Wire USB adapter (a DS9490R) plugged into
+Gate.  The outside end goes out the window with the Starlink cable.
+
+** The Abbey Weather Host Setup
+
+The Ansible code in the ~abbey-weather~ role assumes it is working
+with a cloistered host (as described in [[*Cloistering][Cloistering]]) and proceeds in
+two phases.  The first installs the ~ow-server~ package and configures
+it to use a DS9490 (USB adapter) rather than a debugging fake.  After
+the first ~./abbey config new~, the new weather host seems to need a
+reboot before the 1-Wire bus becomes visible via ~owdir~.
+
+After a reboot ~owdir~ should list one or more type 26 device IDs.
+Listing them (e.g. running ~owdir /26.nnnnnnnn~ or ~owdir
+/26.nnnnnnnn/HIH~) should reveal "files" named =temperature= and
+=HIH/humidity=.  These pseudo-file paths are used in the daemon script
+below.  A test session is shown below.
+
+#+BEGIN_EXAMPLE
+monkey@new$ owdir
+...
+    /26.2153B6000000/
+...
+monkey@new$ owdir /26.2153B6000000
+...
+    /26.2153B6000000/temperature
+...
+monkey@new$ owread /26.2153B6000000/temperature; echo
+26.125
+monkey@new$ 
+#+END_EXAMPLE
+
+The second phase of weather host configuration waits for the host-
+specific weather daemon script to appear in the role's =files/=.
+
+** The Abbey Weather Daemons
+
+Different weather hosts, with different 1-Wire devices, need different
+daemon scripts, to call ~owread~ with different paths (containing the
+IDs of each host's devices).  At the moment there is just the
+one weather host, ~anoat~.
+
+#+CAPTION: =roles_t/abbey-weather/files/daemon-anoat=
+#+BEGIN_SRC perl :tangle roles_t/abbey-weather/files/daemon-anoat :mkdirp yes
+#!/usr/bin/perl -w
+# -*- CPerl -*-
+#
+# Weather/daemon
+#
+# Fetches data from the local owserver once per minute.  Appends to
+# Log/{In,Out}side/YEAR/MONTH/DAY.txt.
+
+use strict;
+use IO::File;
+use Date::Format;
+
+my $ILOG;
+my $OLOG;
+my $ymd = "";
+sub mymkdir ($);
+sub reopen_logs ()
+{
+  my $time = time;
+  my $datime = time2str ("%Y-%m-%d %H:%M:%S", $time, "UTC");
+  my ($year, $month, $day) = $datime =~ /^(\d{4})-(\d\d)-(\d\d) /;
+  my $new_ymd = "$year/$month/$day";
+  return if $new_ymd eq $ymd;
+  close $ILOG if defined $ILOG;
+  close $OLOG if defined $OLOG;
+  umask 07;
+  mymkdir "Inside/$year/$month";
+  mymkdir "Outside/$year/$month";
+  umask 027;
+  my $filename = "Inside/$new_ymd.txt";
+  $ILOG = new IO::File;
+  open $ILOG, ">>$filename" or die "Could not open $filename: $!\n";
+  $filename = "Outside/$new_ymd.txt";
+  $OLOG = new IO::File;
+  open $OLOG, ">>$filename" or die "Could not open $filename: $!\n";
+  $ymd = $new_ymd;
+}
+
+sub logit ($$$);
+sub main () {
+  die "usage: $0\n" if @ARGV != 0;
+  $0 = "weatherd";
+  chdir "/home/monkey/Weather/Log" or die;
+  umask 027;
+  my $start = time;
+  {
+    my $secs = 60 - $start % 60;
+    $start += $secs;
+    sleep ($secs);
+  }
+  while (1) {
+    reopen_logs;
+    logit $OLOG, "T", "/26.2153B6000000/temperature";
+    logit $OLOG, "H", "/26.2153B6000000/HIH4000/humidity";
+    logit $ILOG, "T", "/26.8859B6000000/temperature";
+    logit $ILOG, "H", "/26.8859B6000000/HIH4000/humidity";
+    $start += 60;
+    my $now = time;
+    while ($start < $now) { $start += 60; }
+    my $secs = $start - $now;
+    sleep  ($secs);
+  }
+}
+
+sub logit ($$$)
+{
+  my ($log, $name, $query) = @_;
+
+  my $tries = 0;
+  while ($tries < 3) {
+    my $time = time;
+    my $datime = time2str ("%Y-%m-%d %H:%M:%S", $time, "UTC");
+    $tries += 1;
+    my @lines = `/usr/bin/owread $query`;
+    chomp @lines;
+    my $status = $?;
+    my $sig = $status & 127;
+    $status >>= 8;
+    if ($status != 0) {
+      my $L = join "\\n", @lines;
+      print $log "$datime\t$name\terror: status $status: $L\n";
+      $log->flush;
+    } elsif (@lines != 1) {
+      my $L = join "\\n", @lines;
+      print $log "$datime\t$name\terror: multiple lines: $L\n";
+      $log->flush;
+    } elsif ($lines[0] !~ /^ *(-?\d+(\.\d+)?)$/) {
+      my $L = $lines[0];
+      print $log "$datime\t$name\terror: bogus line: $L\n";
+      $log->flush;
+    } else {
+      my $datum = $1;
+      print $log "$datime\t$name\t$datum\n";
+      $log->flush;
+      return;
+    }
+  }
+}
+
+sub mymkdir ($)
+{
+  my ($dirpath) = @_;
+
+  my @path_names = split /\//, $dirpath;
+  my $path;
+  if (!$path_names[0]) {
+    $path = "/";
+    shift @path_names;
+  } else {
+    $path = ".";
+  }
+  my @created;
+  while (@path_names) {
+    $path .= "/" . shift @path_names;
+    if (! -d $path) {
+      if (-e $path) {
+       die "mkdir $dirpath: already exists; not a directory!\n";
+      }
+      if (! mkdir $path) {
+       die "mkdir $path: $!\n";
+      } else {
+       chmod 02775, $path;
+       push @created, $path;
+      }
+    }
+  }
+  return @created;
+}
+
+main;
+#+END_SRC
+
+The above Perl script uses the ~Date::Format~ module, which is
+installed by the following task.
+
+#+CAPTION: =roles_t/abbey-weather/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-weather/tasks/main.yml :mkdirp yes
+---
+- name: Install weather daemon packages.
+  become: yes
+  apt: pkg=libtimedate-perl
+#+END_SRC
+
+** Install 1-Wire Server
+
+The following task installs the 1-Wire server and shell commands.  The
+abbey uses the Dallas Semiconductor DS9490R, a USB to 1-Wire adapter,
+on all its weather hosts, so it also configures the server to use the
+USB adapter (rather than a test "fake" adapter).
+
+#+CAPTION: =roles_t/abbey-weather/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-weather/tasks/main.yml
+
+- name: Install 1-Wire server.
+  become: yes
+  apt:
+    pkg: [ owserver, ow-shell ]
+
+- name: Configure 1-Wire server.
+  become: yes
+  lineinfile:
+    path: /etc/owfs.conf
+    regexp: "{{ item.regexp }}"
+    line: "{{ item.line }}"
+    backrefs: yes
+  loop:
+  - { regexp: '^[# ]*server: *FAKE(.*)$', line: '#server: FAKE\1' }
+  - { regexp: '^[# ]*server: *usb(.*)$', line: 'server: usb\1' }
+#+END_SRC
+
+** Install Rsync
+
+Monkey on Core will want to download log records (files) using
+~rsync(1)~.
+
+#+CAPTION: =roles_t/abbey-weather/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-weather/tasks/main.yml
+
+- name: Install Rsync.
+  become: yes
+  apt: pkg=rsync
+#+END_SRC
+
+** Create Monkey
+
+The weather daemon is run by an unprivileged ~monkey~ account (/not/
+~sysadm~) which allows ~monkey~ on Core shell access.  This is also
+executed during the initial phase of configuration, allowing the
+administrator to login on the new weather host as ~monkey~ and thus to
+test access to the 1-Wire adapter and devices.  To facilitate
+debugging the ~sysadm~ account is included in the ~monkey~ group.
+
+#+CAPTION: =roles_t/abbey-weather/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-weather/tasks/main.yml
+
+- name: Create monkey.
+  become: yes
+  user:
+    name: monkey
+    system: yes
+
+- name: Authorize monkey@core.
+  become: yes
+  vars:
+    pubkeyfile: ../Secret/ssh_monkey/id_rsa.pub
+  authorized_key:
+    user: monkey
+    key: "{{ lookup('file', pubkeyfile) }}"
+    manage_dir: yes
+
+- name: Add {{ ansible_user }} to monkey group.
+  become: yes
+  user:
+    name: "{{ ansible_user }}"
+    append: yes
+    groups: monkey
+#+END_SRC
+
+** Install Weather Daemon
+
+The weather daemon is kept alive as a Systemd service unit.  This task
+creates and starts that service /after/ the host-specific
+=files/daemon-HOST= file becomes available.
+
+The ~ExecStartPre=/bin/sleep 30~ is intended to avoid recent hangs in
+~owread~.
+
+#+CAPTION: =roles_t/abbey-weather/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-weather/tasks/main.yml :noweb yes
+
+- name: Install weather directory.
+  become: yes
+  file:
+    path: /home/monkey/Weather/Log
+    state: directory
+    owner: monkey
+    group: monkey
+    mode: u=rwx,g=rx,o=rx
+
+- name: Test for weather daemon script.
+  vars:
+    dir: ../roles/abbey-weather/files
+    file: "{{ dir }}/daemon-{{ inventory_hostname }}"
+  stat: path="{{ file }}"
+  delegate_to: localhost
+  register: weather
+
+- name: Note missing weather daemon script.
+  vars:
+    dir: ../roles/abbey-weather/files
+    script: "{{ dir }}/daemon-{{ inventory_hostname }}"
+  debug:
+    msg: "{{ script }}: not found"
+  when: not weather.stat.exists
+
+- name: Install weather daemon.
+  become: yes
+  vars:
+    dir: ../roles/abbey-weather/files
+    script: "{{ dir }}/daemon-{{ inventory_hostname }}"
+  copy:
+    src: "{{ script }}"
+    dest: /home/monkey/Weather/daemon
+    owner: monkey
+    group: monkey
+    mode: u=rwx,g=rx,o=
+  when: weather.stat.exists
+
+- name: Install weatherd service.
+  become: yes
+  copy:
+    content: |
+      [Unit]
+      Description=Weather Logger
+      After=owserver.service
+
+      [Service]
+      User=monkey
+      ExecStartPre=/bin/sleep 30
+      ExecStart=/home/monkey/Weather/daemon
+      Restart=always
+
+      [Install]
+      WantedBy=multi-user.target
+    dest: /etc/systemd/system/weatherd.service
+  when: weather.stat.exists
+  notify:
+  - Reload Systemd.
+  - Restart weather daemon.
+
+- name: Enable/Start weather daemon.
+  become: yes
+  systemd:
+    service: weatherd
+    enabled: yes
+    state: started
+  when: weather.stat.exists
+#+END_SRC
+
+#+CAPTION: =roles_t/abbey-weather/handlers/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-weather/handlers/main.yml :mkdirp yes
+---
+- name: Reload Systemd.
+  become: yes
+  command: systemctl daemon-reload
+
+- name: Restart weather daemon.
+  become: yes
+  systemd:
+    service: weatherd
+    state: restarted
+#+END_SRC
+
+
+* The Abbey DVR Role
+
+The abbey uses Zoneminder to record video from PoE IP HD security
+cameras.  The Abbey DVR Role installs Zoneminder and configures it to
+record to =/Zoneminder/=, the mount point for a separate, large
+storage volume.  It follows the instructions in
+=/usr/share/doc/zoneminder/README.Debian= to create the ~zm~ database
+and configuring Apache.
+
+** DVR Machine Setup
+
+The installation process involves some manual intervention.  The first
+time a host is enrolled, Ansible will install the necessary packages,
+but it cannot create the database, nor the database user (yet, in the
+first pass).  After adding the new machine to the ~dvrs~ group in
+[[=hosts=]], run Ansible to get the Zoneminder software installed.
+
+: ./abbey config HOST
+
+Several configuration steps will be skipped because =/Zoneminder/= has
+not been created yet.  To proceed, first create the database and
+database user manually, as described in section [[*Manually Create Zoneminder DB and User][Manually Create
+Zoneminder DB and User]].
+
+** Create =/Zoneminder/=
+
+=/Zoneminder/= should be a separate, large volume lest Zoneminder fill
+the root file system.  For acceptable performance, =/Zoneminder/=
+should also be the mount point of a solid-state disk (SSD).  A
+symbolic link at =/var/cache/zoneminder/events= targets =/Zoneminder=
+to make it Zoneminder's "default" storage area.  (The ~PurgeWhenFull~
+filter only works with the default storage area in v1.34.)
+
+** Continue Zoneminder Configuration
+
+Once the ~zm~ database (and ~zmuser~ database user) are created, and a
+large volume mounted at =/Zoneminder/=, Ansible can continue with the
+Zoneminder configuration.
+
+: ./abbey configure HOST
+
+Configuring Zoneminder's cameras is still a manual process as
+described in the final section, [[*Configure Cameras][Configure Cameras]], below.
+
+** Include Abbey Variables
+
+In this abbey specific document, most abbey particulars are not
+replaced with variables, but specified in-line.  Some, however, are
+not published (e.g. database passwords).  The variables that replace
+them are included from =private/vars-abbey.yml=.  Example values are
+given in this document.
+
+#+CAPTION: =roles_t/abbey-dvr/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-dvr/tasks/main.yml :mkdirp yes
+---
+- name: Include private abbey variables.
+  include_vars: ../private/vars-abbey.yml
+#+END_SRC
+
+The relative filename should be found only in the playbook's
+directory, =playbooks/=.
+
+** Install Zoneminder v1.34
+
+The latest version of Zoneminder (1.36) was manually downloaded, built
+and installed, but it immediately had problems, randomly producing
+short events, dropping "problem" cameras entirely, etc.  Version 1.34
+did not have those problems, but could still melt down (thrash?) when
+=/Zoneminder/= was a Seagate Barracuda in a USB3.1gen2 external drive
+enclosure.  A Western Digital Passport Ultra seemed to work much
+better, for a short while.  Ultimately a solid-state drive (a 2TB
+USB3.2 Gen2 Samsung T7 Shield) mounted at =/Zoneminder/= got
+Zoneminder 1.34 to work reliably.
+
+After uninstalling 1.36, the Debian 11 package (1.34) was installed
+and configured per the instructions in sections "Web server set-up"
+and "Time Zone" in =/usr/share/doc/zoneminder/README.Debian.gz=.
+
+#+CAPTION: =roles_t/abbey-dvr/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-dvr/tasks/main.yml
+
+- name: Install Zoneminder.
+  become: yes
+  apt: pkg=zoneminder
+
+- name: Enable Apache modules for Zoneminder.
+  become: yes
+  apache2_module:
+    name: "{{ item }}"
+  loop: [ cgi, rewrite, expires, headers ]
+  notify: Restart Apache2.
+
+- name: Enable Zoneminder Apache configuration.
+  become: yes
+  command:
+    cmd: a2enconf zoneminder
+    creates: /etc/apache2/conf-enabled/zoneminder.conf
+  notify: Restart Apache2.
+
+- name: Configure MySQL for Zoneminder.
+  become: yes
+  copy:
+    content: |
+      [mysqld]
+      sql_mode = NO_ENGINE_SUBSTITUTION
+    dest: /etc/mysql/conf.d/zoneminder.cnf
+  notify: Restart MySQL.
+
+- name: Configure PHP date.timezone.
+  become: yes
+  lineinfile:
+    regexp: date.timezone ?=
+    line: date.timezone = {{ lookup('file', '/etc/timezone') }}
+    path: "{{ item }}"
+  loop:
+  - /etc/php/7.4/cli/php.ini
+  - /etc/php/7.4/apache2/php.ini
+  notify: Restart Apache2.
+
+- name: Enable/Start Apache2.
+  become: yes
+  systemd:
+    service: apache2
+    enabled: yes
+    state: started
+#+END_SRC
+
+#+CAPTION: =roles_t/abbey-dvr/handlers/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-dvr/handlers/main.yml :mkdirp yes
+---
+- name: Restart MySQL.
+  become: yes
+  systemd:
+    service: mysql
+    state: restarted
+
+- name: Restart Apache2.
+  become: yes
+  systemd:
+    service: apache2
+    state: restarted
+#+END_SRC
+
+The following Rsyslog configuration drop-in gets Zoneminder's natter
+out of =/var/log/syslog=.
+
+#+CAPTION: =roles_t/abbey-dvr/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-dvr/tasks/main.yml
+
+- name: Use /var/log/zoneminder.log
+  become: yes
+  copy:
+    content: |
+      :programname,startswith,"zm" -/var/log/zoneminder.log
+      & stop
+    dest: /etc/rsyslog.d/40-zoneminder.conf
+#+END_SRC
+
+** Create Zoneminder Database
+
+Zoneminder's MariaDB database is created by the following task, when
+the ~mysql_db~ Ansible module supports ~check_implicit_admin~.
+
+#+BEGIN_SRC conf
+
+- name: Create Zoneminder DB.
+  become: yes
+  mysql_db:
+    check_implicit_admin: yes
+    name: zm
+    collation: utf8mb4_general_ci
+    encoding: utf8mb4
+#+END_SRC
+
+Unfortunately it does not currently, yet the institute prefers the
+more secure Unix socket authentication method.  Rather than create a
+privileged DB user, the ~zm~ database is created manually (below).
+
+** Create Zoneminder DB User
+
+The following task would create the DB user (~mysql_user~ supports
+~check_implicit_admin~) /but/ the ~zm~ database was not created above.
+
+The DB user's password is taken from the ~zoneminder_dbpass~
+variable, kept in =private/vars-abbey.yml=, and generated e.g. with
+the ~apg -n 1 -x 12 -m 12~ command.
+
+#+CAPTION: =private/vars-abbey.yml=
+#+BEGIN_SRC conf
+---
+zoneminder_dbpass:           gakJopbikJadsEdd
+#+END_SRC
+
+#+BEGIN_SRC conf
+
+- name: Create Zoneminder DB user.
+  become: yes
+  mysql_user:
+    check_implicit_admin: yes
+    name: zmuser
+    password: "{{ zoneminder_dbpass }}"
+    priv: >-
+      zm.*:
+      lock tables,alter,create,index,select,insert,update,delete
+#+END_SRC
+
+** Manually Create Zoneminder DB and User
+
+The Zoneminder database and database user are created manually with
+the following SQL (with the ~zoneminder_dbpass~ spliced in).  The SQL
+commands are entered at the SQL prompt of the ~sudo mysql~ command, or
+perhaps piped into the command.
+
+#+BEGIN_SRC sql
+create database zm
+    character set utf8mb4
+    collate utf8mb4_general_ci;
+grant lock tables,alter,create,index,select,insert,update,delete
+    on zm.*
+    to 'zmuser'@'localhost'
+    identified by '{{ zoneminder_dbpass }}';
+flush privileges;
+exit;
+#+END_SRC
+
+Finally, ~zm~'s tables are created, completing the database setup,
+
+#+BEGIN_SRC sh
+sudo mysql < /usr/share/zoneminder/db/zm_create.sql
+#+END_SRC
+
+** Use =/Zoneminder/=
+
+The following tasks start with a test for the existence of
+=/Zoneminder=.  Configuration tasks that require =/Zoneminder/= or the
+~zm~ database are executed only when ~zoneminder.stat.exists~.  The
+last "Link..." task below "forces" the link, whether the target exists
+or not (yet).
+
+#+CAPTION: =roles_t/abbey-dvr/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-dvr/tasks/main.yml
+
+- name: Test for /Zoneminder/.
+  stat:
+    path: /Zoneminder
+  register: zoneminder
+- debug:
+    msg: "/Zoneminder/ does not yet exist"
+  when: not zoneminder.stat.exists
+
+- name: Check /Zoneminder/.
+  become: yes
+  file:
+    state: directory
+    path: /Zoneminder
+    owner: www-data
+    group: www-data
+    mode: u=rwx,g=rx,o=rx
+  when: zoneminder.stat.exists
+
+- name: Link to /Zoneminder/.
+  become: yes
+  file:
+    state: link
+    src: /Zoneminder
+    path: /var/cache/zoneminder/events
+    force: yes
+    follow: no
+#+END_SRC
+
+** Configure Zoneminder
+
+The remaining tasks ensure that the =/etc/zm/zm.conf= file has the
+proper permissions and contains the correct password.
+
+#+CAPTION: =roles_t/abbey-dvr/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-dvr/tasks/main.yml
+
+- name: Set /etc/zm/zm.conf permissions.
+  become: yes
+  file:
+    path: /etc/zm/zm.conf
+    owner: root
+    group: www-data
+    mode: u=rw,g=r,o=
+
+- name: Set Zoneminder passphrase.
+  become: yes
+  lineinfile:
+    regexp: '^ *ZM_DB_PASS *='
+    line: ZM_DB_PASS={{ zoneminder_dbpass }}
+    path: /etc/zm/zm.conf
+#+END_SRC
+
+Finally, Zoneminder's service unit can be enabled (and started) /if/
+=/Zoneminder/= exists.  It is assumed that, if =/Zoneminder/= exists,
+the ~zm~ database has also been created, and the service is ready to
+run.
+
+#+CAPTION: =roles_t/abbey-dvr/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-dvr/tasks/main.yml
+
+- name: Enable/Start Zoneminder.
+  become: yes
+  systemd:
+    service: zoneminder
+    enabled: yes
+    state: started
+  when: zoneminder.stat.exists
+#+END_SRC
+
+** Configure Cameras
+
+A new security camera is setup as described in [[*Cloistering][Cloistering]], after
+which the camera should be accessible by name on the abbey networks.
+Assuming ~ping -c1 new~ works, the camera's web interface will be
+accessible at ~http://new/~.
+
+The abbey's administrator logs into ~http://new/~ and turns off any
+OSD (on-screen display).  Zoneminder will add its own timestamp, for
+the best accuracy and reliability.  The administrator also turns down
+the frame rate to 5fps.  The abbey prefers HD resolution (e.g. 1080p)
+and long duration logs, thus fewer frames per second.  The
+administrator also creates an unprivileged user with a short password
+e.g. ~user:gobbledygook~.
+
+After Ansible has configured and started Zoneminder, a camera can be
+created by clicking on "Add" in the Zoneminder console.  (If the
+Zoneminder host was named "security", the Zoneminder console can be
+found at ~http://security/zm/~.)  In the Add dialog, the following
+settings should be changed.  (The parenthesized settings are default
+settings that should be checked but are probably already correctly
+set.)
+
+  - In the "General" tab, specify:
+    - Name: Front
+    - (Server: None)
+    - (Source type: Ffmpeg)
+    - Function: Record
+    - Enabled: yes
+    - (Analysis FPS: <blank>)
+    - (Maximum FPS: <blank>)
+    - (Alarm Maximum FPS: <blank>)
+  - In the "Source" tab, specify:
+    - Src path: rtsp://user:gobbledygook@new.small.private.:554/11
+    - (Method: TCP)
+    - (Target colorspace: 32 bit colour)
+    - Capture Resolution: 1920x1080 1080p
+  - In the "Timestamp" tab, specify:
+    - Timestamp Label X: 10
+    - Timestamp Label Y: 10
+    - Font Size: Large
+  - In the "Buffers" tab, specify:
+    - Image Buffer Size (frames): 40
+
+
+* The Abbey TVR Role
+
+The abbey has a few TV tuners and a subscription to [[https://schedulesdirect.org/][Schedules Direct]]
+for North American TV broadcast schedules.  It uses one (master)
+MythTV server and its MythWeb interface to make and serve recordings
+of area broadcasts.
+
+The Abbey TVR Role installs the MythTV backend and the MythWeb web
+interface on the master server.  It configures the Apache web server
+to serve MythWeb pages at e.g. ~http://NEW/mythweb/~.
+
+** Building MythTV and MythWeb
+
+Neither Debian nor the MythTV project provide binary packages of
+MythTV and MythWeb.  The project recommends building from source
+according to their [[https://www.mythtv.org/wiki/Build_from_Source][Build from Source]] wiki page.  To do this, the
+target host will need several dozen "developer" packages installed.
+Thus the abbey's TVR role proceeds in two phases.
+
+In the first phase, the MythTV project's Ansible code, in
+=mythtv-ansible/=, is used to assemble a list of packages needed
+during the build.  The packages are installed and the rest of the
+role's tasks are skipped.  This allows the administrator to manually
+build and install MythTV, creating =/usr/local/bin/mythtv-setup=.
+The administrator will also download and install MythWeb before
+running the TVR role again for its second phase.  The administrator
+will /not/ be able to run ~mythtv-setup~ before completing the second
+phase.
+
+In the second phase, the role finds =mythtv-setup= has been installed
+on the target host and so proceeds with the "Post-installation tasks"
+section of the wiki page.  This still leaves a number of manual steps
+to be performed with the ~mythtv-setup~ program, e.g. configuring a
+video source and capture card, after which the backend can be started.
+
+** TVR Machine Setup
+
+A new TVR machine needs only [[*Cloistering][Cloistering]] to prepare it for
+Ansible.  As part of that process, it should be added to the ~tvrs~
+group in the =hosts= file.  An existing server can become a TVR
+machine simply by adding it to the ~tvrs~ group.
+
+** Include Abbey Variables
+
+In this abbey specific document, most abbey particulars are not
+replaced with variables, but specified in-line.  Some, however, are
+not published (e.g. database passwords).  The variables that replace
+them are included from =private/vars-abbey.yml=.  Example values are
+given in this document.
+
+#+CAPTION: =roles_t/abbey-tvr/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-tvr/tasks/main.yml :mkdirp yes
+---
+- name: Include private abbey variables.
+  include_vars: ../private/vars-abbey.yml
+#+END_SRC
+
+The relative filename should be found only in the playbook's
+directory, =playbooks/=.
+
+** Install MythTV Build Requisites
+
+A number of developer packages are needed to build MythTV.  The wiki
+page recommends Ansible playbooks to assemble the appropriate list of
+package names (several dozen count) depending on the target OS
+version.  The playbooks are in [[https://github.com/MythTV/ansible]] which
+contains a =README.md=.
+
+The instructions in the =README.md= are to clone the repository and
+run ~sudo ansible-playbook -i hosts qt5.yml~ on the build machine.
+However the abbey prefers to keep the Ansible code on an
+administrator's machine with the rest of the abbey's roles.  The
+following commands were used to create a =mythtv-ansible/=
+subdirectory.  (A ~git pull origin~ command in this subdirectory might
+be appropriate to download updates.)
+
+#+BEGIN_SRC sh
+git clone https://github.com/MythTV/ansible mythtv-ansible
+cd mythtv-ansible
+git checkout fixes/32
+#+END_SRC
+
+The ~abbey-tvr~ role uses a couple tasks files in =mythtv-ansible/=
+directly, bypassing the inventories, playbooks and roles, /after/
+"fixing" the final ~apt~ tasks by adding ~become: yes~.  After making
+these edits, the ~git diff~ command should produce something like the
+following.
+
+#+BEGIN_SRC diff
+diff --git a/roles/mythtv-deb/tasks/main.yml b/roles/mythtv-deb/tasks/main.yml
+index 868c9b7..3dcf115 100644
+--- a/roles/mythtv-deb/tasks/main.yml
++++ b/roles/mythtv-deb/tasks/main.yml
+@@ -366,6 +366,7 @@
+       '{{ lookup("flattened", deb_pkg_lst) }}'
+ - name: install packages
++  become: yes
+   apt:
+     name:
+       '{{ lookup("flattened", deb_pkg_lst ) }}'
+diff --git a/roles/qt5/tasks/qt5-deb.yml b/roles/qt5/tasks/qt5-deb.yml
+index 7a1a0bc..26ba782 100644
+--- a/roles/qt5/tasks/qt5-deb.yml
++++ b/roles/qt5/tasks/qt5-deb.yml
+@@ -25,6 +25,7 @@
+       '{{ lookup("flattened", deb_pkg_lst) }}'
+ - name: install deb qt5 packages
++  become: yes
+   apt:
+     name:
+       '{{ lookup("flattened", deb_pkg_lst ) }}'
+#+END_SRC
+
+#+CAPTION: =roles_t/abbey-tvr/tasks/mains.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-tvr/tasks/main.yml
+
+- name: Install MythTV runtime requisites.
+  become: yes
+  apt:
+    pkg: [ mariadb-server, xmltv ]
+
+- name: Install MythTV build requisites.
+  include_tasks: "{{ item }}"
+  loop:
+  - ../mythtv-ansible/roles/mythtv-deb/tasks/main.yml
+  - ../mythtv-ansible/roles/qt5/tasks/qt5-deb.yml
+#+END_SRC
+
+The tasks above install runtime and compile-time requisites during the
+"first" run of e.g. ~./abbey config NEW~.  The "first" run can be
+repeated until successful.  The remaining tasks are skipped until
+MythTV is built and installed.
+
+** Build and Install MythTV
+
+After a successful "first" run of e.g. ~./abbey config NEW~, the
+target machine is prepared to build (and install) MythTV.  The
+following commands are used.
+
+#+BEGIN_SRC sh
+cd /usr/local/src/
+git clone https://github.com/MythTV/mythtv
+cd mythtv/
+git checkout fixes/32
+cd mythtv/
+./configure
+make
+sudo make install
+#+END_SRC
+
+The ~make install~ command does not need to be run as ~root~ if
+=bin/=, =lib/=, =include/=, =share/= in =/usr/local/= and
+=dist-packages/= in =/usr/local/lib/python3.9/= on the target machine
+are writable by the builder.
+
+The following task probes for the =mythtv-setup= program, installed in
+=/usr/local/bin/=, to detect that the build/install process has
+completed.  It registers the results in the ~mythtv~ variable.
+Several of the remaining installation steps are skipped unless
+~mythtv.stat.exists~.
+
+#+CAPTION: =roles_t/abbey-tvr/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-tvr/tasks/main.yml
+
+- name: Test for MythTV binary packages.
+  stat:
+    path: /usr/local/bin/mythtv-setup
+  register: mythtv
+- debug:
+    msg: "/usr/local/bin/mythtv-setup does not yet exist"
+  when: not mythtv.stat.exists
+#+END_SRC
+
+** Create MythTV User
+
+MythTV Backend needs to run as its own user: ~mythtv~.
+
+#+CAPTION: =roles_t/abbey-tvr/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-tvr/tasks/main.yml
+
+- name: Create mythtv.
+  become: yes
+  user:
+    name: mythtv
+    system: yes
+#+END_SRC
+
+** Create MythTV DB
+
+MythTV's MariaDB database is created by the following task, when the
+~mysql_db~ Ansible module supports ~check_implicit_admin~.
+
+#+BEGIN_SRC conf
+
+- name: Create MythTV DB.
+  become: yes
+  mysql_db:
+    check_implicit_admin: yes
+    name: mythconverg
+    collation: utf8mb4_general_ci
+    encoding: utf8mb4
+#+END_SRC
+
+Unfortunately it does not currently, yet the institute prefers the
+more secure Unix socket authentication method.  Rather than create a
+privileged DB user, the ~mythconverg~ database is created manually
+(below).
+
+** Create MythTV DB User
+
+The DB user's password is taken from the ~mythtv_dbpass~ variable,
+kept in =private/vars-abbey.yml=, and generated e.g. with the ~apg -n
+1 -x 12 -m 12~ command.
+
+#+CAPTION: =private/vars-abbey.yml=
+#+BEGIN_SRC conf
+mythtv_dbpass:           daJkibpoJkag
+#+END_SRC
+
+The following task would create the DB user (~mysql_user~ supports
+~check_implicit_admin~) /but/ the ~mythconverg~ database was not
+created above.
+
+#+BEGIN_SRC conf
+
+- name: Create MythTV DB user.
+  become: yes
+  mysql_user:
+    check_implicit_admin: yes
+    name: mythtv
+    password: "{{ mythtv_dbpass }}"
+    priv: "mythconverg.*:all"
+#+END_SRC
+
+** Manually Create MythTV DB and DB User
+
+The MythTV database and database user are created manually with the
+following SQL (with the ~mythtv_dbpass~ spliced in).  The SQL commands
+are entered at the SQL prompt of the ~sudo mysql~ command, or perhaps
+piped into the command.
+
+#+BEGIN_SRC sql
+create database mythconverg
+    character set utf8mb4
+    collate utf8mb4_general_ci;
+create user 'mythtv'@'%' identified by '{{ mythtv_dbpass }}';
+create user 'mythtv'@'localhost' identified by '{{ mythtv_dbpass }}';
+grant all privileges on mythconverg.*
+    to 'mythtv'@'%' with grant option;
+grant all privileges on mythconverg.*
+    to 'mythtv'@'localhost' with grant option;
+flush privileges;
+exit;
+#+END_SRC
+
+** Load DB Timezone Info
+
+Starting with MythTV version 0.26, the time zone tables must be loaded
+into MySQL.  The MariaDB installed by Debian 11 seems to need this
+too.  The test SQL produced ~NULL~.
+
+#+BEGIN_SRC sql
+SELECT CONVERT_TZ(NOW(), 'SYSTEM', 'Etc/UTC');
+#+END_SRC
+
+After running the following command line, the test SQL produced
+e.g. ~2022-09-13 20:15:41~.
+
+#+BEGIN_SRC sh
+mysql_tzinfo_to_sql /usr/share/zoneinfo | sudo mysql mysql
+#+END_SRC
+
+** Create MythTV Backend Service
+
+This task installs the =mythtv-backend.service= file.
+
+#+CAPTION: =roles_t/abbey-tvr/tasks/mains.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-tvr/tasks/main.yml
+
+- name: Create mythtv-backend service.
+  become: yes
+  copy:
+    content: |
+      [Unit]
+      Description=MythTV Backend
+      Documentation=https://www.mythtv.org/wiki/Mythbackend
+      After=mysql.service network.target
+
+      [Service]
+      User=mythtv
+      ExecStartPre=/bin/sleep 30
+      #TimeoutStartSec=infinity
+      ExecStart=/usr/local/bin/mythbackend --quiet --syslog local7
+      StartLimitBurst=10
+      StartLimitInterval=10m
+      Restart=on-failure
+      RestartSec=1
+
+      [Install]
+      WantedBy=multi-user.target
+    dest: /etc/systemd/system/mythtv-backend.service
+  when: mythtv.stat.exists
+  notify: Reload Systemd.
+#+END_SRC
+
+#+CAPTION: =roles_t/abbey-tvr/handlers/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-tvr/handlers/main.yml :mkdirp yes
+---
+- name: Reload Systemd.
+  become: yes
+  command: systemctl daemon-reload
+#+END_SRC
+
+** Set PHP Timezone
+
+This task checks PHP's timezone.  If unset, MythTV's backend logs
+bitter complaints.
+
+#+CAPTION: =roles_t/abbey-tvr/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-tvr/tasks/main.yml
+
+- name: Configure PHP date.timezone.
+  become: yes
+  lineinfile:
+    regexp: date.timezone ?=
+    line: date.timezone = {{ lookup('file', '/etc/timezone') }}
+    path: "{{ item }}"
+  loop:
+  - /etc/php/7.4/cli/php.ini
+  - /etc/php/7.4/apache2/php.ini
+  when: mythtv.stat.exists
+  notify: Restart Apache2.
+#+END_SRC
+
+#+CAPTION: =roles_t/abbey-tvr/handlers/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-tvr/handlers/main.yml
+
+- name: Restart Apache2.
+  become: yes
+  systemd:
+    service: apache2
+    state: restarted
+#+END_SRC
+
+** Create MythTV Storage Area
+
+The backend does not have a default storage area for its recordings.
+A path to an appropriate directory must be set with the ~mythtv-setup~
+program (as described below).  The abbey uses
+=/home/mythtv/Recordings/= for MythTV's default storage.  This task
+creates that directory and ensures it has appropriate permissions.
+
+#+CAPTION: =roles_t/abbey-tvr/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-tvr/tasks/main.yml
+
+- name: Create MythTV storage area.
+  become: yes
+  file:
+    state: directory
+    dest: /home/mythtv/Recordings
+    owner: mythtv
+    group: mythtv
+    mode: u=rwx,g+rwx,o=rx
+#+END_SRC
+
+** Configure MythTV Backend
+
+With MythTV built and installed, and the post-installation tasks
+addressed, MythTV Setup (the ~mythtv-setup~ program) can be run.  It
+must be run by the ~mythtv~ user, whose home directory will contain
+the MythTV (and XMLTV) configuration files.  The program is best run
+remotely (unless there is a graphical desktop on the server) by a
+command like ~ssh -X mythtv@NEW mythtv-setup~.
+
+Patience is required.  The ~mythtv-setup~ program was not written for
+X11 and the X11 adapter has a difficult job.  It is often hard to
+determine what button is selected or how to proceed (sometimes simply
+with ~ESC~!).  Sticking to the arrow, enter and escape keys best
+emulates a TV remote (for which the interface was designed).
+
+In MythTV Setup:
+
+- In the initial MythTV Startup Status ("Unable to connect to
+  Database."), use the "Setup" button to get to "Database
+  Configuration".  Leave the default hostname (~localhost~), port
+  (~3306~), database name (~mythconverg~) and user (~mythtv~).  Enter
+  the value of ~mythtv_dbpass~ (in =private/vars-abbey.yml=) for the
+  password.  Leave the rest of the settings at their default values.
+  Leave "Database Configuration" by pressing Escape and confirming
+  "Save and Exit".
+
+- Once in MythTV Setup proper, you will see the main menu.  Scroll
+  down and choose "Storage Directories".  In the Local Storage Groups
+  dialog, add to the "Local 'Default' Storage Group Directories" a new
+  directory: =/home/mythtv/Recordings=.
+
+** Configure Tuner
+
+The abbey has a Silicon Dust Homerun HDTV Duo (with two tuners).  It
+is setup as described in [[*Cloistering][Cloistering]], after which the tuner is
+accessible by name (e.g. ~new~) on the cloister network.  Assuming
+~ping -c1 new~ works, the tuner should be accessible via the
+~hdhomerun_config_gui~ command, a graphical interface contributed to
+Debian by Silicon Dust and found in the ~hdhomerun-config-gui~
+package.  The program, run with the command ~hdhomerun_config_gui~,
+will broadcast on the localnet to find any Homeruns there, but the new
+tuner's domain name or IP address can also be entered.
+
+** Add HDHomerun and Mr.Antenna
+
+In MythTV Setup:
+- Choose "Capture cards".
+  - Choose "(Add Capture Card)", then the "New Capture Card".
+  - Choose Card Type and select "HDHomeRun Networked Tuner".
+  - Press the right arrow key to see card type parameters.  Choose the
+    tuner's address, which should be listed assuming the tuner and TVR
+    are on the same subnet (e.g. the private Ethernet).
+  - Save and Exit (via Escape key).
+- Choose "Video sources".
+  - Choose "(New Video Source)", then the "New Video Source".
+  - Enter video source name "Mr.Antenna".
+  - Choose listings grabber "Schedules Direct JSON API (xmltv)".
+  - Save and Exit.
+- Choose "Input Connections".
+  - Choose the HDHomeRun.
+  - Choose video source "Mr.Antenna".
+  - Save and Exit.
+- Choose "Capture cards".
+  - Add a second HDHomeRun as above.
+  - Save and Exit.
+- Choose "Input connections".
+  - Connect the second HDHomeRun to Mr.Antenna as above.
+  - Save and Exit.
+- Exit MythTV Setup or continue directly to Scan for New Channels.  In
+  any case, do /not/ run ~mythfilldatabase~.
+
+** Scan for New Channels
+
+In MythTV Setup:
+- Choose "Channel Editor".
+  - Navigate to the "Delete" button, leaving Video Source All (right
+    and down and down, or left six times, or sump'n).  Confirm
+    deletion of all channels.
+  - Choose video source Mr.Antenna, then Channel Scan.  Scroll down to
+    the "scan" button and choose it (select and Enter).
+  - Choose "Insert All" when the scan is complete and the count of
+    channels is presented.  Delete All unused transports.
+  - Save and Exit from the scan.  Exit from the channel editor.
+- Exit MythTV Setup.  Do /not/ run ~mythfilldatabase~.
+
+** Configure XMLTV
+
+The ~xmltv~ package, specifically its ~tv_grab_zz_sdjson~ program, is
+used to download broadcast listings from Schedules Direct.  The
+program is run by the ~mythtv~ user (like ~mythtv-setup~) and is
+initially configured (the /first/ time) using its ~--configure~
+option.
+
+#+BEGIN_SRC sh
+tv_grab_zz_sdjson --configure
+cp ~/.xmltv/tv_grab_zz_sdjson.conf ~/.mythtv/Mr.Antenna.xmltv
+#+END_SRC
+
+The ~--configure~ command above prompts with many questions and
+creates =~/.xmltv/tv_grab_zz_sdjson.conf=, which is copied to
+=~/.mythtv/Mr.Antenna.xmltv= where ~mythfilldatabase~ will find it.
+Afterwards any re-configuration should use the following command.
+
+#+BEGIN_SRC sh
+tv_grab_zz_sdjson --configure --config-file ~/.mythtv/Mr.Antenna.xmltv
+#+END_SRC
+
+Here is a transcript of a session with ~tv_grab_zz_sdjson~.  Note that
+the list of "inputs" available in a postal code typically ends with
+the OTA (over the air) broadcasts.
+
+#+BEGIN_EXAMPLE
+ $ tv_grab_zz_sdjson --configure --config-file .mythtv/Mr.Antenna.xmltv
+ Cache file for lineups, schedules and programs.
+ Cache file: [/home/mythtv/.xmltv/tv_grab_zz_sdjson.cache]
+ If you are migrating from a different grabber selecting an alternate
+  channel ID format can make the migration easier.
+ Select channel ID format:
+ 0: Default Format (eg: I12345.json.schedulesdirect.org)
+ 1: tv_grab_na_dd Format (eg: I12345.labs.zap2it.com)
+ 2: MythTV Internal DD Grabber Format (eg: 12345)
+ Select one: [0,1,2 (default=0)] 
+ As the JSON data only includes the previously shown date normally the
+  XML output should only have the date. However some programs such as
+  older versions of MythTV also need a time.
+ Select previously shown format:
+ 0: Date Only
+ 1: Date And Time
+ Select one: [0,1 (default=0)] 
+ Schedules Direct username.
+ Username: USERNAME
+ Schedules Direct password.
+ Password: PASSWORD
+ ** POST https://json.schedulesdirect.org/20141201/token ==> 200 OK
+ ** GET https://json.schedulesdirect.org/20141201/status ==> 200 OK (1s)
+ ** GET https://json.schedulesdirect.org/20141201/lineups ==> 200 OK
+ This step configures the lineups enabled for your Schedules Direct
+  account. It impacts all other configurations and programs using the
+  JSON API with your account. A maximum of 4 lineups can by added to
+  your account. In a later step you will choose which lineups or
+  channels to actually use for this configuration.
+ Current lineups enabled for your Schedules Direct account:
+ #. Lineup ID | Name | Location | Transport
+ 1. USA-OTA-57719 | Local Over the Air Broadcast | 57719 | Antenna
+ Edit account lineups: [continue,add,delete (default=continue)] 
+ Choose whether you want to include complete lineups or individual
+  channels for this configuration.
+ Select mode: [lineups,channels (default=lineups)] 
+ ** GET https://json.schedulesdirect.org/20141201/lineups ==> 200 OK
+ Choose lineups to use for this configuration.
+ USA-OTA-57719 [yes,no,all,none (default=no)] all
+#+END_EXAMPLE
+
+Once configured, the ~mythfilldatabase~ program should be able to use
+~tv_grab_zz_sdjson~ to connect to Schedules Direct and download the
+chosen line-up.  However ~mythfilldatabase~ is happiest when the
+backend is running, so it is not run until then.
+
+** Debug XMLTV
+
+If the ~mythfilldatabase~ command fails or expected listings do not
+appear, more information is available by adding the ~--verbose~
+option.  The ~--help~ option also reveals much, including a ~--manual~
+option for "interactive configuration".
+
+#+BEGIN_SRC sh
+sudo -H -u mythtv mythfilldatabase --verbose
+#+END_SRC
+
+The command might, for example, show that it is failing to run a
+~tv_grab_zz_sdjson~ command like the following.
+
+#+BEGIN_SRC sh
+nice tv_grab_zz_sdjson \
+        --config-file '/home/mythtv/.mythtv/Mr.Antenna.xmltv' \
+        --output /tmp/myths5Sq35 --quiet
+#+END_SRC
+
+Running a similar command (without ~--quiet~) might be more revealing.
+
+#+BEGIN_SRC sh
+sudo -H -u mythtv \
+    tv_grab_zz_sdjson \
+        --config-file '/home/mythtv/.mythtv/Mr.Antenna.xmltv' \
+        --output /tmp/mythFUBAR
+#+END_SRC
+
+** Configure MythTV Backend Logging
+
+The abbey directs MythTV log messages to =/var/log/mythtv.log= (and
+away from =/var/log/syslog=) and rotates the log file.
+
+#+CAPTION: =roles_t/abbey-tvr/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-tvr/tasks/main.yml
+
+- name: Install =/etc/rsyslog.d/40-mythtv.conf.
+  become: yes
+  copy:
+    content: |
+      :msg,startswith," myth" -/var/log/mythtv.log
+      & stop
+    dest: /etc/rsyslog.d/40-mythtv.conf
+
+- name: Install =/etc/logrotate.d/mythtv=.
+  become: yes
+  copy:
+    content: |
+      /var/log/mythtv.log {
+          daily
+          size=10M
+          rotate 7
+          notifempty
+          copytruncate
+          missingok
+          postrotate
+              reload rsyslog >/dev/null 2>&1 || true
+          endscript
+      }
+    dest: /etc/logrotate.d/mythtv
+#+END_SRC
+
+** Start MythTV Backend
+
+After configuring with ~mythtv-setup~ as discussed above, start and
+enable (at boot time) the ~mythtv-backend~ service.
+
+#+BEGIN_SRC sh
+sudo systemctl enable mythtv-backend
+sudo systemctl start mythtv-backend
+systemctl status -l mythtv-backend
+sudo -u mythtv mythfilldatabase
+#+END_SRC
+
+** Install MythWeb
+
+MythWeb, like MythTV, is installed from a Git repository.  The
+following commands create =/usr/local/share/mythtv/mythweb/= by
+cloning the MythWeb repository in =/usr/local/src/mythweb/=, checking
+out the appropriate branch, and copying the appropriate portion.
+
+#+BEGIN_SRC sh
+cd /usr/local/src/
+git clone https://github.com/MythTV/mythweb
+( cd mythweb/; git checkout fixes/32 )
+rsync -C mythweb /usr/local/share/mythtv/
+#+END_SRC
+
+The following tasks take care of the rest of the installation.
+
+#+CAPTION: =roles_t/abbey-tvr/tasks/main.yml=
+#+BEGIN_SRC conf :tangle roles_t/abbey-tvr/tasks/main.yml
+
+- name: Install MythWeb requisites.
+  become: yes
+  apt:
+    pkg: [ apache2, php, php-mysql ]
+
+- name: Install MythWeb in web server DocumentRoot.
+  file:
+    state: link
+    src: /usr/local/share/mythtv/mythweb
+    dest: /var/www/html/mythweb
+
+- name: Configure MythWeb data directory.
+  file:
+    state: directory
+    dest: /var/www/html/mythweb/data
+    group: www-data
+    mode: u=rwx,g+rwx,o=rx
+
+- name: Install MythWeb configuration.
+  become: yes
+  template:
+    src: mythweb.conf.j2
+    dest: /etc/apache2/sites-available/mythweb.conf
+  notify: Restart Apache2.
+
+- name: Enable MythWeb configuration.
+  become: yes
+  command:
+    cmd: a2ensite -q mythweb
+    creates: /etc/apache2/sites-enabled/mythweb.conf
+  notify: Restart Apache2.
+#+END_SRC
+
+#+CAPTION: =roles_t/abbey-tvr/templates/mythweb.conf.j2=
+#+BEGIN_SRC conf :tangle roles_t/abbey-tvr/templates/mythweb.conf.j2 :mkdirp yes
+#
+# Apache configuration directives for MythWeb.
+#
+# Note that this file is maintained by the network administration.
+<Directory "/var/www/html/mythweb/data">
+    # For Apache 2.2
+    #Options -All +FollowSymLinks +IncludesNoExec
+    # For Apache 2.4+
+    Options +FollowSymLinks +IncludesNoExec
+</Directory>
+<Directory "/var/www/html/mythweb" >
+    <Files mythweb.*>
+    setenv db_server "127.0.0.1"
+    setenv db_name "mythconverg"
+    setenv db_login "mythtv"
+    setenv db_password "{{ mythtv_dbpass }}"
+    </Files>
+    <Files *.php>
+       php_value file_uploads                  0
+       php_value allow_url_fopen               On
+       php_value zlib.output_handler           Off
+       php_value memory_limit                  64M
+       php_value max_execution_time 30
+       php_value display_startup_errors        On
+       php_value display_errors                On
+    </Files>
+    RewriteEngine  on
+    RewriteRule \
+^(css|data|images|js|themes|skins|README|INSTALL|[a-z_]+\.(php|pl))(/|$)\
+        - [L]
+    RewriteRule ^(pl(/.*)?)$            mythweb.pl/$1  [QSA,L]
+    RewriteRule ^(.+)$                  mythweb.php/$1 [QSA,L]
+    RewriteRule ^(.*)$                  mythweb.php    [QSA,L]
+    AllowOverride All
+    Options         FollowSymLinks
+    AddType video/nuppelvideo   .nuv
+    AddType image/x-icon        .ico
+    <IfModule deflate_module>
+       BrowserMatch ^Mozilla/4 gzip-only-text/html
+       BrowserMatch ^Mozilla/4\.0[678] no-gzip
+       BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
+       AddOutputFilterByType DEFLATE text/html
+       AddOutputFilterByType DEFLATE text/css
+       AddOutputFilterByType DEFLATE application/x-javascript
+    </IfModule>
+    <IfModule headers_module>
+       Header append Vary User-Agent env=!dont-vary
+    </IfModule>
+    <Files *.pl>
+       SetHandler cgi-script
+       Options +ExecCGI
+    </Files>
+
+</Directory>
+#+END_SRC
+
+** Change Broadcast Area
+
+The abbey changes location almost weekly, so its HDTV broadcast area
+changes frequently.  At the start of a long stay the administrator
+uses the MythTV Setup program to scan for the new area's channels, as
+described in [[*Scan for New Channels][Scan for New Channels]].
+
+To change MythTV's "listings", the administrator needs the new area's
+postal code and the username and password of the abbey's Schedules
+Direct account.  The administrator then runs the ~tv_grab_zz_sdjson~
+program as user ~mythtv~.
+
+#+BEGIN_SRC sh
+tv_grab_zz_sdjson --configure --config-file ~/.mythtv/Mr.Antenna.xmltv
+#+END_SRC
+
+The program will prompt for the zip code and offer a list of "inputs"
+available in that area, as described in [[*Configure XMLTV][Configure XMLTV]].
+
+
+* The Ansible Configuration
+
+The abbey's Ansible configuration, like that of [[file:Institute/README.org][A Small Institute]], is
+kept on an administrator's notebook.  The private SSH key that allows
+remote access to privileged accounts on all abbey servers is kept on
+an encrypted, off-line volume plugged into the administrator's
+notebook only when running ~./abbey~ commands.
+
+The small institute provided examples of both public and private
+variables.  This document includes the abbey's actual public
+variables, and examples of the private variables.  As in A Small
+Institute, this document's roles tangle into =roles_t/=, separate from
+the running (and perhaps recently debugged!) code in =roles/=.
+
+The configuration of a small institute is included as a git sub-module
+in =Institute/=.  Its roles are included in the ~roles_path~ setting
+in =ansible.cfg=.  Its example =hosts= inventory, and =public/= and
+=private/= directories are /not/ included, and are replaced by abbey
+specific versions.
+
+NOTE: if you have not read at least the [[file:Institute/README.org::*Overview][Overview]] of [[file:Institute/README.org][A Small Institute]]
+you are lost.
+
+The Ansible configuration:
+
+  - =ansible.cfg= :: The Ansible configuration file.
+  - =hosts= :: The inventory of hosts.
+  - =playbooks/site.yml= :: The play that assigns roles to hosts.
+  - =public/= :: Variables, certificates.
+  - =public/vars.yml= :: The institutional variables.
+  - =private/= :: Sensitive variables, files, templates.
+  - =private/vars.yml= :: Sensitive institutional variables.
+  - =private/vars-abbey.yml= :: Sensitive liturgical variables.
+  - =roles/= :: The running copy of =roles_t/=.
+  - =roles_t/= :: The liturgical roles as tangled from this document.
+  - =Institute/roles/= :: The running copy of =Institute/roles_t/=.
+  - =Institute/roles_t/= :: The institutional roles as tangled from
+    =Institute/README.org=.
+
+The first three files in the list are included in this chapter.  The
+rest are built up piecemeal by (tangled from) this document,
+=README.org=, and [[file:Institute/README.org][=Institute/README.org=]].
+
+** =ansible.cfg=
+
+This is much like the example (test) institutional configuration file,
+except the roles are found in =Institute/roles/= as well as =roles/=.
+
+#+CAPTION: =ansible.cfg=
+#+BEGIN_SRC conf :tangle ansible.cfg
+[defaults]
+interpreter_python=/usr/bin/python3
+vault_password_file=Secret/vault-password
+inventory=hosts
+roles_path=roles:Institute/roles
+#+END_SRC
+
+** =hosts=
+
+#+NAME: hosts
+#+CAPTION: =hosts=
+#+BEGIN_SRC conf :tangle hosts
+all:
+  vars:
+    ansible_user: sysadm
+    ansible_ssh_extra_args: -i Secret/ssh_admin/id_rsa
+  hosts:
+    # The Main Servers: Front, Gate and Core.
+    droplet:
+      ansible_host: 159.65.75.60
+      ansible_become_password: "{{ become_droplet }}"
+    anoat:
+      ansible_host: {{ gate_addr }}
+      ansible_become_password: "{{ become_anoat }}"
+    dantooine:
+      ansible_host: {{ core_addr }}
+      ansible_become_password: "{{ become_dantooine }}"
+    # WebTVs (Desktops)
+    devaron:
+    kamino:
+      ansible_become_password: "{{ become_kamino }}"
+    kessel:
+      ansible_become_password: "{{ become_kessel }}"
+    # Notebooks
+    endor:
+      ansible_become_password: "{{ become_endor }}"
+    geonosis:
+      ansible_host: 127.0.0.1
+      ansible_user: matt
+      ansible_become_password: "{{ become_geonosis }}"
+      postfix_mydestination: >-
+        geonosis.{{ domain_priv }}
+        geonosis
+        geonosis.localdomain
+        localhost.localdomain
+        localhost
+  children:
+    front:
+      hosts:
+        droplet:
+    gate:
+      hosts:
+        anoat:
+    core:
+      hosts:
+        dantooine:
+    campus:
+      hosts:
+        anoat:
+        devaron:
+        kamino:
+        kessel:
+    weather:
+      hosts:
+        anoat:
+    dvrs:
+      hosts:
+        dantooine:
+    tvrs:
+      hosts:
+        dantooine:
+    notebooks:
+      hosts:
+        endor:
+        geonosis:
+    builders:
+      hosts:
+        devaron:
+        geonosis:
+        kamino:
+#+END_SRC
+
+** =playbooks/site.yml=
+
+This playbook provisions the entire network by applying first the
+institutional roles, then the liturgical roles.
+
+#+CAPTION: =playbooks/site.yml=
+#+BEGIN_SRC conf :tangle playbooks/site.yml :mkdirp yes
+---
+- name: Configure Front
+  hosts: front
+  roles: [ front, abbey-front ]
+
+- name: Configure Core
+  hosts: core
+  roles: [ core, abbey-core ]
+
+- name: Configure Gate
+  hosts: gate
+  roles: [ gate ]
+
+- name: Configure Campus
+  hosts: campus
+  roles: [ campus, abbey-cloister ]
+
+- name: Configure Weather
+  hosts: weather
+  roles: [ abbey-weather ]
+
+- name: Configure DVRs
+  hosts: dvrs
+  roles: [ abbey-dvr ]
+
+- name: Configure TVRs
+  hosts: tvrs
+  roles: [ abbey-tvr ]
+#+END_SRC
+
+
+* The Abbey Commands
+
+The ~./abbey~ script encodes the abbey's canonical procedures.  It
+includes [[file:Institute/README.org::*The Institute Commands][The Institute Commands]] and adds a few abbey-specific
+sub-commands.
+
+** Abbey Command Overview
+
+Institutional sub-commands:
+
+- config :: Check/Set the configuration of one or all hosts.
+- new :: Create system accounts for a new member.
+- old :: Disable system accounts for a former member.
+- pass :: Set the password of a current member.
+- client :: Produce an OpenVPN configuration (=.ovpn=) file for a
+  member's device.
+
+Liturgical sub-commands:
+
+- tz :: Run ~timedatectl set-timezone~ on cloister servers.
+- upgrade :: Run ~apt update; apt full-upgrade --autoremove~ on all
+  hosts.
+- reboots :: Look for =/run/reboot*= on all hosts.
+- versions :: Report ~ansible_distribution~, ~_distribution_version~,
+  and ~_architecture~ for all hosts.
+
+** Abbey Command Script
+
+The script begins with the following prefix and trampolines.
+
+#+CAPTION: =abbey=
+#+BEGIN_SRC perl :tangle abbey :tangle-mode u=rwx,g=rx
+#!/usr/bin/perl -w
+#
+# DO NOT EDIT.  This file was tangled from README.org.
+
+use strict;
+
+if ($ARGV[0] eq "config") {
+  exec "./Institute/inst", @ARGV;
+}
+if ($ARGV[0] eq "new") {
+  exec "./Institute/inst", @ARGV;
+}
+if ($ARGV[0] eq "old") {
+  exec "./Institute/inst", @ARGV;
+}
+if ($ARGV[0] eq "pass") {
+  exec "./Institute/inst", @ARGV;
+}
+if ($ARGV[0] eq "client") {
+  exec "./Institute/inst", @ARGV;
+}
+#+END_SRC
+
+The small institute's ~./inst~ command expects to be running in
+=Institute/=, not =./=, but it only references =public/=, =private/=,
+=Secret/= and =playbooks/check-inst-vars.yml=, and will find the abbey
+specific versions of these.  The ~roles_path~ setting in [[*=ansible.cfg=][=ansible.cfg=]]
+effectively merges the institutional roles into the distinctly named
+abbey specific roles.  The roles likewise reference files with
+relative names, and will find the abbey specific =private/=
+directory (named =../private/= relative to =playbooks/=).
+
+Ansible does not implement a ~playbooks_path~ key, so the following
+code block "duplicates" the action of the institute's
+=check-inst-vars.yml=.
+
+#+CAPTION: =playbooks/check-inst-vars.yml=
+#+BEGIN_SRC conf :tangle playbooks/check-inst-vars.yml
+- import_playbook: ../Institute/playbooks/check-inst-vars.yml
+#+END_SRC
+
+** The Upgrade Command
+
+The script implements an ~upgrade~ sub-command that runs ~apt update~
+and ~apt full-upgrade --autoremove~ on all abbey managed machines.  It
+recognizes an optional ~-n~ flag indicating that the upgrade tasks
+should only be checked.  Any other (single, optional) argument must be
+a limit pattern.  For example:
+
+: ./abbey upgrade
+: ./abbey upgrade -n
+: ./abbey upgrade core
+: ./abbey upgrade -n core
+: ./abbey upgrade '!front'
+
+#+CAPTION: =abbey=
+#+BEGIN_SRC perl :tangle abbey
+
+if ($ARGV[0] eq "upgrade") {
+  shift;
+  my @args = ( "-e", "\@Secret/become.yml" );
+  if (defined $ARGV[0] && $ARGV[0] eq "-n") {
+    shift;
+    push @args, "--check", "--diff";
+  }
+  if (defined $ARGV[0]) {
+    my $limit = $ARGV[0];
+    shift;
+    die "illegal characters: $limit"
+      if $limit !~ /^!?[a-z][-a-z0-9,!]+$/;
+    push @args, "-l", $limit;
+  }
+  exec ("ansible-playbook", @args, "playbooks/upgrade.yml");
+}
+#+END_SRC
+
+#+CAPTION: =playbooks/upgrade.yml=
+#+BEGIN_SRC conf :tangle playbooks/upgrade.yml
+- hosts: all
+  tasks:
+
+  - name: Upgrade packages.
+    become: yes
+    apt:
+      update_cache: yes
+      upgrade: full
+      autoremove: yes
+      purge: yes
+      autoclean: yes
+
+  - name: Check for /run/reboot-required.
+    stat:
+      path: /run/reboot-required
+    no_log: true
+    register: st
+
+  - debug:
+      msg: Reboot required.
+    when: st.stat.exists
+#+END_SRC
+
+** The Reboots Command
+
+The script implements a ~reboots~ sub-command that looks for
+=/run/reboot-required= on all abbey managed machines.
+
+#+CAPTION: =abbey=
+#+BEGIN_SRC perl :tangle abbey
+if ($ARGV[0] eq "reboots") {
+  exec ("ansible-playbook", "-e", "\@Secret/become.yml",
+       "playbooks/reboots.yml");
+}
+#+END_SRC
+
+#+CAPTION: =playbooks/reboots.yml=
+#+BEGIN_SRC conf :tangle playbooks/reboots.yml
+---
+- hosts: all
+  tasks:
+
+  - stat:
+      path: /run/reboot-required
+    register: st
+
+  - debug:
+      msg: Reboot required.
+    when: st.stat.exists
+#+END_SRC
+
+** The Versions Command
+
+The script implements a ~versions~ sub-command that reports the
+operating system version of all abbey managed machines.
+
+#+CAPTION: =abbey=
+#+BEGIN_SRC perl :tangle abbey
+if ($ARGV[0] eq "versions") {
+  exec ("ansible-playbook", "-e", "\@Secret/become.yml",
+       "playbooks/versarch.yml");
+}
+#+END_SRC
+
+#+CAPTION: =playbooks/versarch.yml=
+#+BEGIN_SRC conf :tangle playbooks/versarch.yml
+- hosts: all
+  tasks:
+  - debug:
+      msg: >-
+        {{ ansible_distribution }}
+        {{ ansible_distribution_version }}
+        {{ ansible_architecture }}
+#+END_SRC
+
+** The TZ Command
+
+The abbey changes location almost weekly, so its timezone changes
+occasionally.  Droplet does not move.  Gate and other simple servers
+(the weather monitors) are kept in UTC.  Core, the DVRs, TVRs, and the
+desktops all want updating to the current local timezone.  The
+desktops are managed maually, but the rest can all be updated using
+Ansible.
+
+The ~tz~ sub-command runs the =timezone.yml= playbook, which uses the
+current timezone/city on the administrator's notebook and updates
+Core, the DVRs and TVRs.  Each runs ~timedatectl set-timezone~ and
+restarts the affected services.
+
+This is an experimental playbook until it is used/tested with separate
+machines hosting the DVR and TVR services.  It assumes each host sees
+the ~new_tz~ result registered by it in a previous play and not by the
+last host in the previous play.
+
+#+CAPTION: =abbey=
+#+BEGIN_SRC perl :tangle abbey
+if ($ARGV[0] eq "tz") {
+  my $city = `cat /etc/timezone`; chomp $city;
+  my $zone = `date +%Z`; chomp $zone;
+  print "Setting timezones to $city.\n";
+  exec ("ansible-playbook", "-e", "\@Secret/become.yml",
+       "-e", "zone=$zone", "-e", "city=$city",
+       "playbooks/timezone.yml");
+}
+#+END_SRC
+
+#+CAPTION: =playbooks/timezone.yml=
+#+BEGIN_SRC conf :tangle playbooks/timezone.yml
+---
+- hosts: core, dvrs, tvrs
+  tasks:
+  - name: Update timezone.
+    become: yes
+    command: timedatectl set-timezone {{ city }}
+    when: ansible_date_time.tz != zone
+    register: new_tz
+  - debug: msg={{ new_tz }}
+
+- hosts: dvrs
+  tasks:
+  - name: Restart Zoneminder.
+    become: yes
+    systemd:
+      service: "{{ item }}"
+      restarted: yes
+    loop: [ mysql, zoneminder ]
+    when: new_tz.changed
+
+- hosts: tvrs
+  tasks:
+  - name: Restart MythTV.
+    become: yes
+    systemd:
+      service: "{{ item }}"
+      restarted: yes
+    loop: [ mysql, mythtv-backend ]
+    when: new_tz.changed
+
+- hosts: core
+  tasks:
+  - name: Update PHP date.timezone.
+    become: yes
+    lineinfile:
+      regexp: date.timezone ?=
+      line: date.timezone = {{ city }}
+      path: "{{ item }}"
+    loop:
+    - /etc/php/7.4/cli/php.ini
+    - /etc/php/7.4/apache2/php.ini
+    notify: Restart Apache2.
+  handlers:
+  - name: Restart Apache2.
+    become: yes
+    systemd:
+      service: apache2
+      state: restarted
+#+END_SRC
+
+** Abbey Command Help
+
+#+CAPTION: =abbey=
+#+BEGIN_SRC perl :tangle abbey
+die
+  "usage: $0 [config,new,old,pass,client,upgrade,reboots,versions]\n";
+#+END_SRC
+
+
+* Cloistering
+
+This is how a new machine is brought into the cloister.  The process
+is initially quite different depending on the device type but then
+narrows down to the common preparation of all machines administered by
+Ansible.
+
+** IoT Devices
+
+A wireless IoT device (smart TV, Blu-ray deck, etc.) cannot install
+Debian nor even an OpenVPN app from F-Droid.  And it shouldn't.  As an
+untrustworthy bit of kit, it should have no access to the cloister,
+merely the Internet.  It need not appear in the Ansible inventory.
+
+IoT devices trusted enough to be patched to the cloister Ethernet (IP
+cameras, TV Tuners, etc.) are added to =/etc/dhcp/dhcpd.conf= and
+given a private domain name as described in the following steps.
+
+- [[*Add to Core DHCP][Add to Core DHCP]]
+- [[*Create Wired Domain Name][Create Wired Domain Name]]
+
+Wireless IoT devices are manually configured with the cloister Wi-Fi
+password and may be given a private domain name as described here.
+
+- [[*Create Wireless Domain Name][Create Wireless Domain Name]]
+
+** Raspberry Pis
+
+The abbey's Raspberry Pis run Raspberry Pi OS, either the desktop
+(PIXEL) or the Lite version (for headless servers).  The following was
+the installation process with a wireless desktop Raspberry Pi OS
+Bookworm (12) machine.
+
+- Write the disk image, =2023-10-10-raspios-bookworm-arm64.img.xz=, to
+  a fast (U3 and/or A1) ÂµSD card and insert it in the Pi.
+- Attach an HDMI monitor, a USB keyboard/mouse, and the cloister
+  Ethernet, and power up.
+- Answer first-boot installation questions:
+  + Language: English (USA)
+  + Keyboard: English (USA)
+  + new username: sysadm
+  + new password: fubar
+- [[*Add to Core DHCP][Add to Core DHCP]]
+- [[*Create Wired Domain Name][Create Wired Domain Name]]
+- Log in as ~sysadm~ on the console.
+- Run ~sudo raspi-config~ and use the following menu items.
+  + S4 Hostname (Set name for this computer on a network): new
+  + I1 SSH (Enable/disable remote command line access using SSH): enable
+  + A1 Expand Filesystem (Ensures that all of the SD card is available)
+- [[*Update From Cloister Apt Cache][Update From Cloister Apt Cache]]
+- [[*Authorize Remote Administration][Authorize Remote Administration]]
+- [[*Configure with Ansible][Configure with Ansible]]
+
+If the Pi is going to operate wirelessly, the following additional
+steps are taken.
+
+- [[*Connect to Cloister Wi-Fi][Connect to Cloister Wi-Fi]]
+- [[*Connect to Cloister VPN][Connect to Cloister VPN]]
+- [[*Create Wireless Domain Name][Create Wireless Domain Name]]
+
+** PCs
+
+Most of the abbey's machines, like Core and Gate, are general-purpose
+PCs running Debian.  The process of cloistering these machines
+follows.
+
+- Write the disk image, e.g. =debian-12.2.0-amd64-netinst.iso=, to a
+  USB drive and insert it in the PC.
+- Attach an HDMI monitor, a USB keyboard/mouse, and the cloister
+  Ethernet, and power up.  Choose to boot from the USB drive.
+- Answer first-boot installation questions:
+  + Language: English (USA)
+  + Keyboard: English (USA)
+  + new username: sysadm
+  + new password: fubar
+- [[*Add to Core DHCP][Add to Core DHCP]]
+- [[*Create Wired Domain Name][Create Wired Domain Name]]
+- Log in as ~sysadm~ on the console.
+- [[*Update From Cloister Apt Cache][Update From Cloister Apt Cache]]
+- Install OpenSSH.  Plain Debian does not come with OpenSSH installed.
+  : sudo apt install openssh-server
+- [[*Authorize Remote Administration][Authorize Remote Administration]]
+- [[*Configure with Ansible][Configure with Ansible]]
+
+If the PC is going to operate wirelessly, the following additional
+steps are taken.
+
+- [[*Connect to Cloister Wi-Fi][Connect to Cloister Wi-Fi]]
+- [[*Connect to Cloister VPN][Connect to Cloister VPN]]
+- [[*Create Wireless Domain Name][Create Wireless Domain Name]]
+
+** Add to Core DHCP
+
+When a new machine is connected to the cloister Ethernet, its MAC
+address must be added to Core's DHCP configuration.  Core does not
+provide network addresses to new devices automatically.
+
+IoT devices (IP cameras, HDTV tuners, etc.) often have their MAC
+address printed on their case or mentioned in a configuration page.
+The MAC address /must/ also appear in the device's DHCP Discover
+broadcasts, which are logged to =/var/log/daemon.log= on Core.  As a
+last (or first!) resort, the following command line should reveal the
+new device's MAC.
+
+#+BEGIN_SRC sh
+tail -100 /var/log/daemon.log | grep DISCOVER
+#+END_SRC
+
+With the new device's Ethernet MAC in hand, a stanza like the
+following is added to the bottom of =private/core-dhcpd.conf=.  The IP
+address must be unique.  Typically the next host number after the
+last entry is chosen.
+
+#+BEGIN_SRC conf
+host new {
+  hardware ethernet 08:00:27:f3:41:66; fixed-address 192.168.56.4; }
+#+END_SRC
+
+The DHCP service is then /restarted/.
+
+#+BEGIN_SRC sh
+sudo systemctl restart isc-dhcp-server
+#+END_SRC
+
+Soon after this the device should be offered a lease for its IP
+address, ~192.168.56.4~.  It might be power cycled to speed up the
+process.
+
+When successful, the following command shows the device is accessible,
+reporting ~1 packets transmitted, 1 received, 0% packet loss...~.
+
+#+BEGIN_SRC sh
+ping -c1 192.168.56.4
+#+END_SRC
+
+** Create Wired Domain Name
+
+A wired device is assigned an IP address when it is added to Core's
+DHCP configuration (as in [[*Add to Core DHCP][Add to Core DHCP]]).  A private domain name is
+then associated with this address.  If the device is intended to
+operate wirelessly, the name for its address is modified with a ~-w~
+suffix.  Thus ~new-w.birchwood.private~ would be the name of the new
+device while it is temporarily connected to the cloister Ethernet, and
+~new.birchwood.private~ would be its "normal" name used when it is on
+the cloister Wi-Fi.
+
+The private domain name is created by adding a line like the following
+to =private/db.domain= and incrementing the serial number at the top
+of the file.
+
+#+BEGIN_SRC conf
+new-w  IN      A       192.168.56.4
+#+END_SRC
+
+The reverse mapping is also created by adding a line like the
+following to =private/db.private= and incrementing the serial number
+at the top of that file.
+
+#+BEGIN_SRC conf
+4      IN      PTR     new-w.birchwood.private.
+#+END_SRC
+
+After ~./abbey config core~ updates Core, resolution of the ~new-w~
+name can be tested.
+
+#+BEGIN_SRC sh
+resolvectl query new-w.birchwood.private.
+resolvectl query 192.168.56.4
+#+END_SRC
+
+** Update From Cloister Apt Cache
+
+- Log in as ~sysadm~ on the console.
+- Create =/etc/apt/apt.conf.d/01proxy=.
+  : D=apt-cacher.birchwood.private.
+  : echo "Acquire::http::Proxy \"http://$D:3142\";" \
+  : > | sudo tee /etc/apt/apt.conf.d/01proxy
+- Update the system and reboot.
+  : sudo apt update
+  : sudo apt full-upgrade --autoremove
+  : sudo reboot
+
+** Authorize Remote Administration
+
+To remotely administer ~new-w~, Ansible must be authorized to login as
+~sysadm@new-w~ without a login password, using an SSH key pair.  This is
+accomplished by copying Ansible's SSH public key to ~new-w~.
+
+#+BEGIN_SRC sh
+scp Secret/ssh_admin/id_rsa.pub sysadm@new-w:admin_key
+#+END_SRC
+
+Then on ~new-w~ (logged in as ~sysadm~) the public key is installed in
+=~sysadm/.ssh/authorized_keys=.
+
+#+BEGIN_SRC sh
+( cd; umask 077; mkdir .ssh; cp admin_key .ssh/authorized_keys )
+#+END_SRC
+
+Now the administrator can test access to ~new-w~ using Ansible's SSH
+key.
+
+#+BEGIN_SRC sh
+ssh -i Secret/ssh_admin/id_rsa sysadm@new-w
+#+END_SRC
+
+** Configure with Ansible
+
+With remote administration authorized and tested (as in [[*Authorize Remote Administration][Authorize
+Remote Administration]]), and the machine connected to the cloister
+Ethernet, the configuration of ~new-w~ can be completed by Ansible.
+Note that if the machine is staying on the cloister Ethernet, its
+domain name will be ~new~ (having had no ~-w~ suffix added).
+
+First ~new-w~ is added to Ansible's inventory in [[*=hosts=][=hosts=]].  A ~new-w~
+section is added to the list of all hosts, and an empty section of the
+same name is added to the list of ~campus~ hosts.  If the machine uses
+the usual privileged account name, ~sysadm~, the ~ansible_user~ key in
+not needed.
+
+#+BEGIN_SRC conf
+  hosts:
+    ...
+    new-w:
+      ansible_user: pi
+      ansible_become_password: "{{ become_new }}"
+    ...
+  children:
+    ...
+    campus:
+      hosts:
+        ...
+        new-w:
+#+END_SRC
+
+If the ~sudo~ command on ~new-w~ never prompts ~sysadm~ for a
+password, then the ~ansible_become_password~ setting is also not
+needed.  Otherwise, the password is added to =Secret/become.yml= as
+shown below.
+
+#+BEGIN_SRC sh
+echo -n "become_new: " >>Secret/become.yml
+ansible-vault encrypt_string PASSWORD >>Secret/become.yml
+#+END_SRC
+
+Finally the ~./abbey config new-w~ command is run.  It will install
+several additional software packages and change several more
+configuration files.
+
+#+BEGIN_SRC sh
+./abbey config new-w
+#+END_SRC
+
+** Connect to Cloister Wi-Fi
+
+On an IoT device, or a Debian or Android "desktop", the cloister Wi-Fi
+name and password are entered manually.  Once the device is connected,
+its Wi-Fi IP address may be discovered in its network settings, and
+perhaps via the access point's local domain, e.g. as ~new.lan~ on a
+desktop connected to the cloister Wi-Fi.
+
+Wireless Debian machines use ~ifupdown~ configured with a short
+=/etc/network/interfaces.d/wifi= drop-in.  In this example, the Wi-Fi
+interface on ~new~ is named ~wlan0~.
+
+#+CAPTION: =/etc/network/interfaces.d/wifi
+#+BEGIN_SRC conf
+auto wlan0
+iface wlan0 inet dhcp
+    wpa-ssid "Birchwood Abbey"
+    wpa-psk "PASSWORD"
+#+END_SRC
+
+Once the ~sudo ifup wlan0~ command is successful, the machine will get
+an IP address on the access point's local network (revealed by the
+command ~ip addr show dev wlan0~).
+
+The new Wi-Fi IP address, e.g. ~192.168.10.225~, should be tested on a
+desktop connected to the Wi-Fi using the following ~ping~ command.
+
+#+BEGIN_SRC sh
+ping -c1 192.168.10.225
+#+END_SRC
+
+** Connect to Cloister VPN
+
+Wireless devices connected to the cloister Wi-Fi will get an IP
+address on the access point's local network and a default route to the
+Internet, per the default configuration of a commodity cable modem
+with Wi-Fi access point included.  Access to further abbey resources,
+however, is possible only via the cloister VPN.
+
+Connections to the cloister VPN are authorized by OpenVPN
+configuration (=.ovpn=) files generated by the ~./abbey client...~
+command (aka [[file:Institute/README.org::*The Client Command][The Client Command]]).  These are secret files, kept
+readable only by their owners and are deleted after use.  They are
+copied to new OpenVPN clients using secure (~ssh~) connections.
+
+*** Debian Servers
+
+Wireless Debian servers (without NetworkManager) are connected to the
+cloister VPN via the following process.
+
+  - Create a new client certificate and OpenVPN configuration for the
+    new campus server.
+  - Copy the =campus.ovpn= file to =/etc/openvpn/cloister.conf=.
+  - In a secure shell session on the new machine as ~sysadm~:
+  - Install the ~openvpn~ and ~openvpn-systemd-resolved~ software
+    packages.
+  - Start the SystemD service unit.
+  - Test the connection (and name resolution).
+  - Enable the SystemD service unit.
+  - Clean up secrets on the new machine.
+  - Clean up secrets on the administrator's machine.
+
+And these are the commands.
+
+#+BEGIN_SRC sh
+./abbey client campus new
+scp campus.ovpn sysadm@new-w:
+ssh sysadm@new-w
+sudo apt install openvpn openvpn-systemd-resolved
+( cd; umask 077; sudo cp campus.ovpn /etc/openvpn/cloister.conf )
+sudo systemctl start openvpn@cloister
+ping -c1 core
+sudo systemctl enable openvpn@cloister
+rm campus.ovpn
+logout
+rm campus.ovpn
+#+END_SRC
+
+*** Debian Desktops
+
+Wireless Debian desktop machines (both PCs and Pis, running
+NetworkManager) and are connected to the cloister VPN via the
+following process.  Note that they do not appear in the set of
+~campus~ hosts and are not configured by Ansible.  They do not appear
+in Ansible's host inventory at all unless the desktop owner is willing
+to provide the password to a privileged account on their machine.
+
+  - Create a new client certificate and campus/public OpenVPN
+    configurations for the new abbey desktop.
+  - Copy the =campus.ovpn= and =public.ovpn= files to the new desktop.
+  - Install the ~openvpn~, ~openvpn-systemd-resolved~ and
+    ~network-manager-openvpn-gnome~ packages on the new desktop.
+  - Open the desktop Settings > Network > VPN + > Import from
+    file... and choose =~/campus.ovpn=.
+  - Open the Routes dialogues for both IPv4 and IPv6 and choose
+    "Use this connection only for resources on its network.".
+  - Save the new VPN.
+  - Do the same with the =~/public.ovpn= file.
+  - Connected the cloister VPN and test it with ~ping -c1 core~.
+  - Expunge the =~/campus.ovpn= and =~/public.ovpn= just as the system
+    administrator will have already done.
+
+And these are the commands, assuming there is a privileged ~sysadm~
+account available on the new desktop machine.
+
+#+BEGIN_SRC sh
+./abbey client debian dicks-notebook dick
+scp campus.ovpn public.ovpn sysadm@dicks-notebook.lan:
+rm campus.ovpn public.ovpn
+ssh sysadm@dicks-notebook.lan
+sudo apt install openvpn openvpn-systemd-resolved \
+                 network-manager-openvpn-gnome
+ping -c1 core.birchwood.private.
+#+END_SRC
+
+Note that Dick's notebook does not need to connect to the cloister
+Ethernet.  It is authorized simply by copying the =.ovpn= files
+securely (e.g. using ~ssh~) to a local domain name provided by the
+Wi-Fi AP (~dicks-notebook.lan~).  If the AP does not provide a local
+domain name, the machine's Wi-Fi IP address,
+e.g. ~sysadm@192.168.10.225~, can be used instead.  (This IP address
+is often revealed in the desktop network settings.)
+
+*** Android
+
+Android phones and tablets are connected to the cloister VPN via the
+following process.  Note that they do not appear in the set of
+~campus~ hosts, are not configured by Ansible, and do not appear in
+the host inventory.
+
+  - Create a new client certificate and campus/public OpenVPN
+    configurations for the new abbey Android.
+  - Copy the =campus.ovpn= and =public.ovpn= files to a USB drive.
+  - On the Android machine:
+  - Connect to the cloister Wi-Fi.
+  - Install [[https://f-droid.org][F-Droid]] and use it to install OpenVPN.
+  - Connect the USB drive, perhaps with an OTG (On The Go) adapter,
+    and open the =campus.ovpn= file.  The file should be opened with
+    the OpenVPN app, which will appear to ask for confirmation before
+    creating the new VPN.
+  - Open the =public.ovpn= file and create a second VPN.
+
+The =.ovpn= files must be transferred to the Android via a secure
+medium: the ~scp~ command, a USB drive, a cloud download, or perhaps
+an encrypted email.  In the following commands, the files are copied
+to a USB drive labeled ~Transfers~.  After insertion into the Android,
+its "storage" is viewed with the Files app, which should launch
+OpenVPN when a =.ovpn= file is opened.
+
+#+BEGIN_SRC sh
+./abbey client android dicks-tablet dick
+cp campus.ovpn public.ovpn /media/sysadm/Transfers/
+rm campus.ovpn public.ovpn
+#+END_SRC
+
+** Create Wireless Domain Name
+
+A wireless machine is assigned a Wi-Fi address when it connects to the
+cloister Wi-Fi, and a "VPN address" when it connects to Gate's OpenVPN
+server.  The VPN address can be discovered by running ~ip addr show
+dev ovpn~ on the machine, or inspecting =/etc/openvpn/ipp.txt= on
+Gate.  Once discovered, a private domain name,
+e.g. ~new.birchwood.private~, can be associated with the VPN address,
+e.g ~10.84.138.7~.  The administrator adds a line like the following
+to =private/db.domain= and increments the serial number at the top of
+the file.
+
+#+BEGIN_SRC conf
+new    IN      A       10.84.138.7
+#+END_SRC
+
+The administrator also creates the reverse mapping by adding a line
+like the following to =private/db.campus_vpn= and incrementing the
+serial number at the top of that file.
+
+#+BEGIN_SRC conf
+7      IN      PTR     new.birchwood.private.
+#+END_SRC
+
+After ~./abbey config core~ updates Core, the administrator can test
+resolution of the new name.
+
+#+BEGIN_SRC sh
+resolvectl query new.birchwood.private.
+resolvectl query 10.84.138.7
+#+END_SRC
+
+A wireless device with no Ethernet interface and unable to run OpenVPN
+gets just a Wi-Fi address.  It can be given a private domain name
+(e.g. ~new.birchwood.private~) associated with the Wi-Fi address
+(e.g. ~192.168.10.225~), but a reverse lookup on a machine connected
+to the Wi-Fi may yield a name like ~new.lan~ (provided by the access
+point) while elsewhere (e.g. on the cloister Ethernet) the IP address
+will not resolve at all.  (There is no "reverse mapping" to be added
+to =private/db.campus_vpn=.)
diff --git a/abbey b/abbey
new file mode 100755 (executable)
index 0000000..db426cc
--- /dev/null
+++ b/abbey
@@ -0,0 +1,60 @@
+#!/usr/bin/perl -w
+#
+# DO NOT EDIT.  This file was tangled from README.org.
+
+use strict;
+
+if ($ARGV[0] eq "config") {
+  exec "./Institute/inst", @ARGV;
+}
+if ($ARGV[0] eq "new") {
+  exec "./Institute/inst", @ARGV;
+}
+if ($ARGV[0] eq "old") {
+  exec "./Institute/inst", @ARGV;
+}
+if ($ARGV[0] eq "pass") {
+  exec "./Institute/inst", @ARGV;
+}
+if ($ARGV[0] eq "client") {
+  exec "./Institute/inst", @ARGV;
+}
+
+if ($ARGV[0] eq "upgrade") {
+  shift;
+  my @args = ( "-e", "\@Secret/become.yml" );
+  if (defined $ARGV[0] && $ARGV[0] eq "-n") {
+    shift;
+    push @args, "--check", "--diff";
+  }
+  if (defined $ARGV[0]) {
+    my $limit = $ARGV[0];
+    shift;
+    die "illegal characters: $limit"
+      if $limit !~ /^!?[a-z][-a-z0-9,!]+$/;
+    push @args, "-l", $limit;
+  }
+  exec ("ansible-playbook", @args, "playbooks/upgrade.yml");
+}
+
+if ($ARGV[0] eq "reboots") {
+  exec ("ansible-playbook", "-e", "\@Secret/become.yml",
+       "playbooks/reboots.yml");
+}
+
+if ($ARGV[0] eq "versions") {
+  exec ("ansible-playbook", "-e", "\@Secret/become.yml",
+       "playbooks/versarch.yml");
+}
+
+if ($ARGV[0] eq "tz") {
+  my $city = `cat /etc/timezone`; chomp $city;
+  my $zone = `date +%Z`; chomp $zone;
+  print "Setting timezones to $city.\n";
+  exec ("ansible-playbook", "-e", "\@Secret/become.yml",
+       "-e", "zone=$zone", "-e", "city=$city",
+       "playbooks/timezone.yml");
+}
+
+die
+  "usage: $0 [config,new,old,pass,client,upgrade,reboots,versions]\n";
diff --git a/ansible.cfg b/ansible.cfg
new file mode 100644 (file)
index 0000000..89d90fa
--- /dev/null
@@ -0,0 +1,5 @@
+[defaults]
+interpreter_python=/usr/bin/python3
+vault_password_file=Secret/vault-password
+inventory=hosts
+roles_path=roles:Institute/roles
diff --git a/hosts b/hosts
new file mode 100644 (file)
index 0000000..68b3df1
--- /dev/null
+++ b/hosts
@@ -0,0 +1,68 @@
+all:
+  vars:
+    ansible_user: sysadm
+    ansible_ssh_extra_args: -i Secret/ssh_admin/id_rsa
+  hosts:
+    # The Main Servers: Front, Gate and Core.
+    droplet:
+      ansible_host: 159.65.75.60
+      ansible_become_password: "{{ become_droplet }}"
+    anoat:
+      ansible_host: {{ gate_addr }}
+      ansible_become_password: "{{ become_anoat }}"
+    dantooine:
+      ansible_host: {{ core_addr }}
+      ansible_become_password: "{{ become_dantooine }}"
+    # WebTVs (Desktops)
+    devaron:
+    kamino:
+      ansible_become_password: "{{ become_kamino }}"
+    kessel:
+      ansible_become_password: "{{ become_kessel }}"
+    # Notebooks
+    endor:
+      ansible_become_password: "{{ become_endor }}"
+    geonosis:
+      ansible_host: 127.0.0.1
+      ansible_user: matt
+      ansible_become_password: "{{ become_geonosis }}"
+      postfix_mydestination: >-
+        geonosis.{{ domain_priv }}
+        geonosis
+        geonosis.localdomain
+        localhost.localdomain
+        localhost
+  children:
+    front:
+      hosts:
+        droplet:
+    gate:
+      hosts:
+        anoat:
+    core:
+      hosts:
+        dantooine:
+    campus:
+      hosts:
+        anoat:
+        devaron:
+        kamino:
+        kessel:
+    weather:
+      hosts:
+        anoat:
+    dvrs:
+      hosts:
+        dantooine:
+    tvrs:
+      hosts:
+        dantooine:
+    notebooks:
+      hosts:
+        endor:
+        geonosis:
+    builders:
+      hosts:
+        devaron:
+        geonosis:
+        kamino:
diff --git a/jquery.js b/jquery.js
new file mode 120000 (symlink)
index 0000000..3df239e
--- /dev/null
+++ b/jquery.js
@@ -0,0 +1 @@
+Institute/jquery.js
\ No newline at end of file
diff --git a/org.css b/org.css
new file mode 120000 (symlink)
index 0000000..e04f852
--- /dev/null
+++ b/org.css
@@ -0,0 +1 @@
+Institute/org.css
\ No newline at end of file
diff --git a/org.js b/org.js
new file mode 120000 (symlink)
index 0000000..f691465
--- /dev/null
+++ b/org.js
@@ -0,0 +1 @@
+Institute/org.js
\ No newline at end of file
diff --git a/playbooks/check-inst-vars.yml b/playbooks/check-inst-vars.yml
new file mode 100644 (file)
index 0000000..f22b887
--- /dev/null
@@ -0,0 +1 @@
+- import_playbook: ../Institute/playbooks/check-inst-vars.yml
diff --git a/playbooks/reboots.yml b/playbooks/reboots.yml
new file mode 100644 (file)
index 0000000..f40e964
--- /dev/null
@@ -0,0 +1,11 @@
+---
+- hosts: all
+  tasks:
+
+  - stat:
+      path: /run/reboot-required
+    register: st
+
+  - debug:
+      msg: Reboot required.
+    when: st.stat.exists
diff --git a/playbooks/site.yml b/playbooks/site.yml
new file mode 100644 (file)
index 0000000..a1d9059
--- /dev/null
@@ -0,0 +1,28 @@
+---
+- name: Configure Front
+  hosts: front
+  roles: [ front, abbey-front ]
+
+- name: Configure Core
+  hosts: core
+  roles: [ core, abbey-core ]
+
+- name: Configure Gate
+  hosts: gate
+  roles: [ gate ]
+
+- name: Configure Campus
+  hosts: campus
+  roles: [ campus, abbey-cloister ]
+
+- name: Configure Weather
+  hosts: weather
+  roles: [ abbey-weather ]
+
+- name: Configure DVRs
+  hosts: dvrs
+  roles: [ abbey-dvr ]
+
+- name: Configure TVRs
+  hosts: tvrs
+  roles: [ abbey-tvr ]
diff --git a/playbooks/timezone.yml b/playbooks/timezone.yml
new file mode 100644 (file)
index 0000000..db889c0
--- /dev/null
@@ -0,0 +1,48 @@
+---
+- hosts: core, dvrs, tvrs
+  tasks:
+  - name: Update timezone.
+    become: yes
+    command: timedatectl set-timezone {{ city }}
+    when: ansible_date_time.tz != zone
+    register: new_tz
+  - debug: msg={{ new_tz }}
+
+- hosts: dvrs
+  tasks:
+  - name: Restart Zoneminder.
+    become: yes
+    systemd:
+      service: "{{ item }}"
+      restarted: yes
+    loop: [ mysql, zoneminder ]
+    when: new_tz.changed
+
+- hosts: tvrs
+  tasks:
+  - name: Restart MythTV.
+    become: yes
+    systemd:
+      service: "{{ item }}"
+      restarted: yes
+    loop: [ mysql, mythtv-backend ]
+    when: new_tz.changed
+
+- hosts: core
+  tasks:
+  - name: Update PHP date.timezone.
+    become: yes
+    lineinfile:
+      regexp: date.timezone ?=
+      line: date.timezone = {{ city }}
+      path: "{{ item }}"
+    loop:
+    - /etc/php/7.4/cli/php.ini
+    - /etc/php/7.4/apache2/php.ini
+    notify: Restart Apache2.
+  handlers:
+  - name: Restart Apache2.
+    become: yes
+    systemd:
+      service: apache2
+      state: restarted
diff --git a/playbooks/upgrade.yml b/playbooks/upgrade.yml
new file mode 100644 (file)
index 0000000..f9aa427
--- /dev/null
@@ -0,0 +1,21 @@
+- hosts: all
+  tasks:
+
+  - name: Upgrade packages.
+    become: yes
+    apt:
+      update_cache: yes
+      upgrade: full
+      autoremove: yes
+      purge: yes
+      autoclean: yes
+
+  - name: Check for /run/reboot-required.
+    stat:
+      path: /run/reboot-required
+    no_log: true
+    register: st
+
+  - debug:
+      msg: Reboot required.
+    when: st.stat.exists
diff --git a/playbooks/versarch.yml b/playbooks/versarch.yml
new file mode 100644 (file)
index 0000000..5a4ea69
--- /dev/null
@@ -0,0 +1,7 @@
+- hosts: all
+  tasks:
+  - debug:
+      msg: >-
+        {{ ansible_distribution }}
+        {{ ansible_distribution_version }}
+        {{ ansible_architecture }}
diff --git a/private_ex/vars-abbey.yml b/private_ex/vars-abbey.yml
new file mode 100644 (file)
index 0000000..c2cc595
--- /dev/null
@@ -0,0 +1,4 @@
+---
+zoneminder_dbpass:           gakJopbikJadsEdd
+
+mythtv_dbpass:           daJkibpoJkag
diff --git a/public/vars.yml b/public/vars.yml
new file mode 100644 (file)
index 0000000..ed02b8a
--- /dev/null
@@ -0,0 +1,7 @@
+---
+domain_name: birchwood-abbey.net
+domain_priv: birchwood.private
+
+full_name: Birchwood Abbey
+
+front_addr: 159.65.75.60
diff --git a/publish b/publish
new file mode 100755 (executable)
index 0000000..7b4f76e
--- /dev/null
+++ b/publish
@@ -0,0 +1,34 @@
+#!/bin/bash -e
+#
+# ./publish [-f]
+
+cd ~/Network/Abbey/
+
+errs=0
+
+if ! grep -HnE '^[#*].*TODO|\?\?\?' Institute/README.org
+then errs=$(( $errs + 1 )); fi
+
+if ! grep -HnE '^[*#].*TODO|\?\?\?' README.org
+then errs=$(( $errs + 1 )); fi
+
+if [ $errs != 0 ]; then echo ""; fi
+
+function silence {
+  grep -vE "^($PUNT)\$"
+}
+PUNT="Indentation variables are now local\\."
+PUNT="$PUNT|(Setting up indent|Indentation setup) for shell type bash"
+
+EARGS="--no-init-file --batch --script publish.el"
+
+if [ "$#" == 1 -a "$1" == "-f" ]
+then
+    emacs $EARGS -f pub-forced |& silence
+elif [ "$#" == 0 ]
+then
+    emacs $EARGS -f pub |& silence
+else
+    echo "usage: ./publish [-f]"
+    exit 1
+fi
diff --git a/publish.el b/publish.el
new file mode 100644 (file)
index 0000000..f47dcf8
--- /dev/null
@@ -0,0 +1,56 @@
+;;; The Network documentation: ~/Network/Abbey/**/*.org -> *.html
+
+(add-to-list 'load-path (expand-file-name "~/Emacs/emacs-htmlize"))
+(add-to-list 'load-path (expand-file-name "~/Emacs/org-mode/lisp"))
+(require 'org)
+
+(setq org-publish-project-alist
+      '(("Network"
+        :base-directory "~/Network/Abbey/"
+        :publishing-directory "~/Network/Abbey/"
+        :recursive t
+;;;         :publishing-function org-html-publish-to-abbey-html
+         :publishing-function org-html-publish-to-html
+         ;;:section-numbers nil
+         :headline-levels 4
+         :with-toc nil
+        :html-head-include-default-style nil
+        :html-head-include-scripts nil
+        :html-head "<link rel=\"stylesheet\" href=\"org.css\">
+<script language=\"javascript\" type=\"text/javascript\" src=\"jquery.js\"></script>
+<script language=\"javascript\" type=\"text/javascript\" src=\"org.js\"></script>
+"
+        :broken-links 'mark)))
+
+(require 'ox)
+(setq org-html-htmlize-output-type 'css)
+
+;; Hack rendition of verbatim text.  Use Q rather than CODE.
+(let ((cons (assq 'verbatim org-html-text-markup-alist)))
+  (if (not cons) (error "verbatim not found!"))
+  (setcdr cons "<q>%s</q>"))
+
+;;;(defun abbey-html-src-block (src-block contents info)
+;;;  "Transcode a SRC-BLOCK element from Org to ASCII.
+;;;CONTENTS is nil.  INFO is a plist used as a communication
+;;;channel."
+;;;  (let ((filename (org-element-property :tangle src-block)))
+;;;    (concat "Excerpt of "filename":</br>\n"
+;;;        (org-element-normalize-string
+;;;         (org-export-format-code-default src-block info)))))
+;;;
+;;;(org-export-define-derived-backend 'abbey-html 'html
+;;;  :translate-alist '((src-block . abbey-html-src-block)))
+;;;
+;;;(defun org-html-publish-to-abbey-html (plist filename pub-dir)
+;;;  "Publish an org file to Abbey HTML.
+;;;
+;;;FILENAME is the filename of the Org file to be published.  PLIST
+;;;is the property list for the given project.  PUB-DIR is the
+;;;publishing directory.
+;;;
+;;;Return output file name."
+;;;  (org-publish-org-to 'abbey-html filename ".html" plist pub-dir))
+
+(defun pub () (org-publish "Network"))
+(defun pub-forced () (org-publish "Network" 't))
diff --git a/roles_t/abbey-cloister/handlers/main.yml b/roles_t/abbey-cloister/handlers/main.yml
new file mode 100644 (file)
index 0000000..927f9e4
--- /dev/null
@@ -0,0 +1,5 @@
+- name: Reload NRPE server.
+  become: yes
+  systemd:
+    service: nagios-nrpe-server
+    state: reloaded
diff --git a/roles_t/abbey-cloister/tasks/main.yml b/roles_t/abbey-cloister/tasks/main.yml
new file mode 100644 (file)
index 0000000..f326a8b
--- /dev/null
@@ -0,0 +1,29 @@
+---
+- name: Use the local Apt package cache.
+  become: yes
+  copy:
+    content: |
+     Acquire::http::Proxy "http://apt-cacher.{{ domain_priv }}.:3142";
+    dest: /etc/apt/apt.conf.d/01proxy
+    mode: u=rw,g=r,o=r
+
+- name: Install abbey_pisensors NAGIOS plugin.
+  become: yes
+  copy:
+    src: ../abbey-core/files/abbey_pisensors
+    dest: /usr/local/sbin/abbey_pisensors
+    mode: u=rwx,g=rx,o=rx
+  when: ansible_architecture == 'aarch64'
+
+- name: Configure NAGIOS command.
+  become: yes
+  copy:
+    content: |
+      command[abbey_pisensors]=/usr/local/sbin/abbey_pisensors
+    dest: /etc/nagios/nrpe.d/abbey.cfg
+  when: ansible_architecture == 'aarch64'
+  notify: Reload NRPE server.
+
+- name: Install monastic software.
+  become: yes
+  apt: pkg=emacs
diff --git a/roles_t/abbey-core/files/abbey_pisensors b/roles_t/abbey-core/files/abbey_pisensors
new file mode 100644 (file)
index 0000000..e50d134
--- /dev/null
@@ -0,0 +1,76 @@
+#!/bin/sh
+
+PATH="/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin"
+export PATH
+PROGNAME=`basename $0`
+REVISION="2.3.1"
+
+. /usr/lib/nagios/plugins/utils.sh
+
+print_usage() {
+       echo "Usage: $PROGNAME" [--ignore-fault]
+}
+
+print_help() {
+       print_revision $PROGNAME $REVISION
+       echo ""
+       print_usage
+       echo ""
+       echo "This plugin checks hardware status using the lm_sensors package."
+       echo ""
+       support
+       exit $STATE_OK
+}
+
+brief_data() {
+    echo "$1" | sed -n -E -e '
+  /^temp[0-9]+: +[-+][0-9.]+°C/ { s/^temp[0-9]+: +([-+][0-9.]+)°C.*/ \1/; H }
+  $ { x; s/\n//g; p }'
+}
+
+case "$1" in
+       --help)
+               print_help
+               exit $STATE_OK
+               ;;
+       -h)
+               print_help
+               exit $STATE_OK
+               ;;
+       --version)
+               print_revision $PROGNAME $REVISION
+               exit $STATE_OK
+               ;;
+       -V)
+               print_revision $PROGNAME $REVISION
+               exit $STATE_OK
+               ;;
+       *)
+               sensordata=`sensors 2>&1`
+               status=$?
+               if test ${status} -eq 127; then
+                       text="SENSORS UNKNOWN - command not found"
+                       text="$text (did you install lmsensors?)"
+                       exit=$STATE_UNKNOWN
+               elif test ${status} -ne 0; then
+                       text="WARNING - sensors returned state $status"
+                       exit=$STATE_WARNING
+               elif echo ${sensordata} | egrep ALARM > /dev/null; then
+                       text="SENSOR CRITICAL -`brief_data "${sensordata}"`"
+                       exit=$STATE_CRITICAL
+               elif echo ${sensordata} | egrep FAULT > /dev/null \
+                   && test "$1" != "-i" -a "$1" != "--ignore-fault"; then
+                       text="SENSOR UNKNOWN - Sensor reported fault"
+                       exit=$STATE_UNKNOWN
+               else
+                       text="SENSORS OK -`brief_data "${sensordata}"`"
+                       exit=$STATE_OK
+               fi
+
+               echo "$text"
+               if test "$1" = "-v" -o "$1" = "--verbose"; then
+                       echo ${sensordata}
+               fi
+               exit $exit
+               ;;
+esac
diff --git a/roles_t/abbey-core/handlers/main.yml b/roles_t/abbey-core/handlers/main.yml
new file mode 100644 (file)
index 0000000..23e7299
--- /dev/null
@@ -0,0 +1,20 @@
+---
+- name: New aliases.
+  become: yes
+  command: newaliases
+
+- name: Restart git daemon.
+  become: yes
+  command: systemctl restart git-daemon
+
+- name: Restart Apache2.
+  become: yes
+  systemd:
+    service: apache2
+    state: restarted
+
+- name: Reload NAGIOS4.
+  become: yes
+  systemd:
+    service: nagios4
+    state: reloaded
diff --git a/roles_t/abbey-core/tasks/main.yml b/roles_t/abbey-core/tasks/main.yml
new file mode 100644 (file)
index 0000000..627d630
--- /dev/null
@@ -0,0 +1,315 @@
+---
+- name: Install additional packages.
+  apt:
+    pkg: [ libhtml-tree-perl, libjs-jquery, mit-scheme, gnuplot ]
+
+- name: Install abbey email aliases.
+  become: yes
+  blockinfile:
+    block: |
+        sysadm:                matt
+        house:         sysadm
+        mythtv:                sysadm
+        scanner:       sysadm
+    dest: /etc/aliases
+    marker: "# {mark} ABBEY MANAGED BLOCK"
+  notify: New aliases.
+
+- name: Install git daemon.
+  become: yes
+  apt: pkg=git-daemon-sysvinit
+
+- name: Configure git daemon.
+  become: yes
+  lineinfile:
+    path: /etc/default/git-daemon
+    regexp: "{{ item.patt }}"
+    line: "{{ item.line }}"
+  loop:
+  - patt: '^GIT_DAEMON_ENABLE *='
+    line: 'GIT_DAEMON_ENABLE=true'
+  - patt: '^GIT_DAEMON_OPTIONS *='
+    line: 'GIT_DAEMON_OPTIONS="--user-path=Public/Git"'
+  - patt: '^GIT_DAEMON_BASE_PATH *='
+    line: 'GIT_DAEMON_BASE_PATH="/var/www/git"'
+  - patt: '^GIT_DAEMON_DIRECTORY *='
+    line: 'GIT_DAEMON_DIRECTORY=" "'
+  notify: Restart git daemon.
+
+- name: Create /var/www/git/.
+  become: yes
+  file:
+    path: /var/www/git
+    state: directory
+    group: staff
+    mode: u=rwx,g=srwx,o=rx
+
+- name: Configure live website.
+  become: yes
+  vars:
+    docroot: /WWW/live
+  copy:
+    content: |
+        <Directory {{ docroot }}/Abbey/>
+            AllowOverride Indexes FileInfo
+            Options +Indexes +FollowSymLinks
+        </Directory>
+        
+        RedirectMatch /Photos$ /Photos/
+        RedirectMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])$ \
+                      /Photos/$1_$2_$3/
+        AliasMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])/(.+)$ \
+                   {{ docroot }}/Photos/$1/$2/$3/$4
+        AliasMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])/$ \
+                   {{ docroot }}/Photos/$1/$2/$3/index.html
+        AliasMatch /Photos/$ {{ docroot }}/Photos/index.html
+        
+        Alias /gitweb-static/ /usr/share/gitweb/static/
+        <Directory "/usr/share/gitweb/static/">
+            Options MultiViews
+        </Directory>
+        RewriteEngine on
+        RewriteRule ^/gitweb(\.cgi)?(/.*)?$ \
+                    /cgi-bin/gitweb.cgi$2 [QSA,L,PT]
+        RewriteRule ^/\~([^\/]+)/gitweb(\.cgi)?(/.*)?$ \
+                    /cgi-bin/gitweb.cgi$3 \
+                    [QSA,E=GITWEB_PROJECTROOT:/home/$1/Public/Git/,L,PT]
+        
+        ScriptAlias /cgit/ /usr/lib/cgit/cgit.cgi/
+        Alias /cgit-css/ /usr/share/cgit/
+        <Directory "/usr/lib/cgit/">
+           AllowOverride None
+           Options ExecCGI FollowSymlinks
+           Require all granted
+        </Directory>
+        RewriteRule ^/cgit?(/.*)$ \
+                    /cgit$1 [QSA,E=CGIT_SCANPATH:/var/www/git/,L,PT]
+        RewriteRule ^/\~([^\/]+)/cgit(/.*)?$ \
+                    /cgit$2 [QSA,E=CGIT_SCANPATH:/home/$1/Public/Git/,L,PT]
+    dest: /etc/apache2/sites-available/live-vhost.conf
+    mode: u=rw,g=r,o=r
+  notify: Restart Apache2.
+
+- name: Configure test website.
+  become: yes
+  vars:
+    docroot: /WWW/test
+  copy:
+    content: |
+        <Directory {{ docroot }}/Abbey/>
+            AllowOverride Indexes FileInfo
+            Options +Indexes +FollowSymLinks
+        </Directory>
+        
+        RedirectMatch /Photos$ /Photos/
+        RedirectMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])$ \
+                      /Photos/$1_$2_$3/
+        AliasMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])/(.+)$ \
+                   {{ docroot }}/Photos/$1/$2/$3/$4
+        AliasMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])/$ \
+                   {{ docroot }}/Photos/$1/$2/$3/index.html
+        AliasMatch /Photos/$ {{ docroot }}/Photos/index.html
+        
+        Alias /gitweb-static/ /usr/share/gitweb/static/
+        <Directory "/usr/share/gitweb/static/">
+            Options MultiViews
+        </Directory>
+        RewriteEngine on
+        RewriteRule ^/gitweb(\.cgi)?(/.*)?$ \
+                    /cgi-bin/gitweb.cgi$2 [QSA,L,PT]
+        RewriteRule ^/\~([^\/]+)/gitweb(\.cgi)?(/.*)?$ \
+                    /cgi-bin/gitweb.cgi$3 \
+                    [QSA,E=GITWEB_PROJECTROOT:/home/$1/Public/Git/,L,PT]
+        
+        ScriptAlias /cgit/ /usr/lib/cgit/cgit.cgi/
+        Alias /cgit-css/ /usr/share/cgit/
+        <Directory "/usr/lib/cgit/">
+           AllowOverride None
+           Options ExecCGI FollowSymlinks
+           Require all granted
+        </Directory>
+        RewriteRule ^/cgit?(/.*)$ \
+                    /cgit$1 [QSA,E=CGIT_SCANPATH:/var/www/git/,L,PT]
+        RewriteRule ^/\~([^\/]+)/cgit(/.*)?$ \
+                    /cgit$2 [QSA,E=CGIT_SCANPATH:/home/$1/Public/Git/,L,PT]
+    dest: /etc/apache2/sites-available/test-vhost.conf
+    mode: u=rw,g=r,o=r
+  notify: Restart Apache2.
+
+- name: Enable Apache2 rewrite module for Gitweb.
+  become: yes
+  apache2_module: name=rewrite
+  notify: Restart Apache2.
+
+- name: Enable Apache2 cgid module for Gitweb (Ubuntu).
+  become: yes
+  apache2_module: name=cgid
+  when: ansible_distribution == 'Ubuntu'
+  notify: Restart Apache2.
+
+- name: Enable Apache2 cgi module for Gitweb (Debian).
+  become: yes
+  apache2_module: name=cgi
+  when: ansible_distribution == 'Debian'
+  notify: Restart Apache2.
+
+- name: Install libcgi-pm-perl for Gitweb.
+  become: yes
+  apt: pkg=libcgi-pm-perl
+
+- name: Link Gitweb into /cgi-bin/.
+  become: yes
+  file:
+    state: link
+    path: /usr/lib/cgi-bin/{{ item }}
+    src: /usr/share/gitweb/{{ item }}
+  loop: [ gitweb.cgi, index.cgi ]
+
+- name: Override Gitweb assets location.
+  become: yes
+  copy:
+    content: |
+      $projectroot = $ENV{'GITWEB_PROJECTROOT'} || "/var/www/git";
+      @stylesheets = ("/gitweb-static/gitweb.css");
+      $logo = "/gitweb-static/git-logo.png";
+      $favicon = "/favicon.ico";
+      $javascript = "/gitweb-static/gitweb.js";
+    dest: /etc/gitweb.conf
+    mode: u=rw,g=r,o=r
+
+- name: Install CGit.
+  become: yes
+  apt: pkg=cgit
+
+- name: Disable CGit default configuration.
+  become: yes
+  command:
+    cmd: a2disconf -q cgit
+    removes: /etc/apache2/conf-enabled/cgit.conf
+
+- name: Override CGit scan path.
+  become: yes
+  lineinfile:
+    path: /etc/cgitrc
+    regexp: "^scan-path *="
+    line: "scan-path=$CGIT_SCANPATH"
+  notify: Reload Apache2.
+
+- name: Configure house website.
+  become: yes
+  copy:
+    content: |
+      Alias /doc /usr/share/doc
+      <Directory /usr/share/doc/>
+          Options Indexes
+      </Directory>
+      
+      Alias /gitweb-static/ /usr/share/gitweb/static/
+      <Directory "/usr/share/gitweb/static/">
+          Options MultiViews
+      </Directory>
+      RewriteEngine on
+      RewriteRule ^/gitweb(\.cgi)?(/.*)?$ \
+                  /cgi-bin/gitweb.cgi$2 [QSA,L,PT]
+      RewriteRule ^/\~([^\/]+)/gitweb(\.cgi)?(/.*)?$ \
+                  /cgi-bin/gitweb.cgi$3 \
+                  [QSA,E=GITWEB_PROJECTROOT:/home/$1/Public/Git/,L,PT]
+      
+      ScriptAlias /cgit/ /usr/lib/cgit/cgit.cgi/
+      Alias /cgit-css/ /usr/share/cgit/
+      <Directory "/usr/lib/cgit/">
+         AllowOverride None
+         Options ExecCGI FollowSymlinks
+         Require all granted
+      </Directory>
+      RewriteRule ^/cgit?(/.*)$ \
+                  /cgit$1 [QSA,E=CGIT_SCANPATH:/var/www/git/,L,PT]
+      RewriteRule ^/\~([^\/]+)/cgit(/.*)?$ \
+                  /cgit$2 [QSA,E=CGIT_SCANPATH:/home/$1/Public/Git/,L,PT]
+    dest: /etc/apache2/sites-available/www-vhost.conf
+    mode: u=rw,g=r,o=r
+  notify: Restart Apache2.
+
+- name: Install Apt-Cacher:TNG.
+  become: yes
+  apt: pkg=apt-cacher-ng
+
+- name: Use the local Apt package cache.
+  become: yes
+  copy:
+    content: |
+     Acquire::http::Proxy "http://apt-cacher.{{ domain_priv }}.:3142";
+    dest: /etc/apt/apt.conf.d/01proxy
+    mode: u=rw,g=r,o=r
+
+- name: Configure NAGIOS monitoring for Core /home/.
+  become: yes
+  copy:
+    content: |
+      define service {
+          use                     local-service
+          host_name               core
+          service_description     Home Partition
+          check_command           check_local_disk!20%!10%!/home
+      }
+    dest: /etc/nagios4/conf.d/abbey.cfg
+  notify: Reload NAGIOS4.
+
+- name: Configure cloister NAGIOS monitoring.
+  become: yes
+  template:
+    src: nagios-{{ item }}.cfg
+    dest: /etc/nagios4/conf.d/{{ item }}.cfg
+  loop: [ devaron, kamino, kessel ]
+  notify: Reload NAGIOS4.
+
+- name: Install Analog.
+  become: yes
+  apt: pkg=analog
+
+- name: Configure Analog (removing old /var/log/apache/ LOGFILEs).
+  become: yes
+  lineinfile:
+    path: /etc/analog.cfg
+    regexp: '^LOGFILE /var/log/apache/'
+    state: absent
+
+- name: Configure Analog (adding new configuration lines).
+  become: yes
+  lineinfile:
+    path: /etc/analog.cfg
+    line: "{{ item }}"
+    insertafter: EOF
+  loop:
+  - "LOGFILE /Logs/apache2-public/*-access.log.gz"
+  - "ALLCHART OFF"
+  - "DNS WRITE"
+  - "HOSTNAME \"{{ full_name }}\""
+  - "OUTFILE /WWW/campus/analog.html"
+
+- name: Create /Logs/.
+  become: yes
+  file:
+    path: /Logs
+    state: directory
+    mode: u=rwx,g=rx,o=rx
+
+- name: Create /Logs/apache2-public/.
+  become: yes
+  file:
+    path: /Logs/apache2-public
+    state: directory
+    owner: monkey
+    group: staff
+    mode: u=rwx,g=srwx,o=rx
+
+- name: Add Monkey to Nextcloud group.
+  become: yes
+  user:
+    name: monkey
+    append: yes
+    groups: www-data
+
+- name: Install netpbm.
+  become: yes
+  apt: pkg=netpbm
diff --git a/roles_t/abbey-core/templates/nagios-devaron.cfg b/roles_t/abbey-core/templates/nagios-devaron.cfg
new file mode 100644 (file)
index 0000000..27cd1e2
--- /dev/null
@@ -0,0 +1,47 @@
+define host {
+    use                     linux-server
+    host_name               devaron
+    address                 {{ devaron_addr }}
+}
+
+define service {
+    use                     generic-service
+    host_name               devaron
+    service_description     Root Partition
+    check_command           check_nrpe!inst_root
+}
+
+# define service {
+#     use                     generic-service
+#     host_name               devaron
+#     service_description     Current Load
+#     check_command           check_nrpe!check_load
+# }
+
+define service {
+    use                     generic-service
+    host_name               devaron
+    service_description     Zombie Processes
+    check_command           check_nrpe!check_zombie_procs
+}
+
+# define service {
+#     use                     generic-service
+#     host_name               devaron
+#     service_description     Total Processes
+#     check_command           check_nrpe!check_total_procs
+# }
+
+define service {
+    use                     generic-service
+    host_name               devaron
+    service_description     Swap Usage
+    check_command           check_nrpe!inst_swap
+}
+
+define service {
+    use                     generic-service
+    host_name               devaron
+    service_description     Temperature Sensors
+    check_command           check_nrpe!abbey_pisensors
+}
diff --git a/roles_t/abbey-core/templates/nagios-kamino.cfg b/roles_t/abbey-core/templates/nagios-kamino.cfg
new file mode 100644 (file)
index 0000000..410b980
--- /dev/null
@@ -0,0 +1,47 @@
+define host {
+    use                     linux-server
+    host_name               kamino
+    address                 {{ kamino_addr }}
+}
+
+define service {
+    use                     generic-service
+    host_name               kamino
+    service_description     Root Partition
+    check_command           check_nrpe!inst_root
+}
+
+define service {
+    use                     generic-service
+    host_name               kamino
+    service_description     Current Load
+    check_command           check_nrpe!check_load
+}
+
+define service {
+    use                     generic-service
+    host_name               kamino
+    service_description     Zombie Processes
+    check_command           check_nrpe!check_zombie_procs
+}
+
+# define service {
+#     use                     generic-service
+#     host_name               kamino
+#     service_description     Total Processes
+#     check_command           check_nrpe!check_total_procs
+# }
+
+define service {
+    use                     generic-service
+    host_name               kamino
+    service_description     Swap Usage
+    check_command           check_nrpe!inst_swap
+}
+
+define service {
+    use                     generic-service
+    host_name               kamino
+    service_description     Temperature Sensors
+    check_command           check_nrpe!inst_sensors
+}
diff --git a/roles_t/abbey-core/templates/nagios-kessel.cfg b/roles_t/abbey-core/templates/nagios-kessel.cfg
new file mode 100644 (file)
index 0000000..ffbd9f8
--- /dev/null
@@ -0,0 +1,47 @@
+define host {
+    use                     linux-server
+    host_name               kessel
+    address                 {{ kessel_addr }}
+}
+
+define service {
+    use                     generic-service
+    host_name               kessel
+    service_description     Root Partition
+    check_command           check_nrpe!inst_root
+}
+
+# define service {
+#     use                     generic-service
+#     host_name               kessel
+#     service_description     Current Load
+#     check_command           check_nrpe!check_load
+# }
+
+define service {
+    use                     generic-service
+    host_name               kessel
+    service_description     Zombie Processes
+    check_command           check_nrpe!check_zombie_procs
+}
+
+# define service {
+#     use                     generic-service
+#     host_name               kessel
+#     service_description     Total Processes
+#     check_command           check_nrpe!check_total_procs
+# }
+
+define service {
+    use                     generic-service
+    host_name               kessel
+    service_description     Swap Usage
+    check_command           check_nrpe!inst_swap
+}
+
+define service {
+    use                     generic-service
+    host_name               kessel
+    service_description     Temperature Sensors
+    check_command           check_nrpe!inst_sensors
+}
diff --git a/roles_t/abbey-dvr/handlers/main.yml b/roles_t/abbey-dvr/handlers/main.yml
new file mode 100644 (file)
index 0000000..aaada22
--- /dev/null
@@ -0,0 +1,12 @@
+---
+- name: Restart MySQL.
+  become: yes
+  systemd:
+    service: mysql
+    state: restarted
+
+- name: Restart Apache2.
+  become: yes
+  systemd:
+    service: apache2
+    state: restarted
diff --git a/roles_t/abbey-dvr/tasks/main.yml b/roles_t/abbey-dvr/tasks/main.yml
new file mode 100644 (file)
index 0000000..3bf4614
--- /dev/null
@@ -0,0 +1,106 @@
+---
+- name: Include private abbey variables.
+  include_vars: ../private/vars-abbey.yml
+
+- name: Install Zoneminder.
+  become: yes
+  apt: pkg=zoneminder
+
+- name: Enable Apache modules for Zoneminder.
+  become: yes
+  apache2_module:
+    name: "{{ item }}"
+  loop: [ cgi, rewrite, expires, headers ]
+  notify: Restart Apache2.
+
+- name: Enable Zoneminder Apache configuration.
+  become: yes
+  command:
+    cmd: a2enconf zoneminder
+    creates: /etc/apache2/conf-enabled/zoneminder.conf
+  notify: Restart Apache2.
+
+- name: Configure MySQL for Zoneminder.
+  become: yes
+  copy:
+    content: |
+      [mysqld]
+      sql_mode = NO_ENGINE_SUBSTITUTION
+    dest: /etc/mysql/conf.d/zoneminder.cnf
+  notify: Restart MySQL.
+
+- name: Configure PHP date.timezone.
+  become: yes
+  lineinfile:
+    regexp: date.timezone ?=
+    line: date.timezone = {{ lookup('file', '/etc/timezone') }}
+    path: "{{ item }}"
+  loop:
+  - /etc/php/7.4/cli/php.ini
+  - /etc/php/7.4/apache2/php.ini
+  notify: Restart Apache2.
+
+- name: Enable/Start Apache2.
+  become: yes
+  systemd:
+    service: apache2
+    enabled: yes
+    state: started
+
+- name: Use /var/log/zoneminder.log
+  become: yes
+  copy:
+    content: |
+      :programname,startswith,"zm" -/var/log/zoneminder.log
+      & stop
+    dest: /etc/rsyslog.d/40-zoneminder.conf
+
+- name: Test for /Zoneminder/.
+  stat:
+    path: /Zoneminder
+  register: zoneminder
+- debug:
+    msg: "/Zoneminder/ does not yet exist"
+  when: not zoneminder.stat.exists
+
+- name: Check /Zoneminder/.
+  become: yes
+  file:
+    state: directory
+    path: /Zoneminder
+    owner: www-data
+    group: www-data
+    mode: u=rwx,g=rx,o=rx
+  when: zoneminder.stat.exists
+
+- name: Link to /Zoneminder/.
+  become: yes
+  file:
+    state: link
+    src: /Zoneminder
+    path: /var/cache/zoneminder/events
+    force: yes
+    follow: no
+
+- name: Set /etc/zm/zm.conf permissions.
+  become: yes
+  file:
+    path: /etc/zm/zm.conf
+    owner: root
+    group: www-data
+    mode: u=rw,g=r,o=
+
+- name: Set Zoneminder passphrase.
+  become: yes
+  lineinfile:
+    regexp: '^ *ZM_DB_PASS *='
+    line: ZM_DB_PASS={{ zoneminder_dbpass }}
+    path: /etc/zm/zm.conf
+
+- name: Enable/Start Zoneminder.
+  become: yes
+  systemd:
+    service: zoneminder
+    enabled: yes
+    state: started
+  when: zoneminder.stat.exists
diff --git a/roles_t/abbey-front/files/certbot_logrotate b/roles_t/abbey-front/files/certbot_logrotate
new file mode 100644 (file)
index 0000000..9e20015
--- /dev/null
@@ -0,0 +1,6 @@
+/var/log/letsencrypt/*.log {
+    rotate 12
+    weekly
+    compress
+    missingok
+}
diff --git a/roles_t/abbey-front/files/cron.daily_letsencrypt b/roles_t/abbey-front/files/cron.daily_letsencrypt
new file mode 100644 (file)
index 0000000..24c4eef
--- /dev/null
@@ -0,0 +1,18 @@
+#!/bin/bash -e
+
+cd /etc/
+
+[ -d letsencrypt~ ] \
+&& diff -rq letsencrypt/ letsencrypt~/ \
+&& exit 0
+
+( echo "Subject: New /etc/letsencrypt/ on Droplet."
+  echo ""
+  tar czf - letsencrypt/ \
+  | gpg --encrypt --armor \
+       --trust-model always --recipient root@core ) \
+| sendmail root \
+|| exit $?
+
+rm -rf letsencrypt~
+cp -a letsencrypt letsencrypt~
diff --git a/roles_t/abbey-front/files/logrotate-mailer b/roles_t/abbey-front/files/logrotate-mailer
new file mode 100644 (file)
index 0000000..4505083
--- /dev/null
@@ -0,0 +1,31 @@
+#!/bin/bash -e
+
+if [ "$#" != 3 -o "$1" != "-s" ]; then
+    echo "usage: $0 -s subject recipient" 1>&2
+    exit 1
+fi
+
+D=`date -d yesterday "+%Y%m%d"`
+if [[ "$2" == *error.log* ]]; then
+    F="$D-error.log.gz"
+else
+    F="$D.log.gz"
+fi
+
+( echo "Subject: $2"
+  echo "Content-Type: multipart/mixed; boundary=\"boundary\""
+  echo "MIME-Version: 1.0"
+  echo ""
+  echo "--boundary"
+  echo "Content-Type: text/plain"
+  echo "Content-Transfer-Encoding: 8bit"
+  echo ""
+  echo "$F"
+  echo "--boundary"
+  echo "Content-Type: application/gzip; name=\"$F\""
+  echo "Content-Disposition: attachment; filename=\"$F\""
+  echo "Content-Transfer-Encoding: base64"
+  echo ""
+  gzip | base64
+  echo ""
+  echo "--boundary--" ) | sendmail "$3"
diff --git a/roles_t/abbey-front/files/logrotate-mailer.conf b/roles_t/abbey-front/files/logrotate-mailer.conf
new file mode 100644 (file)
index 0000000..41308e5
--- /dev/null
@@ -0,0 +1,5 @@
+[Service]
+ExecStart=
+ExecStart=/usr/sbin/logrotate \
+               --mail /usr/local/sbin/logrotate-mailer \
+               /etc/logrotate.conf
diff --git a/roles_t/abbey-front/handlers/main.yml b/roles_t/abbey-front/handlers/main.yml
new file mode 100644 (file)
index 0000000..0955bd0
--- /dev/null
@@ -0,0 +1,23 @@
+---
+- name: New aliases.
+  become: yes
+  command: newaliases
+
+- name: Restart git daemon.
+  become: yes
+  command: systemctl restart git-daemon
+
+- name: Restart Apache2.
+  become: yes
+  systemd:
+    service: apache2
+    state: restarted
+
+- name: Reload systemd.
+  become: yes
+  systemd:
+    daemon_reload: yes
+
+- name: Import root@core's public key.
+  become: yes
+  command: gpg --import ~/.gnupg-root-pub.pem
diff --git a/roles_t/abbey-front/tasks/main.yml b/roles_t/abbey-front/tasks/main.yml
new file mode 100644 (file)
index 0000000..d369815
--- /dev/null
@@ -0,0 +1,234 @@
+---
+- name: Install Emacs.
+  become: yes
+  apt: pkg=emacs
+
+- name: Install abbey email aliases.
+  become: yes
+  blockinfile:
+    block: |
+        sysadm:                matt
+        keymaster:     root
+        codemaster:    matt
+        all:           matt, lori, erica
+        elders:                matt, lori
+        rents:         elders
+        puck:          matt
+        abbess:                lori
+    dest: /etc/aliases
+    marker: "# {mark} ABBEY MANAGED BLOCK"
+  notify: New aliases.
+
+- name: Install git daemon.
+  become: yes
+  apt: pkg=git-daemon-sysvinit
+
+- name: Configure git daemon.
+  become: yes
+  lineinfile:
+    path: /etc/default/git-daemon
+    regexp: "{{ item.patt }}"
+    line: "{{ item.line }}"
+  loop:
+  - patt: '^GIT_DAEMON_ENABLE *='
+    line: 'GIT_DAEMON_ENABLE=true'
+  - patt: '^GIT_DAEMON_OPTIONS *='
+    line: 'GIT_DAEMON_OPTIONS="--user-path=Public/Git"'
+  - patt: '^GIT_DAEMON_BASE_PATH *='
+    line: 'GIT_DAEMON_BASE_PATH="/var/www/git"'
+  - patt: '^GIT_DAEMON_DIRECTORY *='
+    line: 'GIT_DAEMON_DIRECTORY=" "'
+  notify: Restart git daemon.
+
+- name: Create /var/www/git/.
+  become: yes
+  file:
+    path: /var/www/git
+    state: directory
+    group: staff
+    mode: u=rwx,g=srwx,o=rx
+
+- name: Configure Apache.
+  become: yes
+  vars:
+    docroot: /home/www
+  copy:
+    content: |
+        <Directory {{ docroot }}/Abbey/>
+            AllowOverride Indexes FileInfo
+            Options +Indexes +FollowSymLinks
+        </Directory>
+        
+        RedirectMatch /Photos$ /Photos/
+        RedirectMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])$ \
+                      /Photos/$1_$2_$3/
+        AliasMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])/(.+)$ \
+                   {{ docroot }}/Photos/$1/$2/$3/$4
+        AliasMatch /Photos/(20[0-9][0-9])_([0-9][0-9])_([0-9][0-9])/$ \
+                   {{ docroot }}/Photos/$1/$2/$3/index.html
+        AliasMatch /Photos/$ {{ docroot }}/Photos/index.html
+        
+        Alias /gitweb-static/ /usr/share/gitweb/static/
+        <Directory "/usr/share/gitweb/static/">
+            Options MultiViews
+        </Directory>
+        RewriteEngine on
+        RewriteRule ^/gitweb(\.cgi)?(/.*)?$ \
+                    /cgi-bin/gitweb.cgi$2 [QSA,L,PT]
+        RewriteRule ^/\~([^\/]+)/gitweb(\.cgi)?(/.*)?$ \
+                    /cgi-bin/gitweb.cgi$3 \
+                    [QSA,E=GITWEB_PROJECTROOT:/home/$1/Public/Git/,L,PT]
+        
+        ScriptAlias /cgit/ /usr/lib/cgit/cgit.cgi/
+        Alias /cgit-css/ /usr/share/cgit/
+        <Directory "/usr/lib/cgit/">
+           AllowOverride None
+           Options ExecCGI FollowSymlinks
+           Require all granted
+        </Directory>
+        RewriteRule ^/cgit?(/.*)$ \
+                    /cgit$1 [QSA,E=CGIT_SCANPATH:/var/www/git/,L,PT]
+        RewriteRule ^/\~([^\/]+)/cgit(/.*)?$ \
+                    /cgit$2 [QSA,E=CGIT_SCANPATH:/home/$1/Public/Git/,L,PT]
+        IncludeOptional /etc/letsencrypt/options-ssl-apache.conf
+    dest: /etc/apache2/sites-available/{{ domain_name }}-vhost.conf
+  notify: Restart Apache2.
+
+- name: Enable Apache2 rewrite module for Gitweb.
+  become: yes
+  apache2_module: name=rewrite
+  notify: Restart Apache2.
+
+- name: Enable Apache2 cgid module for Gitweb (Ubuntu).
+  become: yes
+  apache2_module: name=cgid
+  when: ansible_distribution == 'Ubuntu'
+  notify: Restart Apache2.
+
+- name: Enable Apache2 cgi module for Gitweb (Debian).
+  become: yes
+  apache2_module: name=cgi
+  when: ansible_distribution == 'Debian'
+  notify: Restart Apache2.
+
+- name: Install libcgi-pm-perl for Gitweb.
+  become: yes
+  apt: pkg=libcgi-pm-perl
+
+- name: Link Gitweb into /cgi-bin/.
+  become: yes
+  file:
+    state: link
+    path: /usr/lib/cgi-bin/{{ item }}
+    src: /usr/share/gitweb/{{ item }}
+  loop: [ gitweb.cgi, index.cgi ]
+
+- name: Override Gitweb assets location.
+  become: yes
+  copy:
+    content: |
+      $projectroot = $ENV{'GITWEB_PROJECTROOT'} || "/var/www/git";
+      @stylesheets = ("/gitweb-static/gitweb.css");
+      $logo = "/gitweb-static/git-logo.png";
+      $favicon = "/favicon.ico";
+      $javascript = "/gitweb-static/gitweb.js";
+    dest: /etc/gitweb.conf
+    mode: u=rw,g=r,o=r
+
+- name: Install CGit.
+  become: yes
+  apt: pkg=cgit
+
+- name: Disable CGit default configuration.
+  become: yes
+  command:
+    cmd: a2disconf -q cgit
+    removes: /etc/apache2/conf-enabled/cgit.conf
+
+- name: Override CGit scan path.
+  become: yes
+  lineinfile:
+    path: /etc/cgitrc
+    regexp: "^scan-path *="
+    line: "scan-path=$CGIT_SCANPATH"
+  notify: Reload Apache2.
+
+- name: Configure Apache log archival.
+  become: yes
+  lineinfile:
+    path: /etc/logrotate.d/apache2
+    regexp: "{{ item.regexp }}"
+    line: "{{ item.line }}"
+  loop:
+  - { regexp: '^ *daily', line: "\tweekly" }
+  - { regexp: '^ *rotate', line: "\trotate 12" }
+
+- name: Configure Apache log email.
+  become: yes
+  lineinfile:
+    path: /etc/logrotate.d/apache2
+    regexp: "{{ item.regexp }}"
+    line: "{{ item.line }}"
+    insertbefore: " *}"
+    firstmatch: yes
+  loop:
+  - { regexp: "^\tmail ", line: "\tmail webmaster" }
+  - { regexp: "^\tmailfirst", line: "\tmailfirst" }
+
+- name: Configure logrotate.
+  become: yes
+  copy:
+    src: logrotate-mailer.conf
+    dest: /etc/systemd/system/logrotate.service.d/mailer.conf
+  notify: Reload systemd.
+
+- name: Install logrotate mailer.
+  become: yes
+  copy:
+    src: logrotate-mailer
+    dest: /usr/local/sbin/logrotate-mailer
+    mode: u=rwx,g=rx,o=rx
+
+- name: Install Certbot for Apache.
+  become: yes
+  apt: pkg=python3-certbot-apache
+
+- name: Ensure Let's Encrypt certificate is readable.
+  become: yes
+  file:
+    mode: u=rwx,g=rx,o=rx
+    path: /etc/letsencrypt/live
+
+- name: Use Let's Encrypt certificate&key.
+  file:
+    state: link
+    src: "{{ item.target }}"
+    path: "{{ item.link }}"
+    force: yes
+  loop:
+  - target: /etc/letsencrypt/live/birchwood-abbey.net/fullchain.pem
+    link: /etc/server.crt
+  - target: /etc/letsencrypt/live/birchwood-abbey.net/privkey.pem
+    link: /etc/server.key
+
+- name: Install Certbot logrotate configuration.
+  become: yes
+  copy:
+    src: certbot_logrotate
+    dest: /etc/logrotate.d/certbot
+    mode: u=rw,g=r,o=r
+
+- name: Install Let's Encrypt archive script.
+  become: yes
+  copy:
+    src: cron.daily_letsencrypt
+    dest: /etc/cron.daily/letsencrypt
+    mode: u=rwx,g=rx,o=rx
+
+- name: Copy root@core's public key.
+  become: yes
+  copy:
+    src: ../Secret/root-pub.pem
+    dest: /root/.gnupg-root-pub.pem
+    mode: u=r,g=r,o=r
+  notify: Import root@core's public key.
diff --git a/roles_t/abbey-tvr/handlers/main.yml b/roles_t/abbey-tvr/handlers/main.yml
new file mode 100644 (file)
index 0000000..7dcbae3
--- /dev/null
@@ -0,0 +1,10 @@
+---
+- name: Reload Systemd.
+  become: yes
+  command: systemctl daemon-reload
+
+- name: Restart Apache2.
+  become: yes
+  systemd:
+    service: apache2
+    state: restarted
diff --git a/roles_t/abbey-tvr/tasks/main.yml b/roles_t/abbey-tvr/tasks/main.yml
new file mode 100644 (file)
index 0000000..d111372
--- /dev/null
@@ -0,0 +1,131 @@
+---
+- name: Include private abbey variables.
+  include_vars: ../private/vars-abbey.yml
+
+- name: Install MythTV runtime requisites.
+  become: yes
+  apt:
+    pkg: [ mariadb-server, xmltv ]
+
+- name: Install MythTV build requisites.
+  include_tasks: "{{ item }}"
+  loop:
+  - ../mythtv-ansible/roles/mythtv-deb/tasks/main.yml
+  - ../mythtv-ansible/roles/qt5/tasks/qt5-deb.yml
+
+- name: Test for MythTV binary packages.
+  stat:
+    path: /usr/local/bin/mythtv-setup
+  register: mythtv
+- debug:
+    msg: "/usr/local/bin/mythtv-setup does not yet exist"
+  when: not mythtv.stat.exists
+
+- name: Create mythtv.
+  become: yes
+  user:
+    name: mythtv
+    system: yes
+
+- name: Create mythtv-backend service.
+  become: yes
+  copy:
+    content: |
+      [Unit]
+      Description=MythTV Backend
+      Documentation=https://www.mythtv.org/wiki/Mythbackend
+      After=mysql.service network.target
+
+      [Service]
+      User=mythtv
+      ExecStartPre=/bin/sleep 30
+      #TimeoutStartSec=infinity
+      ExecStart=/usr/local/bin/mythbackend --quiet --syslog local7
+      StartLimitBurst=10
+      StartLimitInterval=10m
+      Restart=on-failure
+      RestartSec=1
+
+      [Install]
+      WantedBy=multi-user.target
+    dest: /etc/systemd/system/mythtv-backend.service
+  when: mythtv.stat.exists
+  notify: Reload Systemd.
+
+- name: Configure PHP date.timezone.
+  become: yes
+  lineinfile:
+    regexp: date.timezone ?=
+    line: date.timezone = {{ lookup('file', '/etc/timezone') }}
+    path: "{{ item }}"
+  loop:
+  - /etc/php/7.4/cli/php.ini
+  - /etc/php/7.4/apache2/php.ini
+  when: mythtv.stat.exists
+  notify: Restart Apache2.
+
+- name: Create MythTV storage area.
+  become: yes
+  file:
+    state: directory
+    dest: /home/mythtv/Recordings
+    owner: mythtv
+    group: mythtv
+    mode: u=rwx,g+rwx,o=rx
+
+- name: Install =/etc/rsyslog.d/40-mythtv.conf.
+  become: yes
+  copy:
+    content: |
+      :msg,startswith," myth" -/var/log/mythtv.log
+      & stop
+    dest: /etc/rsyslog.d/40-mythtv.conf
+
+- name: Install =/etc/logrotate.d/mythtv=.
+  become: yes
+  copy:
+    content: |
+      /var/log/mythtv.log {
+          daily
+          size=10M
+          rotate 7
+          notifempty
+          copytruncate
+          missingok
+          postrotate
+              reload rsyslog >/dev/null 2>&1 || true
+          endscript
+      }
+    dest: /etc/logrotate.d/mythtv
+
+- name: Install MythWeb requisites.
+  become: yes
+  apt:
+    pkg: [ apache2, php, php-mysql ]
+
+- name: Install MythWeb in web server DocumentRoot.
+  file:
+    state: link
+    src: /usr/local/share/mythtv/mythweb
+    dest: /var/www/html/mythweb
+
+- name: Configure MythWeb data directory.
+  file:
+    state: directory
+    dest: /var/www/html/mythweb/data
+    group: www-data
+    mode: u=rwx,g+rwx,o=rx
+
+- name: Install MythWeb configuration.
+  become: yes
+  template:
+    src: mythweb.conf.j2
+    dest: /etc/apache2/sites-available/mythweb.conf
+  notify: Restart Apache2.
+
+- name: Enable MythWeb configuration.
+  become: yes
+  command:
+    cmd: a2ensite -q mythweb
+    creates: /etc/apache2/sites-enabled/mythweb.conf
+  notify: Restart Apache2.
diff --git a/roles_t/abbey-tvr/templates/mythweb.conf.j2 b/roles_t/abbey-tvr/templates/mythweb.conf.j2
new file mode 100644 (file)
index 0000000..a0380c9
--- /dev/null
@@ -0,0 +1,54 @@
+#
+# Apache configuration directives for MythWeb.
+#
+# Note that this file is maintained by the network administration.
+<Directory "/var/www/html/mythweb/data">
+    # For Apache 2.2
+    #Options -All +FollowSymLinks +IncludesNoExec
+    # For Apache 2.4+
+    Options +FollowSymLinks +IncludesNoExec
+</Directory>
+<Directory "/var/www/html/mythweb" >
+    <Files mythweb.*>
+    setenv db_server "127.0.0.1"
+    setenv db_name "mythconverg"
+    setenv db_login "mythtv"
+    setenv db_password "{{ mythtv_dbpass }}"
+    </Files>
+    <Files *.php>
+       php_value file_uploads                  0
+       php_value allow_url_fopen               On
+       php_value zlib.output_handler           Off
+       php_value memory_limit                  64M
+       php_value max_execution_time 30
+       php_value display_startup_errors        On
+       php_value display_errors                On
+    </Files>
+    RewriteEngine  on
+    RewriteRule \
+^(css|data|images|js|themes|skins|README|INSTALL|[a-z_]+\.(php|pl))(/|$)\
+        - [L]
+    RewriteRule ^(pl(/.*)?)$            mythweb.pl/$1  [QSA,L]
+    RewriteRule ^(.+)$                  mythweb.php/$1 [QSA,L]
+    RewriteRule ^(.*)$                  mythweb.php    [QSA,L]
+    AllowOverride All
+    Options         FollowSymLinks
+    AddType video/nuppelvideo   .nuv
+    AddType image/x-icon        .ico
+    <IfModule deflate_module>
+       BrowserMatch ^Mozilla/4 gzip-only-text/html
+       BrowserMatch ^Mozilla/4\.0[678] no-gzip
+       BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
+       AddOutputFilterByType DEFLATE text/html
+       AddOutputFilterByType DEFLATE text/css
+       AddOutputFilterByType DEFLATE application/x-javascript
+    </IfModule>
+    <IfModule headers_module>
+       Header append Vary User-Agent env=!dont-vary
+    </IfModule>
+    <Files *.pl>
+       SetHandler cgi-script
+       Options +ExecCGI
+    </Files>
+
+</Directory>
diff --git a/roles_t/abbey-weather/files/daemon-anoat b/roles_t/abbey-weather/files/daemon-anoat
new file mode 100644 (file)
index 0000000..6006c14
--- /dev/null
@@ -0,0 +1,130 @@
+#!/usr/bin/perl -w
+# -*- CPerl -*-
+#
+# Weather/daemon
+#
+# Fetches data from the local owserver once per minute.  Appends to
+# Log/{In,Out}side/YEAR/MONTH/DAY.txt.
+
+use strict;
+use IO::File;
+use Date::Format;
+
+my $ILOG;
+my $OLOG;
+my $ymd = "";
+sub mymkdir ($);
+sub reopen_logs ()
+{
+  my $time = time;
+  my $datime = time2str ("%Y-%m-%d %H:%M:%S", $time, "UTC");
+  my ($year, $month, $day) = $datime =~ /^(\d{4})-(\d\d)-(\d\d) /;
+  my $new_ymd = "$year/$month/$day";
+  return if $new_ymd eq $ymd;
+  close $ILOG if defined $ILOG;
+  close $OLOG if defined $OLOG;
+  umask 07;
+  mymkdir "Inside/$year/$month";
+  mymkdir "Outside/$year/$month";
+  umask 027;
+  my $filename = "Inside/$new_ymd.txt";
+  $ILOG = new IO::File;
+  open $ILOG, ">>$filename" or die "Could not open $filename: $!\n";
+  $filename = "Outside/$new_ymd.txt";
+  $OLOG = new IO::File;
+  open $OLOG, ">>$filename" or die "Could not open $filename: $!\n";
+  $ymd = $new_ymd;
+}
+
+sub logit ($$$);
+sub main () {
+  die "usage: $0\n" if @ARGV != 0;
+  $0 = "weatherd";
+  chdir "/home/monkey/Weather/Log" or die;
+  umask 027;
+  my $start = time;
+  {
+    my $secs = 60 - $start % 60;
+    $start += $secs;
+    sleep ($secs);
+  }
+  while (1) {
+    reopen_logs;
+    logit $OLOG, "T", "/26.2153B6000000/temperature";
+    logit $OLOG, "H", "/26.2153B6000000/HIH4000/humidity";
+    logit $ILOG, "T", "/26.8859B6000000/temperature";
+    logit $ILOG, "H", "/26.8859B6000000/HIH4000/humidity";
+    $start += 60;
+    my $now = time;
+    while ($start < $now) { $start += 60; }
+    my $secs = $start - $now;
+    sleep  ($secs);
+  }
+}
+
+sub logit ($$$)
+{
+  my ($log, $name, $query) = @_;
+
+  my $tries = 0;
+  while ($tries < 3) {
+    my $time = time;
+    my $datime = time2str ("%Y-%m-%d %H:%M:%S", $time, "UTC");
+    $tries += 1;
+    my @lines = `/usr/bin/owread $query`;
+    chomp @lines;
+    my $status = $?;
+    my $sig = $status & 127;
+    $status >>= 8;
+    if ($status != 0) {
+      my $L = join "\\n", @lines;
+      print $log "$datime\t$name\terror: status $status: $L\n";
+      $log->flush;
+    } elsif (@lines != 1) {
+      my $L = join "\\n", @lines;
+      print $log "$datime\t$name\terror: multiple lines: $L\n";
+      $log->flush;
+    } elsif ($lines[0] !~ /^ *(-?\d+(\.\d+)?)$/) {
+      my $L = $lines[0];
+      print $log "$datime\t$name\terror: bogus line: $L\n";
+      $log->flush;
+    } else {
+      my $datum = $1;
+      print $log "$datime\t$name\t$datum\n";
+      $log->flush;
+      return;
+    }
+  }
+}
+
+sub mymkdir ($)
+{
+  my ($dirpath) = @_;
+
+  my @path_names = split /\//, $dirpath;
+  my $path;
+  if (!$path_names[0]) {
+    $path = "/";
+    shift @path_names;
+  } else {
+    $path = ".";
+  }
+  my @created;
+  while (@path_names) {
+    $path .= "/" . shift @path_names;
+    if (! -d $path) {
+      if (-e $path) {
+       die "mkdir $dirpath: already exists; not a directory!\n";
+      }
+      if (! mkdir $path) {
+       die "mkdir $path: $!\n";
+      } else {
+       chmod 02775, $path;
+       push @created, $path;
+      }
+    }
+  }
+  return @created;
+}
+
+main;
diff --git a/roles_t/abbey-weather/handlers/main.yml b/roles_t/abbey-weather/handlers/main.yml
new file mode 100644 (file)
index 0000000..e314f9d
--- /dev/null
@@ -0,0 +1,10 @@
+---
+- name: Reload Systemd.
+  become: yes
+  command: systemctl daemon-reload
+
+- name: Restart weather daemon.
+  become: yes
+  systemd:
+    service: weatherd
+    state: restarted
diff --git a/roles_t/abbey-weather/tasks/main.yml b/roles_t/abbey-weather/tasks/main.yml
new file mode 100644 (file)
index 0000000..4764eda
--- /dev/null
@@ -0,0 +1,114 @@
+---
+- name: Install weather daemon packages.
+  become: yes
+  apt: pkg=libtimedate-perl
+
+- name: Install 1-Wire server.
+  become: yes
+  apt:
+    pkg: [ owserver, ow-shell ]
+
+- name: Configure 1-Wire server.
+  become: yes
+  lineinfile:
+    path: /etc/owfs.conf
+    regexp: "{{ item.regexp }}"
+    line: "{{ item.line }}"
+    backrefs: yes
+  loop:
+  - { regexp: '^[# ]*server: *FAKE(.*)$', line: '#server: FAKE\1' }
+  - { regexp: '^[# ]*server: *usb(.*)$', line: 'server: usb\1' }
+
+- name: Install Rsync.
+  become: yes
+  apt: pkg=rsync
+
+- name: Create monkey.
+  become: yes
+  user:
+    name: monkey
+    system: yes
+
+- name: Authorize monkey@core.
+  become: yes
+  vars:
+    pubkeyfile: ../Secret/ssh_monkey/id_rsa.pub
+  authorized_key:
+    user: monkey
+    key: "{{ lookup('file', pubkeyfile) }}"
+    manage_dir: yes
+
+- name: Add {{ ansible_user }} to monkey group.
+  become: yes
+  user:
+    name: "{{ ansible_user }}"
+    append: yes
+    groups: monkey
+
+- name: Install weather directory.
+  become: yes
+  file:
+    path: /home/monkey/Weather/Log
+    state: directory
+    owner: monkey
+    group: monkey
+    mode: u=rwx,g=rx,o=rx
+
+- name: Test for weather daemon script.
+  vars:
+    dir: ../roles/abbey-weather/files
+    file: "{{ dir }}/daemon-{{ inventory_hostname }}"
+  stat: path="{{ file }}"
+  delegate_to: localhost
+  register: weather
+
+- name: Note missing weather daemon script.
+  vars:
+    dir: ../roles/abbey-weather/files
+    script: "{{ dir }}/daemon-{{ inventory_hostname }}"
+  debug:
+    msg: "{{ script }}: not found"
+  when: not weather.stat.exists
+
+- name: Install weather daemon.
+  become: yes
+  vars:
+    dir: ../roles/abbey-weather/files
+    script: "{{ dir }}/daemon-{{ inventory_hostname }}"
+  copy:
+    src: "{{ script }}"
+    dest: /home/monkey/Weather/daemon
+    owner: monkey
+    group: monkey
+    mode: u=rwx,g=rx,o=
+  when: weather.stat.exists
+
+- name: Install weatherd service.
+  become: yes
+  copy:
+    content: |
+      [Unit]
+      Description=Weather Logger
+      After=owserver.service
+
+      [Service]
+      User=monkey
+      ExecStartPre=/bin/sleep 30
+      ExecStart=/home/monkey/Weather/daemon
+      Restart=always
+
+      [Install]
+      WantedBy=multi-user.target
+    dest: /etc/systemd/system/weatherd.service
+  when: weather.stat.exists
+  notify:
+  - Reload Systemd.
+  - Restart weather daemon.
+
+- name: Enable/Start weather daemon.
+  become: yes
+  systemd:
+    service: weatherd
+    enabled: yes
+    state: started
+  when: weather.stat.exists
diff --git a/roles_t/abbey-weather/templates/weather-daemon.j2 b/roles_t/abbey-weather/templates/weather-daemon.j2
new file mode 100644 (file)
index 0000000..dc8f19a
--- /dev/null
@@ -0,0 +1,176 @@
+#!/usr/bin/perl -w
+# -*- CPerl -*-
+#
+# Weather/daemon
+#
+# Fetches data from the local owserver once per minute.  Appends to
+# Log/YEAR/MONTH/DAY.txt.
+
+use strict;
+use IO::File;
+use Date::Format;
+
+sub mysystem (@)
+{
+  if (system (@_) != 0) {
+    my $status = $?;
+    my $sig = $status & 127;
+    my $CMD = join ", ", map { "\"$_\"" } @_;
+    if ($status == -1) {
+      die "Failed to execute: $CMD: $!\n";
+    }
+    elsif ($sig) {
+      $! = 1;
+      die "Died with signal $sig: $CMD\n";
+    }
+    else {
+      return $status >> 8;
+    }
+  }
+  return 0;
+}
+
+sub mybacktick ($)
+{
+  my ($cmdline) = @_;
+
+  my @lines = `$cmdline`;
+  my $status = $?;
+  my $sig = $status & 127;
+  $status >>= 8;
+  if ($status == 0) { return @lines; }
+  elsif ($sig) {
+    $! = 1;
+    die "Died with signal $sig: $cmdline\n";
+  }
+  else {
+    die "Failed to execute: $cmdline: $!\n";
+  }
+}
+
+sub mymkdir ($)
+{
+  my ($dirpath) = @_;
+
+  my @path_names = split /\//, $dirpath;
+  my $path;
+  if (!$path_names[0]) {
+    $path = "/";
+    shift @path_names;
+  } else {
+    $path = ".";
+  }
+  my @created;
+  while (@path_names) {
+    $path .= "/" . shift @path_names;
+    if (! -d $path) {
+      if (-e $path) {
+       die "mkdir $dirpath: already exists; not a directory!\n";
+      }
+      if (! mkdir $path) {
+       die "mkdir $path: $!\n";
+      } else {
+       chmod 02775, $path;
+       push @created, $path;
+      }
+    }
+  }
+  return @created;
+}
+
+my $LOG;
+my $ymd = "";
+sub reopen_log ($$$)
+{
+  my ($year, $month, $day) = @_;
+  my $new_ymd = "$year/$month/$day";
+  return if $new_ymd eq $ymd;
+  close $LOG if defined $LOG;
+  umask 07;
+  mymkdir "$year/$month";
+  umask 027;
+  my $filename = "$new_ymd.txt";
+  $LOG = new IO::File;
+  open $LOG, ">>$filename" or die "Could not open $filename: $!\n";
+  $ymd = $new_ymd;
+}
+
+my $hostname;
+sub sensor_name ($)
+{
+  my ($num) = @_;
+  if ($hostname eq "carida") {
+    my $name = ("pool")[$num];
+    return $name;
+  }
+  elsif ($hostname eq "dathomir") {
+    my $name = ("inside", "outside")[$num];
+    warn "Bogus sensor number ($num) for host $hostname.\n" if !$name;
+    return $name;
+  }
+  warn "Bogus hostname: $hostname\n";
+  return undef;
+}
+
+sub owread ($$)
+{
+  my ($name, $query) = @_;
+
+  my $tries = 0;
+  while ($tries < 3) {
+    my $time = time;
+    my $datime = time2str ("%Y-%m-%d %H:%M:%S", $time, "UTC");
+    my ($y, $m, $d) = $datime =~ /^(\d{4})-(\d\d)-(\d\d) /;
+    reopen_log $y, $m, $d;
+    $tries += 1;
+    my @lines = `/usr/bin/owread $query`;
+    chomp @lines;
+    my $status = $?;
+    my $sig = $status & 127;
+    $status >>= 8;
+    if ($status != 0) {
+      my $L = join "\\n", @lines;
+      print $LOG "$datime\t$name\terror: status $status: $L\n";
+      $LOG->flush;
+    } elsif (@lines != 1) {
+      my $L = join "\\n", @lines;
+      print $LOG "$datime\t$name\terror: multiple lines: $L\n";
+      $LOG->flush;
+    } elsif ($lines[0] !~ /^ *(-?\d+(\.\d+)?)$/) {
+      my $L = $lines[0];
+      print $LOG "$datime\t$name\terror: bogus line: $L\n";
+      $LOG->flush;
+    } else {
+      my $datum = $1;
+      print $LOG "$datime\t$name\t$datum\n";
+      $LOG->flush;
+      return;
+    }
+  }
+}
+
+sub main () {
+  $hostname = `hostname`;
+  chomp $hostname;
+  die "usage: $0\n" if @ARGV != 0;
+  $0 = "weatherd";
+  chdir "/home/monkey/Weather/Log" or die;
+  umask 027;
+  my $start = time;
+  {
+    my $secs = 60 - $start % 60;
+    $start += $secs;
+    sleep ($secs);
+  }
+  while (1) {
+    owread "T", "{{ owread_path }}temperature";
+    owread "H", "{{ owread_path }}HIH4000/humidity";
+    $start += 60;
+    my $now = time;
+    while ($start < $now) { $start += 60; }
+    my $secs = $start - $now;
+    sleep  ($secs);
+  }
+}
+
+main;