server-motd 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- server_motd-0.1.0/.ansible-lint +5 -0
- server_motd-0.1.0/.gitattributes +3 -0
- server_motd-0.1.0/.github/workflows/ci.yml +149 -0
- server_motd-0.1.0/.github/workflows/release.yml +152 -0
- server_motd-0.1.0/.gitignore +43 -0
- server_motd-0.1.0/CHANGELOG.md +24 -0
- server_motd-0.1.0/CLAUDE.md +13 -0
- server_motd-0.1.0/GLOSSARY.md +45 -0
- server_motd-0.1.0/LICENSE +674 -0
- server_motd-0.1.0/PKG-INFO +756 -0
- server_motd-0.1.0/README.md +65 -0
- server_motd-0.1.0/ansible/ci-absent.yml +7 -0
- server_motd-0.1.0/ansible/ci-present.yml +12 -0
- server_motd-0.1.0/ansible/roles/server_motd/defaults/main.yml +26 -0
- server_motd-0.1.0/ansible/roles/server_motd/files/requirements.lock +5 -0
- server_motd-0.1.0/ansible/roles/server_motd/filter_plugins/server_motd.py +73 -0
- server_motd-0.1.0/ansible/roles/server_motd/meta/argument_specs.yml +48 -0
- server_motd-0.1.0/ansible/roles/server_motd/meta/main.yml +13 -0
- server_motd-0.1.0/ansible/roles/server_motd/tasks/absent.yml +45 -0
- server_motd-0.1.0/ansible/roles/server_motd/tasks/main.yml +8 -0
- server_motd-0.1.0/ansible/roles/server_motd/tasks/present.yml +108 -0
- server_motd-0.1.0/ansible/roles/server_motd/templates/50-server-motd.j2 +27 -0
- server_motd-0.1.0/docs/adr/0001-pure-python-single-package.md +3 -0
- server_motd-0.1.0/docs/adr/0002-break-upstream-compatibility.md +3 -0
- server_motd-0.1.0/docs/agents/domain.md +51 -0
- server_motd-0.1.0/docs/agents/issue-tracker.md +59 -0
- server_motd-0.1.0/docs/agents/triage-labels.md +7 -0
- server_motd-0.1.0/docs/configuration.md +77 -0
- server_motd-0.1.0/docs/example-config.toml +96 -0
- server_motd-0.1.0/docs/install-debian.md +48 -0
- server_motd-0.1.0/docs/install-windows.md +30 -0
- server_motd-0.1.0/docs/migrating-from-v1.md +51 -0
- server_motd-0.1.0/docs/releasing.md +47 -0
- server_motd-0.1.0/docs/research/debian-motd-mechanics.md +141 -0
- server_motd-0.1.0/docs/research/download-clients-api.md +192 -0
- server_motd-0.1.0/docs/research/servarr-api.md +79 -0
- server_motd-0.1.0/docs/research/tautulli-api.md +105 -0
- server_motd-0.1.0/docs/research/windows-profile-mechanics.md +121 -0
- server_motd-0.1.0/docs/secrets.md +9 -0
- server_motd-0.1.0/docs/troubleshooting.md +41 -0
- server_motd-0.1.0/pyproject.toml +37 -0
- server_motd-0.1.0/scripts/record-resolution.sh +39 -0
- server_motd-0.1.0/src/server_motd/__init__.py +1 -0
- server_motd-0.1.0/src/server_motd/__main__.py +5 -0
- server_motd-0.1.0/src/server_motd/cli.py +126 -0
- server_motd-0.1.0/src/server_motd/config.py +155 -0
- server_motd-0.1.0/src/server_motd/model.py +38 -0
- server_motd-0.1.0/src/server_motd/modules/__init__.py +54 -0
- server_motd-0.1.0/src/server_motd/modules/_common.py +42 -0
- server_motd-0.1.0/src/server_motd/modules/_http.py +78 -0
- server_motd-0.1.0/src/server_motd/modules/cpu.py +31 -0
- server_motd-0.1.0/src/server_motd/modules/disks.py +32 -0
- server_motd-0.1.0/src/server_motd/modules/ips.py +31 -0
- server_motd-0.1.0/src/server_motd/modules/load.py +30 -0
- server_motd-0.1.0/src/server_motd/modules/logins.py +29 -0
- server_motd-0.1.0/src/server_motd/modules/memory.py +38 -0
- server_motd-0.1.0/src/server_motd/modules/processes.py +21 -0
- server_motd-0.1.0/src/server_motd/modules/qbittorrent.py +82 -0
- server_motd-0.1.0/src/server_motd/modules/rtorrent.py +112 -0
- server_motd-0.1.0/src/server_motd/modules/sabnzbd.py +59 -0
- server_motd-0.1.0/src/server_motd/modules/servarr.py +103 -0
- server_motd-0.1.0/src/server_motd/modules/tautulli.py +85 -0
- server_motd-0.1.0/src/server_motd/modules/updates.py +56 -0
- server_motd-0.1.0/src/server_motd/modules/uptime.py +38 -0
- server_motd-0.1.0/src/server_motd/profile.py +167 -0
- server_motd-0.1.0/src/server_motd/py.typed +0 -0
- server_motd-0.1.0/src/server_motd/render.py +167 -0
- server_motd-0.1.0/src/server_motd/runner.py +56 -0
- server_motd-0.1.0/src/server_motd/terminal.py +35 -0
- server_motd-0.1.0/tests/__init__.py +0 -0
- server_motd-0.1.0/tests/conftest.py +74 -0
- server_motd-0.1.0/tests/fakescgi.py +128 -0
- server_motd-0.1.0/tests/fakeserver.py +184 -0
- server_motd-0.1.0/tests/fixtures/lidarr/calendar.json +1 -0
- server_motd-0.1.0/tests/fixtures/lidarr/health_ok.json +1 -0
- server_motd-0.1.0/tests/fixtures/lidarr/queue_status.json +1 -0
- server_motd-0.1.0/tests/fixtures/lidarr/wanted_missing.json +1 -0
- server_motd-0.1.0/tests/fixtures/qbittorrent/maindata_4x.json +13 -0
- server_motd-0.1.0/tests/fixtures/qbittorrent/maindata_50.json +13 -0
- server_motd-0.1.0/tests/fixtures/qbittorrent/maindata_52.json +13 -0
- server_motd-0.1.0/tests/fixtures/qbittorrent/maindata_empty.json +2 -0
- server_motd-0.1.0/tests/fixtures/radarr/calendar.json +1 -0
- server_motd-0.1.0/tests/fixtures/radarr/health_error.json +1 -0
- server_motd-0.1.0/tests/fixtures/radarr/queue_status.json +1 -0
- server_motd-0.1.0/tests/fixtures/radarr/wanted_missing.json +1 -0
- server_motd-0.1.0/tests/fixtures/rtorrent/multicall.json +7 -0
- server_motd-0.1.0/tests/fixtures/sabnzbd/error.json +1 -0
- server_motd-0.1.0/tests/fixtures/sabnzbd/queue_active.json +1 -0
- server_motd-0.1.0/tests/fixtures/sabnzbd/queue_idle.json +1 -0
- server_motd-0.1.0/tests/fixtures/sabnzbd/server_stats.json +1 -0
- server_motd-0.1.0/tests/fixtures/sonarr/calendar.json +1 -0
- server_motd-0.1.0/tests/fixtures/sonarr/health_ok.json +1 -0
- server_motd-0.1.0/tests/fixtures/sonarr/health_warn.json +1 -0
- server_motd-0.1.0/tests/fixtures/sonarr/queue_status.json +1 -0
- server_motd-0.1.0/tests/fixtures/sonarr/wanted_missing.json +1 -0
- server_motd-0.1.0/tests/fixtures/tautulli/activity_empty.json +1 -0
- server_motd-0.1.0/tests/fixtures/tautulli/activity_none.json +1 -0
- server_motd-0.1.0/tests/fixtures/tautulli/activity_streams.json +1 -0
- server_motd-0.1.0/tests/fixtures/tautulli/server_status_down.json +1 -0
- server_motd-0.1.0/tests/fixtures/tautulli/server_status_up.json +1 -0
- server_motd-0.1.0/tests/golden/.gitkeep +0 -0
- server_motd-0.1.0/tests/golden/cpu.txt +6 -0
- server_motd-0.1.0/tests/golden/disks.txt +7 -0
- server_motd-0.1.0/tests/golden/ips.txt +7 -0
- server_motd-0.1.0/tests/golden/layout-110.txt +17 -0
- server_motd-0.1.0/tests/golden/layout-50.txt +24 -0
- server_motd-0.1.0/tests/golden/layout-80.txt +17 -0
- server_motd-0.1.0/tests/golden/layout-ascii.txt +17 -0
- server_motd-0.1.0/tests/golden/layout-figlet.txt +18 -0
- server_motd-0.1.0/tests/golden/load.txt +6 -0
- server_motd-0.1.0/tests/golden/logins.txt +7 -0
- server_motd-0.1.0/tests/golden/memory.txt +7 -0
- server_motd-0.1.0/tests/golden/processes.txt +6 -0
- server_motd-0.1.0/tests/golden/sonarr.txt +9 -0
- server_motd-0.1.0/tests/golden/updates_none.txt +7 -0
- server_motd-0.1.0/tests/golden/updates_pending.txt +8 -0
- server_motd-0.1.0/tests/golden/updates_reboot.txt +8 -0
- server_motd-0.1.0/tests/golden/uptime.txt +6 -0
- server_motd-0.1.0/tests/test_ansible_role.py +148 -0
- server_motd-0.1.0/tests/test_check.py +133 -0
- server_motd-0.1.0/tests/test_cli.py +15 -0
- server_motd-0.1.0/tests/test_docs.py +107 -0
- server_motd-0.1.0/tests/test_layout.py +127 -0
- server_motd-0.1.0/tests/test_profile.py +123 -0
- server_motd-0.1.0/tests/test_qbittorrent.py +119 -0
- server_motd-0.1.0/tests/test_render.py +161 -0
- server_motd-0.1.0/tests/test_rtorrent.py +66 -0
- server_motd-0.1.0/tests/test_sabnzbd.py +81 -0
- server_motd-0.1.0/tests/test_servarr.py +155 -0
- server_motd-0.1.0/tests/test_system_modules.py +184 -0
- server_motd-0.1.0/tests/test_tautulli.py +110 -0
- server_motd-0.1.0/tests/test_timeouts.py +61 -0
- server_motd-0.1.0/tests/test_updates.py +68 -0
- server_motd-0.1.0/tests/test_windows.py +151 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
name: ${{ matrix.os }} py${{ matrix.python }}
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
include:
|
|
16
|
+
- { os: ubuntu-latest, python: "3.11" }
|
|
17
|
+
- { os: ubuntu-latest, python: "3.12" }
|
|
18
|
+
- { os: ubuntu-latest, python: "3.13" }
|
|
19
|
+
- { os: ubuntu-latest, python: "3.14" }
|
|
20
|
+
- { os: windows-latest, python: "3.11" }
|
|
21
|
+
- { os: windows-latest, python: "3.14" }
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
- uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: ${{ matrix.python }}
|
|
27
|
+
- run: python -m pip install -e ".[dev]"
|
|
28
|
+
- run: ruff check .
|
|
29
|
+
- run: ruff format --check .
|
|
30
|
+
- run: mypy
|
|
31
|
+
- run: pytest
|
|
32
|
+
- run: python -m build
|
|
33
|
+
- name: Console script runs
|
|
34
|
+
run: server-motd
|
|
35
|
+
|
|
36
|
+
ansible-lint:
|
|
37
|
+
name: ansible-lint
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
- uses: actions/setup-python@v5
|
|
42
|
+
with:
|
|
43
|
+
python-version: "3.12"
|
|
44
|
+
- run: python -m pip install ansible-lint
|
|
45
|
+
- run: ansible-lint
|
|
46
|
+
|
|
47
|
+
debian-hook:
|
|
48
|
+
name: role and hook render
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
steps:
|
|
51
|
+
- uses: actions/checkout@v4
|
|
52
|
+
- uses: actions/setup-python@v5
|
|
53
|
+
with:
|
|
54
|
+
python-version: "3.12"
|
|
55
|
+
- run: python -m pip install build ansible-core
|
|
56
|
+
- run: python -m build --wheel
|
|
57
|
+
- name: Apply the role to the runner
|
|
58
|
+
env:
|
|
59
|
+
ANSIBLE_ROLES_PATH: ${{ github.workspace }}/ansible/roles
|
|
60
|
+
ANSIBLE_STDOUT_CALLBACK: default
|
|
61
|
+
run: |
|
|
62
|
+
wheel=$(ls "$PWD"/dist/*.whl)
|
|
63
|
+
ansible-playbook -i localhost, -c local -b ansible/ci-present.yml \
|
|
64
|
+
-e "ansible_python_interpreter=$(command -v python3)" \
|
|
65
|
+
-e "server_motd_package_src=$wheel"
|
|
66
|
+
- name: Second run is idempotent
|
|
67
|
+
env:
|
|
68
|
+
ANSIBLE_ROLES_PATH: ${{ github.workspace }}/ansible/roles
|
|
69
|
+
run: |
|
|
70
|
+
wheel=$(ls "$PWD"/dist/*.whl)
|
|
71
|
+
ansible-playbook -i localhost, -c local -b ansible/ci-present.yml \
|
|
72
|
+
-e "ansible_python_interpreter=$(command -v python3)" \
|
|
73
|
+
-e "server_motd_package_src=$wheel" | tee run2.log
|
|
74
|
+
grep -E "changed=0 .*failed=0" run2.log
|
|
75
|
+
- name: Config is valid and read-only to the service user
|
|
76
|
+
run: |
|
|
77
|
+
sudo stat -c '%U %G %a' /etc/server-motd/config.toml | grep -x 'root server-motd 640'
|
|
78
|
+
sudo -u server-motd /opt/server-motd/venv/bin/server-motd check
|
|
79
|
+
- name: MOTD renders through the hook
|
|
80
|
+
run: |
|
|
81
|
+
sudo /etc/update-motd.d/50-server-motd | tee motd.txt
|
|
82
|
+
grep -q "ci-host" motd.txt
|
|
83
|
+
sudo env -i PATH=/usr/sbin:/usr/bin:/sbin:/bin run-parts --lsbsysinit /etc/update-motd.d | grep -q "ci-host"
|
|
84
|
+
- name: Hook exits 0 with the venv missing
|
|
85
|
+
run: |
|
|
86
|
+
sudo mv /opt/server-motd/venv /opt/server-motd/venv.off
|
|
87
|
+
sudo /etc/update-motd.d/50-server-motd
|
|
88
|
+
sudo mv /opt/server-motd/venv.off /opt/server-motd/venv
|
|
89
|
+
- name: state absent removes everything
|
|
90
|
+
env:
|
|
91
|
+
ANSIBLE_ROLES_PATH: ${{ github.workspace }}/ansible/roles
|
|
92
|
+
run: |
|
|
93
|
+
ansible-playbook -i localhost, -c local -b ansible/ci-absent.yml \
|
|
94
|
+
-e "ansible_python_interpreter=$(command -v python3)"
|
|
95
|
+
test ! -e /etc/update-motd.d/50-server-motd
|
|
96
|
+
test ! -e /opt/server-motd
|
|
97
|
+
test ! -e /etc/server-motd
|
|
98
|
+
! getent passwd server-motd
|
|
99
|
+
|
|
100
|
+
debian-hook-custom-paths:
|
|
101
|
+
name: role and hook render (non-default paths)
|
|
102
|
+
runs-on: ubuntu-latest
|
|
103
|
+
env:
|
|
104
|
+
ANSIBLE_ROLES_PATH: ${{ github.workspace }}/ansible/roles
|
|
105
|
+
ANSIBLE_STDOUT_CALLBACK: default
|
|
106
|
+
steps:
|
|
107
|
+
- uses: actions/checkout@v4
|
|
108
|
+
- uses: actions/setup-python@v5
|
|
109
|
+
with:
|
|
110
|
+
python-version: "3.12"
|
|
111
|
+
- run: python -m pip install build ansible-core
|
|
112
|
+
- run: python -m build --wheel
|
|
113
|
+
- name: Write the non-default role variables
|
|
114
|
+
run: |
|
|
115
|
+
cat > custom-paths.yml <<'EOF'
|
|
116
|
+
server_motd_user: motd-ci
|
|
117
|
+
server_motd_base_dir: /srv/motd-ci
|
|
118
|
+
server_motd_config_dir: /etc/motd-ci
|
|
119
|
+
EOF
|
|
120
|
+
- name: Apply the role with a custom user, base dir and config dir
|
|
121
|
+
run: |
|
|
122
|
+
wheel=$(ls "$PWD"/dist/*.whl)
|
|
123
|
+
ansible-playbook -i localhost, -c local -b ansible/ci-present.yml \
|
|
124
|
+
-e "ansible_python_interpreter=$(command -v python3)" \
|
|
125
|
+
-e "server_motd_package_src=$wheel" \
|
|
126
|
+
-e @custom-paths.yml
|
|
127
|
+
- name: Nothing landed at the default paths
|
|
128
|
+
run: |
|
|
129
|
+
test ! -e /opt/server-motd
|
|
130
|
+
test ! -e /etc/server-motd
|
|
131
|
+
! getent passwd server-motd
|
|
132
|
+
- name: Config is valid and read-only to the custom user
|
|
133
|
+
run: |
|
|
134
|
+
sudo stat -c '%U %G %a' /etc/motd-ci/config.toml | grep -x 'root motd-ci 640'
|
|
135
|
+
sudo -u motd-ci /srv/motd-ci/venv/bin/server-motd check --config /etc/motd-ci/config.toml
|
|
136
|
+
- name: MOTD renders through the hook
|
|
137
|
+
run: |
|
|
138
|
+
sudo /etc/update-motd.d/50-server-motd | tee motd.txt
|
|
139
|
+
grep -q "ci-host" motd.txt
|
|
140
|
+
sudo env -i PATH=/usr/sbin:/usr/bin:/sbin:/bin run-parts --lsbsysinit /etc/update-motd.d | grep -q "ci-host"
|
|
141
|
+
- name: state absent removes the custom paths
|
|
142
|
+
run: |
|
|
143
|
+
ansible-playbook -i localhost, -c local -b ansible/ci-absent.yml \
|
|
144
|
+
-e "ansible_python_interpreter=$(command -v python3)" \
|
|
145
|
+
-e @custom-paths.yml
|
|
146
|
+
test ! -e /etc/update-motd.d/50-server-motd
|
|
147
|
+
test ! -e /srv/motd-ci
|
|
148
|
+
test ! -e /etc/motd-ci
|
|
149
|
+
! getent passwd motd-ci
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Tag-triggered release: build, publish to PyPI (trusted publishing, no
|
|
4
|
+
# tokens), create a GitHub Release, then open a PR refreshing the role lockfile.
|
|
5
|
+
# Maintainer prerequisites: docs/releasing.md.
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
tags: ["v*"]
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
build:
|
|
16
|
+
name: Build and verify
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: "3.12"
|
|
23
|
+
- name: Tag matches pyproject version
|
|
24
|
+
run: |
|
|
25
|
+
version=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
26
|
+
if [ "v${version}" != "${GITHUB_REF_NAME}" ]; then
|
|
27
|
+
echo "Tag ${GITHUB_REF_NAME} does not match pyproject version ${version}" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
- name: Changelog has an entry for this version
|
|
31
|
+
run: grep -q "^## \[${GITHUB_REF_NAME#v}\]" CHANGELOG.md
|
|
32
|
+
- run: python -m pip install -e ".[dev]"
|
|
33
|
+
- run: ruff check .
|
|
34
|
+
- run: ruff format --check .
|
|
35
|
+
- run: mypy
|
|
36
|
+
- run: pytest
|
|
37
|
+
- run: python -m build
|
|
38
|
+
- uses: actions/upload-artifact@v4
|
|
39
|
+
with:
|
|
40
|
+
name: dist
|
|
41
|
+
path: dist/
|
|
42
|
+
if-no-files-found: error
|
|
43
|
+
|
|
44
|
+
publish-pypi:
|
|
45
|
+
name: Publish to PyPI
|
|
46
|
+
needs: build
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
environment:
|
|
49
|
+
name: pypi
|
|
50
|
+
url: https://pypi.org/project/server-motd/
|
|
51
|
+
permissions:
|
|
52
|
+
id-token: write
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/download-artifact@v4
|
|
55
|
+
with:
|
|
56
|
+
name: dist
|
|
57
|
+
path: dist/
|
|
58
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
59
|
+
|
|
60
|
+
github-release:
|
|
61
|
+
name: GitHub Release
|
|
62
|
+
needs: publish-pypi
|
|
63
|
+
runs-on: ubuntu-latest
|
|
64
|
+
permissions:
|
|
65
|
+
contents: write
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/checkout@v4
|
|
68
|
+
- uses: actions/download-artifact@v4
|
|
69
|
+
with:
|
|
70
|
+
name: dist
|
|
71
|
+
path: dist/
|
|
72
|
+
- name: Extract changelog section
|
|
73
|
+
run: |
|
|
74
|
+
version="${GITHUB_REF_NAME#v}"
|
|
75
|
+
awk -v v="$version" '
|
|
76
|
+
/^## \[/ { if (found) exit; if (index($0, "[" v "]")) { found = 1; next } }
|
|
77
|
+
found { print }
|
|
78
|
+
' CHANGELOG.md > release-notes.md
|
|
79
|
+
- name: Create release
|
|
80
|
+
env:
|
|
81
|
+
GH_TOKEN: ${{ github.token }}
|
|
82
|
+
run: |
|
|
83
|
+
gh release create "$GITHUB_REF_NAME" dist/* \
|
|
84
|
+
--title "$GITHUB_REF_NAME" \
|
|
85
|
+
--notes-file release-notes.md \
|
|
86
|
+
--verify-tag
|
|
87
|
+
|
|
88
|
+
lockfile-pr:
|
|
89
|
+
name: Role lockfile PR
|
|
90
|
+
needs: publish-pypi
|
|
91
|
+
runs-on: ubuntu-latest
|
|
92
|
+
permissions:
|
|
93
|
+
contents: write
|
|
94
|
+
pull-requests: write
|
|
95
|
+
env:
|
|
96
|
+
# Role lockfile; a placeholder until the first release fills it.
|
|
97
|
+
LOCKFILE: ansible/roles/server_motd/files/requirements.lock
|
|
98
|
+
steps:
|
|
99
|
+
- uses: actions/checkout@v4
|
|
100
|
+
with:
|
|
101
|
+
ref: master
|
|
102
|
+
- name: Check for role lockfile
|
|
103
|
+
id: check
|
|
104
|
+
run: |
|
|
105
|
+
if [ -f "$LOCKFILE" ]; then
|
|
106
|
+
echo "present=true" >> "$GITHUB_OUTPUT"
|
|
107
|
+
else
|
|
108
|
+
echo "::notice::$LOCKFILE does not exist yet; skipping lockfile PR"
|
|
109
|
+
echo "present=false" >> "$GITHUB_OUTPUT"
|
|
110
|
+
fi
|
|
111
|
+
- if: steps.check.outputs.present == 'true'
|
|
112
|
+
uses: actions/setup-python@v5
|
|
113
|
+
with:
|
|
114
|
+
python-version: "3.12"
|
|
115
|
+
- name: Regenerate lockfile
|
|
116
|
+
if: steps.check.outputs.present == 'true'
|
|
117
|
+
run: |
|
|
118
|
+
version="${GITHUB_REF_NAME#v}"
|
|
119
|
+
python -m pip install pip-tools
|
|
120
|
+
# The role always installs the figlet banner (MOTD layout decision).
|
|
121
|
+
echo "server-motd[figlet]==${version}" > requirements.in
|
|
122
|
+
# PyPI's index can lag a few minutes behind the upload.
|
|
123
|
+
# --strip-extras keeps the pin as "server-motd==", which the role checks for.
|
|
124
|
+
for attempt in 1 2 3 4 5 6; do
|
|
125
|
+
if pip-compile --quiet --generate-hashes --allow-unsafe --strip-extras \
|
|
126
|
+
--output-file "$LOCKFILE" requirements.in; then
|
|
127
|
+
break
|
|
128
|
+
fi
|
|
129
|
+
[ "$attempt" = 6 ] && exit 1
|
|
130
|
+
sleep 30
|
|
131
|
+
done
|
|
132
|
+
rm requirements.in
|
|
133
|
+
- name: Open pull request
|
|
134
|
+
if: steps.check.outputs.present == 'true'
|
|
135
|
+
env:
|
|
136
|
+
GH_TOKEN: ${{ github.token }}
|
|
137
|
+
run: |
|
|
138
|
+
if git diff --quiet -- "$LOCKFILE"; then
|
|
139
|
+
echo "Lockfile already current"
|
|
140
|
+
exit 0
|
|
141
|
+
fi
|
|
142
|
+
branch="release/lockfile-${GITHUB_REF_NAME}"
|
|
143
|
+
git config user.name "github-actions[bot]"
|
|
144
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
145
|
+
git checkout -b "$branch"
|
|
146
|
+
git add "$LOCKFILE"
|
|
147
|
+
git commit -m "Update role lockfile for ${GITHUB_REF_NAME}"
|
|
148
|
+
git push origin "$branch"
|
|
149
|
+
# Explicit --repo: this repo is a fork, and gh would otherwise target the parent.
|
|
150
|
+
gh pr create --repo "$GITHUB_REPOSITORY" --base master --head "$branch" \
|
|
151
|
+
--title "Update role lockfile for ${GITHUB_REF_NAME}" \
|
|
152
|
+
--body "Automated: refreshes the hash-pinned lockfile used by the Ansible role to install server-motd ${GITHUB_REF_NAME#v} from PyPI."
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# User-specific stuff:
|
|
2
|
+
.idea/*
|
|
3
|
+
|
|
4
|
+
## File-based project format:
|
|
5
|
+
*.iws
|
|
6
|
+
|
|
7
|
+
# IntelliJ
|
|
8
|
+
/out/
|
|
9
|
+
|
|
10
|
+
# Byte-compiled / optimized / DLL files
|
|
11
|
+
__pycache__/
|
|
12
|
+
*.py[cod]
|
|
13
|
+
*$py.class
|
|
14
|
+
|
|
15
|
+
# logs
|
|
16
|
+
*.log*
|
|
17
|
+
|
|
18
|
+
# databases
|
|
19
|
+
*.db
|
|
20
|
+
|
|
21
|
+
# configs
|
|
22
|
+
*.cfg
|
|
23
|
+
*.json
|
|
24
|
+
|
|
25
|
+
# generators
|
|
26
|
+
*.bat
|
|
27
|
+
|
|
28
|
+
# locks
|
|
29
|
+
locks/*
|
|
30
|
+
|
|
31
|
+
# venv
|
|
32
|
+
/venv/
|
|
33
|
+
.claude/worktrees/
|
|
34
|
+
|
|
35
|
+
# dev
|
|
36
|
+
/.venv/
|
|
37
|
+
/uv.lock
|
|
38
|
+
.mypy_cache/
|
|
39
|
+
.ruff_cache/
|
|
40
|
+
.pytest_cache/
|
|
41
|
+
/dist/
|
|
42
|
+
*.egg-info/
|
|
43
|
+
!tests/fixtures/**/*.json
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Hand-written. Versions follow `pyproject.toml` and stay 0.x until the v1 scope
|
|
4
|
+
ships. Each release needs a `## [x.y.z]` heading here; the release workflow
|
|
5
|
+
fails without one and uses the section as the GitHub Release notes.
|
|
6
|
+
|
|
7
|
+
## [0.1.0]
|
|
8
|
+
|
|
9
|
+
First release of `server-motd`, a from-scratch rewrite of the Cloudbox-MOTD
|
|
10
|
+
fork as an independent project. Nothing is compatible with v1 (`config.json`,
|
|
11
|
+
`/opt/motd`, the old fragment names); see `docs/migrating-from-v1.md`.
|
|
12
|
+
|
|
13
|
+
- One pure-Python package for Python 3.11+, with `psutil` as its only required dependency; HTTP and XML-RPC use the standard library.
|
|
14
|
+
- System modules: `ips`, `uptime`, `load`, `cpu`, `memory`, `disks`, `logins`, `processes` and `updates` (pending updates and reboot-required).
|
|
15
|
+
- Service modules, each with any number of named instances: Tautulli, qBittorrent, SABnzbd, rTorrent, Sonarr, Radarr and Lidarr.
|
|
16
|
+
- Modules are fetched concurrently, with a 3 s timeout per module and a 6 s total; a failing module is shown as unavailable instead of breaking the MOTD.
|
|
17
|
+
- TOML config with an ordered `[[modules]]` list, per-module `warn`/`crit` thresholds, `${ENV_VAR}` and `*_file` secrets, and strict validation scoped to each module.
|
|
18
|
+
- Built-in ANSI renderer: two-column boxed layout on wide terminals, a single column on narrow ones, several box styles (including ASCII), and an optional figlet banner (the `figlet` extra).
|
|
19
|
+
- CLI: `server-motd`, `server-motd check`, `install-profile` and `uninstall-profile`, with `--config`, `--width`, `--color` and `--verbose`.
|
|
20
|
+
- Debian/Ubuntu: an Ansible role (`ansible/roles/server_motd`) installs the package and the figlet banner from a hash-pinned lockfile into a root-owned venv, adds a single `update-motd.d` hook that runs as a dedicated user and always exits 0, writes `config.toml` as `root:<user> 0640`, and removes everything with `state: absent`. It never wipes the distro's other MOTD fragments.
|
|
21
|
+
- Windows: a guarded block in the current user's PowerShell 7 and 5.1 profiles, with a per-user `%APPDATA%` config and a read-only `%ProgramData%` fallback.
|
|
22
|
+
- Documentation: README with a golden-tested sample output, a tested example config covering every module kind, install guides for Debian and Windows, and pages on configuration, secrets, troubleshooting, releasing and migrating from v1.
|
|
23
|
+
- Tautulli's bandwidth unit (assumed kbps) is not yet verified against a real server.
|
|
24
|
+
- The legacy v1 bash/Python code is removed; it is kept in the `legacy-v1` git tag.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Run all Python through `uv`: `uv run --extra dev pytest` (dev tools are the `dev` extra), `uv run python …`, `uv tool` in place of pipx. `uv` works on the Windows host; a bare `python3` there is a Store stub that hangs or fails silently, and there is no `jq`. Ansible needs the Debian WSL distro, which has no `uv` installed.
|
|
4
|
+
|
|
5
|
+
Plan and decisions: GitHub issue #1 (the wayfinder map) and `docs/adr/`. Before touching an external API, the Debian MOTD, or the Windows profile hook, read the matching note in `docs/research/`.
|
|
6
|
+
|
|
7
|
+
The removed v1 bash/Python code lives in the local git tag `legacy-v1`; keep that tag local (never push it).
|
|
8
|
+
|
|
9
|
+
## Agent skills
|
|
10
|
+
|
|
11
|
+
- Issue tracker: GitHub Issues via `gh`. See `docs/agents/issue-tracker.md`.
|
|
12
|
+
- Triage labels: `docs/agents/triage-labels.md`.
|
|
13
|
+
- Domain docs (single-context): `GLOSSARY.md` and `docs/adr/`. See `docs/agents/domain.md`.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# server-motd
|
|
2
|
+
|
|
3
|
+
A cross-platform login banner that shows a server's identity, system health and the status of the services it runs.
|
|
4
|
+
|
|
5
|
+
## Language
|
|
6
|
+
|
|
7
|
+
**Module**:
|
|
8
|
+
One self-contained unit of MOTD output, either a system section or a service integration.
|
|
9
|
+
_Avoid_: Plugin, fragment, section
|
|
10
|
+
|
|
11
|
+
**Instance**:
|
|
12
|
+
One named configuration of a service module, so the same service can appear more than once (e.g. two Sonarrs).
|
|
13
|
+
_Avoid_: Profile, server entry
|
|
14
|
+
|
|
15
|
+
**System module**:
|
|
16
|
+
A module reporting on the host itself (uptime, memory, disks, pending updates).
|
|
17
|
+
_Avoid_: Stat, system section
|
|
18
|
+
|
|
19
|
+
**Service module**:
|
|
20
|
+
A module reporting on an external application the host runs (Sonarr, qBittorrent, Tautulli).
|
|
21
|
+
_Avoid_: Integration, provider
|
|
22
|
+
|
|
23
|
+
**Banner**:
|
|
24
|
+
The identity header at the top of the MOTD, showing the server's title.
|
|
25
|
+
_Avoid_: Header, logo
|
|
26
|
+
|
|
27
|
+
**Host integration**:
|
|
28
|
+
The OS-specific hook that makes the MOTD print at login (`update-motd.d` on Debian-family, a PowerShell profile on Windows).
|
|
29
|
+
_Avoid_: Installer, shim
|
|
30
|
+
|
|
31
|
+
**Result**:
|
|
32
|
+
What a module returns: a title, a list of rows and an overall status.
|
|
33
|
+
_Avoid_: Output, section
|
|
34
|
+
|
|
35
|
+
**Row**:
|
|
36
|
+
One labelled value line within a result, with its own status.
|
|
37
|
+
_Avoid_: Line, stat
|
|
38
|
+
|
|
39
|
+
**Status**:
|
|
40
|
+
The semantic health of a row or result (info, ok, warn, crit, unavailable); the renderer maps it to colour.
|
|
41
|
+
_Avoid_: Level, severity, colour
|
|
42
|
+
|
|
43
|
+
**Profile block**:
|
|
44
|
+
The marked span `install-profile` writes into a PowerShell profile to run the MOTD at login; everything outside the markers belongs to the user.
|
|
45
|
+
_Avoid_: Snippet, hook
|