parent | nav_order |
---|---|
Architecture |
2 |
{: .no_toc }
- TOC {:toc}
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".
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.in
).
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
).
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.
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
.
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).