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')

Monday, September 8, 2014 » Programming Python Linux DBus