Matrix Docker Ansible eploy
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

232 regels
12 KiB

  1. # SPDX-FileCopyrightText: 2026 Slavi Pantaleev
  2. #
  3. # SPDX-License-Identifier: AGPL-3.0-or-later
  4. ---
  5. - name: Verify matrix-bot-baibot
  6. hosts: all
  7. become: true
  8. vars_files:
  9. - "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/vars.yml"
  10. - "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/../../../molecule-shared/playbook-context.yml"
  11. gather_facts: false
  12. tasks:
  13. # Read from the role's own defaults rather than pinned in molecule.yml, so the version
  14. # assertion compares the running image against what defaults/main.yml ships.
  15. # Pinning it here would make that assertion compare the scenario with itself.
  16. - name: Load the role's defaults under a separate name
  17. ansible.builtin.include_vars:
  18. file: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/main.yml"
  19. name: matrix_bot_baibot_role_defaults
  20. - name: Wait for the matrix-bot-baibot service to become active
  21. ansible.builtin.systemd_service:
  22. name: matrix-bot-baibot.service
  23. register: matrix_bot_baibot_service
  24. until: matrix_bot_baibot_service.status.ActiveState == 'active'
  25. retries: 30
  26. delay: 5
  27. failed_when: false
  28. # `Restart=always` means a crash-looping container still reports `active`, so the restart
  29. # counter is checked too. Asserted `is defined` because `| int` turns a missing property
  30. # into 0 and would pass vacuously.
  31. - name: Assert the service is active and has not been restarting
  32. ansible.builtin.assert:
  33. that:
  34. - matrix_bot_baibot_service.status.ActiveState == 'active'
  35. - matrix_bot_baibot_service.status.NRestarts is defined
  36. - matrix_bot_baibot_service.status.NRestarts | int == 0
  37. fail_msg: >-
  38. matrix-bot-baibot.service is
  39. {{ matrix_bot_baibot_service.status.ActiveState | default('unknown') }}
  40. after {{ matrix_bot_baibot_service.status.NRestarts | default('?') }}
  41. automatic restart(s)
  42. success_msg: "matrix-bot-baibot.service is active and has not restarted"
  43. # baibot is a Matrix client, not a server, so what it says about itself has to come from
  44. # its output. The unit runs `docker start --attach`, so `--log-driver=none` does not stop
  45. # the journal from carrying it.
  46. #
  47. # `Syncing..` is what carries this scenario, not the unit check above. baibot does not exit
  48. # when startup goes wrong: a profile step it cannot complete is retried forever with a
  49. # growing delay, so the unit stays `active` with `NRestarts` at 0 while the bot never
  50. # reaches its message loop. Point the avatar at a missing file and the assertion above
  51. # still passes; this one does not.
  52. - name: Wait for baibot to reach its sync loop
  53. ansible.builtin.shell:
  54. cmd: >-
  55. set -o pipefail && journalctl -u matrix-bot-baibot.service --no-pager -o cat
  56. | sed -e 's/\x1b\[[0-9;]*m//g'
  57. executable: /bin/bash
  58. register: matrix_bot_baibot_journal
  59. changed_when: false
  60. until: "'Syncing..' in matrix_bot_baibot_journal.stdout"
  61. retries: 24
  62. delay: 5
  63. failed_when: false
  64. - name: Assert baibot got past startup and into its sync loop
  65. ansible.builtin.assert:
  66. that:
  67. - "'Syncing..' in matrix_bot_baibot_journal.stdout"
  68. - "'Failed to prepare profile' not in matrix_bot_baibot_journal.stdout"
  69. fail_msg: >-
  70. baibot never reached its sync loop; it is still in startup or stuck
  71. retrying profile setup
  72. success_msg: "baibot got past startup and is syncing"
  73. # The scenario's display name is neither the role's default nor what the stub reports the
  74. # account already has, so the bot wanting it can only have come from what the role rendered.
  75. - name: Assert the display name the role configured reached the process
  76. ansible.builtin.assert:
  77. that:
  78. - >-
  79. 'desired_display_name="' ~ matrix_bot_baibot_config_user_name ~ '"'
  80. in matrix_bot_baibot_journal.stdout
  81. fail_msg: >-
  82. baibot did not report {{ matrix_bot_baibot_config_user_name }} as the
  83. display name it wants, so `user.name` did not reach the process
  84. success_msg: "baibot acts on the display name the role configured"
  85. # `logging` is one string carrying per-target levels, so proving it arrived means proving
  86. # different targets ended up at different levels. A single global level satisfies neither half.
  87. #
  88. # First clause: baibot's own records appear at DEBUG, which the role's default of `info`
  89. # would not produce.
  90. #
  91. # Second clause is the control, and it is not vacuous. At DEBUG the crates underneath are
  92. # extremely talkative, so raising the catch-all turns these two records into roughly a
  93. # hundred. Their silence is the `warn` catch-all being enforced.
  94. #
  95. # The trap here: a control on mxlink was tried first, and mxlink emits no DEBUG records at
  96. # all on a first run - so asserting their absence passed just as happily with mxlink set
  97. # to `debug`.
  98. - name: Assert the per-target logging levels reached the process
  99. ansible.builtin.assert:
  100. that:
  101. - matrix_bot_baibot_debug_lines | select('search', 'baibot::') | list | length > 0
  102. - matrix_bot_baibot_debug_lines | reject('search', 'baibot::') | list | length == 0
  103. fail_msg: >-
  104. The rendered `logging` string did not take effect:
  105. {{ matrix_bot_baibot_debug_lines | length }} DEBUG record(s), of which
  106. {{ matrix_bot_baibot_debug_lines | select('search', 'baibot::') | list | length }}
  107. from baibot itself
  108. success_msg: >-
  109. baibot logs at DEBUG while everything under it stays at the catch-all
  110. level, as the rendered `logging` string asks
  111. vars:
  112. matrix_bot_baibot_debug_lines: >-
  113. {{ matrix_bot_baibot_journal.stdout_lines | select('search', ' DEBUG ') | list }}
  114. - name: Read the configuration file the role rendered
  115. ansible.builtin.slurp:
  116. src: "{{ matrix_bot_baibot_config_path }}/config.yml"
  117. register: matrix_bot_baibot_config_file
  118. - name: Assert the rendered configuration carries this scenario's Matrix settings
  119. ansible.builtin.assert:
  120. that:
  121. - matrix_bot_baibot_config.homeserver.server_name == matrix_domain
  122. - matrix_bot_baibot_config.homeserver.url == matrix_bot_baibot_config_homeserver_url
  123. - matrix_bot_baibot_config.user.mxid_localpart == matrix_bot_baibot_config_user_mxid_localpart
  124. - matrix_bot_baibot_config.user.name == matrix_bot_baibot_config_user_name
  125. - matrix_bot_baibot_config.command_prefix == matrix_bot_baibot_config_command_prefix
  126. - matrix_bot_baibot_config.room.post_join_self_introduction_enabled is false
  127. - matrix_bot_baibot_config.access.admin_patterns == matrix_bot_baibot_config_access_admin_patterns
  128. - matrix_bot_baibot_config.initial_global_config.user_patterns == ['@*:' ~ matrix_domain]
  129. fail_msg: "The rendered configuration does not carry the scenario's Matrix settings"
  130. success_msg: "The rendered configuration carries the scenario's Matrix settings"
  131. vars:
  132. matrix_bot_baibot_config: "{{ matrix_bot_baibot_config_file.content | b64decode | from_yaml }}"
  133. # The role refuses a configuration that sets both authentication modes. This scenario uses
  134. # password mode, so the access-token keys must render as nulls, not be omitted or set.
  135. - name: Assert only the password authentication mode is rendered
  136. ansible.builtin.assert:
  137. that:
  138. - matrix_bot_baibot_config.user.password == matrix_bot_baibot_config_user_password
  139. - matrix_bot_baibot_config.user.access_token is none
  140. - matrix_bot_baibot_config.user.device_id is none
  141. fail_msg: "The rendered configuration does not use password authentication exclusively"
  142. success_msg: "The rendered configuration uses password authentication exclusively"
  143. vars:
  144. matrix_bot_baibot_config: "{{ matrix_bot_baibot_config_file.content | b64decode | from_yaml }}"
  145. # The agent presets are the most involved templating in this role: a per-provider template
  146. # rendered to YAML, parsed, merged with an extension, nested into the list. Asserted as a
  147. # whole round trip, key by key.
  148. #
  149. # No provider is ever contacted. baibot calls one only when a message asks an agent to do
  150. # something, and the base URL here resolves nowhere on purpose.
  151. - name: Assert the statically-defined agent survived the provider templating
  152. ansible.builtin.assert:
  153. that:
  154. - matrix_bot_baibot_agents | length == 1
  155. - matrix_bot_baibot_agent.id == matrix_bot_baibot_config_agents_static_definitions_anthropic_id
  156. - matrix_bot_baibot_agent.provider == 'anthropic'
  157. - matrix_bot_baibot_agent.config.base_url == matrix_bot_baibot_config_agents_static_definitions_anthropic_config_base_url
  158. - matrix_bot_baibot_agent.config.api_key == matrix_bot_baibot_config_agents_static_definitions_anthropic_config_api_key
  159. - matrix_bot_baibot_agent.config.text_generation.model_id == matrix_bot_baibot_config_agents_static_definitions_anthropic_config_text_generation_model_id
  160. - matrix_bot_baibot_agent.config.text_generation.temperature == matrix_bot_baibot_config_agents_static_definitions_anthropic_config_text_generation_temperature
  161. - matrix_bot_baibot_agent.config.text_generation.max_response_tokens == matrix_bot_baibot_config_agents_static_definitions_anthropic_config_text_generation_max_response_tokens
  162. - matrix_bot_baibot_agent.config.text_generation.max_context_tokens == matrix_bot_baibot_config_agents_static_definitions_anthropic_config_text_generation_max_context_tokens
  163. fail_msg: >-
  164. The statically-defined agent is not what the role's preset variables ask
  165. for: {{ matrix_bot_baibot_agents }}
  166. success_msg: "The statically-defined agent carries the scenario's provider settings"
  167. vars:
  168. matrix_bot_baibot_agents: "{{ (matrix_bot_baibot_config_file.content | b64decode | from_yaml).agents.static_definitions }}"
  169. matrix_bot_baibot_agent: "{{ matrix_bot_baibot_agents | first }}"
  170. - name: Read the container's runtime configuration
  171. ansible.builtin.command:
  172. argv:
  173. - docker
  174. - container
  175. - inspect
  176. - matrix-bot-baibot
  177. - --format
  178. - "{{ '{{' }} .Config.Image {{ '}}' }} {{ '{{' }} .Config.User {{ '}}' }}"
  179. register: matrix_bot_baibot_container
  180. changed_when: false
  181. - name: Assert the image carries the version defaults/main.yml pins
  182. ansible.builtin.assert:
  183. that:
  184. - matrix_bot_baibot_role_defaults.matrix_bot_baibot_version in matrix_bot_baibot_container.stdout
  185. fail_msg: >-
  186. The running container is {{ matrix_bot_baibot_container.stdout }},
  187. which does not carry the pinned version
  188. {{ matrix_bot_baibot_role_defaults.matrix_bot_baibot_version }}
  189. success_msg: "The running container is the version defaults/main.yml pins"
  190. # The uid/gid come from outside the role and are deliberately not 1000, which the base
  191. # image already uses, so this cannot pass by coinciding with the image's own user.
  192. - name: Assert the container runs as the identity the playbook supplies
  193. ansible.builtin.assert:
  194. that:
  195. - "matrix_user_uid ~ ':' ~ matrix_user_gid in matrix_bot_baibot_container.stdout"
  196. fail_msg: >-
  197. The container does not run as {{ matrix_user_uid }}:{{ matrix_user_gid }}
  198. ({{ matrix_bot_baibot_container.stdout }})
  199. success_msg: "The container runs as the uid/gid the playbook supplies"
  200. # baibot keeps its session and crypto store here. The file existing proves the bind mount
  201. # is writable by the user the container runs as.
  202. - name: Stat the session file baibot persists
  203. ansible.builtin.stat:
  204. path: "{{ matrix_bot_baibot_data_path }}/session.json"
  205. register: matrix_bot_baibot_session_file
  206. - name: Assert baibot persisted its session as the matrix user
  207. ansible.builtin.assert:
  208. that:
  209. - matrix_bot_baibot_session_file.stat.exists
  210. - matrix_bot_baibot_session_file.stat.uid | int == matrix_user_uid | int
  211. fail_msg: >-
  212. {{ matrix_bot_baibot_data_path }}/session.json is missing or not owned
  213. by uid {{ matrix_user_uid }}
  214. success_msg: "baibot persisted its session into the data path as the matrix user"