Skip to content

Using BlueChi’s D-Bus API

BlueChi provides introspection data for its public API. These XML files are located in the data directory of the BlueChi project and can be used either as reference when writing custom clients or as input to generate them (see here).

There are a number of bindings for the D-Bus protocol for a range of programming languages. A (not complete) list of bindings can be found on the freedesktop D-Bus bindings website.

The following sections demonstrate how the D-Bus API of BlueChi can be interacted with using bindings from different programming languages by showing small snippets.

Note

All snippets require to be run on the machine where BlueChi is running. The only exception is the example for monitoring the connection status of the agent, which has to be executed on the managed node where the BlueChi agent is running.

Setup

Installing Go

First of all, install Go as described in the official documentation.

Setup sample project

The Go snippets require at least a simple project setup. Create it via:

# create project directory
mkdir bluechi
cd bluechi

# initialize the project
go mod init bluechi

Installing dependencies

The Go snippets require godbus. Navigate into the bluechi directory and add it as a dependency via:

go get github.com/godbus/dbus/v5

Build and run the sample project

The snippet chosen to run needs to be copied into the bluechi directory. After that the example can be build and run via:

go build <filename>
./<filename> args...

Installing Python

First of all, install Python via:

# on Ubuntu
apt install python3 python3-dev
# on Fedora/CentOS
dnf install python3 python3-devel

Installing dependencies

The python snippets require the dasbus package. Install it via pip:

# ensure pip has been installed
python3 -m ensurepip --default-pip
pip3 install dasbus

Running the examples

The snippets can be run by simply copy and pasting it into a new file running it via:

python3 <filename>.py args...

Installing Rust

First of all, install Rust as describe in the official documentation.

Installing dependencies

The Rust snippets requires dbus. Since this crate needs the reference implementation libdbus, it has to be installed:

# on Ubuntu
apt install libdbus-1-dev pkg-config
# on Fedora/CentOS
dnf install dbus-devel pkgconf-pkg-config

Build and run the sample project

The snippet chosen to run needs to be copied into the bluechi directory. After that the example can be build and run via:

# clone the BlueChi project
$ git clone https://github.com/containers/bluechi.git

# navigate into the rust api-examples directory
$ cd bluechi/doc/api-examples/rust

# build all example applications
$ cargo build

# run a specific example, e.g. start-unit
$ ./target/debug/start-unit laptop cow.service
$ ...

Note

Depending on the setup of BlueChi root privileges might be needed when running the samples.

Getting information

List all nodes

// SPDX-License-Identifier: MIT-0

package main

import (
    "fmt"
    "os"

    "github.com/godbus/dbus/v5"
)

const (
    BcDusInterface  = "org.eclipse.bluechi"
    BcObjectPath    = "/org/eclipse/bluechi"
    MethodListNodes = "org.eclipse.bluechi.Manager.ListNodes"
)

func main() {
    conn, err := dbus.ConnectSystemBus()
    if err != nil {
        fmt.Fprintln(os.Stderr, "Failed to connect to system bus:", err)
        os.Exit(1)
    }
    defer conn.Close()

    var nodes [][]interface{}
    busObject := conn.Object(BcDusInterface, BcObjectPath)
    err = busObject.Call(MethodListNodes, 0).Store(&nodes)
    if err != nil {
        fmt.Fprintln(os.Stderr, "Failed to list nodes:", err)
        os.Exit(1)
    }

    for _, node := range nodes {
        fmt.Printf("Name: %s, Status: %s\n", node[0], node[2])
    }
}
#!/usr/bin/python3
# SPDX-License-Identifier: MIT-0

from collections import namedtuple
import dasbus.connection

bus = dasbus.connection.SystemMessageBus()

NodeInfo = namedtuple("NodeInfo", ["name", "object_path", "status"])

manager = bus.get_proxy("org.eclipse.bluechi", "/org/eclipse/bluechi")
nodes = manager.ListNodes()
for n in nodes:
    info = NodeInfo(*n)
    print(f"Node: {info.name}, State: {info.status}")
// SPDX-License-Identifier: MIT-0

use dbus::blocking::Connection;
use std::time::Duration;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let conn = Connection::new_system()?;

    let bluechi = conn.with_proxy(
        "org.eclipse.bluechi",
        "/org/eclipse/bluechi",
        Duration::from_millis(5000),
    );

    let (nodes,): (Vec<(String, dbus::Path, String)>,) =
        bluechi.method_call("org.eclipse.bluechi.Manager", "ListNodes", ())?;

    for (name, _, status) in nodes {
        println!("Node: {}, Status: {}", name, status);
    }

    Ok(())
}

List all units on a node

// SPDX-License-Identifier: MIT-0

package main

import (
    "fmt"
    "os"

    "github.com/godbus/dbus/v5"
)

const (
    BcDusInterface  = "org.eclipse.bluechi"
    BcObjectPath    = "/org/eclipse/bluechi"
    MethodGetNode   = "org.eclipse.bluechi.Manager.GetNode"
    MethodListUnits = "org.eclipse.bluechi.Node.ListUnits"
)

func main() {
    if len(os.Args) != 2 {
        fmt.Println("No node name supplied")
        os.Exit(1)
    }

    nodeName := os.Args[1]

    conn, err := dbus.ConnectSystemBus()
    if err != nil {
        fmt.Fprintln(os.Stderr, "Failed to connect to system bus:", err)
        os.Exit(1)
    }
    defer conn.Close()

    nodePath := ""
    mgrObject := conn.Object(BcDusInterface, BcObjectPath)
    err = mgrObject.Call(MethodGetNode, 0, nodeName).Store(&nodePath)
    if err != nil {
        fmt.Fprintln(os.Stderr, "Failed to get node path:", err)
        os.Exit(1)
    }

    var units [][]interface{}
    nodeObject := conn.Object(BcDusInterface, dbus.ObjectPath(nodePath))
    err = nodeObject.Call(MethodListUnits, 0).Store(&units)
    if err != nil {
        fmt.Fprintln(os.Stderr, "Failed to list units:", err)
        os.Exit(1)
    }

    for _, unit := range units {
        fmt.Printf("%s - %s\n", unit[0], unit[1])
    }
}
#!/usr/bin/python3
# SPDX-License-Identifier: MIT-0

import sys
from collections import namedtuple
import dasbus.connection
bus = dasbus.connection.SystemMessageBus()

UnitInfo = namedtuple("UnitInfo", ["name", "description",
                                   "load_state", "active_state", "sub_state", "follower", "object_path",
                                   "job_id", "job_type", "job_object_path"])

if len(sys.argv) != 2:
    print("No node name supplied")
    sys.exit(1)

node_name = sys.argv[1]

manager = bus.get_proxy("org.eclipse.bluechi", "/org/eclipse/bluechi")
node_path = manager.GetNode(node_name)
node = bus.get_proxy("org.eclipse.bluechi", node_path)

units = node.ListUnits()
for u in units:
    info = UnitInfo(*u)
    print(f"{info.name} - {info.description}")
// SPDX-License-Identifier: MIT-0

use clap::Parser;
use dbus::blocking::Connection;
use dbus::Path;
use std::time::Duration;

#[derive(Parser)]
struct Cli {
    /// The node name to list the units for
    #[clap(short, long)]
    node_name: String,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Cli::parse();

    let conn = Connection::new_system()?;

    let bluechi = conn.with_proxy(
        "org.eclipse.bluechi",
        "/org/eclipse/bluechi",
        Duration::from_millis(5000),
    );

    let (node,): (Path,) =
        bluechi.method_call("org.eclipse.bluechi.Manager", "GetNode", (args.node_name,))?;

    let node_proxy = conn.with_proxy("org.eclipse.bluechi", node, Duration::from_millis(5000));

    // we are only interested in the first two response values - unit name and description
    let (units,): (Vec<(String, String)>,) =
        node_proxy.method_call("org.eclipse.bluechi.Node", "ListUnits", ())?;

    for (name, description) in units {
        println!("{} - {}", name, description);
    }

    Ok(())
}

Get a unit property value

// SPDX-License-Identifier: MIT-0

package main

import (
    "fmt"
    "os"

    "github.com/godbus/dbus/v5"
)

const (
    BcDusInterface        = "org.eclipse.bluechi"
    BcObjectPath          = "/org/eclipse/bluechi"
    MethodGetNode         = "org.eclipse.bluechi.Manager.GetNode"
    MethodGetUnitProperty = "org.eclipse.bluechi.Node.GetUnitProperty"
)

func main() {
    if len(os.Args) != 3 {
        fmt.Printf("Usage: %s node_name unit_name", os.Args[0])
        os.Exit(1)
    }

    nodeName := os.Args[1]
    unitName := os.Args[2]

    conn, err := dbus.ConnectSystemBus()
    if err != nil {
        fmt.Fprintln(os.Stderr, "Failed to connect to system bus:", err)
        os.Exit(1)
    }
    defer conn.Close()

    nodePath := ""
    busObject := conn.Object(BcDusInterface, BcObjectPath)
    err = busObject.Call(MethodGetNode, 0, nodeName).Store(&nodePath)
    if err != nil {
        fmt.Fprintln(os.Stderr, "Failed to get node path:", err)
        os.Exit(1)
    }

    var cpuWeight uint64
    nodeObject := conn.Object(BcDusInterface, dbus.ObjectPath(nodePath))
    err = nodeObject.Call(MethodGetUnitProperty, 0, unitName, "org.freedesktop.systemd1.Service", "CPUWeight").Store(&cpuWeight)
    if err != nil {
        fmt.Fprintln(os.Stderr, "Failed to get cpu weight:", err)
        os.Exit(1)
    }

    fmt.Println(cpuWeight)
}
#!/usr/bin/python3
# SPDX-License-Identifier: MIT-0

import sys
import dasbus.connection
bus = dasbus.connection.SystemMessageBus()

if len(sys.argv) != 3:
    print(f"Usage: python {sys.argv[0]} node_name unit_name")
    sys.exit(1)

node_name = sys.argv[1]
unit_name = sys.argv[2]

manager = bus.get_proxy("org.eclipse.bluechi", "/org/eclipse/bluechi")
node_path = manager.GetNode(node_name)
node = bus.get_proxy("org.eclipse.bluechi", node_path)

value = node.GetUnitProperty(unit_name, "org.freedesktop.systemd1.Service", "CPUWeight")
print(value.get_uint64())
// SPDX-License-Identifier: MIT-0

use clap::Parser;
use dbus::arg::Variant;
use dbus::blocking::Connection;
use dbus::Path;
use std::time::Duration;

#[derive(Parser)]
struct Cli {
    /// The node name to list the units for
    #[clap(short, long)]
    node_name: String,

    /// The unit name to get the cpu weight for
    #[clap(short, long)]
    unit_name: String,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Cli::parse();

    let conn = Connection::new_system()?;

    let bluechi = conn.with_proxy(
        "org.eclipse.bluechi",
        "/org/eclipse/bluechi",
        Duration::from_millis(5000),
    );

    let (node,): (Path,) =
        bluechi.method_call("org.eclipse.bluechi.Manager", "GetNode", (args.node_name,))?;

    let node_proxy = conn.with_proxy("org.eclipse.bluechi", node, Duration::from_millis(5000));

    let (cpu_weight,): (Variant<u64>,) = node_proxy.method_call(
        "org.eclipse.bluechi.Node",
        "GetUnitProperty",
        (
            args.unit_name,
            "org.freedesktop.systemd1.Service",
            "CPUWeight",
        ),
    )?;

    println!("{}", cpu_weight.0);

    Ok(())
}

Operations on units

Start unit

// SPDX-License-Identifier: MIT-0

package main

import (
    "fmt"
    "os"

    "github.com/godbus/dbus/v5"
)

const (
    BcDusInterface  = "org.eclipse.bluechi"
    BcObjectPath    = "/org/eclipse/bluechi"
    MethodGetNode   = "org.eclipse.bluechi.Manager.GetNode"
    MethodStartUnit = "org.eclipse.bluechi.Node.StartUnit"
)

func main() {
    if len(os.Args) != 3 {
        fmt.Printf("Usage: %s node_name unit_name", os.Args[0])
        os.Exit(1)
    }

    nodeName := os.Args[1]
    unitName := os.Args[2]

    conn, err := dbus.ConnectSystemBus()
    if err != nil {
        fmt.Fprintln(os.Stderr, "Failed to connect to system bus:", err)
        os.Exit(1)
    }
    defer conn.Close()

    nodePath := ""
    busObject := conn.Object(BcDusInterface, BcObjectPath)
    err = busObject.Call(MethodGetNode, 0, nodeName).Store(&nodePath)
    if err != nil {
        fmt.Fprintln(os.Stderr, "Failed to get node path:", err)
        os.Exit(1)
    }

    jobPath := ""
    nodeObject := conn.Object(BcDusInterface, dbus.ObjectPath(nodePath))
    err = nodeObject.Call(MethodStartUnit, 0, unitName, "replace").Store(&jobPath)
    if err != nil {
        fmt.Fprintln(os.Stderr, "Failed to start unit:", err)
        os.Exit(1)
    }

    fmt.Printf("Started unit '%s' on node '%s': %s", unitName, nodeName, jobPath)
}
#!/usr/bin/python3
# SPDX-License-Identifier: MIT-0

import sys
from dasbus.connection import SystemMessageBus

bus = SystemMessageBus()

if len(sys.argv) != 3:
    print(f"Usage: python {sys.argv[0]} node_name unit_name")
    sys.exit(1)

node_name = sys.argv[1]
unit_name = sys.argv[2]

manager = bus.get_proxy("org.eclipse.bluechi", "/org/eclipse/bluechi")
node_path = manager.GetNode(node_name)
node = bus.get_proxy("org.eclipse.bluechi", node_path)

my_job_path = node.StartUnit(unit_name, "replace")

print(f"Started unit '{unit_name}' on node '{node_name}': {my_job_path}")
// SPDX-License-Identifier: MIT-0

use clap::Parser;
use dbus::blocking::Connection;
use dbus::Path;
use std::time::Duration;

#[derive(Parser)]
struct Cli {
    /// The node name to list the units for
    #[clap(short, long)]
    node_name: String,

    /// The name of the unit to start
    #[clap(short, long)]
    unit_name: String,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Cli::parse();

    let conn = Connection::new_system()?;

    let bluechi = conn.with_proxy(
        "org.eclipse.bluechi",
        "/org/eclipse/bluechi",
        Duration::from_millis(5000),
    );

    let (node,): (Path,) =
        bluechi.method_call("org.eclipse.bluechi.Manager", "GetNode", (&args.node_name,))?;

    let node_proxy = conn.with_proxy("org.eclipse.bluechi", node, Duration::from_millis(5000));

    let (job_path,): (Path,) = node_proxy.method_call(
        "org.eclipse.bluechi.Node",
        "StartUnit",
        (&args.unit_name, "replace"),
    )?;

    println!(
        "Started unit '{}' on node '{}': {}",
        args.unit_name, args.node_name, job_path
    );

    Ok(())
}

Enable unit

// SPDX-License-Identifier: MIT-0

package main

import (
    "fmt"
    "os"

    "github.com/godbus/dbus/v5"
)

const (
    BcDusInterface        = "org.eclipse.bluechi"
    BcObjectPath          = "/org/eclipse/bluechi"
    MethodGetNode         = "org.eclipse.bluechi.Manager.GetNode"
    MethodEnableUnitFiles = "org.eclipse.bluechi.Node.EnableUnitFiles"
)

func main() {
    if len(os.Args) < 3 {
        fmt.Printf("Usage: %s node_name unit_name...", os.Args[0])
        os.Exit(1)
    }

    nodeName := os.Args[1]
    units := []string{}
    for _, unit := range os.Args[2:] {
        units = append(units, unit)
    }

    conn, err := dbus.ConnectSystemBus()
    if err != nil {
        fmt.Fprintln(os.Stderr, "Failed to connect to system bus:", err)
        os.Exit(1)
    }
    defer conn.Close()

    nodePath := ""
    busObject := conn.Object(BcDusInterface, BcObjectPath)
    err = busObject.Call(MethodGetNode, 0, nodeName).Store(&nodePath)
    if err != nil {
        fmt.Fprintln(os.Stderr, "Failed to get node path:", err)
        os.Exit(1)
    }

    type ChangesResponse struct {
        OperationType string
        SymlinkFile   string
        SymlinkDest   string
    }

    var carriesInstallInfoResponse bool
    var changes []ChangesResponse
    nodeObject := conn.Object(BcDusInterface, dbus.ObjectPath(nodePath))
    err = nodeObject.Call(MethodEnableUnitFiles, 0, units, false, false).Store(&carriesInstallInfoResponse, &changes)
    if err != nil {
        fmt.Fprintln(os.Stderr, "Failed to enable unit files:", err)
        os.Exit(1)
    }

    if carriesInstallInfoResponse {
        fmt.Println("The unit files included enablement information")
    } else {
        fmt.Println("The unit files did not include any enablement information")
    }

    for _, change := range changes {
        if change.OperationType == "symlink" {
            fmt.Printf("Created symlink %s -> %s\n", change.SymlinkFile, change.SymlinkDest)
        } else if change.OperationType == "unlink" {
            fmt.Printf("Removed '%s'", change.SymlinkFile)
        }
    }
}
#!/usr/bin/python3
# SPDX-License-Identifier: MIT-0

import sys
from collections import namedtuple
import dasbus.connection
bus = dasbus.connection.SystemMessageBus()

EnabledServiceInfo = namedtuple("EnabledServicesInfo", ["op_type", "symlink_file", "symlink_dest"])
EnableResponse = namedtuple("EnableResponse", ["carries_install_info", "enabled_services_info"])

if len(sys.argv) < 2:
    print(f"Usage: {sys.argv[0]} node_name unit_name...")
    sys.exit(1)

node_name = sys.argv[1]

manager = bus.get_proxy("org.eclipse.bluechi", "/org/eclipse/bluechi")
node_path = manager.GetNode(node_name)
node = bus.get_proxy("org.eclipse.bluechi", node_path)

response = node.EnableUnitFiles(sys.argv[2:], False, False)
enable_response = EnableResponse(*response)
if enable_response.carries_install_info:
    print("The unit files included enablement information")
else:
    print("The unit files did not include any enablement information")

for e in enable_response.enabled_services_info:
    enabled_service_info = EnabledServiceInfo(*e)
    if enabled_service_info.op_type == "symlink":
        print(f"Created symlink {enabled_service_info.symlink_file} -> {enabled_service_info.symlink_dest}")
    elif enabled_service_info.op_type == "unlink":
        print(f"Removed \"{enabled_service_info.symlink_file}\".")
// SPDX-License-Identifier: MIT-0

use clap::Parser;
use dbus::blocking::Connection;
use dbus::Path;
use std::time::Duration;

#[derive(Parser)]
struct Cli {
    /// The node name to list the units for
    #[clap(short, long)]
    node_name: String,

    /// The names of the units to enable. Names are separated by ','.
    #[clap(short, long, value_delimiter = ',')]
    unit_names: Vec<String>,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Cli::parse();

    let conn = Connection::new_system()?;

    let bluechi = conn.with_proxy(
        "org.eclipse.bluechi",
        "/org/eclipse/bluechi",
        Duration::from_millis(5000),
    );

    let (node,): (Path,) =
        bluechi.method_call("org.eclipse.bluechi.Manager", "GetNode", (args.node_name,))?;

    let node_proxy = conn.with_proxy("org.eclipse.bluechi", node, Duration::from_millis(5000));

    let (carries_install_info, changes): (bool, Vec<(String, String, String)>) = node_proxy
        .method_call(
            "org.eclipse.bluechi.Node",
            "EnableUnitFiles",
            (args.unit_names, false, false),
        )?;

    if carries_install_info {
        println!("The unit files included enablement information");
    } else {
        println!("The unit files did not include any enablement information");
    }

    for (op_type, file_name, file_dest) in changes {
        if op_type == "symlink" {
            println!("Created symlink {} -> {}", file_name, file_dest);
        } else if op_type == "unlink" {
            println!("Removed '{}'", file_name);
        }
    }

    Ok(())
}

Set property of a unit

// SPDX-License-Identifier: MIT-0

package main

import (
    "fmt"
    "os"
    "strconv"

    "github.com/godbus/dbus/v5"
)

const (
    BcDusInterface          = "org.eclipse.bluechi"
    BcObjectPath            = "/org/eclipse/bluechi"
    MethodGetNode           = "org.eclipse.bluechi.Manager.GetNode"
    MethodSetUnitProperties = "org.eclipse.bluechi.Node.SetUnitProperties"
)

func main() {
    if len(os.Args) != 4 {
        fmt.Printf("Usage: %s node_name unit_name cpu_weight\n", os.Args[0])
        os.Exit(1)
    }

    nodeName := os.Args[1]
    unitName := os.Args[2]
    cpuWeight, err := strconv.ParseUint(os.Args[3], 10, 64)
    if err != nil {
        fmt.Printf("Failed to parse cpu weight to uint from value '%s'\n", os.Args[3])
        os.Exit(1)
    }

    conn, err := dbus.ConnectSystemBus()
    if err != nil {
        fmt.Fprintln(os.Stderr, "Failed to connect to system bus:", err)
        os.Exit(1)
    }
    defer conn.Close()

    nodePath := ""
    busObject := conn.Object(BcDusInterface, BcObjectPath)
    err = busObject.Call(MethodGetNode, 0, nodeName).Store(&nodePath)
    if err != nil {
        fmt.Fprintln(os.Stderr, "Failed to get node path:", err)
        os.Exit(1)
    }

    values := []struct {
        UnitName string
        Value    interface{}
    }{
        {
            UnitName: "CPUWeight",
            Value:    dbus.MakeVariant(cpuWeight),
        },
    }
    runtime := true

    nodeObject := conn.Object(BcDusInterface, dbus.ObjectPath(nodePath))
    err = nodeObject.Call(MethodSetUnitProperties, 0, unitName, runtime, values).Store()
    if err != nil {
        fmt.Fprintln(os.Stderr, "Failed to set cpu weight:", err)
        os.Exit(1)
    }
}
#!/usr/bin/python3
# SPDX-License-Identifier: MIT-0

import sys
from dasbus.typing import Variant
import dasbus.connection
bus = dasbus.connection.SystemMessageBus()

if len(sys.argv) != 4:
    print(f"Usage: python {sys.argv[0]} node_name unit_name cpu_weight")
    sys.exit(1)

node_name = sys.argv[1]
unit_name = sys.argv[2]
value = int(sys.argv[3])

# Don't persist change
runtime = True

manager = bus.get_proxy("org.eclipse.bluechi", "/org/eclipse/bluechi")
node_path = manager.GetNode(node_name)
node = bus.get_proxy("org.eclipse.bluechi", node_path)

node.SetUnitProperties(unit_name, runtime, [("CPUWeight", Variant("t", value))])
// SPDX-License-Identifier: MIT-0

use clap::Parser;
use dbus::arg::Variant;
use dbus::blocking::Connection;
use dbus::Path;
use std::time::Duration;

#[derive(Parser)]
struct Cli {
    /// The node name to list the units for
    #[clap(short, long)]
    node_name: String,

    /// The unit name to set the cpu weight for
    #[clap(short, long)]
    unit_name: String,

    /// The new value of the cpu weight
    #[clap(short, long)]
    cpu_weight: u64,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Cli::parse();

    let conn = Connection::new_system()?;

    let bluechi = conn.with_proxy(
        "org.eclipse.bluechi",
        "/org/eclipse/bluechi",
        Duration::from_millis(5000),
    );

    let (node,): (Path,) =
        bluechi.method_call("org.eclipse.bluechi.Manager", "GetNode", (args.node_name,))?;

    let node_proxy = conn.with_proxy("org.eclipse.bluechi", node, Duration::from_millis(5000));

    let new_cpu_weight = Variant(args.cpu_weight);
    let mut values: Vec<(String, Variant<u64>)> = Vec::new();
    values.push(("CPUWeight".to_string(), new_cpu_weight));

    node_proxy.method_call(
        "org.eclipse.bluechi.Node",
        "SetUnitProperties",
        (args.unit_name, false, values),
    )?;

    Ok(())
}

Monitoring

Monitor the connections of all nodes

// SPDX-License-Identifier: MIT-0

package main

import (
    "fmt"
    "os"
    "strings"

    "github.com/godbus/dbus/v5"
)

const (
    BcDusInterface  = "org.eclipse.bluechi"
    BcObjectPath    = "/org/eclipse/bluechi"
    BcNodeInterface = "org.eclipse.bluechi.Node"

    NodePathPrefix = "/org/eclipse/bluechi/node/"
)

func main() {
    conn, err := dbus.ConnectSystemBus()
    if err != nil {
        fmt.Fprintln(os.Stderr, "Failed to connect to system bus:", err)
        os.Exit(1)
    }
    defer conn.Close()

    busObject := conn.Object(BcDusInterface, BcObjectPath)
    err = busObject.AddMatchSignal("org.freedesktop.DBus.Properties", "PropertiesChanged").Err
    if err != nil {
        fmt.Println("Failed to add signal to node: ", err)
        os.Exit(1)
    }

    c := make(chan *dbus.Signal, 10)
    conn.Signal(c)
    for v := range c {
        path := string(v.Path)
        ifaceName := v.Body[0]
        if strings.HasPrefix(path, NodePathPrefix) && ifaceName == BcNodeInterface {
            changedValues, ok := v.Body[1].(map[string]dbus.Variant)
            if !ok {
                fmt.Println("Received invalid property changed signal")
                continue
            }
            if val, ok := changedValues["Status"]; ok {
                nodeName := strings.Replace(path, NodePathPrefix, "", 1)
                fmt.Printf("Node %s: %s\n", nodeName, val.String())
            }
        }
    }
}
# SPDX-License-Identifier: MIT-0

import dasbus.connection
from dasbus.loop import EventLoop
from dasbus.typing import Variant
from typing import Dict


loop = EventLoop()
bus = dasbus.connection.SystemMessageBus()

manager = bus.get_proxy("org.eclipse.bluechi", "/org/eclipse/bluechi")
nodes = manager.ListNodes()
cached_nodes = []
for n in nodes:
    # node: [name, path, status]
    node = bus.get_proxy("org.eclipse.bluechi", n[1], "org.freedesktop.DBus.Properties")

    def changed_wrapper(node_name: str):
        def on_connection_status_changed(
            interface: str,
            changed_props: Dict[str, Variant],
            invalidated_props: Dict[str, Variant],
        ) -> None:
            con_status = changed_props["Status"].get_string()
            print(f"Node {node_name}: {con_status}")
        return on_connection_status_changed

    node.PropertiesChanged.connect(changed_wrapper(n[0]))
    cached_nodes.append(node)

loop.run()
// SPDX-License-Identifier: MIT-0

use dbus::{
    arg::{RefArg, Variant},
    blocking::{stdintf::org_freedesktop_dbus::PropertiesPropertiesChanged, Connection},
    Message,
};
use std::time::Duration;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let conn = Connection::new_system()?;

    let bluechi = conn.with_proxy(
        "org.eclipse.bluechi",
        "/org/eclipse/bluechi",
        Duration::from_millis(5000),
    );

    let (nodes,): (Vec<(String, dbus::Path, String)>,) =
        bluechi.method_call("org.eclipse.bluechi.Manager", "ListNodes", ())?;

    for (name, path, _) in nodes {
        let node_name = name;
        let bluechi = conn.with_proxy("org.eclipse.bluechi", path, Duration::from_millis(5000));
        let _id = bluechi.match_signal(
            move |signal: PropertiesPropertiesChanged, _: &Connection, _: &Message| {
                match signal.changed_properties.get_key_value("Status") {
                    Some((_, Variant(changed_value))) => println!(
                        "Node {}: {}",
                        node_name,
                        changed_value.as_str().unwrap_or("")
                    ),
                    _ => {}
                }
                true
            },
        );
    }

    loop {
        conn.process(Duration::from_millis(1000))?;
    }
}

Monitor the connection on the managed node

// SPDX-License-Identifier: MIT-0

package main

import (
    "fmt"
    "os"

    "github.com/godbus/dbus/v5"
)

const (
    BcDusInterface   = "org.eclipse.bluechi"
    BcObjectPath     = "/org/eclipse/bluechi"
    BcAgentInterface = "org.eclipse.bluechi.Agent"
)

func main() {
    conn, err := dbus.ConnectSystemBus()
    if err != nil {
        fmt.Fprintln(os.Stderr, "Failed to connect to system bus:", err)
        os.Exit(1)
    }
    defer conn.Close()

    busObject := conn.Object(BcDusInterface, BcObjectPath)
    err = busObject.AddMatchSignal("org.freedesktop.DBus.Properties", "PropertiesChanged").Err
    if err != nil {
        fmt.Println("Failed to add signal to node: ", err)
        os.Exit(1)
    }

    c := make(chan *dbus.Signal, 10)
    conn.Signal(c)
    for v := range c {
        ifaceName := v.Body[0]
        if ifaceName == BcAgentInterface {
            changedValues, ok := v.Body[1].(map[string]dbus.Variant)
            if !ok {
                fmt.Println("Received invalid property changed signal")
                continue
            }
            if val, ok := changedValues["Status"]; ok {
                fmt.Printf("Agent status: %s\n", val.String())
            }
        }
    }
}
# SPDX-License-Identifier: MIT-0

import dasbus.connection
from dasbus.loop import EventLoop
from dasbus.typing import Variant
from typing import Dict


loop = EventLoop()
bus = dasbus.connection.SystemMessageBus()

agent_props_proxy = bus.get_proxy("org.eclipse.bluechi.Agent", "/org/eclipse/bluechi",
                                  "org.freedesktop.DBus.Properties")


def on_connection_status_changed(
    interface: str,
    changed_props: Dict[str, Variant],
    invalidated_props: Dict[str, Variant],
) -> None:
    con_status = changed_props["Status"].get_string()
    print(f"Agent status: {con_status}")


agent_props_proxy.PropertiesChanged.connect(on_connection_status_changed)

loop.run()
// SPDX-License-Identifier: MIT-0

use dbus::{
    arg::Variant,
    blocking::{stdintf::org_freedesktop_dbus::PropertiesPropertiesChanged, Connection},
    Message,
};
use std::time::Duration;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let conn = Connection::new_system()?;

    let bluechi = conn.with_proxy(
        "org.eclipse.bluechi.Agent",
        "/org/eclipse/bluechi",
        Duration::from_millis(5000),
    );
    let _id = bluechi.match_signal(
        |signal: PropertiesPropertiesChanged, _: &Connection, _: &Message| {
            match signal.changed_properties.get_key_value("Status") {
                Some((_, Variant(changed_value))) => {
                    println!("Agent status: {}", changed_value.as_str().unwrap_or(""))
                }
                _ => {}
            }
            true
        },
    );

    loop {
        conn.process(Duration::from_millis(1000))?;
    }
}

Monitor unit changes

// SPDX-License-Identifier: MIT-0

package main

import (
    "fmt"
    "os"
    "strings"

    "github.com/godbus/dbus/v5"
)

const (
    BcDusInterface      = "org.eclipse.bluechi"
    BcObjectPath        = "/org/eclipse/bluechi"
    MethodCreateMonitor = "org.eclipse.bluechi.Manager.CreateMonitor"
    MethodSubscribe     = "org.eclipse.bluechi.Monitor.Subscribe"

    SignalUnitNew               = "UnitNew"
    SignalUnitRemoved           = "UnitRemoved"
    SignalUnitPropertiesChanged = "UnitPropertiesChanged"
    SignalUnitStateChanged      = "UnitStateChanged"
)

func main() {
    if len(os.Args) != 2 {
        fmt.Printf("Usage: %s node_name", os.Args[0])
        os.Exit(1)
    }

    nodeName := os.Args[1]
    // use wildcard to match all units
    unitName := "*"

    conn, err := dbus.ConnectSystemBus()
    if err != nil {
        fmt.Fprintln(os.Stderr, "Failed to connect to system bus:", err)
        os.Exit(1)
    }
    defer conn.Close()

    monitorPath := ""
    busObject := conn.Object(BcDusInterface, BcObjectPath)
    err = busObject.Call(MethodCreateMonitor, 0).Store(&monitorPath)
    if err != nil {
        fmt.Fprintln(os.Stderr, "Failed to get node path:", err)
        os.Exit(1)
    }

    monitorObject := conn.Object(BcDusInterface, dbus.ObjectPath(monitorPath))

    err = monitorObject.AddMatchSignal("org.eclipse.bluechi.Monitor", SignalUnitNew).Err
    if err != nil {
        fmt.Println("Failed to add signal to UnitNew: ", err)
        os.Exit(1)
    }
    err = monitorObject.AddMatchSignal("org.eclipse.bluechi.Monitor", SignalUnitRemoved).Err
    if err != nil {
        fmt.Println("Failed to add signal to UnitRemoved: ", err)
        os.Exit(1)
    }
    err = monitorObject.AddMatchSignal("org.eclipse.bluechi.Monitor", SignalUnitPropertiesChanged).Err
    if err != nil {
        fmt.Println("Failed to add signal to UnitPropertiesChanged: ", err)
        os.Exit(1)
    }
    err = monitorObject.AddMatchSignal("org.eclipse.bluechi.Monitor", SignalUnitStateChanged).Err
    if err != nil {
        fmt.Println("Failed to add signal to UnitStateChanged: ", err)
        os.Exit(1)
    }

    var subscriptionID uint64
    err = monitorObject.Call(MethodSubscribe, 0, nodeName, unitName).Store(&subscriptionID)
    if err != nil {
        fmt.Println("Failed to subscribe to all units: ", err)
        os.Exit(1)
    }

    c := make(chan *dbus.Signal, 10)
    conn.Signal(c)
    for v := range c {
        if strings.HasSuffix(v.Name, SignalUnitNew) {
            unitName := v.Body[1]
            reason := v.Body[2]
            fmt.Printf("New Unit %s on node %s, reason: %s\n", unitName, nodeName, reason)
        } else if strings.HasSuffix(v.Name, SignalUnitRemoved) {
            unitName := v.Body[1]
            reason := v.Body[2]
            fmt.Printf("Removed Unit %s on node %s, reason: %s\n", unitName, nodeName, reason)
        } else if strings.HasSuffix(v.Name, SignalUnitPropertiesChanged) {
            unitName := v.Body[1]
            iface := v.Body[2]
            fmt.Printf("Unit %s on node %s changed for iface %s\n", unitName, nodeName, iface)
        } else if strings.HasSuffix(v.Name, SignalUnitStateChanged) {
            unitName := v.Body[1]
            activeState := v.Body[2]
            subState := v.Body[3]
            reason := v.Body[4]
            fmt.Printf("Unit %s on node %s changed to state (%s, %s), reason: %s\n", unitName, nodeName, activeState, subState, reason)
        } else {
            fmt.Println("Unexpected signal name: ", v.Name)
        }
    }
}
#!/usr/bin/python3
# SPDX-License-Identifier: MIT-0

import sys
from dasbus.typing import get_native
from dasbus.loop import EventLoop
import dasbus.connection


def print_dict_changes(old, new):
    for key in sorted(set(old.keys()) | set(new.keys())):
        if key not in old:
            print(f" {key}: {new[key]}")
        elif key in new:
            o = old[key]
            n = new[key]
            if o != n:
                print(f" {key}: {o} -> {n}")


if len(sys.argv) < 2:
    print("No unit name supplied")
    sys.exit(1)

unit_name = sys.argv[1]
node_name = "*"  # Match all

if len(sys.argv) > 2:
    node_name = sys.argv[2]

bus = dasbus.connection.SystemMessageBus()

manager = bus.get_proxy("org.eclipse.bluechi", "/org/eclipse/bluechi")

monitor_path = manager.CreateMonitor()
monitor = bus.get_proxy("org.eclipse.bluechi", monitor_path)

old_values = {}


def unit_property_changed(node, unit, interface, props):
    old_value = old_values.get(node, {})
    new_value = get_native(props)

    print(f"Unit {unit} on node {node} changed for iface {interface}:")
    print_dict_changes(old_value, new_value)

    old_values[node] = {**old_value, **new_value}


def unit_new(node, unit, reason):
    print(f"New Unit {unit} on node {node}, reason: {reason}")


def unit_state_changed(node, unit, active_state, substate, reason):
    print(f"Unit {unit} on node {node}, changed to state: {active_state} ({substate}), reason: {reason}")


def unit_removed(node, unit, reason):
    print(f"Removed Unit {unit} on node {node}, reason: {reason}")
    if node in old_values:
        del old_values[node]


monitor.UnitPropertiesChanged.connect(unit_property_changed)
monitor.UnitNew.connect(unit_new)
monitor.UnitStateChanged.connect(unit_state_changed)
monitor.UnitRemoved.connect(unit_removed)

monitor.Subscribe(node_name, unit_name)

if node_name == "":
    print(f"Waiting for changes to unit `{unit_name}` on any node")
else:
    print(f"Waiting for changes to unit `{unit_name}` on node '{node_name}'")

loop = EventLoop()
loop.run()
// SPDX-License-Identifier: MIT-0

use clap::Parser;
use dbus::{arg, blocking::Connection, Message, Path};
use std::time::Duration;

pub struct UnitNewSignal {
    pub node: String,
    pub unit: String,
    pub reason: String,
}

impl arg::AppendAll for UnitNewSignal {
    fn append(&self, i: &mut arg::IterAppend) {
        arg::RefArg::append(&self.node, i);
    }
}

impl arg::ReadAll for UnitNewSignal {
    fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
        Ok(UnitNewSignal {
            node: i.read()?,
            unit: i.read()?,
            reason: i.read()?,
        })
    }
}

impl dbus::message::SignalArgs for UnitNewSignal {
    const NAME: &'static str = "UnitNew";
    const INTERFACE: &'static str = "org.eclipse.bluechi.Monitor";
}

pub struct UnitRemovedSignal {
    pub node: String,
    pub unit: String,
    pub reason: String,
}

impl arg::AppendAll for UnitRemovedSignal {
    fn append(&self, i: &mut arg::IterAppend) {
        arg::RefArg::append(&self.node, i);
    }
}

impl arg::ReadAll for UnitRemovedSignal {
    fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
        Ok(UnitRemovedSignal {
            node: i.read()?,
            unit: i.read()?,
            reason: i.read()?,
        })
    }
}

impl dbus::message::SignalArgs for UnitRemovedSignal {
    const NAME: &'static str = "UnitRemoved";
    const INTERFACE: &'static str = "org.eclipse.bluechi.Monitor";
}

pub struct UnitPropertiesChangedSignal {
    pub node: String,
    pub unit: String,
    pub interface: String,
}

impl arg::AppendAll for UnitPropertiesChangedSignal {
    fn append(&self, i: &mut arg::IterAppend) {
        arg::RefArg::append(&self.node, i);
    }
}

impl arg::ReadAll for UnitPropertiesChangedSignal {
    fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
        Ok(UnitPropertiesChangedSignal {
            node: i.read()?,
            unit: i.read()?,
            interface: i.read()?,
        })
    }
}

impl dbus::message::SignalArgs for UnitPropertiesChangedSignal {
    const NAME: &'static str = "UnitPropertiesChanged";
    const INTERFACE: &'static str = "org.eclipse.bluechi.Monitor";
}

pub struct UnitStateChangedSignal {
    pub node: String,
    pub unit: String,
    pub active_state: String,
    pub sub_state: String,
    pub reason: String,
}

impl arg::AppendAll for UnitStateChangedSignal {
    fn append(&self, i: &mut arg::IterAppend) {
        arg::RefArg::append(&self.node, i);
    }
}

impl arg::ReadAll for UnitStateChangedSignal {
    fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
        Ok(UnitStateChangedSignal {
            node: i.read()?,
            unit: i.read()?,
            active_state: i.read()?,
            sub_state: i.read()?,
            reason: i.read()?,
        })
    }
}

impl dbus::message::SignalArgs for UnitStateChangedSignal {
    const NAME: &'static str = "UnitStateChanged";
    const INTERFACE: &'static str = "org.eclipse.bluechi.Monitor";
}

#[derive(Parser)]
struct Cli {
    /// The node name to list the units for
    #[clap(short, long)]
    unit_name: String,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Cli::parse();
    let unit_name = args.unit_name;
    let node_name = "*"; // Match all nodes

    let conn = Connection::new_system()?;

    let bluechi = conn.with_proxy(
        "org.eclipse.bluechi",
        "/org/eclipse/bluechi",
        Duration::from_millis(5000),
    );

    let (monitor,): (Path,) =
        bluechi.method_call("org.eclipse.bluechi.Manager", "CreateMonitor", ())?;
    let monitor_proxy =
        conn.with_proxy("org.eclipse.bluechi", monitor, Duration::from_millis(5000));

    let _id =
        monitor_proxy.match_signal(move |signal: UnitNewSignal, _: &Connection, _: &Message| {
            println!(
                "New Unit {} on node {}, reason: {}",
                signal.unit, signal.node, signal.reason
            );
            true
        });

    let _id = monitor_proxy.match_signal(
        move |signal: UnitRemovedSignal, _: &Connection, _: &Message| {
            println!(
                "Removed Unit {} on node {}, reason: {}",
                signal.unit, signal.node, signal.reason
            );
            true
        },
    );

    let _id = monitor_proxy.match_signal(
        move |signal: UnitPropertiesChangedSignal, _: &Connection, _: &Message| {
            println!(
                "Unit {} on node {} changed for iface {}",
                signal.unit, signal.node, signal.interface
            );
            true
        },
    );

    let _id = monitor_proxy.match_signal(
        move |signal: UnitStateChangedSignal, _: &Connection, _: &Message| {
            println!(
                "Unit {} on node {} changed to state({}, {}), reason: {}",
                signal.unit, signal.node, signal.active_state, signal.sub_state, signal.reason
            );
            true
        },
    );

    monitor_proxy.method_call(
        "org.eclipse.bluechi.Monitor",
        "Subscribe",
        (node_name, unit_name),
    )?;

    loop {
        conn.process(Duration::from_millis(1000))?;
    }
}