Getting started with D-Bus using Python and systemd
In this post I’ll take a look at how to get started with D-Bus using Python and systemd.
D-Bus is a system that allows two or more programs to communicate with each other. An application or service can expose various objects using D-Bus. Each object can have multiple methods and properties which are then accessible for other applications via D-Bus.
Some applications also expose signals. Programs can subscribe to those signals so that they get notified if an event occurs. One common use case for this is, for example, Bluetooth: Invoke some custom logic if a device is connected. (bluez exposes the “device connected signal” via D-Bus)
In this post I’ll use D-Bus to communicate with systemd to see if the crate service is running.
To use D-Bus in python there are a couple of libraries available. There is dbus-python which is a binding for libdbus (the reference implementation of D-Bus). The dbus-python library isn’t very pythonic and not available on pypi so in order to install it, it is necessary to use the distributions package manager or to install it from source.
Under Archlinux (the only distribution that matters, really!) it can be installed using pacman:
pacman -S python-dbus
Once installed we can open up a python shell and get started:
from dbus import SystemBus, SessionBus
D-Bus has two “channels” one system channel for system services (like systemd!) and system events (like new bluetooth device detected) and channels that are bound to the login session. The later are mostly used by regular applications.
For now we’ll use the SystemBus as we want to interact with systemd:
bus = SystemBus()
systemd = bus.get_object('org.freedesktop.systemd1',
'/org/freedesktop/systemd1')
The first argument org.freekdestop.systemd1
is the well-known-name of the systemd bus. Think of it as kind of a domain name for an application or service. The second argument /org/freedesktop/systemd1
is an object-path. It is the path to an object that is exposed on the bus.
So we’ve got:
SystemBus
-> 'org.freedesktop.systemd1' # The well-known-name to identify a bus
-> '/org/freedesktop/systemd1' # object-path to identify an object on the bus
The objects that are exposed by systemd are documented on the systemd site.
D-Bus separates objects and the description of their functionality. So for now our systemd
instance is just a proxy that doesn’t really know what it can do.
The description of the functionality is an Interface
. One object can implement one or more interfaces.
In the case of systemd and the /org/freedesktop/systemd1
object it supports (besides others) the org.freedesktop.systemd1.Manager
interface:
from dbus import Interface
manager = Interface(systemd, dbus_interface='org.freedesktop.systemd1.Manager')
So now we have:
SystemBus
-> 'org.freedesktop.systemd1' # The well-known-name to identify a bus
-> '/org/freedesktop/systemd1' # object-path to identify an object on the bus
implements
'org.freedesktop.systemd1.Manager' # Manager defines various methods
* LoadUnit(name)
* StartUnit(name, mode)
* ...
There are a lot more methods in the Manager interface. Take a look at the systemd documentation if you’re interested.
Here we basically told the systemd
proxy object what it can do by telling it his interface, which is identified by a name. With this manager
interface we can now finally call methods that are exposed by the systemd dbus-api:
crate_unit = manager.LoadUnit('crate.service')
The LoadUnit
call returned an object-path to the crate.service
unit that is also available on the systemd bus. Using this object-path it is possible to retrieve a proxy object for the crate service:
crate_proxy = bus.get_object('org.freedesktop.systemd1', str(crate_unit))
Note that I used the same well-known-name as before (which identifies systemd) but a different object-path because this time I don’t want to get the root systemd object but rather the crate service object.
Just like before it is necessary to tell the object what it is capable of. For Units systemd specifies the org.freedesktop.systemd1.Unit
interface:
crate = Interface(crate_proxy, dbus_interface='org.freedesktop.systemd1.Unit')
The .systemd1.Unit
interface defines some methods to interact with the unit and a lot of properties.
Unfortunately while the properties are defined in the org.freedesktop.systemd1.Unit
interface there is no method defined in it to access them. Instead there is a generic org.freedesktop.DBus.Properties
interface that has to be used to access them.
In order to get the state of the crate service we can use our crate_proxy
object using the Properties
interface:
crate_proxy.Get('org.freedesktop.systemd1.Unit', # interface that defines the property
'ActiveState', # name of the property
dbus_interface='org.freedesktop.DBus.Properties') # interface that defines `Get`
The same thing could also have been accomplished if we created an interface instance using the crate_proxy
and the DBus.Properties
interface and then called Get
on that instance:
crate_properties = Interface(crate_proxy,
dbus_interface='org.freedesktop.DBus.Properties')
crate_properties.Get('org.freedesktop.systemd1.Unit', 'ActiveState')
So to wrap up our Bus-Tree:
SystemBus
-> 'org.freedesktop.systemd1' # The well-known-name to identify a bus
-> '/org/freedesktop/systemd1' # object-path to identify an object on the bus
implements
'org.freedesktop.systemd1.Manager'
...
# the object-path we got using LoadUnit('crate.service') on the manager
-> '/org/freedesktop/systemd1/unit/crate_2eservice'
implements
'org.freedesktop.systemd1.Unit'
* Start(mode)
* ...
'org.freedesktop.DBus.Properties'
* Get(propertyName)
* GetAll()
* ...
....
So far we’ve covered pretty much all the basics. Working with dbus-python
feels a tad tedious so it might be worth checking out pydbus which seems to be a bit more pythonic. Pydbus is available on pypi but it requires PyGI which is not available on pypi and needs to be installed beforehand.
Using pydbus the example from above would look a little less frightening:
from pydbus import SystemBus
bus = SystemBus()
systemd = bus.get('org.freedesktop.systemd1')
crate_unit = systemd.LoadUnit('crate.service')
crate = bus.get('.systemd1', crate_unit[0])
crate.Get('org.freedesktop.systemd1.Unit', 'ActiveState')