From b699bdfde802484a8cfe1198071e1473fb6f68d2 Mon Sep 17 00:00:00 2001 From: Mathieu Garcia Date: Tue, 13 Jan 2026 09:13:19 +0100 Subject: [PATCH 01/10] feat: Add support for docker buildx --- ansible/playbooks/saas/image.yml | 5 +- ansible/playbooks/saas/setup-buildx.yml | 65 +++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 ansible/playbooks/saas/setup-buildx.yml diff --git a/ansible/playbooks/saas/image.yml b/ansible/playbooks/saas/image.yml index cc8bb94..da6da51 100644 --- a/ansible/playbooks/saas/image.yml +++ b/ansible/playbooks/saas/image.yml @@ -45,13 +45,16 @@ - name: Build when: image_definition.build block: - - name: Build and publish image + - name: Build and publish multi-arch image community.docker.docker_image_build: name: "{{ docker_private_registry.url }}/{% if docker_private_registry.project is defined %}{{ docker_private_registry.project }}/{% endif %}{{ image_definition.name }}:{{ image_version }}" tag: latest path: "{{ build_work_dir }}" dockerfile: Dockerfile labels: "{{ image_definition.labels }}" + platform: + - linux/amd64 + - linux/arm64 rebuild: always outputs: - type: image diff --git a/ansible/playbooks/saas/setup-buildx.yml b/ansible/playbooks/saas/setup-buildx.yml new file mode 100644 index 0000000..3f78dcc --- /dev/null +++ b/ansible/playbooks/saas/setup-buildx.yml @@ -0,0 +1,65 @@ +--- +- name: Setup Docker Buildx for multi-architecture builds + hosts: "{{ hosts_limit | default('infrastructure') }}" + become: true + gather_facts: true + + tasks: + - name: Ensure Docker is installed and running + ansible.builtin.service: + name: docker + state: started + enabled: true + + - name: Install QEMU user static binaries + ansible.builtin.package: + name: + - qemu-user-static + - binfmt-support + state: present + + - name: Register QEMU binfmt handlers + ansible.builtin.command: + cmd: docker run --rm --privileged tonistiigi/binfmt --install all + register: binfmt_result + changed_when: "'installing' in binfmt_result.stdout" + + - name: Check if buildx builder exists + ansible.builtin.command: + cmd: docker buildx inspect multiarch-builder + register: buildx_inspect + failed_when: false + changed_when: false + + - name: Create buildx builder for multi-arch + ansible.builtin.command: + cmd: docker buildx create --name multiarch-builder --driver docker-container --use --bootstrap + when: buildx_inspect.rc != 0 + register: buildx_create + + - name: Set multiarch-builder as default + ansible.builtin.command: + cmd: docker buildx use multiarch-builder + when: buildx_inspect.rc == 0 + changed_when: false + + - name: Verify buildx platforms support + ansible.builtin.command: + cmd: docker buildx inspect --bootstrap + register: buildx_platforms + changed_when: false + + - name: Display supported platforms + ansible.builtin.debug: + msg: "{{ buildx_platforms.stdout_lines }}" + + - name: Verify multi-arch support is working + ansible.builtin.command: + cmd: docker buildx ls + register: buildx_ls + changed_when: false + + - name: Show buildx configuration + ansible.builtin.debug: + var: buildx_ls.stdout_lines + From 323b7c6a7a3273639a4717d9e670a6a189ce5c95 Mon Sep 17 00:00:00 2001 From: Mathieu Garcia Date: Wed, 14 Jan 2026 15:10:07 +0100 Subject: [PATCH 02/10] fix: Reorganize playbooks for rulebook usage --- ansible/playbooks/paas/main.yml | 4 +++ ansible/playbooks/paas/metrology.yml | 6 ++-- .../{saas => paas}/nomad-clean-errors.yml | 0 ansible/playbooks/paas/nvidia.yml | 4 +++ ansible/playbooks/paas/scan_exporter.yml | 4 +-- ansible/playbooks/paas/sshd.yml | 4 +++ ansible/playbooks/paas/timesyncd.yml | 2 ++ ansible/playbooks/saas/image-forkable.yml | 12 +++---- ansible/rulebook.yml | 34 ++++--------------- 9 files changed, 32 insertions(+), 38 deletions(-) rename ansible/playbooks/{saas => paas}/nomad-clean-errors.yml (100%) diff --git a/ansible/playbooks/paas/main.yml b/ansible/playbooks/paas/main.yml index 4a7f7c1..dd1a0f0 100644 --- a/ansible/playbooks/paas/main.yml +++ b/ansible/playbooks/paas/main.yml @@ -58,10 +58,14 @@ - unattended-upgrades - ansible-ufw +- name: Configure sshd + ansible.builtin.import_playbook: sshd.yml - name: Configure timesyncd ansible.builtin.import_playbook: timesyncd.yml - name: Configure systemd resolved ansible.builtin.import_playbook: systemd-resolved.yml +- name: Configure nvidia + ansible.builtin.import_playbook: nvidia.yml - name: Configure docker ansible.builtin.import_playbook: docker.yml - name: Configure nomad diff --git a/ansible/playbooks/paas/metrology.yml b/ansible/playbooks/paas/metrology.yml index 78b63ab..8554255 100644 --- a/ansible/playbooks/paas/metrology.yml +++ b/ansible/playbooks/paas/metrology.yml @@ -5,9 +5,9 @@ gather_facts: true become: true tasks: - - name: End the play for hosts that are not in admins group - ansible.builtin.meta: end_host - when: fact_instance.location != 'admins' + # - name: End the play for hosts that are not in admins group + # ansible.builtin.meta: end_host + # when: fact_instance.location != 'admins' - name: Install prometheus ansible.builtin.import_role: diff --git a/ansible/playbooks/saas/nomad-clean-errors.yml b/ansible/playbooks/paas/nomad-clean-errors.yml similarity index 100% rename from ansible/playbooks/saas/nomad-clean-errors.yml rename to ansible/playbooks/paas/nomad-clean-errors.yml diff --git a/ansible/playbooks/paas/nvidia.yml b/ansible/playbooks/paas/nvidia.yml index d8f2314..81aae17 100644 --- a/ansible/playbooks/paas/nvidia.yml +++ b/ansible/playbooks/paas/nvidia.yml @@ -18,6 +18,10 @@ pre_tasks: + - name: End the play for hosts that don't have nvidia gpu + ansible.builtin.meta: end_host + when: not nvidia_enable + - name: Créer le répertoire du keyring s'il n'existe pas ansible.builtin.file: path: "{{ nvidia_keyring_path | dirname }}" diff --git a/ansible/playbooks/paas/scan_exporter.yml b/ansible/playbooks/paas/scan_exporter.yml index cfb1171..d199cf3 100644 --- a/ansible/playbooks/paas/scan_exporter.yml +++ b/ansible/playbooks/paas/scan_exporter.yml @@ -1,10 +1,10 @@ --- -- name: Uninstall scan_exporter +- name: Install scan_exporter any_errors_fatal: true hosts: "{{ hosts_limit | default('infrastructure') }}" gather_facts: true become: true pre_tasks: - - name: Uninstall scan_exporter + - name: Install scan_exporter ansible.builtin.include_role: name: scan_exporter \ No newline at end of file diff --git a/ansible/playbooks/paas/sshd.yml b/ansible/playbooks/paas/sshd.yml index 0ecbd44..2472802 100644 --- a/ansible/playbooks/paas/sshd.yml +++ b/ansible/playbooks/paas/sshd.yml @@ -4,5 +4,9 @@ hosts: "{{ hosts_limit | default('infrastructure') }}" gather_facts: true become: true + pre_tasks: + - name: End the play for hosts that are not in frontends group + ansible.builtin.meta: end_host + when: fact_instance.location != 'frontends' roles: - sshd diff --git a/ansible/playbooks/paas/timesyncd.yml b/ansible/playbooks/paas/timesyncd.yml index e96b42a..9fe1055 100644 --- a/ansible/playbooks/paas/timesyncd.yml +++ b/ansible/playbooks/paas/timesyncd.yml @@ -27,6 +27,8 @@ - name: Use RTC in UTC ansible.builtin.command: timedatectl set-local-rtc 0 + register: timedatectl + changed_when: false handlers: - name: Restart timesyncd diff --git a/ansible/playbooks/saas/image-forkable.yml b/ansible/playbooks/saas/image-forkable.yml index 9cbdd56..13b6ae6 100644 --- a/ansible/playbooks/saas/image-forkable.yml +++ b/ansible/playbooks/saas/image-forkable.yml @@ -93,15 +93,15 @@ failed_when: ui_update.status != 200 become: false + post_tasks: + - name: Trigger cleanup on failure + ansible.builtin.meta: clear_host_errors + when: ansible_failed_result is defined + notify: Cleanup build directory + handlers: - name: Cleanup build directory ansible.builtin.file: path: "{{ build_work_dir }}" state: absent listen: cleanup_build - - post_tasks: - - name: Trigger cleanup on failure - ansible.builtin.meta: clear_host_errors - when: ansible_failed_result is defined - notify: Cleanup build directory diff --git a/ansible/rulebook.yml b/ansible/rulebook.yml index 61aa3a4..205b6d2 100644 --- a/ansible/rulebook.yml +++ b/ansible/rulebook.yml @@ -28,39 +28,19 @@ extra_vars: hosts_limit: "{{ event.payload.meta.hosts }}" - - name: paas - condition: event.payload.type == "paas/main" + - name: paas-main + condition: event.payload.type == "paas-main" actions: - run_playbook: name: playbooks/paas/main.yml extra_vars: hosts_limit: "{{ event.payload.meta.hosts }}" + + - name: paas-nomad-clean-errors + condition: event.payload.type == "paas-nomad-clean-errors" + actions: - run_playbook: - name: playbooks/paas/timesyncd.yml - extra_vars: - hosts_limit: "{{ event.payload.meta.hosts }}" - - run_playbook: - name: playbooks/paas/firewall.yml - extra_vars: - hosts_limit: "{{ event.payload.meta.hosts }}" - - run_playbook: - name: playbooks/paas/docker.yml - extra_vars: - hosts_limit: "{{ event.payload.meta.hosts }}" - - run_playbook: - name: playbooks/paas/nomad.yml - extra_vars: - hosts_limit: "{{ event.payload.meta.hosts }}" - - run_playbook: - name: playbooks/paas/coredns.yml - extra_vars: - hosts_limit: "{{ event.payload.meta.hosts }}" - - run_playbook: - name: playbooks/paas/metrology.yml - extra_vars: - hosts_limit: "{{ event.payload.meta.hosts }}" - - run_playbook: - name: playbooks/paas/sshd.yml + name: playbooks/paas/nomad-clean-errors.yml extra_vars: hosts_limit: "{{ event.payload.meta.hosts }}" From 272d4176ae97fe316734d1ca4f048804ad0ab7ac Mon Sep 17 00:00:00 2001 From: Mathieu Garcia Date: Wed, 14 Jan 2026 15:11:33 +0100 Subject: [PATCH 03/10] fix(grafana): add default filter to plugin template --- .../playbooks/saas/roles/grafana/README.md | 41 +++++++++++++++++++ .../provisioning/plugins/llm.yaml.j2 | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/ansible/playbooks/saas/roles/grafana/README.md b/ansible/playbooks/saas/roles/grafana/README.md index b3a0ae2..bc2400e 100644 --- a/ansible/playbooks/saas/roles/grafana/README.md +++ b/ansible/playbooks/saas/roles/grafana/README.md @@ -12,3 +12,44 @@ www.domain.com: domain_alias: domain.com # (string) Primary domain name for the application. ipfilter: [] # (list) List of allowed IPs for access control (empty for unrestricted access). basic_auth: False # (bool) Enable/disable HTTP Basic Authentication (True/False). +``` + +## variable + +```yaml +passwd: s3cret! +user: myuser +``` + +## Secret + +```yaml +plugins: + - disabled: false + jsonData: + models: + default: base + mapping: + base: gpt-oss-120b + large: gpt-oss-120b + openAI: + apiPath: /api/openai_compat/v1 + url: https://llm.public.api + provider: custom + vector: + embed: + grafanaVectorAPI: + authType: no-auth + url: http://vectorStore.default.service.nomad:8687 + type: grafana/vectorapi + enabled: true + model: BAAI/bge-small-en-v1.5 + store: + grafanaVectorAPI: + authType: no-auth + url: http://vectorStore.default.service.nomad:8687 + type: grafana/vectorapi + secureJsonData: + openAIKey: + type: grafana-llm-app +``` \ No newline at end of file diff --git a/ansible/playbooks/saas/roles/grafana/templates/provisioning/plugins/llm.yaml.j2 b/ansible/playbooks/saas/roles/grafana/templates/provisioning/plugins/llm.yaml.j2 index 2d5cdd9..fc1d019 100644 --- a/ansible/playbooks/saas/roles/grafana/templates/provisioning/plugins/llm.yaml.j2 +++ b/ansible/playbooks/saas/roles/grafana/templates/provisioning/plugins/llm.yaml.j2 @@ -1,4 +1,4 @@ apiVersion: 1 apps: -{{ (lookup('simple-stack-ui', type='secret', key=domain, subkey='plugins', missing='error') | from_json) | to_nice_yaml }} \ No newline at end of file +{{ (lookup('simple-stack-ui', type='secret', key=domain, subkey='plugins', missing='error') | from_json) | to_nice_yaml | default() }} \ No newline at end of file From 993839cedc988cf6bbe550d56a79d7451c8a6824 Mon Sep 17 00:00:00 2001 From: Mathieu Garcia Date: Wed, 14 Jan 2026 15:12:11 +0100 Subject: [PATCH 04/10] docs(ansible): add variable and secret sections to role READMEs --- .../playbooks/saas/roles/litellm/README.md | 52 +++++++++++++++++++ ansible/playbooks/saas/roles/milvus/README.md | 7 +++ .../playbooks/saas/roles/postgresql/README.md | 14 +++++ 3 files changed, 73 insertions(+) diff --git a/ansible/playbooks/saas/roles/litellm/README.md b/ansible/playbooks/saas/roles/litellm/README.md index dcb700f..32ff3ce 100644 --- a/ansible/playbooks/saas/roles/litellm/README.md +++ b/ansible/playbooks/saas/roles/litellm/README.md @@ -1 +1,53 @@ # Role: `litellm` + +## variable + +```yaml +litellm_dbhost: postgresql +litellm_config: + general_settings: + store_model_in_db: true + supported_db_objects: + - mcp + litellm_settings: + drop_params: true + mcp_servers: + news_mcp: + description: My MCP description + transport: http + url: http://192.168.0.46:8001/mcp + model_list: + - litellm_params: + api_key: os.environ/OVHCLOUD_API_KEY + model: ovhcloud/gpt-oss-120b + model_name: ovhcloud/gpt-oss-120b + - litellm_params: + api_key: os.environ/OVHCLOUD_API_KEY + model: ovhcloud/bge-multilingual-gemma2 + model_name: ovhcloud/bge-multilingual-gemma2 + - litellm_params: + api_key: os.environ/OVHCLOUD_API_KEY + model: ovhcloud/Deepseek-R1-Distill-Llama-70B + model_name: ovhcloud/Deepseek-R1-Distill-Llama-70B + - litellm_params: + api_key: os.environ/OVHCLOUD_API_KEY + model: ovhcloud/BGE-M3 + model_name: ovhcloud/BGE-M3 +``` + +## Secret + +```yaml +litellm_dbpasswd: 123456 +litellm_env: + - key: LITELLM_MASTER_KEY + value: sk-123456789 + - key: LITELLM_SALT_KEY + value: sk-12345678-123456789-12345678 + - key: DATABASE_URL + value: postgresql://user:passwd@postgresql.default.service.nomad:5432/litellm + - key: STORE_MODEL_IN_DB + value: true + - key: OVHCLOUD_API_KEY + value: APIKEY +``` \ No newline at end of file diff --git a/ansible/playbooks/saas/roles/milvus/README.md b/ansible/playbooks/saas/roles/milvus/README.md index 680a432..370e59b 100644 --- a/ansible/playbooks/saas/roles/milvus/README.md +++ b/ansible/playbooks/saas/roles/milvus/README.md @@ -1 +1,8 @@ # Role: `milvus` + + +## Secret + +```yaml +passwd: 123456 +``` \ No newline at end of file diff --git a/ansible/playbooks/saas/roles/postgresql/README.md b/ansible/playbooks/saas/roles/postgresql/README.md index 823c28b..85c6ec6 100644 --- a/ansible/playbooks/saas/roles/postgresql/README.md +++ b/ansible/playbooks/saas/roles/postgresql/README.md @@ -13,3 +13,17 @@ www.domain.com: ipfilter: [] # (list) List of allowed IPs for access control (empty for unrestricted access). basic_auth: False # (bool) Enable/disable HTTP Basic Authentication (True/False). size: small +``` + +## variable + +```yaml +static_port: 5432 +``` + +## Secret + +```yaml +passwd: 123456 +``` + From 03c8416f7382f1472575a29a361246c938abee0c Mon Sep 17 00:00:00 2001 From: Mathieu Garcia Date: Wed, 14 Jan 2026 15:12:27 +0100 Subject: [PATCH 05/10] build(wordpress): bump php version to 8.4 --- ansible/playbooks/saas/roles/wordpress/vars/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ansible/playbooks/saas/roles/wordpress/vars/main.yml b/ansible/playbooks/saas/roles/wordpress/vars/main.yml index 3aa5fbe..454938e 100644 --- a/ansible/playbooks/saas/roles/wordpress/vars/main.yml +++ b/ansible/playbooks/saas/roles/wordpress/vars/main.yml @@ -5,10 +5,10 @@ image: upstream: source: apk repository: community - package: php83-fpm + package: php84-fpm labels: - version: 83 - conf: /etc/php83 + version: 84 + conf: /etc/php84 name: wordpress origin: alpine:latest dependances: From 7f86e1fdd9866149a34b9d537da04cebc47097c3 Mon Sep 17 00:00:00 2001 From: Mathieu Garcia Date: Wed, 14 Jan 2026 15:18:17 +0100 Subject: [PATCH 06/10] feat(infra): add execute endpoint, UI actions, and schema for infrastructure playbooks Add a new API route `infrastructures_execute/{id}` and corresponding schema action to trigger playbook execution using stored settings. --- ui/controllers/api.js | 1 + ui/public/forms/infrastructures.html | 103 ++++++++++++++++----------- ui/schemas/infrastructures.js | 63 ++++++++++++++-- ui/schemas/inventory.js | 24 +++++++ 4 files changed, 141 insertions(+), 50 deletions(-) diff --git a/ui/controllers/api.js b/ui/controllers/api.js index b0d3763..6067a48 100644 --- a/ui/controllers/api.js +++ b/ui/controllers/api.js @@ -36,6 +36,7 @@ exports.install = function() { ROUTE('+API /api/ +infrastructures_create --> Infrastructures/create'); ROUTE('+API /api/ +infrastructures_update/{id} --> Infrastructures/update'); ROUTE('+API /api/ +infrastructures_remove/{id} --> Infrastructures/remove'); + ROUTE('+API /api/ +infrastructures_execute/{id} --> Infrastructures/execute'); ROUTE('+POST /api/tfstates/{id}/ --> Infrastructures/tfstates_update'); ROUTE('+GET /api/tfstates/{id}/ --> Infrastructures/tfstates_read'); diff --git a/ui/public/forms/infrastructures.html b/ui/public/forms/infrastructures.html index 9865305..a08c350 100644 --- a/ui/public/forms/infrastructures.html +++ b/ui/public/forms/infrastructures.html @@ -18,40 +18,21 @@