From 42f60ca673471f2190b2ea455374323d095f93ca Mon Sep 17 00:00:00 2001 From: Matt Birkholz Date: Sun, 17 Dec 2023 16:24:06 -0700 Subject: [PATCH 1/1] Initial version. --- .gitignore | 4 + .gitmodules | 3 + Institute | 1 + README.html | 5047 +++++++++++++++++ README.org | 3963 +++++++++++++ abbey | 60 + ansible.cfg | 5 + hosts | 68 + jquery.js | 1 + org.css | 1 + org.js | 1 + playbooks/check-inst-vars.yml | 1 + playbooks/reboots.yml | 11 + playbooks/site.yml | 28 + playbooks/timezone.yml | 48 + playbooks/upgrade.yml | 21 + playbooks/versarch.yml | 7 + private_ex/vars-abbey.yml | 4 + public/vars.yml | 7 + publish | 34 + publish.el | 56 + roles_t/abbey-cloister/handlers/main.yml | 5 + roles_t/abbey-cloister/tasks/main.yml | 29 + roles_t/abbey-core/files/abbey_pisensors | 76 + roles_t/abbey-core/handlers/main.yml | 20 + roles_t/abbey-core/tasks/main.yml | 315 + .../abbey-core/templates/nagios-devaron.cfg | 47 + .../abbey-core/templates/nagios-kamino.cfg | 47 + .../abbey-core/templates/nagios-kessel.cfg | 47 + roles_t/abbey-dvr/handlers/main.yml | 12 + roles_t/abbey-dvr/tasks/main.yml | 106 + roles_t/abbey-front/files/certbot_logrotate | 6 + .../abbey-front/files/cron.daily_letsencrypt | 18 + roles_t/abbey-front/files/logrotate-mailer | 31 + .../abbey-front/files/logrotate-mailer.conf | 5 + roles_t/abbey-front/handlers/main.yml | 23 + roles_t/abbey-front/tasks/main.yml | 234 + roles_t/abbey-tvr/handlers/main.yml | 10 + roles_t/abbey-tvr/tasks/main.yml | 131 + roles_t/abbey-tvr/templates/mythweb.conf.j2 | 54 + roles_t/abbey-weather/files/daemon-anoat | 130 + roles_t/abbey-weather/handlers/main.yml | 10 + roles_t/abbey-weather/tasks/main.yml | 114 + .../abbey-weather/templates/weather-daemon.j2 | 176 + 44 files changed, 11017 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 160000 Institute create mode 100644 README.html create mode 100644 README.org create mode 100755 abbey create mode 100644 ansible.cfg create mode 100644 hosts create mode 120000 jquery.js create mode 120000 org.css create mode 120000 org.js create mode 100644 playbooks/check-inst-vars.yml create mode 100644 playbooks/reboots.yml create mode 100644 playbooks/site.yml create mode 100644 playbooks/timezone.yml create mode 100644 playbooks/upgrade.yml create mode 100644 playbooks/versarch.yml create mode 100644 private_ex/vars-abbey.yml create mode 100644 public/vars.yml create mode 100755 publish create mode 100644 publish.el create mode 100644 roles_t/abbey-cloister/handlers/main.yml create mode 100644 roles_t/abbey-cloister/tasks/main.yml create mode 100644 roles_t/abbey-core/files/abbey_pisensors create mode 100644 roles_t/abbey-core/handlers/main.yml create mode 100644 roles_t/abbey-core/tasks/main.yml create mode 100644 roles_t/abbey-core/templates/nagios-devaron.cfg create mode 100644 roles_t/abbey-core/templates/nagios-kamino.cfg create mode 100644 roles_t/abbey-core/templates/nagios-kessel.cfg create mode 100644 roles_t/abbey-dvr/handlers/main.yml create mode 100644 roles_t/abbey-dvr/tasks/main.yml create mode 100644 roles_t/abbey-front/files/certbot_logrotate create mode 100644 roles_t/abbey-front/files/cron.daily_letsencrypt create mode 100644 roles_t/abbey-front/files/logrotate-mailer create mode 100644 roles_t/abbey-front/files/logrotate-mailer.conf create mode 100644 roles_t/abbey-front/handlers/main.yml create mode 100644 roles_t/abbey-front/tasks/main.yml create mode 100644 roles_t/abbey-tvr/handlers/main.yml create mode 100644 roles_t/abbey-tvr/tasks/main.yml create mode 100644 roles_t/abbey-tvr/templates/mythweb.conf.j2 create mode 100644 roles_t/abbey-weather/files/daemon-anoat create mode 100644 roles_t/abbey-weather/handlers/main.yml create mode 100644 roles_t/abbey-weather/tasks/main.yml create mode 100644 roles_t/abbey-weather/templates/weather-daemon.j2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..52df594 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/private +/roles/ +/Secret* +/mythtv-ansible/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..2c3ed3c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "Institute"] + path = Institute + url = ./Institute/ diff --git a/Institute b/Institute new file mode 160000 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 index 0000000..1015cd3 --- /dev/null +++ b/README.html @@ -0,0 +1,5047 @@ + + + + + + + +Birchwood Abbey Networks + + + + + + + +
+

Birchwood Abbey Networks

+

+The abbey's network services are configured by Ansible scripts based +on 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 here). +

+
+

1. 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. +

+ +
+                |                                                   
+                =                                                   
+              _|||_                                                 
+      ----- 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                                  
+
+
+
+
+

2. 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. +

+ +
+public/vars.yml
---
+domain_name: birchwood-abbey.net
+domain_priv: birchwood.private
+
+full_name: Birchwood Abbey
+
+front_addr: 159.65.75.60
+
+
+
+
+
+

3. 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. +

+
+
+

3.1. Install Emacs

+
+

+The monks of the abbey are masters of the staff (bo) and Emacs. +

+ +
+roles_t/abbey-front/tasks/main.yml
---
+- name: Install Emacs.
+  become: yes
+  apt: pkg=emacs
+
+
+
+
+
+

3.2. 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. +

+ +
+roles_t/abbey-front/tasks/main.yml
+- 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.
+
+
+ +
+roles_t/abbey-front/handlers/main.yml
---
+- name: New aliases.
+  become: yes
+  command: newaliases
+
+
+
+
+
+

3.3. 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. +

+ +
+roles_t/abbey-front/tasks/main.yml
+- 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
+
+
+ +
+git-tasks
- 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
+
+
+ +
+roles_t/abbey-front/handlers/main.yml
+
+- name: Restart git daemon.
+  become: yes
+  command: systemctl restart git-daemon
+
+
+ +
+git-handlers
+- name: Restart git daemon.
+  become: yes
+  command: systemctl restart git-daemon
+
+
+
+
+
+

3.4. 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. +

+ +
+apache-gitweb
+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]
+
+
+ +

+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. +

+ +
+apache-gitweb-tasks
- 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
+
+
+ +
+apache-gitweb-handlers
- name: Restart Apache2.
+  become: yes
+  systemd:
+    service: apache2
+    state: restarted
+
+
+
+
+
+

3.5. 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. +

+ +
+apache-cgit
+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]
+
+
+ +
+apache-cgi-tasks
+- 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.
+
+
+
+
+
+

3.6. 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. +

+ +
+apache-abbey
<Directory {{ docroot }}/Abbey/>
+    AllowOverride Indexes FileInfo
+    Options +Indexes +FollowSymLinks
+</Directory>
+
+
+
+
+
+

3.7. 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. +

+ +
+apache-photos
+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
+
+
+
+
+
+

3.8. 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-photos, +apache-gitweb, and 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 section. +

+ +
+roles_t/abbey-front/tasks/main.yml
+- 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.
+
+
+ +
+roles_t/abbey-front/handlers/main.yml
+- name: Restart Apache2.
+  become: yes
+  systemd:
+    service: apache2
+    state: restarted
+
+
+
+
+
+

3.9. 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. +

+ +
+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
+
+
+ +
+roles_t/abbey-front/handlers/main.yml
+- name: Reload systemd.
+  become: yes
+  systemd:
+    daemon_reload: yes
+
+
+ +

+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.) +

+ +
+roles_t/abbey-front/files/logrotate-mailer.conf
[Service]
+ExecStart=
+ExecStart=/usr/sbin/logrotate \
+                --mail /usr/local/sbin/logrotate-mailer \
+                /etc/logrotate.conf
+
+
+ +

+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. +

+ +
+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"
+
+
+
+
+
+

3.10. 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). +

+ +
+$ 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"
+
+ +

+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. +

+ +
+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
+
+
+ +

+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. +

+ +
+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
+
+
+
+
+
+

3.11. Rotate Let's Encrypt Log

+
+

+The following task arranges to rotate Certbot's logs files. +

+ +
+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
+
+
+ +
+roles_t/abbey-front/files/certbot_logrotate
/var/log/letsencrypt/*.log {
+    rotate 12
+    weekly
+    compress
+    missingok
+}
+
+
+
+
+
+

3.12. 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. +

+ +
+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
+
+
+ +
+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~
+
+
+ +

+The message is encrypted with root@core's public key, which is +imported into root@front's GnuPG key file. +

+ +
+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.
+
+
+ +
+roles_t/abbey-front/handlers/main.yml
+- name: Import root@core's public key.
+  become: yes
+  command: gpg --import ~/.gnupg-root-pub.pem
+
+
+
+
+
+
+

4. 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. +

+
+
+

4.1. 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). +

+ +
+roles_t/abbey-core/tasks/main.yml
---
+- name: Install additional packages.
+  apt:
+    pkg: [ libhtml-tree-perl, libjs-jquery, mit-scheme, gnuplot ]
+
+
+
+
+
+

4.2. 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.) +

+ +
+roles_t/abbey-core/tasks/main.yml
+- 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.
+
+
+ +
+roles_t/abbey-core/handlers/main.yml
---
+- name: New aliases.
+  become: yes
+  command: newaliases
+
+
+
+
+
+

4.3. Configure Git Daemon on Core

+
+

+These tasks are identical to those executed on Front, for similar Git +services on Front and Core. See 3.3 and +Configure Gitweb on Front for more information. +

+ +
+roles_t/abbey-core/tasks/main.yml
+- 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
+
+
+ +
+roles_t/abbey-core/handlers/main.yml
+
+- name: Restart git daemon.
+  become: yes
+  command: systemctl restart git-daemon
+
+
+
+
+
+

4.4. 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-photos, apache-gitweb, and apache-cgit used on Front. +

+ +
+roles_t/abbey-core/tasks/main.yml
+- 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.
+
+
+ +
+roles_t/abbey-core/handlers/main.yml
+- name: Restart Apache2.
+  become: yes
+  systemd:
+    service: apache2
+    state: restarted
+
+
+
+
+
+

4.5. 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 here and here respectively). +

+ +
+roles_t/abbey-core/tasks/main.yml
+- 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.
+
+
+
+
+
+

4.6. 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. +

+ +
+roles_t/abbey-core/tasks/main.yml
+- name: Install Apt-Cacher:TNG.
+  become: yes
+  apt: pkg=apt-cacher-ng
+
+
+
+
+
+

4.7. Use Cloister Apt Cache

+
+

+Core itself will benefit from using the package cache. +

+ +
+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
+
+
+
+
+
+

4.8. 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. +

+
+
+
+

4.9. 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.) +

+ +
+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.
+
+
+ +
+roles_t/abbey-core/handlers/main.yml
+- name: Reload NAGIOS4.
+  become: yes
+  systemd:
+    service: nagios4
+    state: reloaded
+
+
+
+
+
+

4.10. 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. +

+ +
+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
+
+
+
+
+
+

4.11. 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. +

+ +
+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.
+
+
+ +
+roles_t/abbey-core/templates/nagios-devaron.cfg
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
+}
+
+
+ +
+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
+}
+
+
+ +
+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
+}
+
+
+
+
+
+

4.12. 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. +

+ +
+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
+
+
+
+
+
+

4.13. 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/. +

+ +
+roles_t/abbey-core/tasks/main.yml
+- name: Add Monkey to Nextcloud group.
+  become: yes
+  user:
+    name: monkey
+    append: yes
+    groups: www-data
+
+
+
+
+
+

4.14. Install netpbm For Photo Processing

+
+

+Monkey's photo processing scripts use netpbm commands like +jpegtopnm. +

+ +
+roles_t/abbey-core/tasks/main.yml
+- name: Install netpbm.
+  become: yes
+  apt: pkg=netpbm
+
+
+
+
+
+

4.15. Configure Weather Updates

+
+

+Monkey on Core runs /WWW/campus/Weather/Private/cronjob every 5 +minutes and cronjob-midnight at midnight. +

+ +
+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
+
+
+
+
+
+
+

5. 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). +

+
+
+

5.1. 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. +

+
+
+
+

5.2. 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. +

+
+
+
+

5.3. 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. +

+ +
+
network:
+  ethernets:
+    tether:
+      match:
+        name: usb0
+      set-name: isp
+      dhcp4: true
+      dhcp4-overrides:
+        use-dns: false
+
+
+ +

+The abbey has occasionally used a campground Wi-Fi for Internet +service, using a 60-isp.yaml file similar to the lines below. +

+ +
+
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": {}
+
+
+
+
+
+
+

6. 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 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. +

+
+
+

6.1. 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. +

+ +
+roles_t/abbey-cloister/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
+
+
+
+
+
+

6.2. 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 Configure +NRPE of A Small Institute). The abbey adds one complication: yet +another check_sensors variant, abbey_pisensors, installed on +Raspberry Pis (architecture aarch64) only. +

+ +
+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.
+
+
+ +
+roles_t/abbey-cloister/handlers/main.yml
+- name: Reload NRPE server.
+  become: yes
+  systemd:
+    service: nagios-nrpe-server
+    state: reloaded
+
+
+
+
+
+

6.3. Install Emacs

+
+

+The monks of the abbey are masters of the staff and Emacs. +

+ +
+roles_t/abbey-cloister/tasks/main.yml
+- name: Install monastic software.
+  become: yes
+  apt: pkg=emacs
+
+
+
+
+
+
+

7. 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). +

+
+
+

7.1. 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. +

+
+
+
+

7.2. 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) 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. +

+ +
+monkey@new$ owdir
+...
+    /26.2153B6000000/
+...
+monkey@new$ owdir /26.2153B6000000
+...
+    /26.2153B6000000/temperature
+...
+monkey@new$ owread /26.2153B6000000/temperature; echo
+26.125
+monkey@new$ 
+
+ +

+The second phase of weather host configuration waits for the host- +specific weather daemon script to appear in the role's files/. +

+
+
+
+

7.3. 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. +

+ +
+roles_t/abbey-weather/files/daemon-anoat
#!/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;
+
+
+ +

+The above Perl script uses the Date::Format module, which is +installed by the following task. +

+ +
+roles_t/abbey-weather/tasks/main.yml
---
+- name: Install weather daemon packages.
+  become: yes
+  apt: pkg=libtimedate-perl
+
+
+
+
+
+

7.4. 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). +

+ +
+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' }
+
+
+
+
+
+

7.5. Install Rsync

+
+

+Monkey on Core will want to download log records (files) using +rsync(1). +

+ +
+roles_t/abbey-weather/tasks/main.yml
+- name: Install Rsync.
+  become: yes
+  apt: pkg=rsync
+
+
+
+
+
+

7.6. 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. +

+ +
+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
+
+
+
+
+
+

7.7. 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. +

+ +
+roles_t/abbey-weather/tasks/main.yml
+- 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
+
+
+ +
+roles_t/abbey-weather/handlers/main.yml
---
+- name: Reload Systemd.
+  become: yes
+  command: systemctl daemon-reload
+
+- name: Restart weather daemon.
+  become: yes
+  systemd:
+    service: weatherd
+    state: restarted
+
+
+
+
+
+
+

8. 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. +

+
+
+

8.1. 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 +10.2, 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. +

+
+
+
+

8.2. 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.) +

+
+
+
+

8.3. 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, below. +

+
+
+
+

8.4. 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. +

+ +
+roles_t/abbey-dvr/tasks/main.yml
---
+- name: Include private abbey variables.
+  include_vars: ../private/vars-abbey.yml
+
+
+ +

+The relative filename should be found only in the playbook's +directory, playbooks/. +

+
+
+
+

8.5. 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. +

+ +
+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
+
+
+ +
+roles_t/abbey-dvr/handlers/main.yml
---
+- name: Restart MySQL.
+  become: yes
+  systemd:
+    service: mysql
+    state: restarted
+
+- name: Restart Apache2.
+  become: yes
+  systemd:
+    service: apache2
+    state: restarted
+
+
+ +

+The following Rsyslog configuration drop-in gets Zoneminder's natter +out of /var/log/syslog. +

+ +
+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
+
+
+
+
+
+

8.6. Create Zoneminder Database

+
+

+Zoneminder's MariaDB database is created by the following task, when +the mysql_db Ansible module supports check_implicit_admin. +

+ +
+
+- name: Create Zoneminder DB.
+  become: yes
+  mysql_db:
+    check_implicit_admin: yes
+    name: zm
+    collation: utf8mb4_general_ci
+    encoding: utf8mb4
+
+
+ +

+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). +

+
+
+
+

8.7. 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. +

+ +
+private/vars-abbey.yml
---
+zoneminder_dbpass:           gakJopbikJadsEdd
+
+
+ +
+
+- 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
+
+
+
+
+
+

8.8. 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. +

+ +
+
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;
+
+
+ +

+Finally, zm's tables are created, completing the database setup, +

+ +
+
sudo mysql < /usr/share/zoneminder/db/zm_create.sql
+
+
+
+
+
+

8.9. 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). +

+ +
+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
+
+
+
+
+
+

8.10. Configure Zoneminder

+
+

+The remaining tasks ensure that the /etc/zm/zm.conf file has the +proper permissions and contains the correct password. +

+ +
+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
+
+
+ +

+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. +

+ +
+roles_t/abbey-dvr/tasks/main.yml
+- name: Enable/Start Zoneminder.
+  become: yes
+  systemd:
+    service: zoneminder
+    enabled: yes
+    state: started
+  when: zoneminder.stat.exists
+
+
+
+
+
+

8.11. Configure Cameras

+
+

+A new security camera is setup as described in 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
    • +
  • +
+
+
+
+
+

9. The Abbey TVR Role

+
+

+The abbey has a few TV tuners and a subscription to 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/. +

+
+
+

9.1. 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 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. +

+
+
+
+

9.2. TVR Machine Setup

+
+

+A new TVR machine needs only 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. +

+
+
+
+

9.3. 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. +

+ +
+roles_t/abbey-tvr/tasks/main.yml
---
+- name: Include private abbey variables.
+  include_vars: ../private/vars-abbey.yml
+
+
+ +

+The relative filename should be found only in the playbook's +directory, playbooks/. +

+
+
+
+

9.4. 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.) +

+ +
+
git clone https://github.com/MythTV/ansible mythtv-ansible
+cd mythtv-ansible
+git checkout fixes/32
+
+
+ +

+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. +

+ +
+
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 ) }}'
+
+
+ +
+roles_t/abbey-tvr/tasks/mains.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
+
+
+ +

+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. +

+
+
+
+

9.5. 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. +

+ +
+
cd /usr/local/src/
+git clone https://github.com/MythTV/mythtv
+cd mythtv/
+git checkout fixes/32
+cd mythtv/
+./configure
+make
+sudo make install
+
+
+ +

+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. +

+ +
+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
+
+
+
+
+
+

9.6. Create MythTV User

+
+

+MythTV Backend needs to run as its own user: mythtv. +

+ +
+roles_t/abbey-tvr/tasks/main.yml
+- name: Create mythtv.
+  become: yes
+  user:
+    name: mythtv
+    system: yes
+
+
+
+
+
+

9.7. Create MythTV DB

+
+

+MythTV's MariaDB database is created by the following task, when the +mysql_db Ansible module supports check_implicit_admin. +

+ +
+
+- name: Create MythTV DB.
+  become: yes
+  mysql_db:
+    check_implicit_admin: yes
+    name: mythconverg
+    collation: utf8mb4_general_ci
+    encoding: utf8mb4
+
+
+ +

+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). +

+
+
+
+

9.8. 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. +

+ +
+private/vars-abbey.yml
mythtv_dbpass:           daJkibpoJkag
+
+
+ +

+The following task would create the DB user (mysql_user supports +check_implicit_admin) but the mythconverg database was not +created above. +

+ +
+
+- name: Create MythTV DB user.
+  become: yes
+  mysql_user:
+    check_implicit_admin: yes
+    name: mythtv
+    password: "{{ mythtv_dbpass }}"
+    priv: "mythconverg.*:all"
+
+
+
+
+
+

9.9. 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. +

+ +
+
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;
+
+
+
+
+
+

9.10. 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. +

+ +
+
SELECT CONVERT_TZ(NOW(), 'SYSTEM', 'Etc/UTC');
+
+
+ +

+After running the following command line, the test SQL produced +e.g. 2022-09-13 20:15:41. +

+ +
+
mysql_tzinfo_to_sql /usr/share/zoneinfo | sudo mysql mysql
+
+
+
+
+
+

9.11. Create MythTV Backend Service

+
+

+This task installs the mythtv-backend.service file. +

+ +
+roles_t/abbey-tvr/tasks/mains.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.
+
+
+ +
+roles_t/abbey-tvr/handlers/main.yml
---
+- name: Reload Systemd.
+  become: yes
+  command: systemctl daemon-reload
+
+
+
+
+
+

9.12. Set PHP Timezone

+
+

+This task checks PHP's timezone. If unset, MythTV's backend logs +bitter complaints. +

+ +
+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.
+
+
+ +
+roles_t/abbey-tvr/handlers/main.yml
+- name: Restart Apache2.
+  become: yes
+  systemd:
+    service: apache2
+    state: restarted
+
+
+
+
+
+

9.13. 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. +

+ +
+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
+
+
+
+
+
+

9.14. 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.
  • +
+
+
+
+

9.15. Configure Tuner

+
+

+The abbey has a Silicon Dust Homerun HDTV Duo (with two tuners). It +is setup as described in 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. +

+
+
+
+

9.16. 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.
  • +
+
+
+
+

9.17. 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.
  • +
+
+
+
+

9.18. 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. +

+ +
+
tv_grab_zz_sdjson --configure
+cp ~/.xmltv/tv_grab_zz_sdjson.conf ~/.mythtv/Mr.Antenna.xmltv
+
+
+ +

+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. +

+ +
+
tv_grab_zz_sdjson --configure --config-file ~/.mythtv/Mr.Antenna.xmltv
+
+
+ +

+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. +

+ +
+$ 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
+
+ +

+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. +

+
+
+
+

9.19. 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". +

+ +
+
sudo -H -u mythtv mythfilldatabase --verbose
+
+
+ +

+The command might, for example, show that it is failing to run a +tv_grab_zz_sdjson command like the following. +

+ +
+
nice tv_grab_zz_sdjson \
+        --config-file '/home/mythtv/.mythtv/Mr.Antenna.xmltv' \
+        --output /tmp/myths5Sq35 --quiet
+
+
+ +

+Running a similar command (without --quiet) might be more revealing. +

+ +
+
sudo -H -u mythtv \
+    tv_grab_zz_sdjson \
+        --config-file '/home/mythtv/.mythtv/Mr.Antenna.xmltv' \
+        --output /tmp/mythFUBAR
+
+
+
+
+
+

9.20. 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. +

+ +
+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
+
+
+
+
+
+

9.21. Start MythTV Backend

+
+

+After configuring with mythtv-setup as discussed above, start and +enable (at boot time) the mythtv-backend service. +

+ +
+
sudo systemctl enable mythtv-backend
+sudo systemctl start mythtv-backend
+systemctl status -l mythtv-backend
+sudo -u mythtv mythfilldatabase
+
+
+
+
+
+

9.22. 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. +

+ +
+
cd /usr/local/src/
+git clone https://github.com/MythTV/mythweb
+( cd mythweb/; git checkout fixes/32 )
+rsync -C mythweb /usr/local/share/mythtv/
+
+
+ +

+The following tasks take care of the rest of the installation. +

+ +
+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.
+
+
+ +
+roles_t/abbey-tvr/templates/mythweb.conf.j2
#
+# 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>
+
+
+
+
+
+

9.23. 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. +

+ +

+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. +

+ +
+
tv_grab_zz_sdjson --configure --config-file ~/.mythtv/Mr.Antenna.xmltv
+
+
+ +

+The program will prompt for the zip code and offer a list of "inputs" +available in that area, as described in Configure XMLTV. +

+
+
+
+
+

10. The Ansible Configuration

+
+

+The abbey's Ansible configuration, like that of 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 Overview of 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 Institute/README.org. +

+
+
+

10.1. ansible.cfg

+
+

+This is much like the example (test) institutional configuration file, +except the roles are found in Institute/roles/ as well as roles/. +

+ +
+ansible.cfg
[defaults]
+interpreter_python=/usr/bin/python3
+vault_password_file=Secret/vault-password
+inventory=hosts
+roles_path=roles:Institute/roles
+
+
+
+
+
+

10.2. hosts

+
+
+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:
+
+
+
+
+
+

10.3. playbooks/site.yml

+
+

+This playbook provisions the entire network by applying first the +institutional roles, then the liturgical roles. +

+ +
+playbooks/site.yml
---
+- 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 ]
+
+
+
+
+
+
+

11. The Abbey Commands

+
+

+The ./abbey script encodes the abbey's canonical procedures. It +includes The Institute Commands and adds a few abbey-specific +sub-commands. +

+
+
+

11.1. 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.
+
+
+
+
+

11.2. Abbey Command Script

+
+

+The script begins with the following prefix and trampolines. +

+ +
+abbey
#!/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;
+}
+
+
+ +

+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 +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. +

+ +
+playbooks/check-inst-vars.yml
- import_playbook: ../Institute/playbooks/check-inst-vars.yml
+
+
+
+
+
+

11.3. 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'
+
+ + +
+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");
+}
+
+
+ +
+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
+
+
+
+
+
+

11.4. The Reboots Command

+
+

+The script implements a reboots sub-command that looks for +/run/reboot-required on all abbey managed machines. +

+ +
+abbey
if ($ARGV[0] eq "reboots") {
+  exec ("ansible-playbook", "-e", "\@Secret/become.yml",
+        "playbooks/reboots.yml");
+}
+
+
+ +
+playbooks/reboots.yml
---
+- hosts: all
+  tasks:
+
+  - stat:
+      path: /run/reboot-required
+    register: st
+
+  - debug:
+      msg: Reboot required.
+    when: st.stat.exists
+
+
+
+
+
+

11.5. The Versions Command

+
+

+The script implements a versions sub-command that reports the +operating system version of all abbey managed machines. +

+ +
+abbey
if ($ARGV[0] eq "versions") {
+  exec ("ansible-playbook", "-e", "\@Secret/become.yml",
+        "playbooks/versarch.yml");
+}
+
+
+ +
+playbooks/versarch.yml
- hosts: all
+  tasks:
+  - debug:
+      msg: >-
+        {{ ansible_distribution }}
+        {{ ansible_distribution_version }}
+        {{ ansible_architecture }}
+
+
+
+
+
+

11.6. 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. +

+ +
+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");
+}
+
+
+ +
+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
+
+
+
+
+
+

11.7. Abbey Command Help

+
+
+abbey
die
+  "usage: $0 [config,new,old,pass,client,upgrade,reboots,versions]\n";
+
+
+
+
+
+
+

12. 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. +

+
+
+

12.1. 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. +

+ + + +

+Wireless IoT devices are manually configured with the cloister Wi-Fi +password and may be given a private domain name as described here. +

+ + +
+
+
+

12.2. 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
  • +
  • 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
  • +
  • Authorize Remote Administration
  • +
  • Configure with Ansible
  • +
+ +

+If the Pi is going to operate wirelessly, the following additional +steps are taken. +

+ + +
+
+
+

12.3. 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
  • +
  • Create Wired Domain Name
  • +
  • Log in as sysadm on the console.
  • +
  • Update From Cloister Apt Cache
  • +
  • +Install OpenSSH. Plain Debian does not come with OpenSSH installed. +

    +
    +sudo apt install openssh-server
    +
  • +
  • Authorize Remote Administration
  • +
  • Configure with Ansible
  • +
+ +

+If the PC is going to operate wirelessly, the following additional +steps are taken. +

+ + +
+
+
+

12.4. 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. +

+ +
+
tail -100 /var/log/daemon.log | grep DISCOVER
+
+
+ +

+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. +

+ +
+
host new {
+  hardware ethernet 08:00:27:f3:41:66; fixed-address 192.168.56.4; }
+
+
+ +

+The DHCP service is then restarted. +

+ +
+
sudo systemctl restart isc-dhcp-server
+
+
+ +

+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.... +

+ +
+
ping -c1 192.168.56.4
+
+
+
+
+
+

12.5. 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). 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. +

+ +
+
new-w   IN      A       192.168.56.4
+
+
+ +

+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. +

+ +
+
4       IN      PTR     new-w.birchwood.private.
+
+
+ +

+After ./abbey config core updates Core, resolution of the new-w +name can be tested. +

+ +
+
resolvectl query new-w.birchwood.private.
+resolvectl query 192.168.56.4
+
+
+
+
+
+

12.6. 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
    +
  • +
+
+
+
+

12.7. 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. +

+ +
+
scp Secret/ssh_admin/id_rsa.pub sysadm@new-w:admin_key
+
+
+ +

+Then on new-w (logged in as sysadm) the public key is installed in +~sysadm/.ssh/authorized_keys. +

+ +
+
( cd; umask 077; mkdir .ssh; cp admin_key .ssh/authorized_keys )
+
+
+ +

+Now the administrator can test access to new-w using Ansible's SSH +key. +

+ +
+
ssh -i Secret/ssh_admin/id_rsa sysadm@new-w
+
+
+
+
+
+

12.8. Configure with Ansible

+
+

+With remote administration authorized and tested (as in 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. 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. +

+ +
+
hosts:
+  ...
+  new-w:
+    ansible_user: pi
+    ansible_become_password: "{{ become_new }}"
+  ...
+children:
+  ...
+  campus:
+    hosts:
+      ...
+      new-w:
+
+
+ +

+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. +

+ +
+
echo -n "become_new: " >>Secret/become.yml
+ansible-vault encrypt_string PASSWORD >>Secret/become.yml
+
+
+ +

+Finally the ./abbey config new-w command is run. It will install +several additional software packages and change several more +configuration files. +

+ +
+
./abbey config new-w
+
+
+
+
+
+

12.9. 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. +

+ +
+=/etc/network/interfaces.d/wifi
auto wlan0
+iface wlan0 inet dhcp
+    wpa-ssid "Birchwood Abbey"
+    wpa-psk "PASSWORD"
+
+
+ +

+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. +

+ +
+
ping -c1 192.168.10.225
+
+
+
+
+
+

12.10. 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 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. +

+
+
+

12.10.1. 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. +

+ +
+
./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
+
+
+
+
+
+

12.10.2. 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. +

+ +
+
./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.
+
+
+ +

+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.) +

+
+
+
+

12.10.3. 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 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. +

+ +
+
./abbey client android dicks-tablet dick
+cp campus.ovpn public.ovpn /media/sysadm/Transfers/
+rm campus.ovpn public.ovpn
+
+
+
+
+
+
+

12.11. 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. +

+ +
+
new     IN      A       10.84.138.7
+
+
+ +

+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. +

+ +
+
7       IN      PTR     new.birchwood.private.
+
+
+ +

+After ./abbey config core updates Core, the administrator can test +resolution of the new name. +

+ +
+
resolvectl query new.birchwood.private.
+resolvectl query 10.84.138.7
+
+
+ +

+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.) +

+
+
+
+
+
+

Author: Matt Birkholz

+

Created: 2023-12-17 Sun 16:05

+

Validate

+
+ + diff --git a/README.org b/README.org new file mode 100644 index 0000000..e66ae48 --- /dev/null +++ b/README.org @@ -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 + +<> +#+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 + +<> +#+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/ + + Options MultiViews + +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/ + + AllowOverride None + Options ExecCGI FollowSymlinks + Require all granted + +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 + + AllowOverride Indexes FileInfo + Options +Indexes +FollowSymLinks + +#+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: | + <> + <> + <> + <> + IncludeOptional /etc/letsencrypt/options-ssl-apache.conf + dest: /etc/apache2/sites-available/{{ domain_name }}-vhost.conf + notify: Restart Apache2. + +<> +<> +#+END_SRC + +#+CAPTION: =roles_t/abbey-front/handlers/main.yml= +#+BEGIN_SRC conf :tangle roles_t/abbey-front/handlers/main.yml :noweb yes + +<> +#+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 + +<> +#+END_SRC + +#+CAPTION: =roles_t/abbey-core/handlers/main.yml= +#+BEGIN_SRC conf :tangle roles_t/abbey-core/handlers/main.yml :noweb yes + +<> +#+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: | + <> + <> + <> + <> + 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: | + <> + <> + <> + <> + dest: /etc/apache2/sites-available/test-vhost.conf + mode: u=rw,g=r,o=r + notify: Restart Apache2. + +<> +<> +#+END_SRC + +#+CAPTION: =roles_t/abbey-core/handlers/main.yml= +#+BEGIN_SRC conf :tangle roles_t/abbey-core/handlers/main.yml :noweb yes + +<> +#+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 + + Options Indexes + + <> + <> + 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: ) + - (Maximum FPS: ) + - (Alarm Maximum FPS: ) + - 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. + + # For Apache 2.2 + #Options -All +FollowSymLinks +IncludesNoExec + # For Apache 2.4+ + Options +FollowSymLinks +IncludesNoExec + + + + setenv db_server "127.0.0.1" + setenv db_name "mythconverg" + setenv db_login "mythtv" + setenv db_password "{{ mythtv_dbpass }}" + + + 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 + + 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 + + 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 + + + Header append Vary User-Agent env=!dont-vary + + + SetHandler cgi-script + Options +ExecCGI + + + +#+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 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 index 0000000..89d90fa --- /dev/null +++ b/ansible.cfg @@ -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 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 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 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 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 index 0000000..f22b887 --- /dev/null +++ b/playbooks/check-inst-vars.yml @@ -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 index 0000000..f40e964 --- /dev/null +++ b/playbooks/reboots.yml @@ -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 index 0000000..a1d9059 --- /dev/null +++ b/playbooks/site.yml @@ -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 index 0000000..db889c0 --- /dev/null +++ b/playbooks/timezone.yml @@ -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 index 0000000..f9aa427 --- /dev/null +++ b/playbooks/upgrade.yml @@ -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 index 0000000..5a4ea69 --- /dev/null +++ b/playbooks/versarch.yml @@ -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 index 0000000..c2cc595 --- /dev/null +++ b/private_ex/vars-abbey.yml @@ -0,0 +1,4 @@ +--- +zoneminder_dbpass: gakJopbikJadsEdd + +mythtv_dbpass: daJkibpoJkag diff --git a/public/vars.yml b/public/vars.yml new file mode 100644 index 0000000..ed02b8a --- /dev/null +++ b/public/vars.yml @@ -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 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 index 0000000..f47dcf8 --- /dev/null +++ b/publish.el @@ -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 " + + +" + :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 "%s")) + +;;;(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":
\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 index 0000000..927f9e4 --- /dev/null +++ b/roles_t/abbey-cloister/handlers/main.yml @@ -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 index 0000000..f326a8b --- /dev/null +++ b/roles_t/abbey-cloister/tasks/main.yml @@ -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 index 0000000..e50d134 --- /dev/null +++ b/roles_t/abbey-core/files/abbey_pisensors @@ -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 index 0000000..23e7299 --- /dev/null +++ b/roles_t/abbey-core/handlers/main.yml @@ -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 index 0000000..627d630 --- /dev/null +++ b/roles_t/abbey-core/tasks/main.yml @@ -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: | + + AllowOverride Indexes FileInfo + Options +Indexes +FollowSymLinks + + + 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/ + + Options MultiViews + + 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/ + + AllowOverride None + Options ExecCGI FollowSymlinks + Require all granted + + 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: | + + AllowOverride Indexes FileInfo + Options +Indexes +FollowSymLinks + + + 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/ + + Options MultiViews + + 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/ + + AllowOverride None + Options ExecCGI FollowSymlinks + Require all granted + + 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 + + Options Indexes + + + Alias /gitweb-static/ /usr/share/gitweb/static/ + + Options MultiViews + + 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/ + + AllowOverride None + Options ExecCGI FollowSymlinks + Require all granted + + 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 index 0000000..27cd1e2 --- /dev/null +++ b/roles_t/abbey-core/templates/nagios-devaron.cfg @@ -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 index 0000000..410b980 --- /dev/null +++ b/roles_t/abbey-core/templates/nagios-kamino.cfg @@ -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 index 0000000..ffbd9f8 --- /dev/null +++ b/roles_t/abbey-core/templates/nagios-kessel.cfg @@ -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 index 0000000..aaada22 --- /dev/null +++ b/roles_t/abbey-dvr/handlers/main.yml @@ -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 index 0000000..3bf4614 --- /dev/null +++ b/roles_t/abbey-dvr/tasks/main.yml @@ -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 index 0000000..9e20015 --- /dev/null +++ b/roles_t/abbey-front/files/certbot_logrotate @@ -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 index 0000000..24c4eef --- /dev/null +++ b/roles_t/abbey-front/files/cron.daily_letsencrypt @@ -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 index 0000000..4505083 --- /dev/null +++ b/roles_t/abbey-front/files/logrotate-mailer @@ -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 index 0000000..41308e5 --- /dev/null +++ b/roles_t/abbey-front/files/logrotate-mailer.conf @@ -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 index 0000000..0955bd0 --- /dev/null +++ b/roles_t/abbey-front/handlers/main.yml @@ -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 index 0000000..d369815 --- /dev/null +++ b/roles_t/abbey-front/tasks/main.yml @@ -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: | + + AllowOverride Indexes FileInfo + Options +Indexes +FollowSymLinks + + + 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/ + + Options MultiViews + + 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/ + + AllowOverride None + Options ExecCGI FollowSymlinks + Require all granted + + 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 index 0000000..7dcbae3 --- /dev/null +++ b/roles_t/abbey-tvr/handlers/main.yml @@ -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 index 0000000..d111372 --- /dev/null +++ b/roles_t/abbey-tvr/tasks/main.yml @@ -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 index 0000000..a0380c9 --- /dev/null +++ b/roles_t/abbey-tvr/templates/mythweb.conf.j2 @@ -0,0 +1,54 @@ +# +# Apache configuration directives for MythWeb. +# +# Note that this file is maintained by the network administration. + + # For Apache 2.2 + #Options -All +FollowSymLinks +IncludesNoExec + # For Apache 2.4+ + Options +FollowSymLinks +IncludesNoExec + + + + setenv db_server "127.0.0.1" + setenv db_name "mythconverg" + setenv db_login "mythtv" + setenv db_password "{{ mythtv_dbpass }}" + + + 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 + + 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 + + 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 + + + Header append Vary User-Agent env=!dont-vary + + + SetHandler cgi-script + Options +ExecCGI + + + diff --git a/roles_t/abbey-weather/files/daemon-anoat b/roles_t/abbey-weather/files/daemon-anoat new file mode 100644 index 0000000..6006c14 --- /dev/null +++ b/roles_t/abbey-weather/files/daemon-anoat @@ -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 index 0000000..e314f9d --- /dev/null +++ b/roles_t/abbey-weather/handlers/main.yml @@ -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 index 0000000..4764eda --- /dev/null +++ b/roles_t/abbey-weather/tasks/main.yml @@ -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 index 0000000..dc8f19a --- /dev/null +++ b/roles_t/abbey-weather/templates/weather-daemon.j2 @@ -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; -- 2.25.1