|
1 | | -# Systemd |
| 1 | +# systemdkit |
2 | 2 |
|
3 | | -Pure Elixir tools for working with systemd. |
| 3 | +Pure Elixir tools for systemd unit files and D-Bus manager control. |
4 | 4 |
|
5 | | -Hex package name: `systemdkit`. The Mix application and public modules remain `:systemd` / `Systemd`. |
| 5 | +`systemdkit` is for Elixir applications and deployment tools that need to generate unit files, inspect systemd state, or control systemd directly over D-Bus without shelling out to `systemctl`. |
6 | 6 |
|
7 | | -```elixir |
8 | | -{:systemdkit, "~> 0.1.0-pre"} |
9 | | -``` |
| 7 | +## Installation |
10 | 8 |
|
11 | | -The package exposes a small D-Bus backed manager client: |
| 9 | +The Hex package is named `systemdkit`; the Mix application and modules are `:systemd` / `Systemd`. |
12 | 10 |
|
13 | 11 | ```elixir |
14 | | -{:ok, conn} = Systemd.Manager.connect() |
15 | | -{:ok, units} = Systemd.Manager.list_units(conn) |
16 | | -{:ok, unit} = Systemd.Manager.get_unit(conn, "dbus.service") |
17 | | -{:ok, state} = Systemd.UnitObject.state(conn, unit) |
18 | | -{:ok, service_state} = Systemd.UnitObject.service_state(conn, unit) |
| 12 | +def deps do |
| 13 | + [ |
| 14 | + {:systemd, "~> 0.1.0", hex: :systemdkit} |
| 15 | + ] |
| 16 | +end |
19 | 17 | ``` |
20 | 18 |
|
21 | | -It also includes a NimbleParsec-backed unit file parser/generator: |
| 19 | +## Unit files |
22 | 20 |
|
23 | | -```elixir |
24 | | -{:ok, unit_file} = Systemd.UnitFile.parse("[Service]\nExecStart=/bin/app start\n") |
25 | | -Systemd.UnitFile.to_string(unit_file) |
| 21 | +Build systemd units with typed helpers, render them, and validate them before installation: |
26 | 22 |
|
| 23 | +```elixir |
27 | 24 | unit_file = |
28 | 25 | Systemd.UnitFile.service( |
29 | | - unit: [description: "My app"], |
30 | | - service: [exec_start: "/bin/app start", restart: :always], |
| 26 | + unit: [description: "My app", after: "network.target"], |
| 27 | + service: [ |
| 28 | + type: :exec, |
| 29 | + user: "deploy", |
| 30 | + working_directory: "/opt/my-app/current", |
| 31 | + exec_start: "/opt/my-app/current/bin/my_app start", |
| 32 | + restart: "on-failure", |
| 33 | + memory_max: "512M", |
| 34 | + tasks_max: 512, |
| 35 | + no_new_privileges: true, |
| 36 | + protect_system: :strict |
| 37 | + ], |
31 | 38 | install: [wanted_by: "multi-user.target"] |
32 | 39 | ) |
| 40 | + |
| 41 | +:ok = Systemd.UnitFile.validate(unit_file, :service) |
| 42 | +Systemd.UnitFile.to_string(unit_file) |
33 | 43 | ``` |
34 | 44 |
|
35 | | -The package depends on [`rebus`](https://hex.pm/packages/rebus) for the D-Bus wire protocol instead of shelling out to `systemctl`. |
| 45 | +Parsing is loss-aware: comments, blank lines, duplicate directives, reset directives, and source spans are preserved. |
36 | 46 |
|
37 | | -APIs return idiomatic `{:ok, value}` / `{:error, %Systemd.Error{}}` tuples. Permission and polkit failures are classified with `category: :permission` and can be checked with `Systemd.Error.permission?/1`. |
| 47 | +```elixir |
| 48 | +{:ok, unit_file} = Systemd.UnitFile.parse("[Service]\nExecStart=/bin/true\n") |
| 49 | +Systemd.UnitFile.get_all(unit_file, "Service", "ExecStart") |
| 50 | +``` |
38 | 51 |
|
39 | | -See `examples/` for service, timer, and user-bus snippets, `guides/dbus-manager.md` for D-Bus manager operations, and `guides/xamal-style-deployment.md` for a deployment-oriented template unit example. |
| 52 | +Builders are available for service, socket, timer, mount, path, and target units. |
40 | 53 |
|
41 | | -## Permissions |
| 54 | +## D-Bus manager control |
42 | 55 |
|
43 | | -Systemd control happens over D-Bus. Read-only calls such as listing units usually work as an unprivileged user. Mutating calls such as daemon reload, starting system units, enabling units, or writing to `/etc/systemd/system` may require root or a polkit rule for the caller. The package returns structured `Systemd.Error` values for D-Bus policy failures instead of retrying through `sudo`. |
| 56 | +Use the top-level API for short-lived D-Bus operations: |
44 | 57 |
|
45 | | -For user units, pass `bus: :session` when a systemd user session bus is available: |
| 58 | +```elixir |
| 59 | +{:ok, units} = Systemd.list_units() |
| 60 | +{:ok, unit_files} = Systemd.list_unit_files() |
| 61 | +{:ok, state} = Systemd.unit_state("dbus.service") |
| 62 | + |
| 63 | +:ok = Systemd.reload() |
| 64 | +:ok = Systemd.start_unit("my_app.service") |
| 65 | +:ok = Systemd.restart_unit("my_app.service") |
| 66 | +``` |
| 67 | + |
| 68 | +Use `Systemd.Manager` when you want to reuse a connection or inspect jobs: |
46 | 69 |
|
47 | 70 | ```elixir |
48 | | -Systemd.list_units(bus: :session) |
| 71 | +Systemd.with_connection([], fn conn -> |
| 72 | + with {:ok, job} <- Systemd.Manager.restart_unit(conn, "my_app.service"), |
| 73 | + :ok <- Systemd.Job.await_signal(conn, job, timeout: 10_000) do |
| 74 | + :ok |
| 75 | + end |
| 76 | +end) |
49 | 77 | ``` |
50 | 78 |
|
51 | | -## Unit files |
| 79 | +## Errors and permissions |
52 | 80 |
|
53 | | -`Systemd.UnitFile` preserves comments, blank lines, duplicate directives, reset directives, and source spans. Validation is intentionally separate from parsing and includes directive-specific value checks for common service, socket, timer, and install keys: |
| 81 | +APIs return `{:ok, value}` or `{:error, %Systemd.Error{}}`. Permission and polkit failures are classified as `:permission`: |
54 | 82 |
|
55 | 83 | ```elixir |
56 | | -unit_file = Systemd.UnitFile.parse!("[Service]\nExecStart=/bin/true\n") |
57 | | -:ok = Systemd.UnitFile.validate(unit_file, :service) |
| 84 | +case Systemd.start_unit("my_app.service") do |
| 85 | + :ok -> :ok |
| 86 | + {:error, error} -> |
| 87 | + if Systemd.Error.permission?(error), do: {:error, :permission_denied}, else: {:error, error} |
| 88 | +end |
58 | 89 | ``` |
59 | 90 |
|
60 | | -## Development |
| 91 | +Read-only calls often work unprivileged. Mutating system units typically require root or appropriate polkit rules. `systemdkit` reports those D-Bus errors directly; it does not retry through `sudo`. |
61 | 92 |
|
62 | | -```sh |
63 | | -mix deps.get |
64 | | -mix ci |
| 93 | +For user units, pass `bus: :session` when a systemd user session bus is available: |
| 94 | + |
| 95 | +```elixir |
| 96 | +Systemd.list_units(bus: :session) |
65 | 97 | ``` |
66 | 98 |
|
67 | | -Integration tests are excluded by default because they require Linux with systemd and a system bus. For local development, run them inside the Lima Debian VM named `systemd-test`: |
| 99 | +## Guides |
| 100 | + |
| 101 | +- [D-Bus manager operations](guides/dbus-manager.md) |
| 102 | +- [Xamal-style deployment units](guides/xamal-style-deployment.md) |
| 103 | + |
| 104 | +## Integration testing |
| 105 | + |
| 106 | +The test suite includes optional integration tests against a real Linux systemd manager. They are excluded by default: |
68 | 107 |
|
69 | 108 | ```sh |
70 | | -~/.local/bin/limactl shell systemd-test |
71 | | -cd /Users/dannote/Development/systemd |
72 | | -SYSTEMD_INTEGRATION=1 mix test |
| 109 | +mix test |
73 | 110 | ``` |
74 | 111 |
|
75 | | -Or from macOS, copy the source into the VM and run the full integration suite: |
| 112 | +Run them on Linux with systemd and a system bus: |
76 | 113 |
|
77 | 114 | ```sh |
78 | | -scripts/integration_test.sh |
| 115 | +SYSTEMD_INTEGRATION=1 mix test |
79 | 116 | ``` |
80 | 117 |
|
81 | | -Quick VM checks: |
| 118 | +This repository also includes a Lima helper used by maintainers: |
82 | 119 |
|
83 | 120 | ```sh |
84 | | -~/.local/bin/limactl shell systemd-test -- systemctl is-system-running |
85 | | -~/.local/bin/limactl shell systemd-test -- busctl --system list --no-pager |
| 121 | +scripts/integration_test.sh |
86 | 122 | ``` |
87 | | - |
88 | | -See `CONTRIBUTING.md` before publishing a release. |
|
0 commit comments