Matrix Docker Ansible eploy
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 

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