diff --git a/llm-server/llm/ansible/README.md b/llm-server/llm/ansible/README.md index b49908b..599e176 100644 --- a/llm-server/llm/ansible/README.md +++ b/llm-server/llm/ansible/README.md @@ -32,15 +32,22 @@ cd llm-server/llm/ansible ## Usage (Testing) +All commands use the operator deploy key +`~/.ssh/id_ed25519_ghostnet_ai_ansible_deploy` (see AGENTS.md; no ssh-agent +required): + ```bash # Preview only – no changes, no downloads, no service start: -../../../.venv/bin/ansible-playbook -i inventories/testing playbooks/ai.yml --check --diff +../../../.venv/bin/ansible-playbook -i inventories/testing playbooks/ai.yml \ + --check --diff --private-key ~/.ssh/id_ed25519_ghostnet_ai_ansible_deploy # Provision model weights only (tag models): -../../../.venv/bin/ansible-playbook -i inventories/testing playbooks/ai.yml --tags models +../../../.venv/bin/ansible-playbook -i inventories/testing playbooks/ai.yml \ + --tags models --private-key ~/.ssh/id_ed25519_ghostnet_ai_ansible_deploy # Render quadlets and start/verify the selected profile (tag service): -../../../.venv/bin/ansible-playbook -i inventories/testing playbooks/ai.yml --tags service +../../../.venv/bin/ansible-playbook -i inventories/testing playbooks/ai.yml \ + --tags service --private-key ~/.ssh/id_ed25519_ghostnet_ai_ansible_deploy ``` `inference_engine_active_profile` selects which rendered quadlet is started; diff --git a/llm-server/llm/ansible/roles/inference_engine/tasks/main.yml b/llm-server/llm/ansible/roles/inference_engine/tasks/main.yml index 43c8532..165cb00 100644 --- a/llm-server/llm/ansible/roles/inference_engine/tasks/main.yml +++ b/llm-server/llm/ansible/roles/inference_engine/tasks/main.yml @@ -48,12 +48,14 @@ - name: Verify the rootless runtime utilities are executable ansible.builtin.assert: that: - - item.stat.exists - - item.stat.executable - loop: "{{ inference_engine_tools.results }}" + - inference_engine_tools.results | selectattr('stat.exists') | list | length + == inference_engine_tools.results | length + - inference_engine_tools.results | selectattr('stat.executable', 'equalto', true) | list | length + == inference_engine_tools.results | length fail_msg: >- - {{ item.item }} is missing or not executable; run the host baseline - roles first (llm-server/host/ansible). + A required rootless runtime utility is missing or not executable + (runuser, podman, systemctl or the Podman system generator); run the + host baseline roles first (llm-server/host/ansible). tags: - service @@ -68,6 +70,9 @@ ansible.builtin.find: paths: /dev/dri patterns: renderD* + # Render nodes are character devices; the default file_type (file) would + # skip them. + file_type: any register: inference_engine_render_node_find tags: - service @@ -105,11 +110,17 @@ - name: Verify every profile model artifact is present ansible.builtin.assert: that: - - item.stat.exists - loop: "{{ inference_engine_models.results }}" + - inference_engine_models.results | selectattr('stat.exists', 'equalto', true) | list | length + == inference_engine_models.results | length fail_msg: >- - Model artifact for {{ item.item.key }} is missing under - {{ model_store_root }}; run the model_store role (--tags models) first. + Missing model artifacts under {{ model_store_root }}: + {{ inference_engine_models.results + | selectattr('stat.exists', 'equalto', false) + | map(attribute='item.key') | list | join(', ') }}; + run the model_store role (--tags models) first. + # Check mode deliberately downloads nothing, so the preview continues and + # only shows the pending quadlet changes; a real run enforces presence. + when: not ansible_check_mode tags: - service diff --git a/llm-server/llm/ansible/roles/model_store/tasks/main.yml b/llm-server/llm/ansible/roles/model_store/tasks/main.yml index f906682..de2185f 100644 --- a/llm-server/llm/ansible/roles/model_store/tasks/main.yml +++ b/llm-server/llm/ansible/roles/model_store/tasks/main.yml @@ -80,7 +80,9 @@ - /bin/df - -m - --output=avail - - "{{ model_store_root }}" + # dirname is used because the store root itself may not exist yet in + # check mode; it lives on the same filesystem as the store. + - "{{ model_store_root | dirname }}" changed_when: false check_mode: false register: model_store_free_space @@ -151,14 +153,14 @@ - name: Enforce model artifact ownership and permissions ansible.builtin.file: - path: "{{ model_store_root }}/{{ item.directory }}/{{ item.file }}" + path: "{{ model_store_root }}/{{ item.item.directory }}/{{ item.item.file }}" state: file owner: root group: root mode: "{{ model_store_file_mode }}" - loop: "{{ model_store_all_artifacts }}" + loop: "{{ model_store_present.results | selectattr('stat.exists', 'equalto', true) | list }}" loop_control: - label: "{{ item.profile }}" + label: "{{ item.item.profile }}" tags: - models - service