Skip to content

Install the collector from the Debian package ​

This guide puts one tally-openstack-collector on an OpenStack control node as a service systemd starts, restarts and logs. At the end the collector runs as the unprivileged tally user, reads its credentials from files only that user may read, and buffers into a directory that survives an upgrade. Which notifications it then consumes, and what the cloud has to publish for it, is connect the collector to an OpenStack cloud.

Before you start ​

  • A Debian 12 or 13, or Ubuntu 24.04 or 26.04, host on amd64, with sudo.
  • The package file, the SHA256SUMS beside it and the attestation bundle attestation.sigstore.json, all three from the releases page. Copy them to the host, or to the machine you check them on.
  • The GitHub CLI wherever you check them, for gh attestation verify.
  • For a version that has no release yet, a build from a checkout with make deb, which writes dist/tally-openstack-collector_<version>_amd64.deb. The build needs Go and nothing else and runs on macOS as well as on Linux, but it produces no checksum file and no attestation, so the next section does not apply to it.
  • The broker's AMQP URL, the cloud name and the base URL of the Reporting API. The account the AMQP URL names needs the permissions of create the broker account.
  • The ingest credential this cloud reports under. Issue and revoke credentials has those steps.
  • The collector settings page, which lists every variable with its default.

Verify the download ​

  1. Check the package against the checksum file. --ignore-missing is what lets you check the package alone against a SHA256SUMS that also lists the SBOM:

    sh
    sha256sum -c --ignore-missing SHA256SUMS
    text
    tally-openstack-collector_<version>_amd64.deb: OK
  2. Check that the file came out of this repository's release workflow. That run signs what it publishes with a short-lived Sigstore certificate and stores the attestation on GitHub, so there is no key to fetch and none to import:

    sh
    gh attestation verify tally-openstack-collector_<version>_amd64.deb --repo B42Labs/tally
    text
    ✓ Verification succeeded!

    The lines above that one name the digest that was read and how many attestations were loaded.

  3. On a host with no route to the attestations API, check against the bundle you downloaded beside the package instead:

    sh
    gh attestation verify tally-openstack-collector_<version>_amd64.deb \
      --repo B42Labs/tally --bundle attestation.sigstore.json

    A file that did not come out of a release of this repository fails both forms, and a file that fails here is one you do not install.

Install the package ​

  1. Install the file. The leading ./ is what tells apt this is a path and not a package name:

    sh
    sudo apt install ./tally-openstack-collector_<version>_amd64.deb
    text
    Setting up tally-openstack-collector (<version>) ...
  2. Read back what it installed. The tally user and group are created by the package, and the state directory belongs to them:

    sh
    dpkg-query -W -f='${Status}\n' tally-openstack-collector
    getent passwd tally
    stat -c '%n %U:%G %a' /var/lib/tally/collector /etc/tally/ingest-token
    text
    install ok installed
    tally:x:998:998::/var/lib/tally/collector:/usr/sbin/nologin
    /var/lib/tally/collector tally:tally 750
    /etc/tally/ingest-token root:tally 640

    The service is installed stopped and disabled. It carries no credentials and no cloud yet, and the collector refuses that configuration before it listens, so nothing is started until you have configured it.

Put the credentials in place ​

  1. Write the broker URL and the ingest token into the two files the package shipped empty. Writing into the existing file keeps its 0640 root:tally mode, which a new file created by a redirect would not have:

    sh
    printf '%s' 'amqp://tally:<password>@rabbitmq.example:5672/' | sudo tee /etc/tally/amqp-url > /dev/null
    printf '%s' 'tly_i_<token>' | sudo tee /etc/tally/ingest-token > /dev/null
  2. Check that both kept their mode and are no longer empty. One trailing newline is trimmed when the file is read, so an echo here would have been fine too; an empty file is refused:

    sh
    stat -c '%n %U:%G %a' /etc/tally/amqp-url /etc/tally/ingest-token
    test -s /etc/tally/amqp-url && test -s /etc/tally/ingest-token && echo filled
    text
    /etc/tally/amqp-url root:tally 640
    /etc/tally/ingest-token root:tally 640
    filled

Configure the collector ​

  1. Open /etc/default/tally-openstack-collector and fill the two empty settings. Every other variable is in that file, commented out with its default beside it:

    sh
    sudoedit /etc/default/tally-openstack-collector
    sh
    TALLY_OSC_CLOUD=os-prod-eu1
    TALLY_OSC_REPORTING_URL=https://tally-reporting.internal
  2. Where the cloud runs octavia, or renamed an exchange, uncomment the exchanges line and list its own:

    sh
    TALLY_OSC_EXCHANGES=nova,neutron,openstack,glance,octavia
  3. Leave TALLY_OSC_BUFFER_PATH as it is. It points into /var/lib/tally/collector, the directory the package owns and the unit recreates; anywhere else the service may not write, because the unit runs under ProtectSystem=strict.

Start the service ​

  1. Enable the unit and start it:

    sh
    sudo systemctl enable --now tally-openstack-collector
  2. Check that it came up and read its first lines. The collector logs JSON to the journal:

    sh
    systemctl is-active tally-openstack-collector
    journalctl -u tally-openstack-collector -n 2 -o cat
    text
    active
    {"time":"2026-07-09T14:22:00.512Z","level":"INFO","msg":"listening","service":"tally-openstack-collector","port":8080}
    {"time":"2026-07-09T14:22:00.731Z","level":"INFO","msg":"the AMQP session is established, consuming","service":"tally-openstack-collector","queue":"tally-notifications","exchanges":["nova","neutron","openstack","glance"],"topics":["notifications.info"]}

    A second line that reads the AMQP session ended, reconnecting carries the broker's refusal in error.

    A start that ends in failed names the value to fix. An empty credential file reports TALLY_OSC_AMQP_URL_FILE: file /etc/tally/amqp-url is empty, an unset cloud reports TALLY_OSC_CLOUD: must be set, and a token set in the environment file beside its _FILE companion reports set TALLY_OSC_TOKEN or TALLY_OSC_TOKEN_FILE, not both.

Upgrade, remove and purge ​

  1. Coming from v0.2.0 with the exchanges line still commented out, check whether cinder sets control_exchange = cinder before you upgrade. The default of TALLY_OSC_EXCHANGES listed cinder then and lists openstack in its place now, and where cinder publishes on cinder the upgrade does not show what it drops: the queue keeps the binding the older version made, so volume notifications keep arriving, and they stop once the queue is recreated. The broker lists that binding:

    sh
    rabbitmqctl list_bindings source_name destination_name | grep -E '^cinder[[:space:]]+tally-notifications'
    text
    cinder	tally-notifications

    Where cinder sets the option, uncomment the exchanges line and list cinder instead of openstack:

    sh
    TALLY_OSC_EXCHANGES=nova,neutron,glance,cinder
  2. Coming from v0.2.0, set the queue type before you upgrade, whichever queue the collector ends on. v0.2.0 declared tally-notifications without a type, and the newer version declares a quorum queue. The broker refuses that declare over the existing queue, so the upgraded collector logs the queue exists with other arguments than TALLY_OSC_QUEUE_TYPE=quorum declares and consumes nothing, while the queue keeps filling. The package does not restart the service, but Ubuntu's needrestart does at the end of the apt run, and so does a crash or a reboot. Add this line to /etc/default/tally-openstack-collector before the upgrade. v0.2.0 ignores a variable it does not know, and the file is a conffile, so the line is kept:

    sh
    TALLY_OSC_QUEUE_TYPE=classic

    The line keeps the queue. To get the quorum queue, move the queue in step 3, after the install. A broker older than RabbitMQ 4.0 keeps the line.

  3. Upgrade by installing the newer file. Your edits to /etc/default/tally-openstack-collector and to the two credential files are kept, because all three are conffiles; a changed default arrives beside them as .dpkg-dist for you to compare. The outbox is untouched, so events that have not reached the Reporting API are delivered after the restart:

    sh
    sudo apt install ./tally-openstack-collector_<newer-version>_amd64.deb

    The package neither stops nor restarts the service. Restart it and read the journal. Coming from v0.2.0 without the classic line of step 2, the journal carries the queue error of that step; add the line and restart again:

    sh
    sudo systemctl restart tally-openstack-collector
    journalctl -u tally-openstack-collector -n 5 -o cat

    To get the quorum queue, follow move the queue to another type. The upgraded collector with the classic line is the one that fits the existing queue. Wait until the message count of that section's step 1 is 0 with it running, then stop the service, delete the queue, take the line out again and start the service:

    sh
    sudo systemctl stop tally-openstack-collector
    rabbitmqctl delete_queue tally-notifications
    sudo sed -i '/^[[:space:]]*TALLY_OSC_QUEUE_TYPE[[:space:]]*=/d' /etc/default/tally-openstack-collector
    sudo systemctl start tally-openstack-collector

    Read the type of the queue the collector declared:

    sh
    rabbitmqctl list_queues name type | grep -E '^tally-notifications[[:space:]]'
    text
    tally-notifications	quorum

    A classic there means the queue was not deleted, or the collector was started with TALLY_OSC_QUEUE_TYPE=classic still set.

  4. Remove the package to stop and disable the service while keeping its configuration and its outbox:

    sh
    sudo apt remove tally-openstack-collector
  5. Purge it to drop the configuration and the credentials as well. The outbox is deliberately kept: between the acknowledgement on the bus and the delivery, an event lives in that file and nowhere else, so no purge destroys usage that exists in no other copy. Delete it by hand once you know it is empty:

    sh
    sudo apt purge tally-openstack-collector
    text
    tally-openstack-collector: /var/lib/tally/collector/outbox.db was kept.
      It may still hold events that never reached the Reporting API.
      Delete it by hand once you know it is empty or no longer needed.

    The tally user and group stay as well: an orphaned system account is harmless, and a later Tally package on this host uses the same one.

Check the result ​

  1. Ask the running service for readiness. It answers 200 while the consumer holds the broker connection and the outbox answers:

    sh
    curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/readyz
    text
    200
  2. Read the two counters twice, a minute apart, while the cloud is in use. Both rise:

    sh
    curl -sS http://127.0.0.1:8080/metrics | grep -E '^tally_collector_(consumed|delivered)_total'
    text
    tally_collector_consumed_total{cloud="os-prod-eu1",event_type="compute.instance.create.end",platform="openstack"} 14
    tally_collector_delivered_total{cloud="os-prod-eu1",platform="openstack"} 12
  3. Confirm the service comes back on its own. Kill it and read the state again a few seconds later; Restart=on-failure brings it back, and the outbox it reopens is the one it was writing:

    sh
    sudo systemctl kill -s KILL tally-openstack-collector
    sleep 10
    systemctl is-active tally-openstack-collector
    text
    active
  4. Read the last summary line. The collector logs one every TALLY_OSC_SUMMARY_INTERVAL_S seconds, 60 by default, with what it consumed and delivered since the previous one; the log lines section states every attribute:

    sh
    journalctl -u tally-openstack-collector -o cat | grep '"msg":"summary"' | tail -1
    json
    {"time":"2026-07-09T14:23:00.514Z","level":"INFO","msg":"summary","service":"tally-openstack-collector","interval_seconds":60,"connected":true,"consumed":14,"skipped":37,"unparseable":0,"delivered":12,"delivery_errors":0,"buffered":2,"oldest_buffered_seconds":3}