Daemon model
Basic architecture recap
There are two main contexts in which rpm-ostree is used: on compose servers to generate OSTree commits from RPMs, and on client systems, which consume these OSTree commits to provide transactional upgrades.
The core architecture document describes in details the process by which RPMs are converted into OSTrees on both the compose server and client systems in “hybrid mode”.
The rpm-ostree daemon
As described in the administration page, rpm-ostree is the CLI program used on rpm-ostree-based systems to e.g. upgrade, rebase, install packages, etc…
These operations follow a client/daemon model where the client talks to the daemon over D-Bus. The reason for this architecture is primarily:
- to ensure serialization/locking of system mutations
- to make it easier for other clients to manage rpm-ostree-based systems (like Cockpit)
- to allow unprivileged users to make system mutations via polkit
The D-Bus API is defined in src/daemon/org.projectatomic.rpmostree1.xml
.
The rpm-ostree daemon runs as a systemd service which owns the org.projectatomic.rpmostree1
name on the system D-Bus (see src/daemon/rpm-ostreed.service
).
When a client wants to talk to the daemon, the D-Bus daemon starts up the systemd service if it’s not already running (see src/daemon/org.projectatomic.rpmostree1.service.in
).
Interacting with the daemon
When a user types rpm-ostree upgrade
, at a high-level what happens is that it reaches out to the daemon (starting it if it’s not yet started) and calls the Upgrade()
D-Bus method. You can see its method signature in the XML definition file linked above.
On the client side, this happens in rpmostree-builtin-upgrade.cxx
where it calls rpmostree_os_call_upgrade_sync
function. This is an auto-generated function (via gdbus-codegen
) which in turns uses glib’s D-Bus bindings to do the actual D-Bus call.
On the daemon side, this call is handled in os_handle_upgrade
(in rpmostreed-os.cxx
) which creates a new DeployTransaction
object (via rpmostreed_transaction_new_deploy
). Transactions are how we ensure that only a single mutation happens at a time, and allow tracking the state of the daemon’s processing of the request.
Transaction objects implement their own separate D-Bus API over an abstract socket. The daemon returns to the client the address of the transaction’s socket and the client then connects to it as a D-Bus peer connection. (Notice how all the methods in the XML return a transaction_address
).
Once the client is connected to the transaction, it calls its Start()
D-Bus method, which invokes the _execute()
function. In the case of DeployTransaction
, this is deploy_transaction_execute()
in rpmostreed-transaction-types.cxx
.
The client then receives signals from the transaction to monitor the progress of the operation (for example, everytime a message must be printed, a Message
signal is emitted).
The same analogous process occurs for other operations. For example, rpm-ostree kargs
creates a KernelArgTransaction
object, which has its execution code in kernel_arg_transaction_execute
.
Once a transaction has finished, it emits the Finished
signal, which tells the client to stop waiting for more updates and disconnect.
Polkit
The daemon integrates with polkit, which is a framework for authorizing actions. For example, a local administrator running as its unprivileged uid should be able to call rpm-ostree upgrade
.
How this works is that the daemon converts D-Bus method calls like Upgrade()
into polkit actions in os_authorize_method
. It then consults polkit to determine whether the client should be permitted to execute the requested actions.
We ship a base policy file which provide actions which should be allowed (see src/daemon/org.projectatomic.rpmostree1.policy
).
Some distros may enhance this policy by shipping rules which dynamically calculate authorization based on e.g. group membership. For example, in Fedora: org.projectatomic.rpmostree1.rules
.
Modifying the D-Bus API
Once a D-Bus API is stable, its signature cannot be modified because it would break other D-Bus clients. This is why many methods have a catch-all options
parameter which allows for extending the API without changing its signature.
When adding new APIs, it’s OK to have separate arguments for each parameter considered essential to the operation, while keeping “auxiliary” parameters in an options
parameter (such as whether to reboot or not).