systemd-search 1.1.0b1__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.
- systemd_search-1.1.0b1/.github/workflows/pr.yml +24 -0
- systemd_search-1.1.0b1/.github/workflows/release.yml +168 -0
- systemd_search-1.1.0b1/.github/workflows/tests.yml +68 -0
- systemd_search-1.1.0b1/.gitignore +41 -0
- systemd_search-1.1.0b1/ARCHITECTURE.md +227 -0
- systemd_search-1.1.0b1/LICENSE +184 -0
- systemd_search-1.1.0b1/PKG-INFO +550 -0
- systemd_search-1.1.0b1/Pipfile +17 -0
- systemd_search-1.1.0b1/Pipfile.lock +1003 -0
- systemd_search-1.1.0b1/README.md +532 -0
- systemd_search-1.1.0b1/molecule/common/converge.yml +75 -0
- systemd_search-1.1.0b1/molecule/common/files/fixture-disabled.service +14 -0
- systemd_search-1.1.0b1/molecule/common/files/fixture-failing.service +13 -0
- systemd_search-1.1.0b1/molecule/common/files/fixture-multi-a.service +15 -0
- systemd_search-1.1.0b1/molecule/common/files/fixture-multi.path +14 -0
- systemd_search-1.1.0b1/molecule/common/files/fixture-multi.timer +14 -0
- systemd_search-1.1.0b1/molecule/common/files/fixture-single.service +13 -0
- systemd_search-1.1.0b1/molecule/common/prepare.yml +22 -0
- systemd_search-1.1.0b1/molecule/common/verify.yml +458 -0
- systemd_search-1.1.0b1/molecule/debian/molecule.yml +35 -0
- systemd_search-1.1.0b1/molecule/rocky/molecule.yml +35 -0
- systemd_search-1.1.0b1/pyproject.toml +38 -0
- systemd_search-1.1.0b1/scripts/build-deb.sh +34 -0
- systemd_search-1.1.0b1/scripts/build-rpm.sh +45 -0
- systemd_search-1.1.0b1/scripts/build-standalone.sh +24 -0
- systemd_search-1.1.0b1/scripts/release-notes.md +19 -0
- systemd_search-1.1.0b1/setup.cfg +4 -0
- systemd_search-1.1.0b1/src/systemd_search/__init__.py +275 -0
- systemd_search-1.1.0b1/src/systemd_search.egg-info/PKG-INFO +550 -0
- systemd_search-1.1.0b1/src/systemd_search.egg-info/SOURCES.txt +36 -0
- systemd_search-1.1.0b1/src/systemd_search.egg-info/dependency_links.txt +1 -0
- systemd_search-1.1.0b1/src/systemd_search.egg-info/entry_points.txt +2 -0
- systemd_search-1.1.0b1/src/systemd_search.egg-info/scm_file_list.json +33 -0
- systemd_search-1.1.0b1/src/systemd_search.egg-info/scm_version.json +8 -0
- systemd_search-1.1.0b1/src/systemd_search.egg-info/top_level.txt +1 -0
- systemd_search-1.1.0b1/systemd-search +3 -0
- systemd_search-1.1.0b1/tests/conftest.py +13 -0
- systemd_search-1.1.0b1/tests/test_unit.py +297 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Runs on every pull request and on direct pushes to master/main.
|
|
3
|
+
# Branch protection must require all jobs from the called tests workflow to pass.
|
|
4
|
+
#
|
|
5
|
+
# Configure under: Settings → Branches → Branch protection rules → master
|
|
6
|
+
# Required status checks:
|
|
7
|
+
# - Tests / Unit tests
|
|
8
|
+
# - Tests / Molecule — rocky
|
|
9
|
+
# - Tests / Molecule — debian
|
|
10
|
+
|
|
11
|
+
name: PR
|
|
12
|
+
|
|
13
|
+
on:
|
|
14
|
+
push:
|
|
15
|
+
branches: [master]
|
|
16
|
+
pull_request:
|
|
17
|
+
|
|
18
|
+
concurrency:
|
|
19
|
+
group: pr-${{ github.ref }}
|
|
20
|
+
cancel-in-progress: true
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
tests:
|
|
24
|
+
uses: ./.github/workflows/tests.yml
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Runs when a tag is pushed. Validates the tag is strict semver, runs the
|
|
3
|
+
# full test suite, then builds and publishes a GitHub Release.
|
|
4
|
+
#
|
|
5
|
+
# Tag format: MAJOR.MINOR.PATCH (no v prefix), with optional pre-release
|
|
6
|
+
# and build metadata as defined by semver.org. Examples: 1.0.0 1.2.0-beta.1
|
|
7
|
+
|
|
8
|
+
name: Release
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
push:
|
|
12
|
+
tags: ["*"]
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
# ── Shared test suite ────────────────────────────────────────────────────────
|
|
16
|
+
tests:
|
|
17
|
+
uses: ./.github/workflows/tests.yml
|
|
18
|
+
|
|
19
|
+
# ── Semver gate ──────────────────────────────────────────────────────────────
|
|
20
|
+
# Validates the tag against the official semver.org regex before any build
|
|
21
|
+
# work starts. Runs in parallel with tests so a bad tag fails fast.
|
|
22
|
+
# https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
|
|
23
|
+
validate-tag:
|
|
24
|
+
name: Validate semver tag
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
steps:
|
|
27
|
+
- name: Check tag matches semver
|
|
28
|
+
run: |
|
|
29
|
+
TAG="${GITHUB_REF#refs/tags/}"
|
|
30
|
+
SEMVER='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$'
|
|
31
|
+
if ! echo "$TAG" | grep -qE "$SEMVER"; then
|
|
32
|
+
echo "::error::Tag '$TAG' is not valid semver. Release aborted."
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
echo "Tag '$TAG' is valid semver."
|
|
36
|
+
|
|
37
|
+
# ── Package builds ───────────────────────────────────────────────────────────
|
|
38
|
+
# Both tests and validate-tag must pass before any package is built.
|
|
39
|
+
package:
|
|
40
|
+
name: Package — ${{ matrix.target }}
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
needs: [tests, validate-tag]
|
|
43
|
+
strategy:
|
|
44
|
+
fail-fast: false
|
|
45
|
+
matrix:
|
|
46
|
+
include:
|
|
47
|
+
- target: rocky9
|
|
48
|
+
image: rockylinux/rockylinux:9
|
|
49
|
+
script: build-rpm.sh
|
|
50
|
+
- target: rocky10
|
|
51
|
+
image: rockylinux/rockylinux:10
|
|
52
|
+
script: build-rpm.sh
|
|
53
|
+
- target: debian12
|
|
54
|
+
image: debian:12
|
|
55
|
+
script: build-deb.sh
|
|
56
|
+
- target: debian13
|
|
57
|
+
image: debian:13
|
|
58
|
+
script: build-deb.sh
|
|
59
|
+
|
|
60
|
+
steps:
|
|
61
|
+
- uses: actions/checkout@v4
|
|
62
|
+
|
|
63
|
+
- name: Build package in ${{ matrix.target }} container
|
|
64
|
+
run: |
|
|
65
|
+
VERSION=${GITHUB_REF#refs/tags/}
|
|
66
|
+
docker run --rm \
|
|
67
|
+
-v "${{ github.workspace }}:/workspace" \
|
|
68
|
+
${{ matrix.image }} \
|
|
69
|
+
bash /workspace/scripts/${{ matrix.script }} ${{ matrix.target }} "$VERSION"
|
|
70
|
+
|
|
71
|
+
- name: Compute SHA-256 checksum
|
|
72
|
+
run: |
|
|
73
|
+
pkg=$(ls systemd-search-*-${{ matrix.target }}.*)
|
|
74
|
+
sha256sum "$pkg" > "${pkg}.sha256"
|
|
75
|
+
cat "${pkg}.sha256"
|
|
76
|
+
|
|
77
|
+
- name: Upload package artifact
|
|
78
|
+
uses: actions/upload-artifact@v4
|
|
79
|
+
with:
|
|
80
|
+
name: pkg-${{ matrix.target }}
|
|
81
|
+
path: systemd-search-*-${{ matrix.target }}.*
|
|
82
|
+
if-no-files-found: error
|
|
83
|
+
|
|
84
|
+
# ── Standalone zipapp executable ──────────────────────────────────────────────
|
|
85
|
+
executable:
|
|
86
|
+
name: Standalone executable
|
|
87
|
+
runs-on: ubuntu-latest
|
|
88
|
+
needs: [tests, validate-tag]
|
|
89
|
+
steps:
|
|
90
|
+
- uses: actions/checkout@v4
|
|
91
|
+
with:
|
|
92
|
+
fetch-depth: 0 # setuptools-scm needs full history to resolve the tag
|
|
93
|
+
|
|
94
|
+
- uses: actions/setup-python@v5
|
|
95
|
+
with:
|
|
96
|
+
python-version: "3.9"
|
|
97
|
+
|
|
98
|
+
- name: Build standalone zipapp
|
|
99
|
+
run: |
|
|
100
|
+
VERSION=${GITHUB_REF#refs/tags/}
|
|
101
|
+
pip install . --quiet
|
|
102
|
+
bash scripts/build-standalone.sh "$VERSION"
|
|
103
|
+
sha256sum "systemd-search-${VERSION}" > "systemd-search-${VERSION}.sha256"
|
|
104
|
+
|
|
105
|
+
- name: Upload executable artifact
|
|
106
|
+
uses: actions/upload-artifact@v4
|
|
107
|
+
with:
|
|
108
|
+
name: pkg-executable
|
|
109
|
+
path: systemd-search-*
|
|
110
|
+
if-no-files-found: error
|
|
111
|
+
|
|
112
|
+
# ── PyPI publish ──────────────────────────────────────────────────────────────
|
|
113
|
+
# Requires a Trusted Publisher configured on PyPI for this repo.
|
|
114
|
+
# See: https://docs.pypi.org/trusted-publishers/
|
|
115
|
+
pypi:
|
|
116
|
+
name: Publish to PyPI
|
|
117
|
+
runs-on: ubuntu-latest
|
|
118
|
+
needs: [tests, validate-tag]
|
|
119
|
+
environment: pypi
|
|
120
|
+
permissions:
|
|
121
|
+
id-token: write # required for OIDC Trusted Publisher
|
|
122
|
+
steps:
|
|
123
|
+
- uses: actions/checkout@v4
|
|
124
|
+
with:
|
|
125
|
+
fetch-depth: 0 # setuptools-scm needs full history to resolve the tag
|
|
126
|
+
|
|
127
|
+
- uses: actions/setup-python@v5
|
|
128
|
+
with:
|
|
129
|
+
python-version: "3.9"
|
|
130
|
+
|
|
131
|
+
- name: Build wheel and sdist
|
|
132
|
+
run: |
|
|
133
|
+
pip install build --quiet
|
|
134
|
+
python -m build
|
|
135
|
+
|
|
136
|
+
- name: Publish to PyPI
|
|
137
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
138
|
+
|
|
139
|
+
# ── GitHub Release ────────────────────────────────────────────────────────────
|
|
140
|
+
release:
|
|
141
|
+
name: GitHub Release
|
|
142
|
+
runs-on: ubuntu-latest
|
|
143
|
+
needs: [package, executable, pypi]
|
|
144
|
+
permissions:
|
|
145
|
+
contents: write
|
|
146
|
+
steps:
|
|
147
|
+
- uses: actions/checkout@v4
|
|
148
|
+
|
|
149
|
+
- name: Download all package artifacts
|
|
150
|
+
uses: actions/download-artifact@v4
|
|
151
|
+
with:
|
|
152
|
+
pattern: pkg-*
|
|
153
|
+
merge-multiple: true
|
|
154
|
+
path: dist/
|
|
155
|
+
|
|
156
|
+
- name: List release assets
|
|
157
|
+
run: ls -lh dist/
|
|
158
|
+
|
|
159
|
+
- name: Create GitHub Release
|
|
160
|
+
env:
|
|
161
|
+
GH_TOKEN: ${{ github.token }}
|
|
162
|
+
run: |
|
|
163
|
+
TAG=${GITHUB_REF#refs/tags/}
|
|
164
|
+
sed "s/{{VERSION}}/$TAG/g" scripts/release-notes.md > /tmp/release-notes.md
|
|
165
|
+
gh release create "$TAG" \
|
|
166
|
+
--title "systemd-search ${TAG}" \
|
|
167
|
+
--notes-file /tmp/release-notes.md \
|
|
168
|
+
dist/*
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Reusable workflow — unit tests + molecule integration tests.
|
|
3
|
+
# Called by pr.yml and release.yml. Never triggered directly.
|
|
4
|
+
|
|
5
|
+
name: Tests
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
workflow_call:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
unit-tests:
|
|
12
|
+
name: Unit tests
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.9"
|
|
20
|
+
|
|
21
|
+
- name: Cache pipenv virtualenv
|
|
22
|
+
uses: actions/cache@v4
|
|
23
|
+
with:
|
|
24
|
+
path: ~/.local/share/virtualenvs
|
|
25
|
+
key: pipenv-${{ runner.os }}-${{ hashFiles('Pipfile') }}
|
|
26
|
+
|
|
27
|
+
- name: Install pipenv
|
|
28
|
+
run: pip install pipenv
|
|
29
|
+
|
|
30
|
+
- name: Install dependencies
|
|
31
|
+
run: pipenv install --dev
|
|
32
|
+
|
|
33
|
+
- name: Run unit tests
|
|
34
|
+
run: pipenv run pytest tests/ -v
|
|
35
|
+
|
|
36
|
+
molecule:
|
|
37
|
+
name: Molecule — ${{ matrix.scenario }}
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
needs: unit-tests
|
|
40
|
+
strategy:
|
|
41
|
+
fail-fast: false
|
|
42
|
+
matrix:
|
|
43
|
+
scenario: [rocky, debian]
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@v4
|
|
46
|
+
|
|
47
|
+
- uses: actions/setup-python@v5
|
|
48
|
+
with:
|
|
49
|
+
python-version: "3.9"
|
|
50
|
+
|
|
51
|
+
- name: Cache pipenv virtualenv
|
|
52
|
+
uses: actions/cache@v4
|
|
53
|
+
with:
|
|
54
|
+
path: ~/.local/share/virtualenvs
|
|
55
|
+
key: pipenv-${{ runner.os }}-${{ hashFiles('Pipfile') }}
|
|
56
|
+
|
|
57
|
+
- name: Install pipenv
|
|
58
|
+
run: pip install pipenv
|
|
59
|
+
|
|
60
|
+
- name: Install dependencies
|
|
61
|
+
run: pipenv install --dev
|
|
62
|
+
|
|
63
|
+
- name: Run molecule scenario
|
|
64
|
+
run: pipenv run molecule test -s ${{ matrix.scenario }}
|
|
65
|
+
env:
|
|
66
|
+
MOLECULE_PROJECT_DIRECTORY: ${{ github.workspace }}
|
|
67
|
+
PY_COLORS: "1"
|
|
68
|
+
ANSIBLE_FORCE_COLOR: "1"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
.pytest_cache/
|
|
5
|
+
|
|
6
|
+
# Virtual environments
|
|
7
|
+
# Pipenv creates .venv when PIPENV_VENV_IN_PROJECT=1 is set
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
env/
|
|
11
|
+
|
|
12
|
+
# Pipfile.lock is committed — do not add it here
|
|
13
|
+
|
|
14
|
+
# Molecule ephemeral state
|
|
15
|
+
molecule/**/molecule.yml.bak
|
|
16
|
+
.molecule/
|
|
17
|
+
|
|
18
|
+
# Built packages (produced by CI, not committed)
|
|
19
|
+
*.rpm
|
|
20
|
+
*.deb
|
|
21
|
+
*.sha256
|
|
22
|
+
dist/
|
|
23
|
+
*.egg-info/
|
|
24
|
+
*.whl
|
|
25
|
+
|
|
26
|
+
# Standalone zipapp versioned executables
|
|
27
|
+
systemd-search-[0-9]*
|
|
28
|
+
|
|
29
|
+
# Staged binary copied by molecule prepare playbook
|
|
30
|
+
molecule/common/files/systemd-search
|
|
31
|
+
|
|
32
|
+
# macOS
|
|
33
|
+
.DS_Store
|
|
34
|
+
|
|
35
|
+
# Editors
|
|
36
|
+
.idea/
|
|
37
|
+
.vscode/
|
|
38
|
+
*.swp
|
|
39
|
+
*.swo
|
|
40
|
+
*~
|
|
41
|
+
.claude/settings.local.json
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Architecture & Design Decisions
|
|
2
|
+
|
|
3
|
+
This document explains how `systemd-search` is built, why each decision was made, and what trade-offs were accepted. It is written for anyone who needs to extend, debug, or maintain the tool in the future.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Problem statement
|
|
8
|
+
|
|
9
|
+
systemd has no native concept of user-defined tags on unit files. Finding all units that belong to a project means either maintaining a separate registry, grepping file contents, or knowing the naming convention ahead of time. None of those scale and all of them break when drop-in override files are involved.
|
|
10
|
+
|
|
11
|
+
`systemd-search` uses a property of the systemd unit file format to solve this without patching systemd or maintaining any external state.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## The `X-` section trick
|
|
16
|
+
|
|
17
|
+
The `systemd.unit(5)` man page specifies that any section whose name begins with `X-` is completely ignored by systemd. The unit loads and runs normally. Third-party applications are explicitly invited to use these sections for their own metadata.
|
|
18
|
+
|
|
19
|
+
This means an `[X-Labels]` block (or any `[X-*]` block) can be added to an existing unit file at any time with zero risk of affecting systemd behaviour. `systemd-search` reads those sections to provide a tagging layer on top of systemd with no daemon, no database, and no infrastructure.
|
|
20
|
+
|
|
21
|
+
Reference: <https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html>
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## How the tool works
|
|
26
|
+
|
|
27
|
+
### Step 1 — enumerate unit files
|
|
28
|
+
|
|
29
|
+
`systemctl list-unit-files --type=<type>` returns all installed units of the requested type and their enabled state (`enabled`, `disabled`, `static`, etc.). This is cheap and does not require reading any file on disk.
|
|
30
|
+
|
|
31
|
+
### Step 2 — read the effective configuration
|
|
32
|
+
|
|
33
|
+
For every unit that passes the enabled/disabled filter, `systemctl cat <unit>` is called. This command resolves the full configuration: the base unit file plus every drop-in file under `<unit>.d/` directories, concatenated in the correct override order. Later definitions of the same key win, which matches systemd's own merge semantics.
|
|
34
|
+
|
|
35
|
+
Parsing the raw file on disk would be wrong — it would miss drop-ins that change or add label keys.
|
|
36
|
+
|
|
37
|
+
### Step 3 — parse
|
|
38
|
+
|
|
39
|
+
The output of `systemctl cat` is INI-format text. It is parsed with Python's `configparser.RawConfigParser` rather than hand-rolled string splitting. Key decisions:
|
|
40
|
+
|
|
41
|
+
- `strict=False` — allows duplicate sections (base file and drop-in both define `[X-Labels]`) and duplicate keys. Later definitions overwrite earlier ones, which is exactly what systemd does when merging drop-ins.
|
|
42
|
+
- `delimiters=('=',)` — systemd never uses `:` as a key-value separator. Preventing configparser from treating it as one avoids misreading values like `ExecStart=/bin/sh -c 'key:value'`.
|
|
43
|
+
- `optionxform = str` — configparser lowercases keys by default. Label keys are case-sensitive (users define `Project`, not `project`), so this must be disabled.
|
|
44
|
+
- `RawConfigParser` rather than `ConfigParser` — prevents interpolation of `%`-style variables that may appear in systemd values.
|
|
45
|
+
|
|
46
|
+
### Step 4 — section gate
|
|
47
|
+
|
|
48
|
+
Every unit is checked for the presence of the target section **unconditionally**, regardless of what flags were passed. If the section is absent the unit is silently skipped. This is a hard invariant: `systemd-search` only surfaces units that carry the section. Running `systemd-search` with no flags does not list every service on the system — it lists only the services the operator has labelled.
|
|
49
|
+
|
|
50
|
+
This was a deliberate late decision. An early design allowed unlabelled units to appear when no label filter was set (to allow browsing). It was rejected because `systemd-search` is a tool for labelled infrastructure, not a general unit browser, and mixing unlabelled system services into the output defeats the purpose.
|
|
51
|
+
|
|
52
|
+
### Step 5 — apply label and exclude filters
|
|
53
|
+
|
|
54
|
+
`--label KEY` — the key must be present in the section.
|
|
55
|
+
`--label KEY=VALUE` — the key must be present and equal the given value.
|
|
56
|
+
`--exclude KEY` — the unit is dropped if the key is present, regardless of value.
|
|
57
|
+
`--exclude KEY=VALUE` — the unit is dropped only if the key is present *and* equals the value.
|
|
58
|
+
|
|
59
|
+
Multiple `--label` filters are ANDed: all must match.
|
|
60
|
+
Multiple `--exclude` filters are ORed: any hit drops the unit.
|
|
61
|
+
|
|
62
|
+
`--label` and `--exclude` compose freely with each other and with state filters.
|
|
63
|
+
|
|
64
|
+
### Step 6 — active state
|
|
65
|
+
|
|
66
|
+
`systemctl is-active <unit>` is called only when needed:
|
|
67
|
+
|
|
68
|
+
- `--active` or `--dead` is passed (state filter)
|
|
69
|
+
- `--json` is passed (the output always includes `is-active`)
|
|
70
|
+
|
|
71
|
+
In all other output modes the active-state subprocess call is skipped for performance. On a system with many labelled units this matters.
|
|
72
|
+
|
|
73
|
+
### Step 7 — output
|
|
74
|
+
|
|
75
|
+
Three output modes are mutually exclusive:
|
|
76
|
+
|
|
77
|
+
| Mode | Flag | Format |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| Plain | (default) | One unit name per line |
|
|
80
|
+
| Verbose | `--verbose` / `-v` | `unit-name\tkey=value …` |
|
|
81
|
+
| JSON | `--json` | JSON array, one object per unit |
|
|
82
|
+
|
|
83
|
+
`--verbose` shows only the keys that were queried with `--label`. If no `--label` filter is set it shows every key in the section.
|
|
84
|
+
|
|
85
|
+
`--json` always shows all section keys under `labels`, plus `enabled` (bool) and `is-active` (bool). The `enabled` field is derived from the enabled-state string returned by `systemctl list-unit-files`: the states `enabled`, `enabled-runtime`, `static`, `alias`, `generated`, and `transient` are treated as enabled; `disabled`, `masked`, `masked-runtime`, and `indirect` are treated as disabled.
|
|
86
|
+
|
|
87
|
+
### Exit code
|
|
88
|
+
|
|
89
|
+
Exit code `0` — at least one result was returned.
|
|
90
|
+
Exit code `1` — no results matched. This allows shell scripts and monitoring agents to detect the empty case without parsing output.
|
|
91
|
+
|
|
92
|
+
In JSON mode an empty result produces `[]` on stdout with exit code `1`, so JSON consumers always receive valid JSON regardless of whether results were found.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Python version target
|
|
97
|
+
|
|
98
|
+
The tool targets **Python 3.9** — the system Python on Rocky Linux 9. Rocky Linux 9 ships 3.9 as its default and that version will not change for the lifetime of the distribution. The tool must work without installing any additional Python version or any third-party packages. All dependencies (`argparse`, `configparser`, `json`, `subprocess`, `sys`) are standard library modules present in 3.9.
|
|
99
|
+
|
|
100
|
+
Unit tests are run against Python 3.9 in CI for the same reason.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Test strategy
|
|
105
|
+
|
|
106
|
+
### Unit tests (`tests/`)
|
|
107
|
+
|
|
108
|
+
Pure function tests with no I/O. They cover every testable function in the script:
|
|
109
|
+
|
|
110
|
+
- `parse_cat_output` — INI parsing, drop-in merge semantics, edge cases
|
|
111
|
+
- `parse_label_filters` — `KEY` and `KEY=VALUE` parsing, whitespace, embedded `=`
|
|
112
|
+
- `section_matches` — positive filter logic
|
|
113
|
+
- `section_excluded` — negative filter logic
|
|
114
|
+
- `format_json_entry` — JSON shape, `enabled` bool derivation, label copy
|
|
115
|
+
- `format_verbose` — tab-separated output, key selection
|
|
116
|
+
|
|
117
|
+
`main()` is not unit-tested directly because it depends on live `systemctl` calls. Integration coverage is provided by molecule.
|
|
118
|
+
|
|
119
|
+
The script is named `systemd-search` (no `.py` extension) to be installable as a plain executable. Importing a hyphen-named file requires `importlib.machinery.SourceFileLoader` rather than the standard import machinery. `tests/conftest.py` handles this once so test files can do a normal `import systemd_search`.
|
|
120
|
+
|
|
121
|
+
### Molecule integration tests (`molecule/`)
|
|
122
|
+
|
|
123
|
+
Two scenarios — `rocky` and `debian` — each define two platforms so a single `molecule test` run covers both versions of the distribution simultaneously. `rocky` tests Rocky Linux 9 and 10; `debian` tests Debian 12 and 13.
|
|
124
|
+
|
|
125
|
+
The fixture units are designed to exercise every code path:
|
|
126
|
+
|
|
127
|
+
| Unit | Labels | State |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| `fixture-single.service` | `Project=single-app` | enabled, not started |
|
|
130
|
+
| `fixture-multi-a.service` | `Project=myapp`, `Domain=foo`, `Env=prod` | enabled, active |
|
|
131
|
+
| `fixture-multi.timer` | `Project=myapp`, `Domain=foo`, `Env=prod` | enabled, active |
|
|
132
|
+
| `fixture-multi.path` | `Project=myapp`, `Domain=foo`, `Env=prod` | enabled, active |
|
|
133
|
+
| `fixture-disabled.service` | `Project=myapp`, `Visibility=disabled-only` | installed, not enabled |
|
|
134
|
+
| `fixture-failing.service` | `Project=myapp`, `Status=failing` | enabled, failed |
|
|
135
|
+
|
|
136
|
+
`fixture-single` deliberately uses `Project=single-app` (different value from the others) to verify that `--verbose` shows per-unit values and that `--exclude Project=myapp` correctly returns only this unit.
|
|
137
|
+
|
|
138
|
+
`fixture-failing` uses `ExecStart=/bin/false` to produce a genuine failed state. Molecule starts it with `ignore_errors: true` because systemd correctly refuses to report success when the service fails.
|
|
139
|
+
|
|
140
|
+
Molecule scenarios share converge and verify playbooks via `molecule/common/`. Only `molecule.yml` (the platform definition) differs between scenarios. The binary is staged from the project root into `molecule/common/files/systemd-search` by a `prepare.yml` playbook that runs on the Ansible controller before the container is provisioned, so the tool does not need to be built or packaged for testing.
|
|
141
|
+
|
|
142
|
+
Every molecule command that is expected to produce empty output has `failed_when: false` to prevent Ansible from treating exit code `1` as a task failure. Assertions on the registered result then check both `rc == 1` and empty stdout/`[]`.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Packaging
|
|
147
|
+
|
|
148
|
+
### RPM (`scripts/build-rpm.sh`)
|
|
149
|
+
|
|
150
|
+
Runs inside the target Rocky Linux container (`rockylinux:9` or `rockylinux:10`). Installs `rpm-build`, generates a spec file with the version from the `VERSION` file, and calls `rpmbuild -bb`. The resulting `.noarch.rpm` is copied back to the host workspace. `BuildArch: noarch` is correct because the tool is a Python script with no compiled components.
|
|
151
|
+
|
|
152
|
+
### DEB (`scripts/build-deb.sh`)
|
|
153
|
+
|
|
154
|
+
Runs inside the target Debian container (`debian:12` or `debian:13`). Builds a package directory tree under `/build/DEBIAN/` with a `control` file, then calls `dpkg-deb --build`. `Architecture: all` is the Debian equivalent of `noarch`.
|
|
155
|
+
|
|
156
|
+
### Version
|
|
157
|
+
|
|
158
|
+
The version is the git tag itself. Both packaging scripts accept it as a second positional argument (`build-rpm.sh <target> <version>`, `build-deb.sh <target> <version>`). In CI the tag is extracted from `GITHUB_REF` and passed through to the container. There is no separate `VERSION` file — the tag is the single source of truth.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## CI/CD pipeline
|
|
163
|
+
|
|
164
|
+
### Pipeline files
|
|
165
|
+
|
|
166
|
+
The CI/CD configuration is split across three files, following the DRY principle via GitHub Actions reusable workflows:
|
|
167
|
+
|
|
168
|
+
| File | Trigger | Responsibility |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| `tests.yml` | `workflow_call` (never directly) | Unit tests + molecule — the shared task definition |
|
|
171
|
+
| `pr.yml` | `pull_request`, push to `master`/`main` | Calls `tests.yml`; nothing else |
|
|
172
|
+
| `release.yml` | Push of any tag | Calls `tests.yml`, validates semver, builds packages, publishes release |
|
|
173
|
+
|
|
174
|
+
`tests.yml` is a reusable workflow (`on: workflow_call`). It is never triggered directly — only called by `pr.yml` and `release.yml`. This is the single definition of what "passing tests" means. Any change to the test suite is made once and is immediately in effect for both contexts.
|
|
175
|
+
|
|
176
|
+
### Semver validation
|
|
177
|
+
|
|
178
|
+
GitHub Actions `on.push.tags` only supports glob patterns, not regex. `release.yml` uses `tags: ["*"]` to catch all tag pushes, then a dedicated `validate-tag` job applies the official semver.org ERE regex via `grep -E`. The regex validates:
|
|
179
|
+
|
|
180
|
+
- `MAJOR.MINOR.PATCH` with no leading zeroes in any component
|
|
181
|
+
- Optional pre-release identifier after `-` (e.g. `1.0.0-beta.1`)
|
|
182
|
+
- Optional build metadata after `+` (e.g. `1.0.0+20250625`)
|
|
183
|
+
|
|
184
|
+
`validate-tag` runs in parallel with the `tests` job so a bad tag is rejected immediately without waiting for the full test suite. `package` and `executable` both `need: [tests, validate-tag]` — both gates must be green before a single package is built.
|
|
185
|
+
|
|
186
|
+
Tags carry no `v` prefix. The tag `1.2.0` produces the release `systemd-search 1.2.0`.
|
|
187
|
+
|
|
188
|
+
### Release
|
|
189
|
+
|
|
190
|
+
The `release` job downloads all artifacts uploaded by `package` and `executable`, then calls `gh release create` with the tag name. Each release includes:
|
|
191
|
+
|
|
192
|
+
- `systemd-search` — plain Python executable, runs on any distro with Python 3.9+
|
|
193
|
+
- `systemd-search-<version>-rocky9.noarch.rpm` + `.sha256`
|
|
194
|
+
- `systemd-search-<version>-rocky10.noarch.rpm` + `.sha256`
|
|
195
|
+
- `systemd-search-<version>-debian12.all.deb` + `.sha256`
|
|
196
|
+
- `systemd-search-<version>-debian13.all.deb` + `.sha256`
|
|
197
|
+
|
|
198
|
+
### Branch protection
|
|
199
|
+
|
|
200
|
+
Branch protection rules for `master` must require the following status checks (reported by `pr.yml`) to pass before merging:
|
|
201
|
+
|
|
202
|
+
- `Tests / Unit tests`
|
|
203
|
+
- `Tests / Molecule — rocky`
|
|
204
|
+
- `Tests / Molecule — debian`
|
|
205
|
+
|
|
206
|
+
This is configured in GitHub under **Settings → Branches → Branch protection rules** and is not encoded in any workflow file.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Local development
|
|
211
|
+
|
|
212
|
+
Dependencies are managed with Pipenv. The `Pipfile` pins Python 3.9 to match the production target. CI also uses Pipenv — there is no separate `requirements-test.txt`.
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
pipenv install --dev # create venv and install all dev deps
|
|
216
|
+
pipenv shell # activate
|
|
217
|
+
|
|
218
|
+
pytest tests/ -v # unit tests
|
|
219
|
+
molecule test -s rocky # integration tests (Docker required)
|
|
220
|
+
molecule test -s debian
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
To update dependencies:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
pipenv install --dev some-new-package
|
|
227
|
+
```
|