multica_agent_host: daemon lifecycle via systemd; fix login guard (WDWL-22)
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>
This commit is contained in:
1 parent
61dba61025
commit
94c77daf13
4 files changed
+89
-10
No files matched your search
@@ -307,13 +307,19 @@ Every step reads before it writes:
|
|||||||
| CLI install | `multica --version` equals the pin |
|
| CLI install | `multica --version` equals the pin |
|
||||||
| Agent CLI install | `command -v <binary>` and its version command |
|
| Agent CLI install | `command -v <binary>` and its version command |
|
||||||
| Config | `multica config show` parsed and compared key by key |
|
| Config | `multica config show` parsed and compared key by key |
|
||||||
| Login | `multica auth status` already names `multica_server_url` |
|
| Login | `multica auth status` already names `multica_server_url` (it prints to **stderr**; the guard reads both streams) |
|
||||||
| Autostart | `multica daemon autostart status` |
|
| Autostart | `multica daemon autostart status` |
|
||||||
| Daemon start | `multica daemon status --output json` reports `running` |
|
| Daemon start | `systemctl --user is-active <unit>`; a daemon running *outside* the unit is stopped first, then the unit is started |
|
||||||
|
|
||||||
A binary, config or credential change notifies a `multica daemon restart`
|
A binary, config or credential change notifies a `systemctl --user restart`
|
||||||
handler, which is flushed *before* verification reads the daemon's state back.
|
handler, which is flushed *before* verification reads the daemon's state back.
|
||||||
|
|
||||||
|
The role never calls `multica daemon start` or `multica daemon restart`. Both
|
||||||
|
fork the daemon inside the caller's session (Ansible's SSH session scope),
|
||||||
|
outside the unit's cgroup. The unit then shows `inactive`, nothing supervises
|
||||||
|
the process, and a later `systemctl --user start` collides with it on
|
||||||
|
`daemon.lock`.
|
||||||
|
|
||||||
A converged host reports zero changed tasks.
|
A converged host reports zero changed tasks.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
---
|
---
|
||||||
|
# Through systemd, not `multica daemon restart`: that respawns the daemon in
|
||||||
|
# the caller's session, outside the unit (see tasks/daemon.yml).
|
||||||
- name: Restart multica daemon
|
- name: Restart multica daemon
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
argv: "{{ _multica_cmd + ['daemon', 'restart'] }}"
|
argv: [systemctl, --user, restart, "{{ _multica_unit | default('multica-daemon.service') }}"]
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ multica_user }}"
|
become_user: "{{ multica_user }}"
|
||||||
environment: "{{ _multica_user_env }}"
|
environment: "{{ _multica_user_env }}"
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
---
|
---
|
||||||
# `multica auth status` prints the server and user when authenticated and exits
|
# `multica auth status` prints the server and user when authenticated and exits
|
||||||
# non-zero otherwise, so it is the idempotency guard for login.
|
# non-zero otherwise, so it is the idempotency guard for login.
|
||||||
|
#
|
||||||
|
# It prints to STDERR, not stdout (0.6.5). A stdout-only guard never matches,
|
||||||
|
# so every run re-logs in and restarts the daemon. It also echoes a token
|
||||||
|
# prefix, hence no_log.
|
||||||
- name: Auth | Read the current authentication state
|
- name: Auth | Read the current authentication state
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
argv: "{{ _multica_cmd + ['auth', 'status'] }}"
|
argv: "{{ _multica_cmd + ['auth', 'status'] }}"
|
||||||
@@ -11,13 +15,15 @@
|
|||||||
changed_when: false
|
changed_when: false
|
||||||
failed_when: false
|
failed_when: false
|
||||||
check_mode: false
|
check_mode: false
|
||||||
|
no_log: true
|
||||||
|
|
||||||
- name: Auth | Decide whether a login is needed
|
- name: Auth | Decide whether a login is needed
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
_multica_needs_login: >-
|
_multica_needs_login: >-
|
||||||
{{ multica_force_relogin | bool
|
{{ multica_force_relogin | bool
|
||||||
or _multica_auth_status.rc != 0
|
or _multica_auth_status.rc != 0
|
||||||
or multica_server_url.rstrip('/') not in (_multica_auth_status.stdout | default('')) }}
|
or multica_server_url.rstrip('/') not in
|
||||||
|
((_multica_auth_status.stdout | default('')) ~ (_multica_auth_status.stderr | default(''))) }}
|
||||||
|
|
||||||
# no_log is mandatory: without it Ansible records the full argv — including the
|
# no_log is mandatory: without it Ansible records the full argv — including the
|
||||||
# PAT — in play output, CI logs, and every callback plugin.
|
# PAT — in play output, CI logs, and every callback plugin.
|
||||||
|
|||||||
@@ -43,8 +43,61 @@
|
|||||||
- multica_autostart | bool
|
- multica_autostart | bool
|
||||||
- _multica_autostart_status.rc != 0
|
- _multica_autostart_status.rc != 0
|
||||||
or 'enabled' not in (_multica_autostart_status.stdout | default('') | lower)
|
or 'enabled' not in (_multica_autostart_status.stdout | default('') | lower)
|
||||||
|
register: _multica_autostart_enable
|
||||||
changed_when: true
|
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
|
- name: Daemon | Read daemon status
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
argv: "{{ _multica_cmd + ['daemon', 'status', '--output', 'json'] }}"
|
argv: "{{ _multica_cmd + ['daemon', 'status', '--output', 'json'] }}"
|
||||||
@@ -56,15 +109,27 @@
|
|||||||
failed_when: false
|
failed_when: false
|
||||||
check_mode: false
|
check_mode: false
|
||||||
|
|
||||||
- name: Daemon | Start it if it is not already running
|
# 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:
|
ansible.builtin.command:
|
||||||
argv: "{{ _multica_cmd + ['daemon', 'start'] }}"
|
argv: "{{ _multica_cmd + ['daemon', 'stop'] }}"
|
||||||
become: true
|
become: true
|
||||||
become_user: "{{ multica_user }}"
|
become_user: "{{ multica_user }}"
|
||||||
environment: "{{ _multica_user_env }}"
|
environment: "{{ _multica_user_env }}"
|
||||||
when: >-
|
when:
|
||||||
_multica_daemon_status.rc != 0
|
- (_multica_unit_state.stdout | trim) != 'active'
|
||||||
or ((_multica_daemon_status.stdout | from_json).status | default('')) != 'running'
|
- _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
|
changed_when: true
|
||||||
|
|
||||||
# Anything that changed the binary, the config or the credentials restarts the
|
# Anything that changed the binary, the config or the credentials restarts the
|
||||||
|
|||||||
Reference in new issue
Block a user