added multica agent hosts

This commit is contained in:
rblundon committed 2026-10-22 12:30:35 -05:00
1 parent 5559cd425d
commit 01cd7b8d1e
32 files changed
+2636 -417

No files matched your search

@@ -0,0 +1,42 @@
---
# Resolve where the binary ACTUALLY is before linking to it.
#
# The npm path used to link unconditionally from multica_npm_prefix. When the
# CLI was already installed by some other means — a distro package, a vendor
# installer, a previous hand-rollout — nothing was installed at that prefix and
# `file: state=link force=true` happily created a DANGLING symlink in
# /usr/local/bin. PATH resolution skips a dangling link, so discovery still
# worked by luck wherever the real binary was already on systemd's PATH, and
# the role's actual guarantee silently did not hold.
- name: "Agents | Resolve the installed binary | {{ _agent_key }}"
ansible.builtin.command:
argv: ["/bin/sh", "-c", "command -v {{ _agent.binary }}"]
become: true
become_user: "{{ multica_user }}"
environment: "{{ _multica_user_env }}"
register: _agent_which
changed_when: false
failed_when: false
check_mode: false
- name: "Agents | Require the binary to be resolvable | {{ _agent_key }}"
ansible.builtin.assert:
that:
- _agent_which.rc == 0
- (_agent_which.stdout | trim) | length > 0
fail_msg: >-
{{ _agent.binary }} is not on {{ multica_user }}'s PATH after the install
step. The daemon discovers provider CLIs by PATH, so it would register no
{{ _agent_key }} runtime.
quiet: true
# Linking the resolved path also repairs a dangling link left by an older run:
# `command -v` skips the broken link, so this overwrites it with a working one.
- name: "Agents | Link onto the default PATH | {{ _agent_key }}"
ansible.builtin.file:
src: "{{ _agent_which.stdout | trim }}"
dest: "{{ multica_install_dir }}/{{ _agent.binary }}"
state: link
force: true
become: true
when: (_agent_which.stdout | trim) != (multica_install_dir ~ '/' ~ _agent.binary)
@@ -0,0 +1,46 @@
---
- name: "Agents | Resolve catalog entry | {{ _agent_key }}"
ansible.builtin.set_fact:
_agent: "{{ multica_agent_cli_catalog[_agent_key] }}"
- name: "Agents | Read the installed version | {{ _agent_key }}"
ansible.builtin.command:
argv: ["/bin/sh", "-c", "command -v {{ _agent.binary }} >/dev/null 2>&1 && {{ _agent.version_command }}"]
become: true
become_user: "{{ multica_user }}"
environment: "{{ _multica_user_env }}"
register: _agent_installed
changed_when: false
failed_when: false
check_mode: false
# Version formats differ per CLI ("2.1.304 (Claude Code)", "Hermes Agent
# v0.21.5+..."), so this matches the pinned version anywhere in the output —
# but on a version-component boundary. A plain substring test would accept
# 2.1.3045 for a pin of 2.1.304, and silently never upgrade.
- name: "Agents | Decide whether an install is needed | {{ _agent_key }}"
ansible.builtin.set_fact:
_agent_needs_install: >-
{{ _agent_installed.rc != 0
or (_agent.package_version | default('') | length > 0
and not ((_agent_installed.stdout | default('')) is search(
'(^|[^0-9.])' ~ (_agent.package_version | regex_escape) ~ '([^0-9.]|$)'))) }}
- name: "Agents | Install via npm as the daemon user | {{ _agent_key }}"
ansible.builtin.command:
argv:
- "{{ multica_install_dir }}/npm"
- install
- --global
- --no-fund
- --no-audit
- "{{ _agent.package }}{{ ('@' ~ _agent.package_version) if _agent.package_version | default('') | length > 0 else '' }}"
become: true
become_user: "{{ multica_user }}"
environment: "{{ _multica_user_env }}"
when: _agent_needs_install | bool
changed_when: true
notify: restart multica daemon
- name: "Agents | Resolve and link onto the default PATH | {{ _agent_key }}"
ansible.builtin.include_tasks: _link.yml
@@ -0,0 +1,53 @@
---
- name: "Agents | Resolve catalog entry | {{ _agent_key }}"
ansible.builtin.set_fact:
_agent: "{{ multica_agent_cli_catalog[_agent_key] }}"
- name: "Agents | Read the installed version | {{ _agent_key }}"
ansible.builtin.command:
argv: ["/bin/sh", "-c", "command -v {{ _agent.binary }} >/dev/null 2>&1 && {{ _agent.version_command }}"]
become: true
become_user: "{{ multica_user }}"
environment: "{{ _multica_user_env }}"
register: _agent_installed
changed_when: false
failed_when: false
check_mode: false
# Piping a vendor script into a shell is a supply-chain decision, so the role
# refuses rather than making it quietly on the operator's behalf.
- name: "Agents | Refuse to run a remote installer unless allowed | {{ _agent_key }}"
ansible.builtin.fail:
msg: >-
{{ _agent_key }} installs by executing {{ _agent.install_url }}.
Set multica_allow_remote_install_scripts: true to permit that, vendor the
installer yourself, or drop {{ _agent_key }} from multica_agent_clis.
when:
- _agent_installed.rc != 0
- not (multica_allow_remote_install_scripts | bool)
- name: "Agents | Install via the vendor script | {{ _agent_key }}"
when:
- _agent_installed.rc != 0
- multica_allow_remote_install_scripts | bool
block:
- name: "Agents | Fetch the installer | {{ _agent_key }}"
ansible.builtin.get_url:
url: "{{ _agent.install_url }}"
dest: "{{ multica_user_home }}/.cache/{{ _agent_key }}-install.sh"
owner: "{{ multica_user }}"
group: "{{ multica_group }}"
mode: "0700"
become: true
- name: "Agents | Run it as the daemon user | {{ _agent_key }}"
ansible.builtin.command:
argv: ["/bin/bash", "{{ multica_user_home }}/.cache/{{ _agent_key }}-install.sh"]
become: true
become_user: "{{ multica_user }}"
environment: "{{ _multica_user_env }}"
changed_when: true
notify: restart multica daemon
- name: "Agents | Resolve and link onto the default PATH | {{ _agent_key }}"
ansible.builtin.include_tasks: _link.yml
@@ -0,0 +1,7 @@
---
- name: Agents | Install each selected agent CLI
ansible.builtin.include_tasks: "agent_cli/{{ multica_agent_cli_catalog[_agent_key].method }}.yml"
loop: "{{ multica_agent_clis }}"
loop_control:
loop_var: _agent_key
label: "{{ _agent_key }}"
@@ -0,0 +1,44 @@
---
# `multica auth status` prints the server and user when authenticated and exits
# non-zero otherwise, so it is the idempotency guard for login.
- name: Auth | Read the current authentication state
ansible.builtin.command:
argv: "{{ _multica_cmd + ['auth', 'status'] }}"
become: true
become_user: "{{ multica_user }}"
environment: "{{ _multica_user_env }}"
register: _multica_auth_status
changed_when: false
failed_when: false
check_mode: false
- name: Auth | Decide whether a login is needed
ansible.builtin.set_fact:
_multica_needs_login: >-
{{ multica_force_relogin | bool
or _multica_auth_status.rc != 0
or multica_server_url.rstrip('/') not in (_multica_auth_status.stdout | default('')) }}
# no_log is mandatory: without it Ansible records the full argv — including the
# PAT — in play output, CI logs, and every callback plugin.
- name: Auth | Log in with the personal access token
ansible.builtin.command:
argv: "{{ _multica_cmd + ['login', '--token', multica_pat] }}"
become: true
become_user: "{{ multica_user }}"
environment: "{{ _multica_user_env }}"
when: _multica_needs_login | bool
changed_when: true
no_log: true
notify: restart multica daemon
- name: Auth | Pin the default workspace
ansible.builtin.command:
argv: "{{ _multica_cmd + ['workspace', 'switch', multica_workspace_id] }}"
become: true
become_user: "{{ multica_user }}"
environment: "{{ _multica_user_env }}"
when: multica_workspace_id | length > 0
register: _multica_ws_switch
changed_when: _multica_needs_login | bool
notify: restart multica daemon
@@ -0,0 +1,56 @@
---
# The CLI owns its config file format, so the role drives `multica config set`
# rather than templating config.json. Each key is read back first, so a
# converged host reports zero changes.
- name: Config | Build the desired configuration
ansible.builtin.set_fact:
_multica_desired_config: >-
{{ {
'server_url': multica_server_url,
'app_url': multica_app_url,
'workspaces_root': multica_workspaces_root,
'device_name': multica_device_name,
'runtime_name': multica_runtime_name,
'max_concurrent_tasks': multica_max_concurrent_tasks,
'poll_interval': multica_poll_interval,
'heartbeat_interval': multica_heartbeat_interval,
'agent_timeout': multica_agent_timeout,
'disable_auto_update': ('true' if multica_disable_auto_update | bool else 'false'),
} | dict2items
| rejectattr('value', 'equalto', '')
| list | items2dict }}
- name: Config | Read the current configuration
ansible.builtin.command:
argv: "{{ _multica_cmd + ['config', 'show'] }}"
become: true
become_user: "{{ multica_user }}"
environment: "{{ _multica_user_env }}"
register: _multica_config_show
changed_when: false
check_mode: false
# `config show` prints "key:<spaces>value", with "(not set)" for unset keys.
- name: Config | Parse it
ansible.builtin.set_fact:
_multica_current_config: >-
{{ dict(
_multica_config_show.stdout_lines
| select('match', '^[a-z_]+:')
| map('regex_replace', '^([a-z_]+):\s*(.*?)\s*$', '\1|\2')
| map('split', '|')
) }}
- name: Config | Apply the keys that differ
ansible.builtin.command:
argv: "{{ _multica_cmd + ['config', 'set', item.key, item.value] }}"
become: true
become_user: "{{ multica_user }}"
environment: "{{ _multica_user_env }}"
loop: "{{ _multica_desired_config | dict2items }}"
loop_control:
label: "{{ item.key }}={{ item.value }}"
when: (_multica_current_config[item.key] | default('(not set)')) != item.value
changed_when: true
notify: restart multica daemon
@@ -0,0 +1,73 @@
---
# 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)
changed_when: true
- 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
- name: Daemon | Start it if it is not already running
ansible.builtin.command:
argv: "{{ _multica_cmd + ['daemon', 'start'] }}"
become: true
become_user: "{{ multica_user }}"
environment: "{{ _multica_user_env }}"
when: >-
_multica_daemon_status.rc != 0
or ((_multica_daemon_status.stdout | from_json).status | default('')) != 'running'
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
@@ -0,0 +1,45 @@
---
- name: Preflight
ansible.builtin.import_tasks: preflight.yml
# Prerequisite for every other block: always runs, even under a --tags subset.
tags: [always]
- name: Service account
ansible.builtin.import_tasks: user.yml
# Builds _multica_uid / _multica_user_env, which every later become_user task
# needs; creating an existing user is a no-op.
tags: [always]
- name: Workspaces root
ansible.builtin.import_tasks: workspaces_root.yml
tags: [multica, multica_workspaces]
- name: Node.js
ansible.builtin.import_tasks: node.yml
when: _multica_node_required | bool
tags: [multica, multica_node]
- name: Multica CLI
ansible.builtin.import_tasks: multica_cli.yml
tags: [multica, multica_cli]
- name: Agent CLIs
ansible.builtin.import_tasks: agent_clis.yml
tags: [multica, multica_agents]
- name: CLI configuration
ansible.builtin.import_tasks: config.yml
tags: [multica, multica_config]
- name: Authentication
ansible.builtin.import_tasks: auth.yml
tags: [multica, multica_auth]
- name: Daemon
ansible.builtin.import_tasks: daemon.yml
tags: [multica, multica_daemon]
- name: Verify
ansible.builtin.import_tasks: verify.yml
when: multica_verify | bool
tags: [multica, multica_verify]
@@ -0,0 +1,72 @@
---
- name: CLI | Check whether the binary is already present
ansible.builtin.stat:
path: "{{ _multica_bin }}"
register: _multica_cli_stat
check_mode: false
- name: CLI | Read the installed version
ansible.builtin.command:
argv: ["{{ _multica_bin }}", "--version"]
register: _multica_cli_installed
when: _multica_cli_stat.stat.exists
changed_when: false
failed_when: false
check_mode: false
# `multica --version` prints e.g. "multica 0.6.5 (commit: ..., built: ...)".
- name: CLI | Extract the installed version string
ansible.builtin.set_fact:
_multica_cli_version_found: >-
{{ ((_multica_cli_installed.stdout_lines | default([]) | first | default('')).split() + ['', ''])[1]
if (_multica_cli_installed.rc | default(1)) == 0 else '' }}
- name: CLI | Decide whether an install is needed
ansible.builtin.set_fact:
_multica_cli_needs_install: "{{ (_multica_cli_version_found | trim) != multica_version }}"
- name: CLI | Install the pinned release
when: _multica_cli_needs_install | bool
block:
- name: CLI | Create a staging directory
ansible.builtin.tempfile:
state: directory
suffix: multica-cli
register: _multica_stage
become: true
- name: CLI | Download the release tarball
ansible.builtin.get_url:
url: "{{ _multica_release_url }}"
dest: "{{ _multica_stage.path }}/{{ _multica_tarball }}"
checksum: "{{ ('sha256:' ~ _multica_checksums_url) if multica_verify_checksum | bool else omit }}"
mode: "0644"
become: true
- name: CLI | Unpack the binary
ansible.builtin.unarchive:
src: "{{ _multica_stage.path }}/{{ _multica_tarball }}"
dest: "{{ _multica_stage.path }}"
remote_src: true
become: true
# Replacing the binary while the daemon runs is fine — the handler restarts
# it, and the daemon also watches its own binary for version changes.
- name: CLI | Install it onto the PATH
ansible.builtin.copy:
src: "{{ _multica_stage.path }}/multica"
dest: "{{ _multica_bin }}"
remote_src: true
owner: root
group: root
mode: "0755"
become: true
notify: restart multica daemon
always:
- name: CLI | Remove the staging directory
ansible.builtin.file:
path: "{{ _multica_stage.path }}"
state: absent
become: true
when: _multica_stage.path is defined
@@ -0,0 +1,93 @@
---
# Installed from the official nodejs.org tarball rather than a distro repo so
# that multica_node_version is a real pin on every distribution.
- name: Node | Check whether the pinned runtime is already present
ansible.builtin.stat:
path: "{{ multica_install_dir }}/node"
register: _multica_node_stat
check_mode: false
- name: Node | Read the installed version
ansible.builtin.command:
argv: ["{{ multica_install_dir }}/node", "--version"]
register: _multica_node_installed
when: _multica_node_stat.stat.exists
changed_when: false
failed_when: false
check_mode: false
- name: Node | Decide whether an install is needed
ansible.builtin.set_fact:
_multica_node_needs_install: >-
{{ (not _multica_node_stat.stat.exists)
or (_multica_node_installed.rc | default(1)) != 0
or (_multica_node_installed.stdout | default('') | trim) != ('v' ~ multica_node_version) }}
- name: Node | Install the pinned runtime
when: _multica_node_needs_install | bool
block:
- name: Node | Create the install prefix
ansible.builtin.file:
path: "{{ multica_node_prefix }}"
state: directory
owner: root
group: root
mode: "0755"
become: true
# get_url resolves `sha256:<url>` against the file's basename in SHASUMS256.txt.
- name: Node | Download the tarball with checksum verification
ansible.builtin.get_url:
url: "{{ multica_node_dist_base_url }}/v{{ multica_node_version }}/node-v{{ multica_node_version }}-linux-{{ _multica_node_arch }}.tar.xz"
dest: "/tmp/node-v{{ multica_node_version }}-linux-{{ _multica_node_arch }}.tar.xz"
checksum: "sha256:{{ multica_node_dist_base_url }}/v{{ multica_node_version }}/SHASUMS256.txt"
mode: "0644"
become: true
- name: Node | Unpack it
ansible.builtin.unarchive:
src: "/tmp/node-v{{ multica_node_version }}-linux-{{ _multica_node_arch }}.tar.xz"
dest: "{{ multica_node_prefix }}"
remote_src: true
owner: root
group: root
creates: "{{ multica_node_prefix }}/node-v{{ multica_node_version }}-linux-{{ _multica_node_arch }}/bin/node"
become: true
- name: Node | Remove the downloaded tarball
ansible.builtin.file:
path: "/tmp/node-v{{ multica_node_version }}-linux-{{ _multica_node_arch }}.tar.xz"
state: absent
become: true
# Symlinked into /usr/local/bin, which is on systemd's default PATH — the
# daemon's autostart unit does not source /etc/profile.d.
- name: Node | Link node, npm and npx onto the default PATH
ansible.builtin.file:
src: "{{ multica_node_prefix }}/node-v{{ multica_node_version }}-linux-{{ _multica_node_arch }}/bin/{{ item }}"
dest: "{{ multica_install_dir }}/{{ item }}"
state: link
force: true
become: true
loop: [node, npm, npx]
- name: Node | Create the daemon user's npm global prefix
ansible.builtin.file:
path: "{{ multica_npm_prefix }}"
state: directory
owner: "{{ multica_user }}"
group: "{{ multica_group }}"
mode: "0755"
become: true
- name: Node | Point the daemon user's npm at that prefix
ansible.builtin.lineinfile:
path: "{{ multica_user_home }}/.npmrc"
regexp: '^prefix='
line: "prefix={{ multica_npm_prefix }}"
create: true
owner: "{{ multica_user }}"
group: "{{ multica_group }}"
mode: "0644"
become: true
@@ -0,0 +1,86 @@
---
- name: Preflight | Require a Linux target
ansible.builtin.assert:
that:
- ansible_facts['system'] == 'Linux'
fail_msg: >-
multica_agent_host supports Linux hosts only
(found {{ ansible_facts['system'] }}).
quiet: true
- name: Preflight | Require a supported CPU architecture
ansible.builtin.assert:
that:
- ansible_facts['architecture'] in _multica_arch_map
fail_msg: >-
No multica CLI release asset for architecture
{{ ansible_facts['architecture'] }}.
Supported: {{ _multica_arch_map.keys() | list | join(', ') }}.
quiet: true
- name: Preflight | Require the server URL
ansible.builtin.assert:
that:
- multica_server_url | length > 0
- multica_server_url is match('^https?://')
fail_msg: "multica_server_url must be set to an http(s) URL."
quiet: true
# no_log: a failed assert would otherwise render the variables it inspected.
- name: Preflight | Require a Multica personal access token
ansible.builtin.assert:
that:
- multica_pat | length > 0
- multica_pat is match('^(mul_|mcn_)')
fail_msg: >-
multica_pat must be set to a Multica personal access token
(mul_... user PAT, or mcn_... Cloud Node PAT). Supply it from the
playbook; see this role's README for Vault / 1Password / env examples.
quiet: true
no_log: true
- name: Preflight | Require every selected agent CLI to be in the catalog
ansible.builtin.assert:
that:
- multica_agent_clis | difference(multica_agent_cli_catalog.keys() | list) | length == 0
fail_msg: >-
Unknown agent CLI(s):
{{ multica_agent_clis | difference(multica_agent_cli_catalog.keys() | list) | join(', ') }}.
Add them to multica_agent_cli_catalog or drop them from multica_agent_clis.
quiet: true
- name: Preflight | Require at least one agent CLI
ansible.builtin.assert:
that:
- multica_agent_clis | length > 0
fail_msg: >-
multica_agent_clis is empty; the daemon would register no runtimes and
the host could never claim a run.
quiet: true
- name: Preflight | Resolve release artefact names and the invocation prefix
ansible.builtin.set_fact:
_multica_arch: "{{ _multica_arch_map[ansible_facts['architecture']] }}"
_multica_node_arch: "{{ _multica_node_arch_map[ansible_facts['architecture']] }}"
_multica_bin: "{{ multica_install_dir }}/multica"
_multica_cmd: >-
{{ [multica_install_dir ~ '/multica']
+ (['--profile', multica_profile] if multica_profile | length > 0 else []) }}
_multica_node_required: >-
{{ multica_agent_clis
| map('extract', multica_agent_cli_catalog, 'method')
| select('equalto', 'npm') | list | length > 0 }}
- name: Preflight | Name the release tarball
ansible.builtin.set_fact:
_multica_tarball: "multica-cli-{{ multica_version }}-linux-{{ _multica_arch }}.tar.gz"
_multica_release_url: >-
{{ multica_release_download_base_url }}/v{{ multica_version }}/multica-cli-{{ multica_version }}-linux-{{ _multica_arch }}.tar.gz
_multica_checksums_url: >-
{{ multica_release_download_base_url }}/v{{ multica_version }}/checksums.txt
- name: Preflight | Require base tooling on the target
ansible.builtin.package:
name: "{{ ['tar', 'xz-utils' if ansible_facts['os_family'] == 'Debian' else 'xz', 'ca-certificates', 'git'] }}"
state: present
become: true
@@ -0,0 +1,63 @@
---
- name: User | Create the daemon group
ansible.builtin.group:
name: "{{ multica_group }}"
state: present
become: true
when: multica_manage_user | bool
# Deliberately not a system account: the daemon needs a real home for CLI
# config and agent CLI credentials, and systemd linger needs a real user.
- name: User | Create the daemon user
ansible.builtin.user:
name: "{{ multica_user }}"
group: "{{ multica_group }}"
groups: "{{ multica_user_extra_groups }}"
append: true
home: "{{ multica_user_home }}"
shell: "{{ multica_user_shell }}"
create_home: true
state: present
become: true
when: multica_manage_user | bool
- name: User | Look up the daemon user
ansible.builtin.getent:
database: passwd
key: "{{ multica_user }}"
become: true
- name: User | Record uid and the per-user runtime environment
ansible.builtin.set_fact:
_multica_uid: "{{ ansible_facts.getent_passwd[multica_user][1] }}"
# systemd user units — which is what `multica daemon autostart enable` writes on
# Linux — are unreachable over a plain `sudo -u` shell without these. Without
# XDG_RUNTIME_DIR, `systemctl --user` fails with "Failed to connect to bus".
- name: User | Build the daemon user's command environment
ansible.builtin.set_fact:
_multica_user_env:
HOME: "{{ multica_user_home }}"
USER: "{{ multica_user }}"
LOGNAME: "{{ multica_user }}"
XDG_RUNTIME_DIR: "/run/user/{{ _multica_uid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ _multica_uid }}/bus"
PATH: "{{ multica_npm_prefix }}/bin:{{ multica_user_home }}/.local/bin:{{ multica_install_dir }}:/usr/local/bin:/usr/bin:/bin"
- name: User | Ensure the user's local bin directory exists
ansible.builtin.file:
path: "{{ multica_user_home }}/.local/bin"
state: directory
owner: "{{ multica_user }}"
group: "{{ multica_group }}"
mode: "0755"
become: true
- name: User | Ensure the user's cache directory exists
ansible.builtin.file:
path: "{{ multica_user_home }}/.cache"
state: directory
owner: "{{ multica_user }}"
group: "{{ multica_group }}"
mode: "0700"
become: true
@@ -0,0 +1,120 @@
---
# Assertions, not hope. Each of these has failed in practice for a different
# reason, so each gets its own check and its own error message.
- name: Verify | Daemon reports running
ansible.builtin.command:
argv: "{{ _multica_cmd + ['daemon', 'status', '--output', 'json'] }}"
become: true
become_user: "{{ multica_user }}"
environment: "{{ _multica_user_env }}"
register: _multica_verify_daemon
until:
- _multica_verify_daemon.rc == 0
- (_multica_verify_daemon.stdout | from_json).status | default('') == 'running'
retries: "{{ multica_verify_retries }}"
delay: "{{ multica_verify_delay }}"
changed_when: false
check_mode: false
- name: Verify | Daemon is the pinned version and sees the configured workspaces root
ansible.builtin.assert:
that:
- (_multica_verify_daemon.stdout | from_json).cli_version == multica_version
- (_multica_verify_daemon.stdout | from_json).server_url | regex_replace('/$', '')
== multica_server_url | regex_replace('/$', '')
fail_msg: >-
Daemon is running but does not match declared state:
{{ _multica_verify_daemon.stdout }}
quiet: true
- name: Verify | Every selected agent CLI is registered as an online runtime
ansible.builtin.command:
argv: "{{ _multica_cmd + ['runtime', 'list', '--output', 'json'] }}"
become: true
become_user: "{{ multica_user }}"
environment: "{{ _multica_user_env }}"
register: _multica_verify_runtimes
until:
- _multica_verify_runtimes.rc == 0
- _multica_expected_providers
| difference(_multica_verify_runtimes.stdout | from_json
| selectattr('daemon_id', 'equalto', (_multica_verify_daemon.stdout | from_json).daemon_id)
| selectattr('status', 'equalto', 'online')
| map(attribute='provider') | list)
| length == 0
retries: "{{ multica_verify_retries }}"
delay: "{{ multica_verify_delay }}"
changed_when: false
check_mode: false
vars:
_multica_expected_providers: >-
{{ multica_agent_clis | map('extract', multica_agent_cli_catalog, 'provider') | list }}
- name: Verify | Runtimes name this host
ansible.builtin.assert:
that:
- _multica_verify_runtimes.stdout | from_json
| selectattr('daemon_id', 'equalto', (_multica_verify_daemon.stdout | from_json).daemon_id)
| rejectattr('device_info', 'search', '^' ~ (multica_device_name | regex_escape))
| list | length == 0
fail_msg: >-
Runtimes registered under this daemon do not report device_name
"{{ multica_device_name }}".
quiet: true
- name: Verify | Stat the workspaces root
ansible.builtin.stat:
path: "{{ multica_workspaces_root }}"
register: _multica_verify_ws
become: true
- name: Verify | Re-prove the daemon user can write to the workspaces root
ansible.builtin.command:
argv:
- /bin/sh
- -c
- 'd="$1"; f="$d/.multica-write-test.$$"; : > "$f" && rm -f "$f"'
- sh
- "{{ multica_workspaces_root }}"
become: true
become_user: "{{ multica_user }}"
register: _multica_verify_ws_write
changed_when: false
failed_when: false
check_mode: false
- name: Verify | Workspaces root assertions
ansible.builtin.assert:
that:
- _multica_verify_ws.stat.exists
- _multica_verify_ws.stat.isdir
- _multica_verify_ws.stat.pw_name == multica_user
- _multica_verify_ws_write.rc == 0
fail_msg: >-
{{ multica_workspaces_root }} is missing, not a directory, or not
owned/writable by {{ multica_user }}.
quiet: true
- name: Verify | Workspaces root is on the expected mount
ansible.builtin.assert:
that:
- multica_workspaces_root_mount in (ansible_facts['mounts'] | map(attribute='mount') | list)
fail_msg: "{{ multica_workspaces_root_mount }} is not mounted."
quiet: true
when: multica_workspaces_root_mount | length > 0
- name: Verify | Report what still needs a human
ansible.builtin.debug:
msg: >-
{{ multica_device_name }} is provisioned: multica {{ multica_version }},
daemon running, runtimes online for
{{ multica_agent_clis | join(', ') }}.
Remaining manual step(s):
{{ multica_agent_clis
| select('in', multica_agent_cli_catalog)
| map('extract', multica_agent_cli_catalog)
| selectattr('manual_login', 'defined')
| selectattr('manual_login')
| map(attribute='manual_login_hint')
| list | join('; ') | default('none', true) }}
@@ -0,0 +1,41 @@
---
# Guard against the mount not being present: without it the daemon happily
# writes run workspaces onto the root filesystem and nobody notices until it
# fills up.
- name: Workspaces root | Assert it lands on the expected mount
ansible.builtin.assert:
that:
- multica_workspaces_root_mount in (ansible_facts['mounts'] | map(attribute='mount') | list)
- multica_workspaces_root == multica_workspaces_root_mount
or multica_workspaces_root.startswith(multica_workspaces_root_mount.rstrip('/') ~ '/')
fail_msg: >-
multica_workspaces_root ({{ multica_workspaces_root }}) is not on
{{ multica_workspaces_root_mount }}, which is not currently mounted or
does not contain it. Mount it before provisioning.
quiet: true
when: multica_workspaces_root_mount | length > 0
- name: Workspaces root | Create the directory
ansible.builtin.file:
path: "{{ multica_workspaces_root }}"
state: directory
owner: "{{ multica_user }}"
group: "{{ multica_group }}"
mode: "{{ multica_workspaces_root_mode }}"
become: true
# Ownership is not writability: a read-only or root-squashed network mount
# passes the file module and fails the first run.
- name: Workspaces root | Prove the daemon user can write to it
ansible.builtin.command:
argv:
- /bin/sh
- -c
- 'd="$1"; f="$d/.multica-write-test.$$"; : > "$f" && rm -f "$f"'
- sh
- "{{ multica_workspaces_root }}"
become: true
become_user: "{{ multica_user }}"
changed_when: false
register: _multica_ws_write
failed_when: _multica_ws_write.rc != 0