Found provisioning spaceship-earth: - Login was not idempotent: `multica auth status` prints to stderr (0.6.5), and the guard read stdout only, so every run re-logged in and restarted the daemon. The guard now reads both streams; the status read is no_log because it echoes a token prefix. - `multica daemon start` / `daemon restart` fork the daemon inside the caller's session (Ansible's SSH session scope), outside the systemd user unit, which stays inactive: the same orphan seen on haunted-mansion on 2026-10-21. Start/restart now go through `systemctl --user`, after a daemon-reload when autostart was (re)registered. A daemon found running outside its unit is stopped first. The unit name is read from `autostart status`, so --profile hosts resolve their own unit. spaceship-earth: second run changed=0, daemon in multica-daemon.service. Not yet run against haunted-mansion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
139 lines
4.9 KiB
YAML
139 lines
4.9 KiB
YAML
---
|
|
# On Linux `multica daemon autostart enable` writes a systemd *user* unit.
|
|
# A user unit does not start at boot unless the user lingers — without this the
|
|
# daemon only comes back when someone logs in, which on a headless agent host
|
|
# is never.
|
|
- name: Daemon | Enable systemd lingering for the daemon user
|
|
ansible.builtin.command:
|
|
argv: ["loginctl", "enable-linger", "{{ multica_user }}"]
|
|
creates: "/var/lib/systemd/linger/{{ multica_user }}"
|
|
become: true
|
|
when:
|
|
- multica_enable_linger | bool
|
|
- multica_autostart | bool
|
|
|
|
- name: Daemon | Wait for the user's runtime directory
|
|
ansible.builtin.wait_for:
|
|
path: "/run/user/{{ _multica_uid }}"
|
|
state: present
|
|
timeout: 30
|
|
become: true
|
|
when:
|
|
- multica_enable_linger | bool
|
|
- multica_autostart | bool
|
|
|
|
- name: Daemon | Read the current autostart registration
|
|
ansible.builtin.command:
|
|
argv: "{{ _multica_cmd + ['daemon', 'autostart', 'status'] }}"
|
|
become: true
|
|
become_user: "{{ multica_user }}"
|
|
environment: "{{ _multica_user_env }}"
|
|
register: _multica_autostart_status
|
|
changed_when: false
|
|
failed_when: false
|
|
check_mode: false
|
|
|
|
- name: Daemon | Register boot autostart
|
|
ansible.builtin.command:
|
|
argv: "{{ _multica_cmd + ['daemon', 'autostart', 'enable'] }}"
|
|
become: true
|
|
become_user: "{{ multica_user }}"
|
|
environment: "{{ _multica_user_env }}"
|
|
when:
|
|
- multica_autostart | bool
|
|
- _multica_autostart_status.rc != 0
|
|
or 'enabled' not in (_multica_autostart_status.stdout | default('') | lower)
|
|
register: _multica_autostart_enable
|
|
changed_when: true
|
|
|
|
# Lifecycle goes through systemd, never `multica daemon start|restart`. Those
|
|
# fork the daemon inside the CALLER's session (here, Ansible's SSH session
|
|
# scope), outside the unit's cgroup: the unit stays inactive, nothing
|
|
# supervises the process, and the next `systemctl --user start` collides with
|
|
# it on daemon.lock. Seen on haunted-mansion (2026-10-21) and spaceship-earth
|
|
# (2026-10-22).
|
|
#
|
|
# The unit name comes from `autostart status` ("Location: .../<name>.service")
|
|
# so a --profile host resolves its own unit.
|
|
- name: Daemon | Re-read the autostart registration
|
|
ansible.builtin.command:
|
|
argv: "{{ _multica_cmd + ['daemon', 'autostart', 'status'] }}"
|
|
become: true
|
|
become_user: "{{ multica_user }}"
|
|
environment: "{{ _multica_user_env }}"
|
|
register: _multica_autostart_reread
|
|
changed_when: false
|
|
failed_when: false
|
|
check_mode: false
|
|
when: _multica_autostart_enable is changed
|
|
|
|
- name: Daemon | Resolve the systemd user unit
|
|
ansible.builtin.set_fact:
|
|
_multica_unit: >-
|
|
{{ ((_s.stdout | default('')) ~ '\n' ~ (_s.stderr | default('')))
|
|
| regex_search('Location:\s*\S*/([^/\s]+\.service)', '\1')
|
|
| default(['multica-daemon.service'], true) | first }}
|
|
vars:
|
|
_s: "{{ _multica_autostart_reread if _multica_autostart_enable is changed else _multica_autostart_status }}"
|
|
|
|
# `autostart enable` may rewrite the unit file while the user manager still
|
|
# holds the old definition.
|
|
- name: Daemon | Reload the user manager after a (re)registration
|
|
ansible.builtin.command:
|
|
argv: [systemctl, --user, daemon-reload]
|
|
become: true
|
|
become_user: "{{ multica_user }}"
|
|
environment: "{{ _multica_user_env }}"
|
|
when: _multica_autostart_enable is changed
|
|
changed_when: true
|
|
|
|
- name: Daemon | Read the unit state
|
|
ansible.builtin.command:
|
|
argv: [systemctl, --user, is-active, "{{ _multica_unit }}"]
|
|
become: true
|
|
become_user: "{{ multica_user }}"
|
|
environment: "{{ _multica_user_env }}"
|
|
register: _multica_unit_state
|
|
changed_when: false
|
|
failed_when: false
|
|
check_mode: false
|
|
|
|
- name: Daemon | Read daemon status
|
|
ansible.builtin.command:
|
|
argv: "{{ _multica_cmd + ['daemon', 'status', '--output', 'json'] }}"
|
|
become: true
|
|
become_user: "{{ multica_user }}"
|
|
environment: "{{ _multica_user_env }}"
|
|
register: _multica_daemon_status
|
|
changed_when: false
|
|
failed_when: false
|
|
check_mode: false
|
|
|
|
# Running, but not under the unit: an orphan. Stop it so the unit can own the
|
|
# lock and the process.
|
|
- name: Daemon | Stop a daemon running outside its unit
|
|
ansible.builtin.command:
|
|
argv: "{{ _multica_cmd + ['daemon', 'stop'] }}"
|
|
become: true
|
|
become_user: "{{ multica_user }}"
|
|
environment: "{{ _multica_user_env }}"
|
|
when:
|
|
- (_multica_unit_state.stdout | trim) != 'active'
|
|
- _multica_daemon_status.rc == 0
|
|
- ((_multica_daemon_status.stdout | from_json).status | default('')) == 'running'
|
|
changed_when: true
|
|
|
|
- name: Daemon | Start the unit if it is not active
|
|
ansible.builtin.command:
|
|
argv: [systemctl, --user, start, "{{ _multica_unit }}"]
|
|
become: true
|
|
become_user: "{{ multica_user }}"
|
|
environment: "{{ _multica_user_env }}"
|
|
when: (_multica_unit_state.stdout | trim) != 'active'
|
|
changed_when: true
|
|
|
|
# Anything that changed the binary, the config or the credentials restarts the
|
|
# daemon here, before verification reads its state back.
|
|
- name: Daemon | Flush handlers
|
|
ansible.builtin.meta: flush_handlers
|