systemd-search 1.1.0__py3-none-any.whl
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/__init__.py +275 -0
- systemd_search-1.1.0.dist-info/METADATA +550 -0
- systemd_search-1.1.0.dist-info/RECORD +9 -0
- systemd_search-1.1.0.dist-info/WHEEL +5 -0
- systemd_search-1.1.0.dist-info/entry_points.txt +2 -0
- systemd_search-1.1.0.dist-info/licenses/LICENSE +184 -0
- systemd_search-1.1.0.dist-info/scm_file_list.json +33 -0
- systemd_search-1.1.0.dist-info/scm_version.json +8 -0
- systemd_search-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""systemd-search — Query systemd units by custom section labels.
|
|
2
|
+
|
|
3
|
+
Relies on `systemctl cat` for effective unit config (handles .d/ drop-ins).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import configparser
|
|
8
|
+
import json
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run(cmd):
|
|
14
|
+
return subprocess.run(cmd, capture_output=True, text=True)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def list_unit_files(types):
|
|
18
|
+
"""Return [(unit_name, enabled_state)] for the given unit types."""
|
|
19
|
+
args = ["systemctl", "list-unit-files", "--no-legend", "--no-pager"]
|
|
20
|
+
if types:
|
|
21
|
+
args.append("--type=" + ",".join(types))
|
|
22
|
+
result = run(args)
|
|
23
|
+
units = []
|
|
24
|
+
for line in result.stdout.splitlines():
|
|
25
|
+
parts = line.split()
|
|
26
|
+
if len(parts) >= 2:
|
|
27
|
+
units.append((parts[0], parts[1]))
|
|
28
|
+
return units
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_cat_output(output):
|
|
32
|
+
"""Parse systemctl cat output into {section: {key: value}}.
|
|
33
|
+
|
|
34
|
+
Uses configparser so the full INI grammar is handled correctly.
|
|
35
|
+
strict=False lets duplicate sections (base file + drop-ins) merge and
|
|
36
|
+
duplicate keys resolve to the last definition, matching systemd semantics.
|
|
37
|
+
delimiters=('=',) avoids treating ':' as a separator (systemd never does).
|
|
38
|
+
optionxform=str preserves key casing.
|
|
39
|
+
"""
|
|
40
|
+
parser = configparser.RawConfigParser(
|
|
41
|
+
strict=False,
|
|
42
|
+
delimiters=("=",),
|
|
43
|
+
)
|
|
44
|
+
parser.optionxform = str # preserve key case (e.g. ExecStart, not execstart)
|
|
45
|
+
parser.read_string(output)
|
|
46
|
+
return {section: dict(parser.items(section)) for section in parser.sections()}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_unit_config(unit):
|
|
50
|
+
result = run(["systemctl", "cat", "--", unit])
|
|
51
|
+
if result.returncode != 0:
|
|
52
|
+
return {}
|
|
53
|
+
return parse_cat_output(result.stdout)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_active(unit):
|
|
57
|
+
result = run(["systemctl", "is-active", "--", unit])
|
|
58
|
+
return result.stdout.strip() == "active"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def build_parser():
|
|
62
|
+
p = argparse.ArgumentParser(
|
|
63
|
+
prog="systemd-search",
|
|
64
|
+
description="Query systemd units by custom section labels",
|
|
65
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
66
|
+
epilog="""
|
|
67
|
+
examples:
|
|
68
|
+
systemd-search --label Project
|
|
69
|
+
systemd-search --verbose --label Project
|
|
70
|
+
systemd-search --label Project=myapp
|
|
71
|
+
systemd-search --label Project=myapp --type service --type timer
|
|
72
|
+
systemd-search --label Project=myapp --type service --enabled
|
|
73
|
+
systemd-search --label Project=myapp --type service --enabled --active
|
|
74
|
+
systemd-search --label Project=myapp --type service --disabled --dead
|
|
75
|
+
systemd-search --label Project=myapp --exclude Domain
|
|
76
|
+
systemd-search --label Project=myapp --exclude Env=staging --type service --enabled
|
|
77
|
+
""",
|
|
78
|
+
)
|
|
79
|
+
p.add_argument(
|
|
80
|
+
"--section",
|
|
81
|
+
default="X-Labels",
|
|
82
|
+
metavar="SECTION",
|
|
83
|
+
help="Unit file section to inspect (default: X-Labels)",
|
|
84
|
+
)
|
|
85
|
+
p.add_argument(
|
|
86
|
+
"--label",
|
|
87
|
+
action="append",
|
|
88
|
+
default=[],
|
|
89
|
+
metavar="KEY[=VALUE]",
|
|
90
|
+
help="Filter by label key, or key=value. Repeatable (all must match).",
|
|
91
|
+
)
|
|
92
|
+
p.add_argument(
|
|
93
|
+
"--exclude",
|
|
94
|
+
action="append",
|
|
95
|
+
default=[],
|
|
96
|
+
metavar="KEY[=VALUE]",
|
|
97
|
+
help=(
|
|
98
|
+
"Exclude units where KEY exists, or where KEY=VALUE matches. "
|
|
99
|
+
"Repeatable. Units that lack the section entirely are always excluded."
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
p.add_argument(
|
|
103
|
+
"--type",
|
|
104
|
+
action="append",
|
|
105
|
+
default=[],
|
|
106
|
+
dest="unit_types",
|
|
107
|
+
metavar="TYPE",
|
|
108
|
+
help="Unit type to include (default: service). Repeatable.",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
state = p.add_argument_group("state filters")
|
|
112
|
+
enabled_grp = state.add_mutually_exclusive_group()
|
|
113
|
+
enabled_grp.add_argument(
|
|
114
|
+
"--enabled", action="store_true", default=False, help="Only enabled units"
|
|
115
|
+
)
|
|
116
|
+
enabled_grp.add_argument(
|
|
117
|
+
"--disabled", action="store_true", default=False, help="Only disabled units"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
active_grp = state.add_mutually_exclusive_group()
|
|
121
|
+
active_grp.add_argument(
|
|
122
|
+
"--active", action="store_true", default=False, help="Only active (running) units"
|
|
123
|
+
)
|
|
124
|
+
active_grp.add_argument(
|
|
125
|
+
"--dead", action="store_true", default=False, help="Only inactive/dead units"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
output_grp = p.add_mutually_exclusive_group()
|
|
129
|
+
output_grp.add_argument(
|
|
130
|
+
"--verbose",
|
|
131
|
+
"-v",
|
|
132
|
+
action="store_true",
|
|
133
|
+
default=False,
|
|
134
|
+
help="Print matched label key=value pairs alongside unit name",
|
|
135
|
+
)
|
|
136
|
+
output_grp.add_argument(
|
|
137
|
+
"--json",
|
|
138
|
+
action="store_true",
|
|
139
|
+
default=False,
|
|
140
|
+
help="Emit results as a JSON array with name, enabled, is-active, and labels",
|
|
141
|
+
)
|
|
142
|
+
return p
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def parse_label_filters(raw_labels):
|
|
146
|
+
"""Return [(key, value_or_None)] from raw --label arguments."""
|
|
147
|
+
filters = []
|
|
148
|
+
for item in raw_labels:
|
|
149
|
+
if "=" in item:
|
|
150
|
+
k, _, v = item.partition("=")
|
|
151
|
+
filters.append((k.strip(), v.strip()))
|
|
152
|
+
else:
|
|
153
|
+
filters.append((item.strip(), None))
|
|
154
|
+
return filters
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def section_matches(section_data, label_filters):
|
|
158
|
+
"""Return True if section_data satisfies all label_filters."""
|
|
159
|
+
for key, value in label_filters:
|
|
160
|
+
if key not in section_data:
|
|
161
|
+
return False
|
|
162
|
+
if value is not None and section_data[key] != value:
|
|
163
|
+
return False
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def section_excluded(section_data, exclude_filters):
|
|
168
|
+
"""Return True if section_data triggers any exclude_filter.
|
|
169
|
+
|
|
170
|
+
--exclude KEY → exclude when KEY is present in the section.
|
|
171
|
+
--exclude KEY=VALUE → exclude when KEY is present and equals VALUE.
|
|
172
|
+
"""
|
|
173
|
+
for key, value in exclude_filters:
|
|
174
|
+
if value is None:
|
|
175
|
+
if key in section_data:
|
|
176
|
+
return True
|
|
177
|
+
else:
|
|
178
|
+
if section_data.get(key) == value:
|
|
179
|
+
return True
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def format_json_entry(unit_name, enabled_state, active, section_data):
|
|
184
|
+
"""Build one JSON result object."""
|
|
185
|
+
return {
|
|
186
|
+
"name": unit_name,
|
|
187
|
+
"enabled": enabled_state in ENABLED_STATES,
|
|
188
|
+
"is-active": active,
|
|
189
|
+
"labels": dict(section_data),
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def format_verbose(unit_name, section_data, label_filters):
|
|
194
|
+
"""Build the output line when --verbose is set."""
|
|
195
|
+
if label_filters:
|
|
196
|
+
keys_to_show = [k for k, _ in label_filters]
|
|
197
|
+
else:
|
|
198
|
+
keys_to_show = list(section_data.keys())
|
|
199
|
+
|
|
200
|
+
pairs = " ".join(
|
|
201
|
+
f"{k}={section_data[k]}" for k in keys_to_show if k in section_data
|
|
202
|
+
)
|
|
203
|
+
return f"{unit_name}\t{pairs}" if pairs else unit_name
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# States that count as "enabled" per systemctl semantics
|
|
207
|
+
ENABLED_STATES = {"enabled", "enabled-runtime", "static", "alias", "generated", "transient"}
|
|
208
|
+
DISABLED_STATES = {"disabled", "masked", "masked-runtime", "indirect"}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def main():
|
|
212
|
+
parser = build_parser()
|
|
213
|
+
args = parser.parse_args()
|
|
214
|
+
|
|
215
|
+
unit_types = args.unit_types if args.unit_types else ["service"]
|
|
216
|
+
label_filters = parse_label_filters(args.label)
|
|
217
|
+
exclude_filters = parse_label_filters(args.exclude)
|
|
218
|
+
|
|
219
|
+
units = list_unit_files(unit_types)
|
|
220
|
+
json_results = []
|
|
221
|
+
found = False
|
|
222
|
+
|
|
223
|
+
for unit_name, enabled_state in units:
|
|
224
|
+
# --- enabled/disabled filter ---
|
|
225
|
+
if args.enabled and enabled_state not in ENABLED_STATES:
|
|
226
|
+
continue
|
|
227
|
+
if args.disabled and enabled_state not in DISABLED_STATES:
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
# --- section gate (always enforced) ---
|
|
231
|
+
# Units without the section are outside the scope of this tool entirely.
|
|
232
|
+
config = get_unit_config(unit_name)
|
|
233
|
+
if args.section not in config:
|
|
234
|
+
continue
|
|
235
|
+
section_data = config[args.section]
|
|
236
|
+
|
|
237
|
+
# --- label / exclude filters ---
|
|
238
|
+
if label_filters and not section_matches(section_data, label_filters):
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
if exclude_filters and section_excluded(section_data, exclude_filters):
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
# --- active/dead filter + active state resolution ---
|
|
245
|
+
# JSON always needs the active state; other modes only fetch it when filtering.
|
|
246
|
+
if args.json or args.active or args.dead:
|
|
247
|
+
active = is_active(unit_name)
|
|
248
|
+
if args.active and not active:
|
|
249
|
+
continue
|
|
250
|
+
if args.dead and active:
|
|
251
|
+
continue
|
|
252
|
+
else:
|
|
253
|
+
active = None
|
|
254
|
+
|
|
255
|
+
found = True
|
|
256
|
+
|
|
257
|
+
# --- output ---
|
|
258
|
+
if args.json:
|
|
259
|
+
json_results.append(
|
|
260
|
+
format_json_entry(unit_name, enabled_state, active, section_data)
|
|
261
|
+
)
|
|
262
|
+
elif args.verbose:
|
|
263
|
+
print(format_verbose(unit_name, section_data, label_filters))
|
|
264
|
+
else:
|
|
265
|
+
print(unit_name)
|
|
266
|
+
|
|
267
|
+
if args.json:
|
|
268
|
+
print(json.dumps(json_results, indent=2))
|
|
269
|
+
|
|
270
|
+
if not found:
|
|
271
|
+
sys.exit(1)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
if __name__ == "__main__":
|
|
275
|
+
main()
|
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: systemd-search
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Query systemd units by custom section labels
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Project-URL: Repository, https://github.com/leventyalcin/systemd-search
|
|
7
|
+
Keywords: systemd,systemctl,units,labels,search
|
|
8
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Topic :: System :: Systems Administration
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# systemd-search
|
|
20
|
+
|
|
21
|
+
A CLI tool for finding systemd units by custom labels embedded directly in the unit files.
|
|
22
|
+
|
|
23
|
+
## The problem
|
|
24
|
+
|
|
25
|
+
Tracking which units belong to which project or domain on a busy system has no good native solution. The usual approach — browsing `/etc/systemd/system/`, grepping file contents, running `systemctl cat` on anything suspicious — is slow and error-prone. There is no built-in way to tag a unit and query by that tag.
|
|
26
|
+
|
|
27
|
+
`systemd-search` fills that gap. Labels are embedded in a dedicated section inside the unit file itself. The tool reads those labels and filters units by them, covering type, enabled state, and active state in a single command.
|
|
28
|
+
|
|
29
|
+
## How it works — the `X-` section trick
|
|
30
|
+
|
|
31
|
+
The `systemd.unit(5)` man page explicitly states:
|
|
32
|
+
|
|
33
|
+
> *Sections whose name is prefixed with `X-` are ignored by systemd.*
|
|
34
|
+
> *Such sections can be used by applications to store additional information in the unit files.*
|
|
35
|
+
|
|
36
|
+
— [systemd.unit(5)](https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#%5BUnit%5D%20Section%20Options)
|
|
37
|
+
|
|
38
|
+
An `[X-Labels]` section (or any `[X-*]` section) can be added to any unit file and systemd will load and run the unit exactly as if that section were not there. `systemd-search` reads those sections and uses them as a lightweight tagging system on top of systemd.
|
|
39
|
+
|
|
40
|
+
The tool resolves the final merged configuration through `systemctl cat` before reading any labels, so drop-in override files (`.d/*.conf`) are always taken into account — the search never operates on stale or partial file content.
|
|
41
|
+
|
|
42
|
+
## Example unit file
|
|
43
|
+
|
|
44
|
+
```ini
|
|
45
|
+
# /etc/systemd/system/myapp-worker.service
|
|
46
|
+
[Unit]
|
|
47
|
+
Description=My Application Worker
|
|
48
|
+
After=network.target
|
|
49
|
+
|
|
50
|
+
[Service]
|
|
51
|
+
User=myapp
|
|
52
|
+
ExecStart=/opt/myapp/bin/worker
|
|
53
|
+
Restart=on-failure
|
|
54
|
+
|
|
55
|
+
[X-Labels]
|
|
56
|
+
Project=myapp
|
|
57
|
+
Domain=backend
|
|
58
|
+
Component=worker
|
|
59
|
+
Environment=production
|
|
60
|
+
ManagedBy=ansible
|
|
61
|
+
|
|
62
|
+
[Install]
|
|
63
|
+
WantedBy=multi-user.target
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Any section name starting with `X-` is valid. The default section `systemd-search` reads is `X-Labels`. A different section can be specified with `--section`.
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
### pip
|
|
71
|
+
|
|
72
|
+
The simplest installation on any system with Python 3.9+:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install systemd-search
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For a user-local install without root:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install --user systemd-search
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### From GitHub Releases
|
|
85
|
+
|
|
86
|
+
Each release ships a self-contained zipapp executable, native packages, and checksums:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# Self-contained zipapp — runs on any host with Python 3.9+, no pip needed
|
|
90
|
+
curl -LO https://github.com/leventyalcin/systemd-search/releases/latest/download/systemd-search-1.0.0
|
|
91
|
+
chmod +x systemd-search-1.0.0
|
|
92
|
+
sudo mv systemd-search-1.0.0 /usr/local/bin/systemd-search
|
|
93
|
+
|
|
94
|
+
# RPM (Rocky Linux 9)
|
|
95
|
+
sudo rpm -i systemd-search-1.0.0-rocky9.noarch.rpm
|
|
96
|
+
|
|
97
|
+
# DEB (Debian 12)
|
|
98
|
+
sudo dpkg -i systemd-search-1.0.0-debian12.all.deb
|
|
99
|
+
|
|
100
|
+
# Verify checksum before installing
|
|
101
|
+
sha256sum -c systemd-search-1.0.0-rocky9.noarch.rpm.sha256
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Manual
|
|
105
|
+
|
|
106
|
+
Copy the script to any directory on the system `PATH`:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
sudo cp systemd-search /usr/local/bin/systemd-search
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Requirements:** Python 3.9+ with no third-party packages. On Rocky Linux 9 this is the system default Python and requires no additional installation.
|
|
113
|
+
|
|
114
|
+
## Usage
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
systemd-search [--section SECTION] [--label KEY[=VALUE]] [--type TYPE]
|
|
118
|
+
[--enabled | --disabled] [--active | --dead] [--verbose]
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
| Flag | Default | Description |
|
|
122
|
+
|-----------------------|------------|-----------------------------------------------------------------------------|
|
|
123
|
+
| `--label KEY` | — | Matches units that have this key in the section |
|
|
124
|
+
| `--label KEY=VALUE` | — | Matches units where the key equals the value |
|
|
125
|
+
| `--exclude KEY` | — | Skips units that have this key in the section. Repeatable. |
|
|
126
|
+
| `--exclude KEY=VALUE` | — | Skips units where the key equals the value. Repeatable. |
|
|
127
|
+
| `--section NAME` | `X-Labels` | Section to read labels from |
|
|
128
|
+
| `--type TYPE` | `service` | Unit type to include (`service`, `timer`, `path`, `socket`, …). Repeatable. |
|
|
129
|
+
| `--enabled` | — | Limits results to enabled units |
|
|
130
|
+
| `--disabled` | — | Limits results to disabled units |
|
|
131
|
+
| `--active` | — | Limits results to active (running) units |
|
|
132
|
+
| `--dead` | — | Limits results to inactive or failed units |
|
|
133
|
+
| `--verbose` / `-v` | — | Prints matched label key=value pairs alongside each unit name |
|
|
134
|
+
|
|
135
|
+
`--enabled` and `--disabled` are mutually exclusive. So are `--active` and `--dead`. Omitting either pair includes all units regardless of that state.
|
|
136
|
+
|
|
137
|
+
When `--exclude` is active, units that lack the section entirely are silently dropped — the filter only operates on units that carry the section in their configuration.
|
|
138
|
+
|
|
139
|
+
## Examples
|
|
140
|
+
|
|
141
|
+
### Find all services that belong to a project
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
systemd-search --label Project=myapp
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
```text
|
|
148
|
+
myapp-worker.service
|
|
149
|
+
myapp-scheduler.service
|
|
150
|
+
myapp-cleanup.service
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Print the label values alongside each unit name
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
systemd-search --verbose --label Project=myapp
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
```text
|
|
160
|
+
myapp-worker.service Project=myapp
|
|
161
|
+
myapp-scheduler.service Project=myapp
|
|
162
|
+
myapp-cleanup.service Project=myapp
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Search across multiple unit types
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
systemd-search --label Project=myapp --type service --type timer --type path
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```text
|
|
172
|
+
myapp-worker.service
|
|
173
|
+
myapp-cleanup.service
|
|
174
|
+
myapp-refresh.timer
|
|
175
|
+
myapp-trigger.path
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Narrow by a specific label and type
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
systemd-search --label Component=worker --type service
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Find only the running services for a project
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
systemd-search --label Project=myapp --type service --enabled --active
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Find services that are enabled but not running
|
|
191
|
+
|
|
192
|
+
Useful for spotting crashed or failed units:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
systemd-search --label Project=myapp --type service --enabled --dead
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Find services that are installed but not enabled
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
systemd-search --label Project=myapp --disabled
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Use a custom section name
|
|
205
|
+
|
|
206
|
+
Labels do not have to live in `[X-Labels]`. Any `[X-*]` section works:
|
|
207
|
+
|
|
208
|
+
```ini
|
|
209
|
+
[X-Meta]
|
|
210
|
+
Project=myapp
|
|
211
|
+
Team=platform
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
systemd-search --section X-Meta --label Team=platform
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Match on multiple labels simultaneously
|
|
219
|
+
|
|
220
|
+
All supplied `--label` filters must match for a unit to appear in the results:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
systemd-search --label Project=myapp --label Environment=production --type service
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Exclude units that have a specific key
|
|
227
|
+
|
|
228
|
+
`--exclude KEY` skips any unit in the section that carries that key, regardless of its value:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
systemd-search --label Project=myapp --exclude Domain
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Only units labelled with `Project=myapp` that have no `Domain` key are returned.
|
|
235
|
+
|
|
236
|
+
### Exclude units where a key matches a specific value
|
|
237
|
+
|
|
238
|
+
`--exclude KEY=VALUE` skips units only when the key exists and holds that exact value. Units where the key is absent or holds a different value still appear:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
systemd-search --label Project=myapp --exclude Env=staging
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Returns all `myapp` services except those explicitly labelled `Env=staging`.
|
|
245
|
+
|
|
246
|
+
### Combine positive and negative filters
|
|
247
|
+
|
|
248
|
+
`--label` and `--exclude` compose freely. All `--label` conditions must hold and no `--exclude` condition must trigger for a unit to appear:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
systemd-search \
|
|
252
|
+
--label Project=myapp \
|
|
253
|
+
--label Domain=backend \
|
|
254
|
+
--exclude Component=worker \
|
|
255
|
+
--exclude Env=staging \
|
|
256
|
+
--type service \
|
|
257
|
+
--enabled
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Reads as: *services for the myapp backend, enabled, excluding workers and staging instances.*
|
|
261
|
+
|
|
262
|
+
### Verbose output with multiple label filters
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
systemd-search --verbose --label Project=myapp --label Domain=backend
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
```text
|
|
269
|
+
myapp-worker.service Project=myapp Domain=backend
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Monitoring integration
|
|
273
|
+
|
|
274
|
+
`systemd-search --json` produces machine-readable output that can be piped directly into monitoring agents. Any combination of filters can precede it — label filters, state filters, unit types — and the result carries enough context for downstream tools to slice and count however the use case demands.
|
|
275
|
+
|
|
276
|
+
A few possibilities:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
# All dead services for a project, as JSON
|
|
280
|
+
systemd-search --json --label Project=myapp --dead
|
|
281
|
+
|
|
282
|
+
# Count of enabled-but-dead units across all labelled services
|
|
283
|
+
systemd-search --json | jq '[.[] | select(.enabled and (.["is-active"] | not))] | length'
|
|
284
|
+
|
|
285
|
+
# Feed into a monitoring agent as a metric
|
|
286
|
+
systemd-search --json --label Project=myapp | jq '.[] | select(.["is-active"] | not) | .name'
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
The examples below show one way to wire this into three common monitoring agents. They are starting points, not prescriptions.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
### Telegraf
|
|
294
|
+
|
|
295
|
+
The `exec` input plugin runs an arbitrary command on a schedule and parses its output as metrics. A small wrapper script calls `systemd-search --json` once per project and uses `jq` to derive all counters from the single result, avoiding repeated invocations. The output is [InfluxDB line protocol](https://docs.influxdata.com/influxdb/v2/reference/syntax/line-protocol/).
|
|
296
|
+
|
|
297
|
+
**`/usr/local/bin/systemd-search-metrics.sh`**
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
#!/bin/bash
|
|
301
|
+
# Emits one influx line per project with unit state counts.
|
|
302
|
+
# Add or remove projects to match the labels used on this host.
|
|
303
|
+
|
|
304
|
+
set -euo pipefail
|
|
305
|
+
|
|
306
|
+
PROJECTS=(myapp payments auth)
|
|
307
|
+
|
|
308
|
+
for project in "${PROJECTS[@]}"; do
|
|
309
|
+
units=$(systemd-search --json \
|
|
310
|
+
--label Project="$project" \
|
|
311
|
+
--type service --type timer --type path)
|
|
312
|
+
|
|
313
|
+
dead=$( echo "$units" | jq '[.[] | select(.enabled and (.["is-active"] | not))] | length')
|
|
314
|
+
active=$( echo "$units" | jq '[.[] | select(.enabled and .["is-active"] )] | length')
|
|
315
|
+
disabled=$(echo "$units" | jq '[.[] | select(.enabled | not) ] | length')
|
|
316
|
+
|
|
317
|
+
echo "systemd_units,project=${project} dead=${dead}i,active=${active}i,disabled=${disabled}i"
|
|
318
|
+
done
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**`/etc/telegraf/telegraf.d/systemd-search.conf`**
|
|
322
|
+
|
|
323
|
+
```toml
|
|
324
|
+
[[inputs.exec]]
|
|
325
|
+
## Script must be executable: chmod +x /usr/local/bin/systemd-search-metrics.sh
|
|
326
|
+
commands = ["/usr/local/bin/systemd-search-metrics.sh"]
|
|
327
|
+
timeout = "15s"
|
|
328
|
+
interval = "60s"
|
|
329
|
+
data_format = "influx"
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
The resulting measurement `systemd_units` carries a `project` tag and `dead`/`active`/`disabled` fields. An alert fires when `dead > 0` for any project.
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
### Datadog
|
|
337
|
+
|
|
338
|
+
The Datadog Agent supports [custom Python checks](https://docs.datadoghq.com/developers/custom_checks/write_agent_check/) that emit arbitrary metrics. The check below calls `systemd-search --json` once per configured project and derives all counters from the single JSON result.
|
|
339
|
+
|
|
340
|
+
**`/etc/datadog-agent/checks.d/systemd_labels.py`**
|
|
341
|
+
|
|
342
|
+
```python
|
|
343
|
+
import json
|
|
344
|
+
import subprocess
|
|
345
|
+
from datadog_checks.base import AgentCheck
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
class SystemdLabelsCheck(AgentCheck):
|
|
349
|
+
__NAMESPACE__ = "systemd"
|
|
350
|
+
|
|
351
|
+
def check(self, instance):
|
|
352
|
+
project = instance["project"]
|
|
353
|
+
section = instance.get("section", "X-Labels")
|
|
354
|
+
label_key = instance.get("label_key", "Project")
|
|
355
|
+
types = instance.get("types", ["service"])
|
|
356
|
+
|
|
357
|
+
type_args = []
|
|
358
|
+
for t in types:
|
|
359
|
+
type_args += ["--type", t]
|
|
360
|
+
|
|
361
|
+
cmd = [
|
|
362
|
+
"systemd-search", "--json",
|
|
363
|
+
"--section", section,
|
|
364
|
+
"--label", f"{label_key}={project}",
|
|
365
|
+
] + type_args
|
|
366
|
+
|
|
367
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
368
|
+
units = json.loads(result.stdout) if result.returncode == 0 else []
|
|
369
|
+
|
|
370
|
+
dead = sum(1 for u in units if u["enabled"] and not u["is-active"])
|
|
371
|
+
active = sum(1 for u in units if u["enabled"] and u["is-active"])
|
|
372
|
+
disabled = sum(1 for u in units if not u["enabled"])
|
|
373
|
+
|
|
374
|
+
tags = [f"project:{project}"]
|
|
375
|
+
self.gauge("units.dead", dead, tags=tags)
|
|
376
|
+
self.gauge("units.active", active, tags=tags)
|
|
377
|
+
self.gauge("units.disabled", disabled, tags=tags)
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
**`/etc/datadog-agent/conf.d/systemd_labels.d/conf.yaml`**
|
|
381
|
+
|
|
382
|
+
```yaml
|
|
383
|
+
instances:
|
|
384
|
+
- project: myapp
|
|
385
|
+
types: [service, timer, path]
|
|
386
|
+
|
|
387
|
+
- project: payments
|
|
388
|
+
types: [service]
|
|
389
|
+
|
|
390
|
+
- project: auth
|
|
391
|
+
section: X-Meta # override if a different section name is used
|
|
392
|
+
label_key: Application
|
|
393
|
+
types: [service, timer]
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
The check emits `systemd.units.dead`, `systemd.units.active`, and `systemd.units.disabled` with a `project` tag. A monitor on `systemd.units.dead > 0` grouped by `project` covers all labelled projects in a single alert rule.
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
### Dynatrace
|
|
401
|
+
|
|
402
|
+
Dynatrace ingests custom metrics through its [Metrics Ingest v2 API](https://docs.dynatrace.com/docs/dynatrace-api/environment-api/metric-v2/metric-ingest). A script pushed by a systemd timer calls `systemd-search --json` once per project and uses `jq` to compute all counters before pushing a single batch payload.
|
|
403
|
+
|
|
404
|
+
**`/usr/local/bin/systemd-search-dynatrace.sh`**
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
#!/bin/bash
|
|
408
|
+
# Push unit state metrics for labelled projects to Dynatrace Metrics Ingest v2.
|
|
409
|
+
|
|
410
|
+
set -euo pipefail
|
|
411
|
+
|
|
412
|
+
DT_URL="${DYNATRACE_URL}" # e.g. https://abc12345.live.dynatrace.com
|
|
413
|
+
DT_TOKEN="${DYNATRACE_API_TOKEN}" # Ingest Metrics (metrics.ingest) scope required
|
|
414
|
+
PROJECTS=(myapp payments auth)
|
|
415
|
+
|
|
416
|
+
payload=""
|
|
417
|
+
|
|
418
|
+
for project in "${PROJECTS[@]}"; do
|
|
419
|
+
units=$(systemd-search --json \
|
|
420
|
+
--label Project="$project" \
|
|
421
|
+
--type service --type timer --type path)
|
|
422
|
+
|
|
423
|
+
dead=$( echo "$units" | jq '[.[] | select(.enabled and (.["is-active"] | not))] | length')
|
|
424
|
+
active=$( echo "$units" | jq '[.[] | select(.enabled and .["is-active"] )] | length')
|
|
425
|
+
disabled=$(echo "$units" | jq '[.[] | select(.enabled | not) ] | length')
|
|
426
|
+
|
|
427
|
+
# Dynatrace line protocol: metric.key,dimensions gauge,value
|
|
428
|
+
payload+="systemd.units.dead,project=${project} gauge,${dead}"$'\n'
|
|
429
|
+
payload+="systemd.units.active,project=${project} gauge,${active}"$'\n'
|
|
430
|
+
payload+="systemd.units.disabled,project=${project} gauge,${disabled}"$'\n'
|
|
431
|
+
done
|
|
432
|
+
|
|
433
|
+
curl -sf -X POST "${DT_URL}/api/v2/metrics/ingest" \
|
|
434
|
+
-H "Authorization: Api-Token ${DT_TOKEN}" \
|
|
435
|
+
-H "Content-Type: text/plain; charset=utf-8" \
|
|
436
|
+
--data-raw "${payload}"
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**`/etc/systemd/system/systemd-search-dynatrace.service`**
|
|
440
|
+
|
|
441
|
+
```ini
|
|
442
|
+
[Unit]
|
|
443
|
+
Description=Push systemd label metrics to Dynatrace
|
|
444
|
+
After=network-online.target
|
|
445
|
+
Wants=network-online.target
|
|
446
|
+
|
|
447
|
+
[Service]
|
|
448
|
+
Type=oneshot
|
|
449
|
+
EnvironmentFile=/etc/systemd-search/dynatrace.env
|
|
450
|
+
ExecStart=/usr/local/bin/systemd-search-dynatrace.sh
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
**`/etc/systemd/system/systemd-search-dynatrace.timer`**
|
|
454
|
+
|
|
455
|
+
```ini
|
|
456
|
+
[Unit]
|
|
457
|
+
Description=Run Dynatrace metric push every 60 seconds
|
|
458
|
+
|
|
459
|
+
[Timer]
|
|
460
|
+
OnBootSec=30s
|
|
461
|
+
OnUnitActiveSec=60s
|
|
462
|
+
AccuracySec=5s
|
|
463
|
+
|
|
464
|
+
[Install]
|
|
465
|
+
WantedBy=timers.target
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
**`/etc/systemd-search/dynatrace.env`**
|
|
469
|
+
|
|
470
|
+
```bash
|
|
471
|
+
DYNATRACE_URL=https://abc12345.live.dynatrace.com
|
|
472
|
+
DYNATRACE_API_TOKEN=dt0c01.XXXXXXXXXXXX...
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Enable the timer:
|
|
476
|
+
|
|
477
|
+
```bash
|
|
478
|
+
systemctl enable --now systemd-search-dynatrace.timer
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
The metric `systemd.units.dead` is then available in Dynatrace with a `project` dimension. An anomaly detection rule or a fixed threshold alert on that metric covers all labelled projects without any per-service configuration in Dynatrace itself.
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
485
|
+
## Development
|
|
486
|
+
|
|
487
|
+
All development dependencies are managed with [Pipenv](https://pipenv.pypa.io). The `Pipfile` pins Python 3.9 to match the system Python on Rocky Linux 9 — the primary deployment target.
|
|
488
|
+
|
|
489
|
+
### First-time setup
|
|
490
|
+
|
|
491
|
+
Install Pipenv if not already present:
|
|
492
|
+
|
|
493
|
+
```bash
|
|
494
|
+
pip install --user pipenv
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
Then create the virtual environment and install all dev dependencies:
|
|
498
|
+
|
|
499
|
+
```bash
|
|
500
|
+
pipenv install --dev
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
This creates a Python 3.9 virtual environment under `.venv/` (or the Pipenv default location) and installs pytest, Molecule, Ansible, and the Docker driver.
|
|
504
|
+
|
|
505
|
+
### Entering the environment
|
|
506
|
+
|
|
507
|
+
```bash
|
|
508
|
+
pipenv shell
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
All subsequent commands in this section assume the environment is active. Alternatively, prefix any single command with `pipenv run`:
|
|
512
|
+
|
|
513
|
+
```bash
|
|
514
|
+
pipenv run pytest tests/ -v
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
### Unit tests
|
|
518
|
+
|
|
519
|
+
```bash
|
|
520
|
+
pytest tests/ -v
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
The unit tests target **Python 3.9** — the system Python on Rocky Linux 9. That version ships as the default on Rocky Linux 9 and will not change for the lifetime of the distribution. Running tests against 3.9 ensures the tool works on that platform without any additional Python installation and catches accidental use of language or stdlib features introduced in later versions.
|
|
524
|
+
|
|
525
|
+
### Integration tests
|
|
526
|
+
|
|
527
|
+
Molecule tests install the tool inside real systemd containers and exercise every search combination against live units. Docker must be running.
|
|
528
|
+
|
|
529
|
+
```bash
|
|
530
|
+
molecule test -s rocky # tests Rocky Linux 9 and 10 in parallel
|
|
531
|
+
molecule test -s debian # tests Debian 12 and 13 in parallel
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
The scenarios use the `geerlingguy/docker-*-ansible` images, which are systemd-capable images built for this kind of testing.
|
|
535
|
+
|
|
536
|
+
### Updating dependencies
|
|
537
|
+
|
|
538
|
+
```bash
|
|
539
|
+
# Add or upgrade a dev dependency
|
|
540
|
+
pipenv install --dev some-package
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### CI/CD
|
|
544
|
+
|
|
545
|
+
Pull requests must pass unit tests and both molecule scenarios before merging. Pushing a semver tag triggers the packaging and release jobs, which build RPM and DEB packages, publish the wheel to PyPI, and create a GitHub Release. The tag is the version — there is no separate version file.
|
|
546
|
+
|
|
547
|
+
```bash
|
|
548
|
+
git tag 1.2.0
|
|
549
|
+
git push origin 1.2.0
|
|
550
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
systemd_search/__init__.py,sha256=zYts7usaEEr5URAly7B6xO1CZlajOLrBW61oGSk0Byo,8669
|
|
2
|
+
systemd_search-1.1.0.dist-info/licenses/LICENSE,sha256=SItcyiWGgtkaNWqFjRplx4-DHIJpG0CkC3hhST0U4gQ,10284
|
|
3
|
+
systemd_search-1.1.0.dist-info/METADATA,sha256=XtdGySID9RUvx--kzASJCFzVvJe0ujtcOL3nhxGMh5A,18285
|
|
4
|
+
systemd_search-1.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
5
|
+
systemd_search-1.1.0.dist-info/entry_points.txt,sha256=6Nwyh2llLgkxANHGmzZD4_Bh9MEmsCaBhBAFFlInnjY,55
|
|
6
|
+
systemd_search-1.1.0.dist-info/scm_file_list.json,sha256=nEgmRnxad1o4o8PA_h36ATGAjYWA7l4pEiyRhHPHlWg,973
|
|
7
|
+
systemd_search-1.1.0.dist-info/scm_version.json,sha256=pm8QrB-GPU67W-3le1ff9XoV62k4gcY8B6jwgUxQHBg,160
|
|
8
|
+
systemd_search-1.1.0.dist-info/top_level.txt,sha256=xi-TLvNd02JOhaN4XChkpRwXHsqD3pV6QexfU-dDcS0,15
|
|
9
|
+
systemd_search-1.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" shall mean an individual or Legal Entity exercising
|
|
24
|
+
permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship made available under
|
|
36
|
+
the License, as indicated by a copyright notice that is included in
|
|
37
|
+
or attached to the work (an example is provided in the Appendix below).
|
|
38
|
+
|
|
39
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
40
|
+
form, that is based on (or derived from) the Work and for which the
|
|
41
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
42
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
43
|
+
of this License, Derivative Works shall not include works that remain
|
|
44
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
45
|
+
the Work and Derivative Works thereof.
|
|
46
|
+
|
|
47
|
+
"Contribution" shall mean, as submitted to the Licensor for inclusion
|
|
48
|
+
in the Work by the copyright owner or by an individual or Legal Entity
|
|
49
|
+
authorized to submit on behalf of the copyright owner. For the purposes
|
|
50
|
+
of this definition, "submitted" means any form of electronic, verbal,
|
|
51
|
+
or written communication sent to the Licensor or its representatives,
|
|
52
|
+
including but not limited to communication on electronic mailing lists,
|
|
53
|
+
source code control systems, and issue tracking systems that are managed
|
|
54
|
+
by, or on behalf of, the Licensor for the purpose of discussing and
|
|
55
|
+
improving the Work, but excluding communication that is conspicuously
|
|
56
|
+
marked or designated in writing by the copyright owner as
|
|
57
|
+
"Not a Contribution."
|
|
58
|
+
|
|
59
|
+
"Contributor" shall mean Licensor and any Legal Entity on behalf of
|
|
60
|
+
whom a Contribution has been received by the Licensor and incorporated
|
|
61
|
+
within the Work.
|
|
62
|
+
|
|
63
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
64
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
65
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
66
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
67
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
68
|
+
Work and such Derivative Works in Source or Object form.
|
|
69
|
+
|
|
70
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
71
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
72
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
73
|
+
(except as stated in this section) patent license to make, have made,
|
|
74
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
75
|
+
where such license applies only to those patent claims licensable
|
|
76
|
+
by such Contributor that are necessarily infringed by their
|
|
77
|
+
Contribution(s) alone or by the combination of their Contribution(s)
|
|
78
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
79
|
+
institute patent litigation against any entity (including a cross-claim
|
|
80
|
+
or counterclaim in a lawsuit) alleging that the Work or any Contributor
|
|
81
|
+
Contribution constitutes patent infringement, then any patent licenses
|
|
82
|
+
granted to You under this License for that Work shall terminate as of
|
|
83
|
+
the date such litigation is filed.
|
|
84
|
+
|
|
85
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
86
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
87
|
+
modifications, and in Source or Object form, provided that You
|
|
88
|
+
meet the following conditions:
|
|
89
|
+
|
|
90
|
+
(a) You must give any other recipients of the Work or Derivative
|
|
91
|
+
Works a copy of this License; and
|
|
92
|
+
|
|
93
|
+
(b) You must cause any modified files to carry prominent notices
|
|
94
|
+
stating that You changed the files; and
|
|
95
|
+
|
|
96
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
97
|
+
that You distribute, all copyright, patent, trademark, and
|
|
98
|
+
attribution notices from the Source form of the Work,
|
|
99
|
+
excluding those notices that do not pertain to any part of
|
|
100
|
+
the Derivative Works; and
|
|
101
|
+
|
|
102
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
103
|
+
distribution, You must include a readable copy of the
|
|
104
|
+
attribution notices contained within such NOTICE file, in
|
|
105
|
+
at least one of the following places: within a NOTICE text
|
|
106
|
+
file distributed as part of the Derivative Works; within
|
|
107
|
+
the Source form or documentation, if provided along with the
|
|
108
|
+
Derivative Works; or, within a display generated by the
|
|
109
|
+
Derivative Works, if and wherever such third-party notices
|
|
110
|
+
normally appear. The contents of the NOTICE file are for
|
|
111
|
+
informational purposes only and do not modify the License.
|
|
112
|
+
You may add Your own attribution notices within Derivative
|
|
113
|
+
Works that You distribute, alongside or as an addendum to
|
|
114
|
+
the NOTICE text from the Work, provided that such additional
|
|
115
|
+
attribution notices cannot be construed as modifying the License.
|
|
116
|
+
|
|
117
|
+
You may add Your own license statement for Your modifications and
|
|
118
|
+
may provide additional grant of rights to use, copy, modify, merge,
|
|
119
|
+
publish, distribute, sublicense, and/or sell copies of the Work,
|
|
120
|
+
and to permit persons to whom the Work is furnished to do so, subject
|
|
121
|
+
to the following conditions, provided that such modifications are
|
|
122
|
+
clearly labeled as such.
|
|
123
|
+
|
|
124
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
125
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
126
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
127
|
+
this License, without any additional terms or conditions.
|
|
128
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
129
|
+
the terms of any separate license agreement you may have executed
|
|
130
|
+
with Licensor regarding such Contributions.
|
|
131
|
+
|
|
132
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
133
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
134
|
+
except as required for reasonable and customary use in describing the
|
|
135
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
136
|
+
|
|
137
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
138
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
139
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
140
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
141
|
+
implied, including, without limitation, any conditions of TITLE,
|
|
142
|
+
MERCHANTABLITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
|
143
|
+
solely responsible for determining the appropriateness of using or
|
|
144
|
+
redistributing the Work and assume any risks associated with Your
|
|
145
|
+
exercise of permissions under this License.
|
|
146
|
+
|
|
147
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
148
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
149
|
+
unless required by applicable law (such as deliberate and grossly
|
|
150
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
151
|
+
liable to You for damages, including any direct, indirect, special,
|
|
152
|
+
incidental, or exemplary damages of any character arising as a
|
|
153
|
+
result of this License or out of the use or inability to use the
|
|
154
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
155
|
+
work stoppage, computer failure or malfunction, or all other
|
|
156
|
+
commercial damages or losses), even if such Contributor has been
|
|
157
|
+
advised of the possibility of such damages.
|
|
158
|
+
|
|
159
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
160
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
161
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
162
|
+
or other liability obligations and/or rights consistent with this
|
|
163
|
+
License. However, in accepting such obligations, You may act only
|
|
164
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
165
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
166
|
+
defend, and hold each Contributor harmless for any liability
|
|
167
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
168
|
+
of your accepting any such warranty or additional liability.
|
|
169
|
+
|
|
170
|
+
END OF TERMS AND CONDITIONS
|
|
171
|
+
|
|
172
|
+
Copyright 2026 systemd-search contributors
|
|
173
|
+
|
|
174
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
175
|
+
you may not use this file except in compliance with the License.
|
|
176
|
+
You may obtain a copy of the License at
|
|
177
|
+
|
|
178
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
179
|
+
|
|
180
|
+
Unless required by applicable law or agreed to in writing, software
|
|
181
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
182
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
183
|
+
See the License for the specific language governing permissions and
|
|
184
|
+
limitations under the License.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": [
|
|
3
|
+
"README.md",
|
|
4
|
+
"LICENSE",
|
|
5
|
+
"systemd-search",
|
|
6
|
+
"ARCHITECTURE.md",
|
|
7
|
+
"pyproject.toml",
|
|
8
|
+
"Pipfile.lock",
|
|
9
|
+
"Pipfile",
|
|
10
|
+
".gitignore",
|
|
11
|
+
"scripts/build-standalone.sh",
|
|
12
|
+
"scripts/build-rpm.sh",
|
|
13
|
+
"scripts/build-deb.sh",
|
|
14
|
+
"scripts/release-notes.md",
|
|
15
|
+
"src/systemd_search/__init__.py",
|
|
16
|
+
"molecule/rocky/molecule.yml",
|
|
17
|
+
"molecule/debian/molecule.yml",
|
|
18
|
+
"molecule/common/verify.yml",
|
|
19
|
+
"molecule/common/prepare.yml",
|
|
20
|
+
"molecule/common/converge.yml",
|
|
21
|
+
"molecule/common/files/fixture-single.service",
|
|
22
|
+
"molecule/common/files/fixture-multi.timer",
|
|
23
|
+
"molecule/common/files/fixture-multi-a.service",
|
|
24
|
+
"molecule/common/files/fixture-multi.path",
|
|
25
|
+
"molecule/common/files/fixture-disabled.service",
|
|
26
|
+
"molecule/common/files/fixture-failing.service",
|
|
27
|
+
"tests/test_unit.py",
|
|
28
|
+
"tests/conftest.py",
|
|
29
|
+
".github/workflows/release.yml",
|
|
30
|
+
".github/workflows/tests.yml",
|
|
31
|
+
".github/workflows/pr.yml"
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
systemd_search
|