From 9f1b37fa346a7dcf77c1b6963a6d2e4b871fe5ed Mon Sep 17 00:00:00 2001 From: Stonewall Jackson Date: Sun, 22 Jan 2023 09:56:17 -0500 Subject: initial commit --- .gitignore | 5 + LICENSE | 21 ++ Makefile | 108 ++++++++++ README.md | 56 +++++ defaults.yaml | 9 + requirements.txt | 2 + scripts/bloglist.py | 26 +++ scripts/common.py | 53 +++++ scripts/rss.py | 48 +++++ src/android-chrome-192x192.png | Bin 0 -> 22287 bytes src/apple-touch-icon.png | Bin 0 -> 20142 bytes src/blog/desktop-linux-with-nfs-homedirs/index.md | 226 ++++++++++++++++++++ src/blog/index.md | 9 + src/blog/makefile-based-blogging/index.md | 249 ++++++++++++++++++++++ src/browserconfig.xml | 9 + src/css/style.css | 89 ++++++++ src/cv/index.md | 102 +++++++++ src/favicon-16x16.png | Bin 0 -> 1230 bytes src/favicon-32x32.png | Bin 0 -> 1989 bytes src/favicon.ico | Bin 0 -> 15086 bytes src/gpg.asc | 41 ++++ src/index.md | 35 +++ src/mstile-150x150.png | Bin 0 -> 11220 bytes src/sacredheart.png | Bin 0 -> 29328 bytes src/safari-pinned-tab.svg | 89 ++++++++ src/site.webmanifest | 14 ++ templates/cv.html | 93 ++++++++ templates/default.html | 89 ++++++++ 28 files changed, 1373 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 defaults.yaml create mode 100644 requirements.txt create mode 100755 scripts/bloglist.py create mode 100644 scripts/common.py create mode 100755 scripts/rss.py create mode 100644 src/android-chrome-192x192.png create mode 100644 src/apple-touch-icon.png create mode 100644 src/blog/desktop-linux-with-nfs-homedirs/index.md create mode 100644 src/blog/index.md create mode 100644 src/blog/makefile-based-blogging/index.md create mode 100644 src/browserconfig.xml create mode 100644 src/css/style.css create mode 100644 src/cv/index.md create mode 100644 src/favicon-16x16.png create mode 100644 src/favicon-32x32.png create mode 100644 src/favicon.ico create mode 100644 src/gpg.asc create mode 100644 src/index.md create mode 100644 src/mstile-150x150.png create mode 100644 src/sacredheart.png create mode 100644 src/safari-pinned-tab.svg create mode 100644 src/site.webmanifest create mode 100644 templates/cv.html create mode 100644 templates/default.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..99dddf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +public +.bloglist.md +*.pyc +*.pyo +*.swp diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6fa4003 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 stonewall@sacredheartsc.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8f81f2b --- /dev/null +++ b/Makefile @@ -0,0 +1,108 @@ +### CHANGE ME ###################### +DOMAIN = www.sacredheartsc.com +URL = https://$(DOMAIN) +RSYNC_TARGET = $(DOMAIN):/var/www/$(DOMAIN) +FEED_TITLE = sacredheartsc blog +FEED_DESCRIPTION = Carolina-grown articles about self-hosting, privacy, unix, and more. +STATIC_REGEX = .*\.(html|css|jpg|jpeg|png|ico|xml|txt|asc) +BLOG_LIST_LIMIT = 5 + + +### VARIABLES ###################### +SHELL = /bin/bash -e -o pipefail + +SOURCE_DIR = src +OUTPUT_DIR = public +SCRIPT_DIR = scripts + +BLOG_DIR = blog + +TEMPLATE = templates/default.html +DEFAULTS = defaults.yaml + +BLOG_LIST_SCRIPT = $(SCRIPT_DIR)/bloglist.py +BLOG_LIST_REPLACE = __BLOG_LIST__ +BLOG_LIST_FILE = .bloglist.md + +BLOG_RSS_SCRIPT = $(SCRIPT_DIR)/rss.py +BLOG_RSS_FILE = $(BLOG_DIR)/feed.xml + +SOURCE_DIRS := $(shell find $(SOURCE_DIR) -mindepth 1 -type d) +SOURCE_MARKDOWN := $(shell find $(SOURCE_DIR) -type f -name '*.md' -and ! -name $(BLOG_LIST_FILE)) +SOURCE_STATIC := $(shell find $(SOURCE_DIR) -type f -regextype posix-extended -iregex '$(STATIC_REGEX)') +BLOG_POSTS := $(shell find $(SOURCE_DIR)/$(BLOG_DIR) -type f -name '*.md' -and ! -name $(BLOG_LIST_FILE) -and ! -path $(SOURCE_DIR)/$(BLOG_DIR)/index.md) + +OUTPUT_DIRS := $(patsubst $(SOURCE_DIR)/%, $(OUTPUT_DIR)/%, $(SOURCE_DIRS)) +OUTPUT_MARKDOWN := $(patsubst $(SOURCE_DIR)/%, $(OUTPUT_DIR)/%, $(patsubst %.md, %.html, $(SOURCE_MARKDOWN))) +OUTPUT_STATIC := $(patsubst $(SOURCE_DIR)/%, $(OUTPUT_DIR)/%, $(SOURCE_STATIC)) + +COPY = cp --preserve=timestamps + +PANDOC = pandoc \ + --highlight-style=kate \ + --metadata=feed:/$(BLOG_RSS_FILE) \ + --defaults=$(DEFAULTS) + +RSSGEN = $(BLOG_RSS_SCRIPT) \ + $(SOURCE_DIR)/$(BLOG_DIR) \ + --title="$(FEED_TITLE)" \ + --description="$(FEED_DESCRIPTION)" \ + --url="$(URL)" \ + --blog-path="/$(BLOG_DIR)" \ + --feed-path="/$(BLOG_RSS_FILE)" + + +### TARGETS ###################### +public: \ + $(OUTPUT_DIRS) \ + $(OUTPUT_MARKDOWN) \ + $(OUTPUT_STATIC) \ + $(OUTPUT_DIR)/$(BLOG_RSS_FILE) + +$(OUTPUT_DIRS): + mkdir -p $@ + +# Homepage +$(OUTPUT_DIR)/index.html: $(SOURCE_DIR)/index.md $(SOURCE_DIR)/$(BLOG_LIST_FILE) $(TEMPLATE) + sed $$'/$(BLOG_LIST_REPLACE)/{r $(SOURCE_DIR)/$(BLOG_LIST_FILE)\nd}' $< | $(PANDOC) --template=$(TEMPLATE) --output=$@ + +# CV +$(OUTPUT_DIR)/cv/index.html: $(SOURCE_DIR)/cv/index.md templates/cv.html + $(PANDOC) --template=templates/cv.html --output=$@ $< + +$(SOURCE_DIR)/$(BLOG_LIST_FILE): $(BLOG_POSTS) $(BLOG_LIST_SCRIPT) + $(BLOG_LIST_SCRIPT) $(SOURCE_DIR)/$(BLOG_DIR) $(BLOG_LIST_LIMIT) > $@ + +# Blog listing +$(OUTPUT_DIR)/$(BLOG_DIR)/index.html: $(SOURCE_DIR)/$(BLOG_DIR)/index.md $(SOURCE_DIR)/$(BLOG_DIR)/$(BLOG_LIST_FILE) $(TEMPLATE) + sed $$'/$(BLOG_LIST_REPLACE)/{r $(SOURCE_DIR)/$(BLOG_DIR)/$(BLOG_LIST_FILE)\nd}' $< | $(PANDOC) --template=$(TEMPLATE) --output=$@ + +$(SOURCE_DIR)/$(BLOG_DIR)/$(BLOG_LIST_FILE): $(BLOG_POSTS) $(BLOG_LIST_SCRIPT) + $(BLOG_LIST_SCRIPT) $(SOURCE_DIR)/$(BLOG_DIR) > $@ + +# RSS feed +$(OUTPUT_DIR)/$(BLOG_RSS_FILE): $(BLOG_POSTS) $(BLOG_RSS_SCRIPT) + $(RSSGEN) > $@ + +# Blog posts +$(OUTPUT_DIR)/%.html: $(SOURCE_DIR)/%.md $(TEMPLATE) + $(PANDOC) --template=$(TEMPLATE) --output=$@ $< + +# Catch-all: static assets +$(OUTPUT_DIR)/%: $(SOURCE_DIR)/% + $(COPY) $< $@ + +.PHONY: install clean serve rsync +install: + pip install -r requirements.txt + +serve: public + cd $(OUTPUT_DIR) && python3 -m http.server + +clean: + rm -rf $(OUTPUT_DIR) + rm -f $(SOURCE_DIR)/$(BLOG_LIST_FILE) + rm -f $(SOURCE_DIR)/$(BLOG_DIR)/$(BLOG_LIST_FILE) + +rsync: public + rsync -rlphv --delete $(OUTPUT_DIR)/ $(RSYNC_TARGET) diff --git a/README.md b/README.md new file mode 100644 index 0000000..3f34cf6 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +www.sacredheadheartsc.com +========================= + +This repository contains the source for [www.sacredheartsc.com](https://www.sacredheartsc.com), +which consists of markdown documents and a Makefile-driven static site generator. + +# Requirements + +- coreutils +- python3 +- pandoc +- make + +# Instructions + +First, install the `pip` requirements: + + make install + +You'll want to edit the [Makefile](/www/tree/Makefile) to set your site URL, +RSS feed title, etc. + +Then, start writing markdown documents in the `src` directory. You can use +whatever naming convention and directory structure you like. Files ending in +`.md` will be converted to `.html` with the same path. + +The `src/blog` directory is special. Markdown files in this directory are +used to populate the front-page blog listing in [index.md](/www/tree/src/index.md). +Before pandoc converts this file to HTML, the special string `__BLOG_LIST__` +is replaced with the output of [bloglist.py](/www/tree/scripts/bloglist.py). +This Python script produces a date-sorted markdown list of all your blog posts. + +Each markdown file can have YAML frontmatter with the following metadata: + + --- + title: A boring blog post + date: YYYY-MM-DD + subtitle: an optional subtitle + heading: optional, if you want the first

to be different from + description: optional, short description for <head> and the blog listing + draft: if set, hides the post from the blog listing + --- + +You can change the resulting HTML by modifying the [template](/www/tree/templates/default.html). +Changing the format of the blog listing requires modifying the Python script. + +Build the website by using the default target: + + make + +This will create a directory called `public` containing all your markdown files +rendered to HTML. + +You also can run a local webserver, which listens on port 8000, using: + + make serve diff --git a/defaults.yaml b/defaults.yaml new file mode 100644 index 0000000..c628134 --- /dev/null +++ b/defaults.yaml @@ -0,0 +1,9 @@ +from: markdown+fenced_divs+bracketed_spans+smart +to: html5 +standalone: true +html-q-tags: true + +css: + - /css/style.css + +metadata: diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ea85e26 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +python-frontmatter +dateparser diff --git a/scripts/bloglist.py b/scripts/bloglist.py new file mode 100755 index 0000000..9e10354 --- /dev/null +++ b/scripts/bloglist.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +import argparse +from common import get_blog_posts + +DATE_FORMAT = '%Y-%m-%d' + +parser = argparse.ArgumentParser('bloglist') +parser.add_argument('BLOG_DIR', type=str, help='Directory containing markdown blog posts') +parser.add_argument('LIMIT', nargs='?', default=None, type=int, help='Maximum number of posts to show') +args = parser.parse_args() + +posts = get_blog_posts(args.BLOG_DIR) + +if args.LIMIT is not None: + posts = posts[0:args.LIMIT] + +if len(posts) == 0: + print('Nothing has been posted yet!') +else: + for post in posts: + post_date = post['date'].strftime(DATE_FORMAT) + + print(f'- [{post["title"]}]({post["href"]}) ({post_date})\n') + if post['description']: + print(f' {post["description"]}\n') diff --git a/scripts/common.py b/scripts/common.py new file mode 100644 index 0000000..9b23816 --- /dev/null +++ b/scripts/common.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 + +import os +import frontmatter +import dateparser +import datetime +from pathlib import Path + +BLOG_LIST_FILE = '.bloglist.md' + +def get_href(path): + path = Path(path) + path = path.relative_to(*path.parts[:1]) + if path.name == 'index.md': + return '/{}/'.format(path.parent) + else: + return '/{}/{}.html'.format(path.parent, path.stem) + +def read_metadata(file): + post = frontmatter.load(file) + + for field in ['title', 'date']: + if post.get(field) is None: + raise Exception("{} is missing metadata field '{}'".format(file, field)) + + if type(post['date']) not in [datetime.datetime, datetime.date]: + date = dateparser.parse(post['date']) + else: + date = post['date'] + + return { + 'title': post.get('title'), + 'date': date, + 'author': post.get('author'), + 'description': post.get('description'), + 'draft': post.get('draft'), + 'href': get_href(file) + } + +def get_blog_posts(blog_dir): + blog_index = os.path.join(blog_dir, 'index.md') + posts = [] + + for root, dirs, files in os.walk(blog_dir): + for file in files: + path = os.path.join(root, file) + if path.endswith('.md') and path != blog_index and file != BLOG_LIST_FILE: + metadata = read_metadata(path) + if not metadata['draft']: + posts.append(metadata) + + posts.sort(key=lambda p: (p is None, p['date']), reverse=True) + return posts diff --git a/scripts/rss.py b/scripts/rss.py new file mode 100755 index 0000000..cf0cb9a --- /dev/null +++ b/scripts/rss.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 + +import argparse +import email.utils +from datetime import datetime +from common import get_blog_posts + +parser = argparse.ArgumentParser('rss') +parser.add_argument('BLOG_DIR', type=str, help='Directory containing markdown blog posts') +parser.add_argument('--limit', default=15, type=int, help='Maximum number of posts to show') +parser.add_argument('--title', help='Feed title', required=True) +parser.add_argument('--description', help='Feed description', required=True) +parser.add_argument('--url', help='Root URL', required=True) +parser.add_argument('--blog-path', help='Blog path', required=True) +parser.add_argument('--feed-path', help='RSS feed path', required=True) +args = parser.parse_args() + +posts = get_blog_posts(args.BLOG_DIR) +posts = posts[0:args.limit] + +build_date = email.utils.format_datetime(datetime.now().astimezone()) + +print(f'''<?xml version="1.0"?> +<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> +<channel> + <title>{args.title} + {args.url}{args.blog_path} + en-US + {args.description} + {build_date} + ''') + +for post in posts: + pub_date = email.utils.format_datetime(post['date'].astimezone()) + + print(f''' + {post["title"]} + {args.url}{post["href"]} + {args.url}{post["href"]} + {pub_date}''') + + if 'description' in post: + print(f' {post["description"]}') + + print(' ') + +print('') +print('') diff --git a/src/android-chrome-192x192.png b/src/android-chrome-192x192.png new file mode 100644 index 0000000..f731faa Binary files /dev/null and b/src/android-chrome-192x192.png differ diff --git a/src/apple-touch-icon.png b/src/apple-touch-icon.png new file mode 100644 index 0000000..0491547 Binary files /dev/null and b/src/apple-touch-icon.png differ diff --git a/src/blog/desktop-linux-with-nfs-homedirs/index.md b/src/blog/desktop-linux-with-nfs-homedirs/index.md new file mode 100644 index 0000000..e01581a --- /dev/null +++ b/src/blog/desktop-linux-with-nfs-homedirs/index.md @@ -0,0 +1,226 @@ +--- +title: Desktop Linux with NFS Home Directories +date: January 19, 2023 +subtitle: Something no one does anymore, apparently. +description: Issues you'll face with NFS-mounted homedirs, and some workarounds. +--- + +I manage multiple [Rocky Linux](https://rockylinux.org/) workstations that automount +users' home directories via kerberized NFS. Unfortunately, I don't think this is a common +setup anymore--I encountered a few bugs and performance issues that needed non-obvious +workarounds. + +## Problems + +### 1. Things break when you log in from two places at once + +If you can somehow restrict your users to a single GNOME session at any given time, +you'll probably be fine. However, as soon as someone leaves his desktop running and +logs into another workstation, strange things begin to happen. Here are some oddities +I've observed: + + - GNOME settings on one machine are clobbered by the other (this may or may not be desirable). + + - Firefox refuses to run, because the profile directory is already in use. + + - `gnome-keyring` freaks out and creates many login keyrings under `~/.local/share/keyrings`, + losing previously stored secrets in the process! + + - Sound quits working (I suspect this is due to `~/.config/pulse/cookie` being clobbered). + + - Flatpak apps completely blow up (each app stores its state in `~/.var`, and + [this is nonconfigurable](https://github.com/flatpak/flatpak/issues/1651)). Running + multiple instances of `signal-dekstop` instantly corrupts the sqlite database. + + - `goa-daemon` generates thousands of syslog messages per minute (I am unsure if this is + due to `~/.config/goa-1.0/accounts.conf` getting clobbered, or a symptom of + [this bug](https://gitlab.gnome.org/GNOME/gnome-online-accounts/-/issues/32)). + I have no idea what `goa-daemon` does, nor do I want to. I have been victimized by + [the bazaar](http://www.catb.org/~esr/writings/cathedral-bazaar/cathedral-bazaar/) + enough for one lifetime. + +### 2. It's slow + +I/O-heavy tasks, like compiling and grepping, will be much slower over NFS than the local +disk. Browser profiles stored on NFS (`~/.mozilla`, `~/.cache/chromium`, etc.) provide +a noticeably poor experience. + +File browsing is also painful if you have lots of images or videos. Thumbnails for +files stored on NFS will be cached in `~/.cache/thumbnails`, which is **also** stored +on NFS! + +## Solution: Move stuff to local storage + +The [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) +lets you change the default locations of `~/.cache`, `~/.config`, and the like by setting +some environment variables in the user's session. We can solve most of these problems +by moving the various XDG directories to the local disk. + +### Automatically provision local home directories + +First, let's write a script that automatically provisions a _local_ home directory +whenever someone logs in: + +````bash +#!/bin/bash + +# /usr/local/sbin/create-local-homedir.sh + +# Log all output to syslog. +exec 1> >(logger -s -t $(basename "$0")) 2>&1 + +PAM_UID=$(id -u "${PAM_USER}") + +if (( PAM_UID >= 1000 )); then + install -o "${PAM_USER}" -g "${PAM_USER}" -m 0700 -d "/usr/local/home/${PAM_USER}" +fi +```` + +Of course, it needs to be executable: + +````bash +chmod 755 /usr/local/sbin/create-local-homedir.sh +```` + +Next, we modify the PAM configuration to execute our script whenever anyone logs in +via GDM or SSH: + +````diff +--- /etc/pam.d/gdm-password ++++ /etc/pam.d/gdm-password +@@ -1,5 +1,6 @@ + auth [success=done ignore=ignore default=bad] pam_selinux_permit.so + auth substack password-auth ++auth optional pam_exec.so /usr/local/sbin/create-local-homedir.sh + auth optional pam_gnome_keyring.so + auth include postlogin + +--- /etc/pam.d/sshd ++++ /etc/pam.d/sshd +@@ -15,3 +15,4 @@ + session optional pam_motd.so + session include password-auth + session include postlogin ++session optional pam_exec.so /usr/local/sbin/create-local-homedir.sh +```` + +
+A note on SELinux + +If you're using SELinux, you'll need a separate copy of the `create-local-homedir` script +for use with GDM, labeled with `xdm_unconfined_exec_t`: + +````bash +ln /usr/local/sbin/create-local-homedir{,-gdm}.sh +semanage fcontext -a -t xdm_unconfined_exec_t /usr/local/sbin/create-local-homedir-gdm.sh +restorecon -v /usr/local/sbin/create-local-homedir-gdm.sh +```` + +Be sure to modify `/etc/pam.d/gdm-password` appropriately. + +
+ +### Set XDG Environment Variables + +We need to tell the user's applications to use the new local home directory +for storage. We have to do this early in the PAM stack for GDM, because `$XDG_DATA_HOME` +must be set before `gnome-keyring` gets executed. + +Edit your PAM files again, adding one more line: + +````diff +--- /etc/pam.d/gdm-password ++++ /etc/pam.d/gdm-password +@@ -1,6 +1,7 @@ + auth [success=done ignore=ignore default=bad] pam_selinux_permit.so + auth substack password-auth + auth optional pam_exec.so /usr/local/sbin/create-local-homedir.sh ++auth optional pam_env.so conffile=/etc/security/pam_env_xdg.conf + auth optional pam_gnome_keyring.so + auth include postlogin + +--- /etc/pam.d/sshd ++++ /etc/pam.d/sshd +@@ -16,3 +16,4 @@ + session include password-auth + session include postlogin + session optional pam_exec.so /usr/local/sbin/create-local-homedir.sh ++session optional pam_env.so conffile=/etc/security/pam_env_xdg.conf +```` + +Then, create the corresponding `pam_env.conf(5)` file: + +````default +# /etc/security/pam_env_xdg.conf + +XDG_DATA_HOME DEFAULT=/usr/local/home/@{PAM_USER}/.local/share +XDG_STATE_HOME DEFAULT=/usr/local/home/@{PAM_USER}/.local/state +XDG_CACHE_HOME DEFAULT=/usr/local/home/@{PAM_USER}/.cache +XDG_CONFIG_HOME DEFAULT=/usr/local/home/@{PAM_USER}/.config +```` + +### Hacks for Non-XDG-Compliant Apps + +Unfortunately, since a majority of open source developers follow the +[CADT model](https://www.jwz.org/doc/cadt.html), there are many apps that ignore the +XDG specification. Sometimes these apps have their own environment variables +for specifying their storage locations. Otherwise, symlinks can provide us with an escape +hatch. + +Create a script in `/etc/profile.d` for these workarounds. Scripts in this directory +are executed within the context of the user's session, so we can freely write inside +his NFS home directory using his UID (and kerberos ticket, if applicable). + +````bash +# /etc/profile.d/local-homedirs.sh + +if (( UID >= 1000 )); then + # Building code is *much* faster on the local disk. Modify as needed: + export PYTHONUSERBASE="/usr/local/home/${USER}/.local" # python + export npm_config_cache="/usr/local/home/${USER}/.npm" # nodejs + export CARGO_HOME="/usr/local/home/${USER}/.cargo" # rust + export GOPATH="/usr/local/home/${USER}/go" # golang + + # Firefox doesn't provide an environment variable for setting the default profile + # path, so we'll just symlink it to /usr/local/home. + mkdir -p "/usr/local/home/${USER}/.mozilla" + ln -sfn "/usr/local/home/${USER}/.mozilla" "${HOME}/.mozilla" + + # Flatpak hardcodes ~/.var, so symlink it to /opt/flatpak. + ln -sfn "/opt/flatpak/${USER}" "${HOME}/.var" +fi +```` + +If you use any Flatpak apps, each user will need his own local Flatpak directory. +The Flatpak runtime appears to shadow the entire `/usr` using mount namespaces, +so any `/usr/local/home` symlinks will disappear into the abyss. Luckily, `/opt` +appears to be undefiled. Modify your original script like so: + +````diff +--- /usr/local/sbin/create-local-homedir.sh ++++ /usr/local/sbin/create-local-homedir.sh +@@ -6,4 +6,5 @@ + + if (( PAM_UID >= 1000 )); then + install -o "${PAM_USER}" -g "${PAM_USER}" -m 0700 -d "/usr/local/home/${PAM_USER}" ++ install -o "${PAM_USER}" -g "${PAM_USER}" -m 0700 -d "/opt/flatpak/${PAM_USER}" + fi +```` + +## Closing Thoughts + +Most of my users are nontechnical, so I'm pleased that these workarounds do not require +any manual intervention on their part. + +I am sad that `$XDG_CONFIG_HOME` can't be shared between multiple workstations reliably. +When I change my desktop background or add a new password to `gnome-keyring`, it only +affects the local machine. + +Initially, I tried symlinking various subdirectories of `~/.config` to the local disk +individually as I encountered different bugs (e.g. `~/.config/pulse`). Unfortunately this +proved brittle, as I was constantly playing whack-a-mole with apps that abused `$XDG_CONFIG_HOME` +for storing local state. In the end, it was less of a headache to just dump the whole thing +onto the local disk. + +I suppose if you verified an app behaved properly with multiple simultaneous NFS clients, +you could always symlink `/usr/local/home/$USER/.config/$APP` **back** onto NFS! diff --git a/src/blog/index.md b/src/blog/index.md new file mode 100644 index 0000000..1c35f0a --- /dev/null +++ b/src/blog/index.md @@ -0,0 +1,9 @@ +--- +title: sacredheartsc blog +header: Blog +description: Carolina-grown articles about self-hosting, privacy, unix, and more. +--- + +::: bloglist +__BLOG_LIST__ +::: diff --git a/src/blog/makefile-based-blogging/index.md b/src/blog/makefile-based-blogging/index.md new file mode 100644 index 0000000..9244a0d --- /dev/null +++ b/src/blog/makefile-based-blogging/index.md @@ -0,0 +1,249 @@ +--- +title: Makefile-Based Blogging +date: December 12, 2022 +subtitle: Yet another static site generator using `pandoc(1)` and `make(1)`. +description: Building a markdown-based static site generator using pandoc and make. +--- + +A few days ago, I got the gumption to start blogging again. The last time I wrote +with any frequency, I lovingly hand-crafted each HTML file before `rsync`ing it to +my web server. This time, I wanted a more efficient workflow. + +I surveyed the [vast number](https://github.com/myles/awesome-static-generators) +of static site generators available on GitHub, but most of them seemed like +overkill for my humble website. I figured that by the time I wrapped by head +around one of them, I could have just written a Makefile. + +Finally, I came across [pandoc-blog](https://github.com/lukasschwab/pandoc-blog), +which gave me inspiration and showed me the ideal pandoc incantations for +generating HTML from markdown files. And thus, my +[Makefile-based static site generator](https://git.sacredheartsc.com/www/about/) +was born. You're reading the inaugural post! + +## Generating the HTML + +The workhorse of this thing is [pandoc](https://pandoc.org), which is a ubiquitous +open-source document converter. Transforming markdown into HTML is as simple as: + +```bash +pandoc document.md -o document.html +``` + +Simple! But to generate an entire website, we'll need some of pandoc's additional +features: custom templates and document metadata. + +### Custom Templates + +The layout of pandoc's output document is determined by the +[template](https://pandoc.org/MANUAL.html#templates) in use. Pandoc includes +default templates for a variety of document formats, but you can also specify +your own. + +A very simple HTML template might look something like this: + +```html + + + + + + +

$title$

+$body$ + + +``` + +[My pandoc template](https://git.sacredheartsc.com/www/tree/templates/default.html) +is what generates the navigation bar at the top of this page. + +The variable `$body$` is replaced by the content of your markdown document when +pandoc renders the template. The other variables are replaced by their +corresponding values from the document's metadata. + +### Document Metadata + +Each pandoc source document can have associated metadata values. There are three +ways of specifying metadata: the `--medatata` [flag](https://pandoc.org/MANUAL.html#option--metadata), +a dedicated [metadata file](https://pandoc.org/MANUAL.html#option--metadata-file), or +a [YAML metadata block](https://pandoc.org/MANUAL.html#extension-yaml_metadata_block) +embedded within the document itself. We'll be using the embedded metadata blocks. + +Each markdown document for my website starts with a YAML metadata block. The +metadata for the post you're +[currently reading](https://git.sacredheartsc.com/www/tree/src/blog/makefile-based-blogging/index.md) +looks like this: + + +```yaml +--- +title: Makefile-Based Blogging +date: December 12, 2022 +subtitle: Yet another static site generator using `pandoc(1)` and `make(1)`. +description: Building a markdown-based static site generator using pandoc and make. +--- +``` + +You can put whatever YAML you like in your markdown files, as long as the metadata +starts and ends with three hyphens. + +## Automating pandoc with make + +Using a Makefile, we can automatically invoke pandoc to convert each markdown +file in our blog to HTML. In addition, `make` will keep track of which source +files have changed since the last run and rebuild them accordingly. + +First, lets describe the project layout: + +- **src/**: the source files of our blog, including markdown files and static + assets (CSS, images, etc). The subdirectory structure is entirely up to you. + +- **public/**: the output directory. After running `make`, the contents of this + directory can be `rsync`'d straight to your web server. + +- **scripts/**: helper scripts for generating the blog artifacts. Currently there + are only two: + + - [bloglist.py](https://git.sacredheartsc.com/www/tree/scripts/bloglist.py) + generates a markdown-formatted list of all your blog posts, sorted by the + `date` field in the YAML metadata block. + + - [rss.py](https://git.sacredheartsc.com/www/tree/scripts/rss.py) generates + an RSS feed for your blog. + +- **templates/**: pandoc templates which generate HTML from markdown files + (currently, there is only one). + +The Makefile used to build this website is located [here](https://git.sacredheartsc.com/www/tree/Makefile). +I've reproduced a simplified version below, to make it easier to step through. + +```makefile +###################### +# Variable definitions +###################### + +# These variables are used to generate the RSS feed +URL = https://www.sacredheartsc.com +FEED_TITLE = sacredheartsc blog +FEED_DESCRIPTION = Carolina-grown articles about self-hosting, privacy, unix, and more. + +# The number of blog posts to show on the homepage +BLOG_LIST_LIMIT = 5 + +# File extensions (other than .md) that should be included in public/ directory +STATIC_REGEX = .*\.(html|css|jpg|jpeg|png|xml|txt) + +# Pandoc template used to generate HTML +TEMPLATE = templates/default.html + +# List of subdirectories to create +SOURCE_DIRS := $(shell find src -mindepth 1 -type d) + +# List of source markdown files +SOURCE_MARKDOWN := $(shell find src -type f -name '*.md' -and ! -name .bloglist.md) + +# List of static assets +SOURCE_STATIC := $(shell find src \ + -type f \ + -regextype posix-extended \ + -iregex '$(STATIC_REGEX)') + +# List of all blog posts (excluding the main blog page) +BLOG_POSTS := $(shell find src/blog \ + -type f \ + -name '*.md' \ + -and ! -name .bloglist.md \ + -and ! -path src/blog/index.md) + +# Subdirectories to create under public/ +OUTPUT_DIRS := $(patsubst src/%, public/%, $(SOURCE_DIRS)) + +# .html files under public/, corresponding to each .md file under src/ +OUTPUT_MARKDOWN := $(patsubst src/%, public/%, $(patsubst %.md, %.html, $(SOURCE_MARKDOWN))) + +# Static file targets under public/ +OUTPUT_STATIC := $(patsubst src/%, public/%, $(SOURCE_STATIC)) + +# Script to generate RSS feed +RSSGEN = scripts/rss.py \ + src/blog \ + --title="$(FEED_TITLE)" \ + --description="$(FEED_DESCRIPTION)" \ + --url=$(URL) \ + --blog-path=/blog \ + --feed-path=/blog/rss/feed.xml + + +###################### +# File Targets +###################### + +# Default target: convert .md to .html, copy static assets, and generate RSS +public: \ + $(OUTPUT_DIRS) \ + $(OUTPUT_MARKDOWN) \ + $(OUTPUT_STATIC) \ + public/blog/feed.xml + +# Homepage (/) +public/index.html: src/index.md src/.bloglist.md $(TEMPLATE) + sed $$'/__BLOG_LIST__/{r src/.bloglist.md\nd}' $< \ + | pandoc --template=$(TEMPLATE) --output=$@ + +# Markdown list of 5 most recent blog posts +src/.bloglist.md: $(BLOG_POSTS) scripts/bloglist.py + scripts/bloglist.py src/blog $(BLOG_LIST_LIMIT) > $@ + +# The main blog listing (/blog/) +public/blog/index.html: src/blog/index.md src/blog/.bloglist.md $(TEMPLATE) + sed $$'/__BLOG_LIST__/{r src/blog/.bloglist.md\nd}' $< \ + | pandoc --template=$(TEMPLATE) --output=$@ + +# Markdown list of _all_ blog posts +src/blog/.bloglist.md: $(BLOG_POSTS) scripts/bloglist.py + scripts/bloglist.py src/blog > $@ + +# Convert all other .md files to .html +public/%.html: src/%.md $(TEMPLATE) + pandoc --template=$(TEMPLATE) --output=$@ $< + +# Catch-all: copy static assets in src/ to public/ +public/%: src/% + cp --preserve=timestamps $< $@ + +# RSS feed +public/blog/feed.xml: $(BLOG_POSTS) scripts/rss.py + $(RSSGEN) > $@ + + +###################### +# Phony Targets +###################### + +.PHONY: serve rsync clean + +# Run a local HTTP server in the output directory +serve: public + cd public && python3 -m http.server + +# Deploy the site to your webserver +rsync: public + rsync -rlphv --delete public/ webserver.example.com:/var/www/html + +clean: + rm -rf public + rm -f src/.bloglist.md + rm -f src/blog/.bloglist.md +``` + +## Closing Thoughts + +I admit, there is a small amount of hackery involved. You obviously can't generate +a time-sorted list of blog posts using pure markdown, so I'm generating the +markdown list using a Python script in an intermediate step. I then (ab)use `sed` +to shove that list into the markdown source on the fly. This means that changing +the look of the [blog list](/blog/) requires hacking up the Python code. + +But overall, I've been quite happy with this little project. There's just something +about writing paragraphs in `vi` and typing `:!make` that warms my soul with +memories of simpler times. diff --git a/src/browserconfig.xml b/src/browserconfig.xml new file mode 100644 index 0000000..b3930d0 --- /dev/null +++ b/src/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #da532c + + + diff --git a/src/css/style.css b/src/css/style.css new file mode 100644 index 0000000..27d6a7f --- /dev/null +++ b/src/css/style.css @@ -0,0 +1,89 @@ +body { + color: #333; + margin: 1em auto 2em auto; + max-width: 43em; + padding: 0 1em; + font-family: "PT Sans", "Myriad Pro", "Trebuchet MS", Helvetica, sans-serif; +} + +a { + color: #0074D9 +} + +a:visited { + color: #941352 +} + +header .subtitle { + margin-top: -1em; + margin-bottom: 21.44px; +} + +.bloglist p:nth-of-type(2) { + margin-top: -8px; +} + +header .date { + font-style: italic; + margin-bottom: 21.44px; +} + +footer .date { + float: right; +} + +footer { + font-style: italic; + font-size: 14px; + margin-top: 32px; +} + +.logo { + float: right; +} + +@media only screen and (max-device-width : 667px) { + .logo { + max-width: 66px; + } + + footer .date { + float: none; + } +} + +@media print { + .navbar { + display: none !important; + } +} + +pre { + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; + font-family: monospace; + line-height: 1.2; + padding: 8px; + white-space: pre; + word-wrap: normal; +} + +.sourceCode { + overflow: auto; +} + +details { + font-size: 87.5%; + margin: 0 1em; +} + +.redacted { + background: black; + color: black; + cursor: default; +} + +.right { + float: right; +} diff --git a/src/cv/index.md b/src/cv/index.md new file mode 100644 index 0000000..76f7f92 --- /dev/null +++ b/src/cv/index.md @@ -0,0 +1,102 @@ +--- +title: Curriculum Vitae +description: The CV of stonewall, a Linux engineer in upstate South Carolina. +date: January 20, 2023 +--- + +[stonewall@sacredheartsc.com](mailto:stonewall@sacredheartsc.com) +[sacredheartsc.com](https://www.sacredheartsc.com){.right} + +Site reliability engineer in upstate South Carolina, specializing in the programming, +administration, and troubleshooting of Linux systems. 8+ years of development and +operations experience. Strong skills in Unix internals, shell scripting, system +administration, and debugging, from userspace to the kernel. Extensive experience +in Linux, BSD, and Solaris-based operating systems, as well as networking and +infrastructure management. + + +## Work Experience + +**Linux Engineer** at [REDACTED, Inc.]{.redacted} (Remote) +[2021-present]{.right} + + - Details forthcoming. + +**Linux Engineer** at **[Jane Street](https://www.janestreet.com/)** (New York, NY) +[2018-2021]{.right} + + - Investigated production errors and performance regressions using packet captures, + userspace profiling, and kernel instrumentation. + - Maintained Linux infrastructure and developed management tools for network + storage systems. + - Diagnosed performance bottlenecks in NFS application workflows and client/server + implementations. + - Designed and implemented automated systems for the deployment, configuration + management, and monitoring of Linux hosts. + - Wrote patches for bug fixes and performance improvements for various open source + applications in the GNU/Linux ecosystem. + +**Site Reliability Engineer** at **Thesys CAT** (Charleston, SC) +[2017-2018]{.right} + + - Designed infrastructure, networking, and operations strategies for the + implementation of the SEC’s Consolidated Audit Trail. + - Led infrastructure rollout and configuration management effort using Ansible + for hundreds of bare metal and cloud servers. + - Implemented Kerberos-based authentication system for both applications and user + accounts. + - Built an object storage solution using FreeBSD and ZFS nearing a petabyte of + usable space. + - Created automated build and packaging pipeline for in-house software repositories. + +**DevOps Engineer** at **Thesys Technologies** (Charleston, SC) +[2016-2017]{.right} + + - Implemented market access software in C for Tilera’s TILE64 architecture, supporting + over $1 billion per day of order volume. + - Improved the accuracy of distributed C-based client risk checks while maintaining + ≤2.5μs order latency. + - Responsible for implementation and monitoring of a C++-based software trading + platform as part of trading technologies DevOps team. + - Coordinated network connectivity and colocation with clients and stock exchanges + in North America. + - Assessed and resolved real-time trading and connectivity issues in a high-stress + environment while managing relationships with customers. + +**Software Engineer** at **SPARC** (Charleston, SC) +[2015-2016]{.right} + + - Developed benefit management software for the Department of Veterans Affairs + using Java, Spring Framework, and AngularJS. + - Led a special performance team which identified and mitigated application + bottlenecks. + - Achieved a >10x speedup by refactoring application-layer JPA logic into more + efficient database queries. + - Created and maintained CentOS VM images for Oracle Database. + +**Graduate Research Assistant** at **[Clemson University](https://www.clemson.edu/)** (Clemson, SC) +[2013-2015]{.right} + + - Designed and implemented a middleware system for watershed-scale sensor networks + using Java and NodeJS. + - Maintained a middleware backend which served over 50 live ecological sensor + deployment sites across South Carolina. + - Independently developed a metadata management system for sensor network hardware, + which was presented at IEEE and ACM conferences. + + +## Education + +**M.S. in Computer Science** at **[Clemson University](https://www.clemson.edu/)** (Clemson, SC) +[2013-2015]{.right} + + - Developed new data analytics and management tools for wireless sensor networks + in Clemson’s Dependable Systems Research Group. + - GPA 3.70 + +**B.S. in Computer Science** at **[Clemson University](https://www.clemson.edu/)** (Clemson, SC) +[2009-2013]{.right} + + - Graduated magna cum laude as an undergraduate researcher in the Calhoun + Honors College. + - GPA 3.82 diff --git a/src/favicon-16x16.png b/src/favicon-16x16.png new file mode 100644 index 0000000..e64368b Binary files /dev/null and b/src/favicon-16x16.png differ diff --git a/src/favicon-32x32.png b/src/favicon-32x32.png new file mode 100644 index 0000000..caa141a Binary files /dev/null and b/src/favicon-32x32.png differ diff --git a/src/favicon.ico b/src/favicon.ico new file mode 100644 index 0000000..4536c08 Binary files /dev/null and b/src/favicon.ico differ diff --git a/src/gpg.asc b/src/gpg.asc new file mode 100644 index 0000000..1a3ad50 --- /dev/null +++ b/src/gpg.asc @@ -0,0 +1,41 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=MkZr +-----END PGP PUBLIC KEY BLOCK----- diff --git a/src/index.md b/src/index.md new file mode 100644 index 0000000..3ec5b6b --- /dev/null +++ b/src/index.md @@ -0,0 +1,35 @@ +--- +title: sacredheartsc.com +heading: Cor Jesu Sacratissimum, miserere nobis +subtitle: Most Sacred Heart of Jesus, have mercy on us! +description: The digital home of a Southern sysadmin. +--- + +![](sacredheart.png "Sacred Heart"){.logo} + +Welcome to my personal website. I'm a husband, father of three, and Unix herder. +The HTML you're currently viewing was artisanally crafted in South Carolina. + +Professionally, I'm something between a programmer and a system administrator. +Nothing here represents the views of any employer. It's just a collection of my +personal projects and interests, which include digital privacy, self-hosting, +and old-school sysadminning. + +- Contact: + [Email](mailto:stonewall@sacredheartsc.com){title="stonewall@sacredheartsc.com"} | + [XMPP](xmpp:stonewall@sacredheartsc.com?message){title="stonewall@sacredheartsc.com"} | + [IRC](ircs://irc.libera.chat/stonewall,isnick){title="stonewall on irc.libera.chat"} +- GPG: [0x6DF66CCB](/gpg.asc) +- Code: + [Git](https://git.sacredheartsc.com/) | + [GitHub](https://github.com/sacredheartsc) +- [Blog](/blog/) +- [Curriculum Vitae](/cv/) + +This website is powered by a [Makefile](https://git.sacredheartsc.com/www/about/)! + +## Recent Posts + +::: bloglist +__BLOG_LIST__ +::: diff --git a/src/mstile-150x150.png b/src/mstile-150x150.png new file mode 100644 index 0000000..8d29538 Binary files /dev/null and b/src/mstile-150x150.png differ diff --git a/src/sacredheart.png b/src/sacredheart.png new file mode 100644 index 0000000..e9eedf0 Binary files /dev/null and b/src/sacredheart.png differ diff --git a/src/safari-pinned-tab.svg b/src/safari-pinned-tab.svg new file mode 100644 index 0000000..0580306 --- /dev/null +++ b/src/safari-pinned-tab.svg @@ -0,0 +1,89 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/site.webmanifest b/src/site.webmanifest new file mode 100644 index 0000000..c9761d6 --- /dev/null +++ b/src/site.webmanifest @@ -0,0 +1,14 @@ +{ + "name": "SacredHeartSC", + "short_name": "SacredHeartSC", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + } + ], + "theme_color": "#ff0000", + "background_color": "#ff0000", + "display": "standalone" +} diff --git a/templates/cv.html b/templates/cv.html new file mode 100644 index 0000000..d61dd33 --- /dev/null +++ b/templates/cv.html @@ -0,0 +1,93 @@ + + + + + +$for(author-meta)$ + +$endfor$ +$if(date-meta)$ + +$endif$ +$if(description)$ + +$endif$ +$if(keywords)$ + +$endif$ + $if(title-prefix)$$title-prefix$ – $endif$$title$ + + + + + + + + + +$if(highlighting-css)$ + +$endif$ +$for(css)$ + +$endfor$ +$for(header-includes)$ + $header-includes$ +$endfor$ + + +$for(include-before)$ +$include-before$ +$endfor$ + + + +
+$if(heading)$ +

$heading$

+$else$ +

$title$

+$endif$ +$if(subtitle)$ +

$subtitle$

+$endif$ +
+ +$if(toc)$ + +$endif$ + +$body$ + +$for(include-after)$ +$include-after$ +$endfor$ +
+

+ Please contact me if you + require a de-anonymized CV. + Last updated $date$ +

+
+ + diff --git a/templates/default.html b/templates/default.html new file mode 100644 index 0000000..4145c9b --- /dev/null +++ b/templates/default.html @@ -0,0 +1,89 @@ + + + + + +$for(author-meta)$ + +$endfor$ +$if(date-meta)$ + +$endif$ +$if(description)$ + +$endif$ +$if(keywords)$ + +$endif$ + $if(title-prefix)$$title-prefix$ – $endif$$title$ + + + + + + + + + +$if(highlighting-css)$ + +$endif$ +$for(css)$ + +$endfor$ +$for(header-includes)$ + $header-includes$ +$endfor$ + + +$for(include-before)$ +$include-before$ +$endfor$ + + + +
+$if(heading)$ +

$heading$

+$else$ +

$title$

+$endif$ +$if(subtitle)$ +

$subtitle$

+$endif$ +$if(date)$ +

$date$

+$endif$ +
+ +$if(toc)$ + +$endif$ + +$body$ + +$for(include-after)$ +$include-after$ +$endfor$ + + -- cgit