Skip to content

Notifications

A notification runs an action on one resource because another resource changed: reload nginx after its configuration file changed, run systemctl daemon-reload after a unit file changed, restart a service when its settings changed. Nothing is triggered when the watched resource was already as the recipe wants it, so a service is only restarted when there is a reason to. By default a notified action runs once, at the end of apply, however many resources asked for it; marked :immediately, it runs right after the change that triggered it.

Minimal example

examples/resources/service/reload_on_change.rb
# Reload nginx when its configuration changes. The reload runs once, at the
# end of apply, and only if the file changed.
package "nginx"

file "/etc/nginx/conf.d/status.conf",
     content: "server {\n  listen 127.0.0.1:8080;\n  location = /status { stub_status; }\n}\n",
     mode: "0644",
     notifies: [:reload, "service[nginx]"]

service "nginx", enable: true, state: :running

Options

Both options are available on every resource.

Option Form Meaning
notifies [:action, "type[name]"] When this resource changes, run action on the named resource.
subscribes [:action, "type[name]"] When the named resource changes, run action on this resource.
either [:action, "type[name]", :immediately] Run the action right after the change instead of at the end.
either [:action, "type[name]", :delayed] The default, written out.
either [[...], [...]] Several notifications from one resource.

subscribes is notifies declared from the other side; use whichever reads better. Actions each resource type accepts:

Resource Actions
service :start, :stop, :restart, :reload, :enable, :disable
exec :run
package, file, directory, user none

exec also takes action: :nothing: the command then never runs on its own, only when notified. That is the usual way to write "run this after that changed".

How notifications run

  • Only a resource that changed during apply triggers its notifications. Unchanged, failed and skipped resources trigger nothing.
  • Delayed notifications (the default) run after the last resource, in the order they were first triggered. Each action on each resource runs once: if three configuration files ask to restart nginx, it restarts once.
  • Immediate notifications run right after the resource that triggered them, every time they are triggered, before the next resource is looked at.
  • If a resource fails, the run stops: later resources are skipped and delayed notifications do not run. A failed immediate notification stops the run in the same way.
  • plan and drift never run notifications. They list, under each resource that would change, what it would trigger.
  • Every notification must name a declared resource and an action it supports. Mistakes are reported when the recipe loads, before anything runs.

Scenarios

The example below manages a small systemd service together with its unit file and settings:

examples/notifications/recipe.rb
# A small systemd service whose unit file and settings are managed here.
# Notifications keep the running process in step with those files.

# Runs only when notified.
exec "systemctl daemon-reload", action: :nothing

# systemd must re-read a changed unit file before the service below is
# started or restarted, so daemon-reload runs immediately. The restart can
# wait until the end of the run.
file "/etc/systemd/system/ticker.service",
     template: "ticker.service.erb",
     mode: "0644",
     notifies: [
       [:run, "exec[systemctl daemon-reload]", :immediately],
       [:restart, "service[ticker]"]
     ]

# Settings the service reads when it starts.
file "/etc/default/ticker", content: "INTERVAL=#{vars[:interval]}\n", mode: "0644"

# The service asks to be restarted when its settings change; this is the
# same as notifies: on the file above, declared from the other side.
service "ticker",
        enable: true,
        state: :running,
        subscribes: [:restart, "file[/etc/default/ticker]"]
examples/notifications/templates/ticker.service.erb
# Managed by sadame. Local changes will be overwritten.
[Unit]
Description=<%= vars[:description] %>

[Service]
EnvironmentFile=/etc/default/ticker
ExecStart=/bin/sh -c 'while true; do date >> /var/log/ticker.log; sleep ${INTERVAL}; done'

[Install]
WantedBy=multi-user.target
examples/notifications/vars.rb
{
  interval: 60,
  description: "Writes the date to /var/log/ticker.log"
}

First apply

The unit file is new, so systemctl daemon-reload runs immediately after it, before the service is enabled and started. At the end, the delayed restart runs once, although both files asked for it:

$ sudo sadame apply examples/notifications
sadame 0.5.1 apply on local (Debian GNU/Linux 12 (bookworm))

unchanged  exec[systemctl daemon-reload]
changed    file[/etc/systemd/system/ticker.service]
           state: absent -> present
           mode: 0644
           --- /dev/null
           +++ /etc/systemd/system/ticker.service (desired)
           @@ -0,0 +1,10 @@
           +# Managed by sadame. Local changes will be overwritten.
           +[Unit]
           +Description=Writes the date to /var/log/ticker.log
           +
           +[Service]
           +EnvironmentFile=/etc/default/ticker
           +ExecStart=/bin/sh -c 'while true; do date >> /var/log/ticker.log; sleep ${INTERVAL}; done'
           +
           +[Install]
           +WantedBy=multi-user.target
notified   exec[systemctl daemon-reload]: run (immediately, from file[/etc/systemd/system/ticker.service])
changed    file[/etc/default/ticker]
           state: absent -> present
           mode: 0644
           --- /dev/null
           +++ /etc/default/ticker (desired)
           @@ -0,0 +1 @@
           +INTERVAL=60
changed    service[ticker]
           enabled: false -> true
           state: stopped -> running
notified   service[ticker]: restart (delayed, from file[/etc/systemd/system/ticker.service])

Summary
  changed      3  file[/etc/systemd/system/ticker.service]
                  file[/etc/default/ticker]
                  service[ticker]
  unchanged    1
  failed       0
  skipped      0
  notified     2  exec[systemctl daemon-reload]: run
                  service[ticker]: restart

Finished in 0.06s.

Changing a setting

plan shows which notifications a change would trigger:

$ sudo sadame plan examples/notifications --var interval=30
sadame 0.5.1 plan on local (Debian GNU/Linux 12 (bookworm))

~ file[/etc/default/ticker]
    --- /etc/default/ticker
    +++ /etc/default/ticker (desired)
    @@ -1 +1 @@
    -INTERVAL=60
    +INTERVAL=30
    notifies: restart service[ticker] (delayed)

Plan: 0 to create, 1 to update, 0 to remove, 0 to run, 3 unchanged.

apply restarts the service once the settings file is written:

$ sudo sadame apply examples/notifications --var interval=30
sadame 0.5.1 apply on local (Debian GNU/Linux 12 (bookworm))

unchanged  exec[systemctl daemon-reload]
unchanged  file[/etc/systemd/system/ticker.service]
changed    file[/etc/default/ticker]
           --- /etc/default/ticker
           +++ /etc/default/ticker (desired)
           @@ -1 +1 @@
           -INTERVAL=60
           +INTERVAL=30
unchanged  service[ticker]
notified   service[ticker]: restart (delayed, from file[/etc/default/ticker])

Summary
  changed      1  file[/etc/default/ticker]
  unchanged    3
  failed       0
  skipped      0
  notified     1  service[ticker]: restart

Finished in 0.01s.

Changing the unit file

A changed unit file triggers both: daemon-reload right away, the restart at the end.

$ sudo sadame apply examples/notifications --var interval=30 --var description='Ticker demo'
sadame 0.5.1 apply on local (Debian GNU/Linux 12 (bookworm))

unchanged  exec[systemctl daemon-reload]
changed    file[/etc/systemd/system/ticker.service]
           --- /etc/systemd/system/ticker.service
           +++ /etc/systemd/system/ticker.service (desired)
           @@ -1,6 +1,6 @@
            # Managed by sadame. Local changes will be overwritten.
            [Unit]
           -Description=Writes the date to /var/log/ticker.log
           +Description=Ticker demo

            [Service]
            EnvironmentFile=/etc/default/ticker
notified   exec[systemctl daemon-reload]: run (immediately, from file[/etc/systemd/system/ticker.service])
unchanged  file[/etc/default/ticker]
unchanged  service[ticker]
notified   service[ticker]: restart (delayed, from file[/etc/systemd/system/ticker.service])

Summary
  changed      1  file[/etc/systemd/system/ticker.service]
  unchanged    3
  failed       0
  skipped      0
  notified     2  exec[systemctl daemon-reload]: run
                  service[ticker]: restart

Finished in 0.03s.

Common mistakes

  • Delayed daemon-reload. systemctl daemon-reload after a unit file change must happen before the service is started or restarted. Mark it :immediately; a delayed reload runs after the restart it was meant to prepare.
  • An exec without action: :nothing. A notified exec that also has no guard runs on every apply anyway, and drift always reports it.
  • Reloading a unit that cannot reload. systemctl reload fails for units without ExecReload=; notify :restart instead.
  • Expecting a notification after a failed run. Delayed notifications are skipped when a resource fails. Fix the failure and apply again; the resource that changed earlier is now unchanged and will not notify again, so restart by hand if needed.
  • Typos in the target. "service[ngnix]" is reported when the recipe loads: file[/etc/nginx/nginx.conf] notifies service[ngnix], which is not declared.

See also