2026-SEP-09

This commit is contained in:
il
2026-09-09 23:13:28 +09:00
parent 9226043696
commit 99536c5355
34 changed files with 2779 additions and 37 deletions
+122 -13
View File
@@ -160,6 +160,84 @@ services:
http: "3000" # app http: "3000" # app
dependency: dependency:
web: "app-public" web: "app-public"
openwebui:
domain:
public: "openwebui"
internal: "openwebui.app"
ports:
http: "8002"
dependency:
web: "app-public"
db: "postgresql"
llama:
ports:
http: "10000"
dependency:
models:
default: |
n-gpu-layers = all
kv-unified = true
cache-prompt = true
cache-type-k = q8_0
cache-type-v = q8_0
flash-attn = auto
reasoning = off
jinja = true
lazy-mode = off
Nemotron3.5-Lightning-30B-A3B: |
model = /models/Nemotron3.5-Lightning-30B-A3B/Nemotron-3.5-Lightning-30B-A3B-Heretic-Uncensored-Q4_K_M-GGUF.gguf
c = 65536
parallel = 2
load-on-startup = true
# version: mradermacher/LFM2-24B-A2B-heretic-ara-GGUF
# LFM2-24B-A2B: |
# model = /models/LFM2-24B-A2B/LFM2-24B-A2B-heretic-ara.Q4_K_M.gguf
# c = 65536
# parallel = 2
# load-on-startup = true
# # version: SC117/LFM2.5-VL-3B-Uncensored-GGUF
# LFM2-VL-3B: |
# model = /models/LFM2-VL-3B/LFM2.5-VL-3B-Uncensored-Q4_K_M.gguf
# mmproj = /models/LFM2-VL-3B/mmproj-LFM2.5-VL-3B-BF16.gguf
# c = 65536
# parallel = 1
# load-on-startup = true
# version: HauhauCS/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q4_K_M
# Qwen3.6: |
# model = /models/Qwen3.6-35B-A3B/Qwen3.6-35B-A3B-Q4_K_M.gguf
# mmproj = /models/Qwen3.6-35B-A3B/mmproj-Qwen3.6-35B-BF16.gguf
# c = 65536
# parallel = 2
# load-on-startup = true
# image-min-tokens = 1024
# # version: HauhauCS/Qwen3.5-2B-Uncensored-HauhauCS-Aggressiv-Q8_0
# Qwen3.5-image: |
# model = /models/Qwen3.5-2B/Qwen3.5-2B-Q8_0.gguf
# c = 32768
# parallel = 1
# load-on-startup = true
# version: LiquidAI/LFM2.5-2.6B-GGUF
# LFM2.5: |
# model = /models/LFM2.5-2.6B/LFM2.5-2.6B-Q8_0.gguf
# c = 32768
# parallel = 2
# load-on-startup = true
stablediffusion:
ports:
http: "10001"
dependency:
models:
active: "zImage"
zImage: |
--diffusion-model /models/zImage/moodyRealMix_zitV7Global-Q8_0.gguf \
--llm /models/zImage/Qwen3-4B-Instruct-2507-Q4_K_M.gguf \
--vae /models/zImage/ae.safetensors \
--backend gpu \
--cfg-scale 1.0 \
--steps 9 \
--vae-tiling \
--vae-tile-size 32x32 \
--vae-tile-overlap 0.5
snappymail: snappymail:
domain: domain:
public: "webmail" public: "webmail"
@@ -212,6 +290,28 @@ services:
dependency: dependency:
web: "app-public" web: "app-public"
db: "postgresql" db: "postgresql"
groupoffice:
domain:
public: "groupoffice"
internal: "groupoffice.app"
ports:
http: "8005"
subuid: "100032"
route:
dav: true
dependency:
web: "app-public"
db: "mariadb"
kaneo:
domain:
public: "kaneo"
internal: "kaneo.app"
ports:
http: "8006"
subuid: "101000"
dependency:
web: "app-public"
db: "postgresql"
nextcloud: nextcloud:
domain: domain:
public: "nextcloud" public: "nextcloud"
@@ -318,9 +418,9 @@ version:
# infra # infra
ca: "0.30.2" ca: "0.30.2"
## Postgresql ## Postgresql
postgresql: "18.4" postgresql: "18.6"
## Mariadb ## Mariadb
mariadb: "12.3.2" mariadb: "12.3.3"
### For immich - https://github.com/immich-app/base-images/blob/main/postgres/versions.yaml ### For immich - https://github.com/immich-app/base-images/blob/main/postgres/versions.yaml
vectorchord: "1.1.1" vectorchord: "1.1.1"
## lldap ## lldap
@@ -328,25 +428,34 @@ version:
## maddy ## maddy
maddy: "0.9.5" maddy: "0.9.5"
x509-exporter: "4.1.0" x509-exporter: "4.1.0"
prometheus: "v3.13.2" prometheus: "v3.14.0"
loki: "3.7.4" loki: "3.7.7"
grafana: "13.1.1" grafana: "13.2.1"
# Auth # Auth
authelia: "4.39.20" authelia: "4.39.20"
# App # App
vaultwarden: "1.37.1" vaultwarden: "1.37.2"
gitea: "1.27.2" gitea: "1.27.3"
redis: "8.10.0" redis: "8.10.1"
immich: "v3.1.0" immich: "v3.1.0"
nextcloud: "34.0.3" nextcloud: "34.0.3"
collabora: "26.04.3.1.1" collabora: "26.04.3.2.1"
sure: "0.7.3" sure: "0.7.4"
outline: "1.9.2" outline: "1.10.0"
homebox: "0.26.2" homebox: "0.26.2"
homepage: "v2.0.0" homepage: "v2.0.0"
snappymail: "v2.38.2" snappymail: "v2.38.2"
mastodon: "v4.6.6" mastodon: "v4.6.7"
papra: "26.6.1-rootless" papra: "26.6.1-rootless"
tandoor: "2.6.13" tandoor: "2.6.13"
bookstack: "26.5.3" bookstack: "26.5.4"
linkding: "1.46.2" linkding: "1.46.2"
openwebui: "v0.11.3"
# https://github.com/ggml-org/llama.cpp/pkgs/container/llama.cpp/versions
llama: "server-vulkan-b10830"
# https://github.com/leejet/stable-diffusion.cpp/pkgs/container/stable-diffusion.cpp
# master-sycl@sha256:f27ebe3fea7dcb5978241425d3a115ba6736cfc33c42e75233f22f7411449e9c has problem in it, it cannot get vRAM from UMA properly
# Pin due to a SYCL/UMA false-OOM regression in newer stable-diffusion.cpp ModelManager memory accounting.
stablediffusion: "master-sycl@sha256:67ccb5d11bf55578f7938daeb7c9fee1d0dd1344c089c090b9065714d7960fd5"
groupoffice: "26.0.43"
kaneo: "2.23.2"
+1 -1
View File
@@ -16,7 +16,7 @@ vm:
<vcpupin vcpu='1' cpuset='7'/> <vcpupin vcpu='1' cpuset='7'/>
<emulatorpin cpuset='6-7'/> <emulatorpin cpuset='6-7'/>
shares: 1024 shares: 1024
memory: 2 memory: 1
storage: 32 storage: 32
lan_mac: "0a:49:6e:4d:02:00" lan_mac: "0a:49:6e:4d:02:00"
lan_net: "lan-net" lan_net: "lan-net"
+60 -10
View File
@@ -103,6 +103,7 @@
- "netcat-openbsd" - "netcat-openbsd"
- "dbus-user-session" - "dbus-user-session"
- "ksmtuned" - "ksmtuned"
- "rsync"
state: "present" state: "present"
become: true become: true
tags: ["init", "site", "install-packages"] tags: ["init", "site", "install-packages"]
@@ -216,16 +217,24 @@
name: "app" name: "app"
tasks_from: "services/set_immich" tasks_from: "services/set_immich"
apply: apply:
tags: ["site", "immich"] tags: ["site", "immich", "redis"]
tags: ["site", "immich"] tags: ["site", "immich", "redis"]
- name: Set nextcloud - name: Set nextcloud
ansible.builtin.include_role: ansible.builtin.include_role:
name: "app" name: "app"
tasks_from: "services/set_nextcloud" tasks_from: "services/set_nextcloud"
apply: apply:
tags: ["site", "nextcloud"] tags: ["site", "nextcloud", "redis"]
tags: ["site", "nextcloud"] tags: ["site", "nextcloud", "redis"]
- name: Set groupoffice
ansible.builtin.include_role:
name: "app"
tasks_from: "services/set_groupoffice"
apply:
tags: ["site", "groupoffice"]
tags: ["site", "groupoffice"]
- name: Set collabora - name: Set collabora
ansible.builtin.include_role: ansible.builtin.include_role:
@@ -235,21 +244,29 @@
tags: ["site", "collabora"] tags: ["site", "collabora"]
tags: ["site", "collabora"] tags: ["site", "collabora"]
- name: Set kaneo
ansible.builtin.include_role:
name: "app"
tasks_from: "services/set_kaneo"
apply:
tags: ["site", "kaneo"]
tags: ["site", "kaneo"]
- name: Set sure - name: Set sure
ansible.builtin.include_role: ansible.builtin.include_role:
name: "app" name: "app"
tasks_from: "services/set_sure" tasks_from: "services/set_sure"
apply: apply:
tags: ["site", "sure"] tags: ["site", "sure", "redis"]
tags: ["site", "sure"] tags: ["site", "sure", "redis"]
- name: Set outline - name: Set outline
ansible.builtin.include_role: ansible.builtin.include_role:
name: "app" name: "app"
tasks_from: "services/set_outline" tasks_from: "services/set_outline"
apply: apply:
tags: ["site", "outline"] tags: ["site", "outline", "redis"]
tags: ["site", "outline"] tags: ["site", "outline", "redis"]
- name: Set homebox - name: Set homebox
ansible.builtin.include_role: ansible.builtin.include_role:
@@ -264,8 +281,8 @@
name: "app" name: "app"
tasks_from: "services/set_mastodon" tasks_from: "services/set_mastodon"
apply: apply:
tags: ["site", "mastodon"] tags: ["site", "mastodon", "redis"]
tags: ["site", "mastodon"] tags: ["site", "mastodon", "redis"]
- name: Set papra - name: Set papra
ansible.builtin.include_role: ansible.builtin.include_role:
@@ -299,6 +316,30 @@
tags: ["site", "linkding"] tags: ["site", "linkding"]
tags: ["site", "linkding"] tags: ["site", "linkding"]
- name: Set llama
ansible.builtin.include_role:
name: "app"
tasks_from: "services/set_llama"
apply:
tags: ["site", "llama"]
tags: ["site", "llama"]
- name: Set stablediffusion
ansible.builtin.include_role:
name: "app"
tasks_from: "services/set_stablediffusion"
apply:
tags: ["site", "stablediffusion"]
tags: ["site", "stablediffusion"]
- name: Set openwebui
ansible.builtin.include_role:
name: "app"
tasks_from: "services/set_openwebui"
apply:
tags: ["site", "openwebui"]
tags: ["site", "openwebui"]
- name: Set homepage - name: Set homepage
ansible.builtin.include_role: ansible.builtin.include_role:
name: "app" name: "app"
@@ -320,10 +361,19 @@
state: "present" state: "present"
become: true become: true
notify: notify:
- "notification_add_user_in_group"
- "notification_update_initramfs" - "notification_update_initramfs"
- "notification_reboot_app" - "notification_reboot_app"
tags: ["init"] tags: ["init"]
handlers: handlers:
- name: Add user in video, render group
ansible.builtin.user:
name: "{{ ansible_user }}"
state: "present"
groups: "video, render"
append: true
become: true
listen: "notification_add_user_in_group"
- name: Update initramfs - name: Update initramfs
ansible.builtin.command: ansible.builtin.command:
update-initramfs -u update-initramfs -u
+55
View File
@@ -183,3 +183,58 @@
changed_when: false changed_when: false
listen: "notification_restart_linkding" listen: "notification_restart_linkding"
ignore_errors: true # noqa: ignore-errors ignore_errors: true # noqa: ignore-errors
- name: Restart openwebui
ansible.builtin.systemd:
name: "openwebui.service"
state: "restarted"
enabled: true
scope: "user"
daemon_reload: true
changed_when: false
listen: "notification_restart_openwebui"
ignore_errors: true # noqa: ignore-errors
- name: Restart llama
ansible.builtin.systemd:
name: "llama.service"
state: "restarted"
enabled: true
scope: "user"
daemon_reload: true
changed_when: false
listen: "notification_restart_llama"
ignore_errors: true # noqa: ignore-errors
- name: Restart stablediffusion
ansible.builtin.systemd:
name: "stablediffusion.service"
state: "restarted"
enabled: true
scope: "user"
daemon_reload: true
changed_when: false
listen: "notification_restart_stablediffusion"
ignore_errors: true # noqa: ignore-errors
- name: Restart groupoffice
ansible.builtin.systemd:
name: "groupoffice.service"
state: "restarted"
enabled: true
scope: "user"
daemon_reload: true
changed_when: false
listen: "notification_restart_groupoffice"
ignore_errors: true # noqa: ignore-errors
- name: Restart kaneo
ansible.builtin.systemd:
name: "kaneo.service"
state: "restarted"
enabled: true
scope: "user"
daemon_reload: true
changed_when: false
listen: "notification_restart_kaneo"
ignore_errors: true # noqa: ignore-errors
@@ -0,0 +1,118 @@
---
- name: Create groupoffice directory
ansible.builtin.file:
path: "{{ node['home_path'] }}/{{ item }}"
state: "directory"
owner: "{{ services['groupoffice']['subuid'] }}"
group: "{{ node['group'] }}"
mode: "0770"
loop:
- "data/containers/groupoffice"
- "data/containers/groupoffice/data"
- "containers/groupoffice"
- "containers/groupoffice/ssl"
- "containers/groupoffice/config"
become: true
- name: Deploy root certificate
ansible.builtin.copy:
content: |
{{ hostvars['console']['ca']['root']['crt'] }}
dest: "{{ node['home_path'] }}/containers/groupoffice/ssl/{{ root_cert_filename }}"
owner: "{{ services['groupoffice']['subuid'] }}"
group: "{{ node['group'] }}"
mode: "0440"
become: true
notify: "notification_restart_groupoffice"
no_log: true
- name: Register secret value to podman secret
containers.podman.podman_secret:
name: "{{ item.name }}"
data: "{{ item.value }}"
state: "present"
force: true
loop:
- name: "GROUPOFFICE_DB_PASSWORD"
value: "{{ hostvars['console']['mariadb']['password']['groupoffice'] }}"
notify: "notification_restart_groupoffice"
no_log: true
- name: Deploy config files
ansible.builtin.template:
src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/groupoffice/config/{{ item }}.j2"
dest: "{{ node['home_path'] }}/containers/groupoffice/config/{{ item }}"
owner: "{{ services['groupoffice']['subuid'] }}"
group: "{{ node['group'] }}"
mode: "0640"
loop:
- "config.php"
- "groupoffice-ca.ini"
- "Connection.php"
- "Authenticator.php"
- "configfile.php"
become: true
notify: "notification_restart_groupoffice"
- name: Pull image
containers.podman.podman_image:
name: "docker.io/intermesh/groupoffice"
tag: "{{ version['containers']['groupoffice'] }}"
state: "present"
pull: true
- name: Deploy groupoffice.container file
ansible.builtin.template:
src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/groupoffice/groupoffice.container.j2"
dest: "{{ node['home_path'] }}/.config/containers/systemd/groupoffice.container"
owner: "{{ ansible_user }}"
group: "{{ node['group'] }}"
mode: "0644"
notify: "notification_restart_groupoffice"
- name: Deploy groupoffice-cron service
ansible.builtin.copy:
src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/groupoffice/services/{{ item }}"
dest: "{{ node['home_path'] }}/.config/systemd/user/{{ item }}"
owner: "{{ ansible_user }}"
group: "{{ node['group'] }}"
mode: "0644"
loop:
- "groupoffice-cron.service"
- "groupoffice-cron.timer"
- name: Enable groupoffice.service
ansible.builtin.systemd:
name: "groupoffice.service"
state: "started"
enabled: true
daemon_reload: true
scope: "user"
- name: Enable groupoffice-cron.timer
ansible.builtin.systemd:
name: "groupoffice-cron.timer"
state: "started"
enabled: true
daemon_reload: true
scope: "user"
- name: Check container archive images
ansible.builtin.stat:
path: "{{ node['home_path'] }}/archives/containers/docker.io_intermesh_groupoffice_{{ version['containers']['groupoffice'] }}.tar"
register: container_archive_images
- name: Save container archive images
containers.podman.podman_save:
image:
- "docker.io/intermesh/groupoffice:{{ version['containers']['groupoffice'] }}"
dest: "{{ node['home_path'] }}/archives/containers/docker.io_intermesh_groupoffice_{{ version['containers']['groupoffice'] }}.tar"
format: "oci-archive"
force: false
when: not container_archive_images.stat.exists
- name: Fetch container archive images
ansible.builtin.fetch:
src: "{{ node['home_path'] }}/archives/containers/docker.io_intermesh_groupoffice_{{ version['containers']['groupoffice'] }}.tar"
dest: "{{ hostvars['console']['node']['data_path'] }}/images/containers/"
flat: true
@@ -43,14 +43,6 @@
scope: "user" scope: "user"
when: is_redis_conf.changed or is_redis_containerfile.changed # noqa: no-handler when: is_redis_conf.changed or is_redis_containerfile.changed # noqa: no-handler
- name: Add user in video, render group
ansible.builtin.user:
name: "{{ ansible_user }}"
state: "present"
groups: "video, render"
append: true
become: true
- name: Create immich directory - name: Create immich directory
ansible.builtin.file: ansible.builtin.file:
path: "{{ node['home_path'] }}/{{ item }}" path: "{{ node['home_path'] }}/{{ item }}"
@@ -0,0 +1,84 @@
---
- name: Create kaneo directory
ansible.builtin.file:
path: "{{ node['home_path'] }}/{{ item }}"
state: "directory"
owner: "{{ services['kaneo']['subuid'] }}"
group: "{{ node['group'] }}"
mode: "0770"
loop:
- "containers/kaneo"
- "containers/kaneo/ssl"
become: true
- name: Deploy root certificate
ansible.builtin.copy:
content: |
{{ hostvars['console']['ca']['root']['crt'] }}
dest: "{{ node['home_path'] }}/containers/kaneo/ssl/{{ root_cert_filename }}"
owner: "{{ services['kaneo']['subuid'] }}"
group: "{{ node['group'] }}"
mode: "0440"
become: true
notify: "notification_restart_kaneo"
no_log: true
- name: Register secret value to podman secret
containers.podman.podman_secret:
name: "{{ item.name }}"
data: "{{ item.value }}"
state: "present"
force: true
loop:
- name: "KANEO_AUTH_SECRET"
value: "{{ hostvars['console']['kaneo']['secret_key'] }}"
- name: "KANEO_DB_PASSWORD"
value: "{{ hostvars['console']['postgresql']['password']['kaneo'] }}"
- name: "KANEO_OIDC_CLIENT_SECRET"
value: "{{ hostvars['console']['kaneo']['oidc']['secret'] }}"
notify: "notification_restart_kaneo"
no_log: true
- name: Pull image
containers.podman.podman_image:
name: "ghcr.io/usekaneo/kaneo"
tag: "{{ version['containers']['kaneo'] }}"
state: "present"
pull: true
- name: Deploy kaneo.container file
ansible.builtin.template:
src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/kaneo/kaneo.container.j2"
dest: "{{ node['home_path'] }}/.config/containers/systemd/kaneo.container"
owner: "{{ ansible_user }}"
group: "{{ node['group'] }}"
mode: "0644"
notify: "notification_restart_kaneo"
- name: Enable kaneo.service
ansible.builtin.systemd:
name: "kaneo.service"
state: "started"
enabled: true
daemon_reload: true
scope: "user"
- name: Check container archive images
ansible.builtin.stat:
path: "{{ node['home_path'] }}/archives/containers/ghcr.io_usekaneo_kane_{{ version['containers']['kaneo'] }}.tar"
register: container_archive_images
- name: Save container archive images
containers.podman.podman_save:
image:
- "ghcr.io/usekaneo/kaneo:{{ version['containers']['kaneo'] }}"
dest: "{{ node['home_path'] }}/archives/containers/ghcr.io_usekaneo_kane_{{ version['containers']['kaneo'] }}.tar"
format: "oci-archive"
force: false
when: not container_archive_images.stat.exists
- name: Fetch container archive images
ansible.builtin.fetch:
src: "{{ node['home_path'] }}/archives/containers/ghcr.io_usekaneo_kane_{{ version['containers']['kaneo'] }}.tar"
dest: "{{ hostvars['console']['node']['data_path'] }}/images/containers/"
flat: true
@@ -0,0 +1,90 @@
---
- name: Check model directory in data volume
ansible.builtin.find:
path: "{{ node['home_path'] }}/data/containers/llama/models"
file_type: "file"
patterns: "*.gguf"
hidden: true
recurse: true
register: llama_models
- name: Failure when models directory in data volume
ansible.builtin.fail:
msg: "There are no models in data directory, please ensure download some model files for llama.cpp"
when: llama_models.matched == 0
- name: Create llama directory
ansible.builtin.file:
path: "{{ node['home_path'] }}/{{ item }}"
state: "directory"
owner: "{{ ansible_user }}"
group: "{{ node['group'] }}"
mode: "0770"
loop:
- "containers/llama"
- "containers/llama/models"
- "containers/llama/config"
- name: Sync model data (--delete=true)
delegate_to: "{{ inventory_hostname }}"
ansible.posix.synchronize:
src: "{{ node['home_path'] }}/data/containers/llama/models/"
dest: "{{ node['home_path'] }}/containers/llama/models/"
owner: true
group: true
perms: true
delete: true
notify: "notification_restart_llama"
- name: Deploy configuration file
ansible.builtin.template:
src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/llama/config/models.ini.j2"
dest: "{{ node['home_path'] }}/containers/llama/config/models.ini"
owner: "{{ ansible_user }}"
group: "{{ node['group'] }}"
mode: "0640"
notify: "notification_restart_llama"
- name: Pull image
containers.podman.podman_image:
name: "ghcr.io/ggml-org/llama.cpp"
tag: "{{ version['containers']['llama'] }}"
state: "present"
pull: true
- name: Deploy container file
ansible.builtin.template:
src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/llama/llama.container.j2"
dest: "{{ node['home_path'] }}/.config/containers/systemd/llama.container"
owner: "{{ ansible_user }}"
group: "{{ node['group'] }}"
mode: "0644"
notify: "notification_restart_llama"
- name: Enable llama.service
ansible.builtin.systemd:
name: "llama.service"
state: "started"
enabled: true
daemon_reload: true
scope: "user"
- name: Check container archive images
ansible.builtin.stat:
path: "{{ node['home_path'] }}/archives/containers/ghcr.io_ggml-org_llama.cpp_{{ version['containers']['llama'] }}.tar"
register: container_archive_images
- name: Save container archive images
containers.podman.podman_save:
image:
- "ghcr.io/ggml-org/llama.cpp:{{ version['containers']['llama'] }}"
dest: "{{ node['home_path'] }}/archives/containers/ghcr.io_ggml-org_llama.cpp_{{ version['containers']['llama'] }}.tar"
format: "oci-archive"
force: false
when: not container_archive_images.stat.exists
- name: Fetch container archive images
ansible.builtin.fetch:
src: "{{ node['home_path'] }}/archives/containers/ghcr.io_ggml-org_llama.cpp_{{ version['containers']['llama'] }}.tar"
dest: "{{ hostvars['console']['node']['data_path'] }}/images/containers/"
flat: true
@@ -0,0 +1,84 @@
---
- name: Create openwebui directory
ansible.builtin.file:
path: "{{ node['home_path'] }}/{{ item }}"
state: "directory"
owner: "{{ ansible_user }}"
group: "{{ node['group'] }}"
mode: "0770"
loop:
- "data/containers/openwebui"
- "data/containers/openwebui/data"
- "containers/openwebui"
- "containers/openwebui/ssl"
- name: Deploy root certificate
ansible.builtin.copy:
content: |
{{ hostvars['console']['ca']['root']['crt'] }}
dest: "{{ node['home_path'] }}/containers/openwebui/ssl/{{ root_cert_filename }}"
owner: "{{ ansible_user }}"
group: "{{ node['group'] }}"
mode: "0440"
notify: "notification_restart_openwebui"
no_log: true
- name: Register secret value to podman secret
containers.podman.podman_secret:
name: "{{ item.name }}"
data: "{{ item.value }}"
state: "present"
force: true
loop:
- name: "OPEN_WEBUI_SECRET_KEY"
value: "{{ hostvars['console']['openwebui']['secret_key'] }}"
- name: "OPEN_WEBUI_POSTGRES_PASSWORD"
value: "{{ hostvars['console']['postgresql']['password']['openwebui'] }}"
- name: "OPEN_WEBUI_OAUTH_CLIENT_SECRET"
value: "{{ hostvars['console']['openwebui']['oidc']['secret'] }}"
notify: "notification_restart_openwebui"
no_log: true
- name: Pull image
containers.podman.podman_image:
name: "ghcr.io/open-webui/open-webui"
tag: "{{ version['containers']['openwebui'] }}"
state: "present"
pull: true
- name: Deploy container file
ansible.builtin.template:
src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/openwebui/openwebui.container.j2"
dest: "{{ node['home_path'] }}/.config/containers/systemd/openwebui.container"
owner: "{{ ansible_user }}"
group: "{{ node['group'] }}"
mode: "0644"
notify: "notification_restart_openwebui"
- name: Enable openwebui.service
ansible.builtin.systemd:
name: "openwebui.service"
state: "started"
enabled: true
daemon_reload: true
scope: "user"
- name: Check container archive images
ansible.builtin.stat:
path: "{{ node['home_path'] }}/archives/containers/ghcr.io_open-webui_open-webui_{{ version['containers']['openwebui'] }}.tar"
register: container_archive_images
- name: Save container archive images
containers.podman.podman_save:
image:
- "ghcr.io/open-webui/open-webui:{{ version['containers']['openwebui'] }}"
dest: "{{ node['home_path'] }}/archives/containers/ghcr.io_open-webui_open-webui_{{ version['containers']['openwebui'] }}.tar"
format: "oci-archive"
force: false
when: not container_archive_images.stat.exists
- name: Fetch container archive images
ansible.builtin.fetch:
src: "{{ node['home_path'] }}/archives/containers/ghcr.io_open-webui_open-webui_{{ version['containers']['openwebui'] }}.tar"
dest: "{{ hostvars['console']['node']['data_path'] }}/images/containers/"
flat: true
@@ -0,0 +1,91 @@
---
- name: Check model directory in data volume
ansible.builtin.find:
path: "{{ node['home_path'] }}/data/containers/stablediffusion/models"
file_type: "file"
patterns:
- "*.gguf"
- "*.safetensors"
hidden: true
recurse: true
register: stablediffusion_models
- name: Failure when models directory in data volume
ansible.builtin.fail:
msg: "There are no models in data directory, please ensure download some model files for stable-diffusion.cpp"
when: stablediffusion_models.matched == 0
- name: Create stablediffusion directory
ansible.builtin.file:
path: "{{ node['home_path'] }}/{{ item }}"
state: "directory"
owner: "{{ ansible_user }}"
group: "{{ node['group'] }}"
mode: "0770"
loop:
- "containers/stablediffusion"
- "containers/stablediffusion/models"
- name: Sync model data (--delete=true)
delegate_to: "{{ inventory_hostname }}"
ansible.posix.synchronize:
src: "{{ node['home_path'] }}/data/containers/stablediffusion/models/"
dest: "{{ node['home_path'] }}/containers/stablediffusion/models/"
owner: true
group: true
perms: true
delete: true
notify: "notification_restart_stablediffusion"
- name: Set active model
ansible.builtin.set_fact:
stabeldiffusion_active_model: "{{ services['stablediffusion']['dependency']['models']['active'] }}"
- name: Pull image
containers.podman.podman_image:
name: "ghcr.io/leejet/stable-diffusion.cpp"
tag: "{{ version['containers']['stablediffusion'] }}"
state: "present"
pull: true
- name: Deploy container file
ansible.builtin.template:
src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/stablediffusion/stablediffusion.container.j2"
dest: "{{ node['home_path'] }}/.config/containers/systemd/stablediffusion.container"
owner: "{{ ansible_user }}"
group: "{{ node['group'] }}"
mode: "0644"
notify: "notification_restart_stablediffusion"
- name: Enable stablediffusion.service
ansible.builtin.systemd:
name: "stablediffusion.service"
state: "started"
enabled: true
daemon_reload: true
scope: "user"
- name: Set stable-diffusion archive version
ansible.builtin.set_fact:
stablediffusion_archive_version: >-
{{ version['containers']['stablediffusion'].split('@sha256:')[0] }}@{{ version['containers']['stablediffusion'].split('@sha256:')[1][0:7] }}
- name: Check container archive images
ansible.builtin.stat:
path: "{{ node['home_path'] }}/archives/containers/ghcr.io_leejet_stable-diffusion.cpp_{{ stablediffusion_archive_version }}.tar"
register: container_archive_images
- name: Save container archive images
containers.podman.podman_save:
image:
- "ghcr.io/leejet/stable-diffusion.cpp:{{ version['containers']['stablediffusion'] }}"
dest: "{{ node['home_path'] }}/archives/containers/ghcr.io_leejet_stable-diffusion.cpp_{{ stablediffusion_archive_version }}.tar"
format: "oci-archive"
force: false
when: not container_archive_images.stat.exists
- name: Fetch container archive images
ansible.builtin.fetch:
src: "{{ node['home_path'] }}/archives/containers/ghcr.io_leejet_stable-diffusion.cpp_{{ stablediffusion_archive_version }}.tar"
dest: "{{ hostvars['console']['node']['data_path'] }}/images/containers/"
flat: true
+1 -1
View File
@@ -147,7 +147,7 @@ table inet filter {
oifname $IF_SERVER ip saddr $HOSTS4_CONSOLE ip daddr { $HOSTS4_BK, $HOSTS4_INFRA, $HOSTS4_APP } tcp dport $PORTS_WEB accept comment "allow ipv4 web connection: CONSOLE > FW > SERVER BK/INFRA/APP" oifname $IF_SERVER ip saddr $HOSTS4_CONSOLE ip daddr { $HOSTS4_BK, $HOSTS4_INFRA, $HOSTS4_APP } tcp dport $PORTS_WEB accept comment "allow ipv4 web connection: CONSOLE > FW > SERVER BK/INFRA/APP"
oifname $IF_SERVER ip6 saddr $HOSTS6_CONSOLE ip6 daddr { $HOSTS6_BK, $HOSTS6_INFRA, $HOSTS6_APP } tcp dport $PORTS_WEB accept comment "allow ipv6 web connection: CONSOLE > FW > SERVER BK/INFRA/APP" oifname $IF_SERVER ip6 saddr $HOSTS6_CONSOLE ip6 daddr { $HOSTS6_BK, $HOSTS6_INFRA, $HOSTS6_APP } tcp dport $PORTS_WEB accept comment "allow ipv6 web connection: CONSOLE > FW > SERVER BK/INFRA/APP"
# Printer # Printer
oifname $IF_CLIENT ip saddr $HOSTS4_CONSOLE ip daddr $HOSTS4_PRINTER accept comment "allow ipv4 printer connection: CONSOLE > FW > PRINTER" ip saddr $HOSTS4_CONSOLE ip daddr $HOSTS4_PRINTER accept comment "allow ipv4 printer connection: CONSOLE > FW > PRINTER"
iifname $IF_WAN jump wan comment "set WAN interface rules" iifname $IF_WAN jump wan comment "set WAN interface rules"
iifname $IF_CLIENT jump client comment "set CLIENT interface rules" iifname $IF_CLIENT jump client comment "set CLIENT interface rules"
+32 -2
View File
@@ -139,6 +139,8 @@ postgresql:
mastodon: ENC[AES256_GCM,data:hq1ETBK9iUI7kWFO+9pZwMQMbmyLExGk7/VzBwDdLFbzptXL2KEi6oYRCS0=,iv:hF1C8lsnP2i3LxDIuOA6op4qKXQKjJ7gqznDst7hVAU=,tag:maR7Jvsv3DJGrO77EzmBDA==,type:str] mastodon: ENC[AES256_GCM,data:hq1ETBK9iUI7kWFO+9pZwMQMbmyLExGk7/VzBwDdLFbzptXL2KEi6oYRCS0=,iv:hF1C8lsnP2i3LxDIuOA6op4qKXQKjJ7gqznDst7hVAU=,tag:maR7Jvsv3DJGrO77EzmBDA==,type:str]
tandoor: ENC[AES256_GCM,data:+5g/jzRMQoxocZntoE+1BU0MS00yEp0tyVNxriEIJ0pW2V1bsLhUAkwQ748=,iv:feNbpOhkdjmKS7Ovy0KGioOTyjQ4sflfH+50niP7Y8Y=,tag:51/u+EE3Pp5o7jnlj0AWIw==,type:str] tandoor: ENC[AES256_GCM,data:+5g/jzRMQoxocZntoE+1BU0MS00yEp0tyVNxriEIJ0pW2V1bsLhUAkwQ748=,iv:feNbpOhkdjmKS7Ovy0KGioOTyjQ4sflfH+50niP7Y8Y=,tag:51/u+EE3Pp5o7jnlj0AWIw==,type:str]
linkding: ENC[AES256_GCM,data:+diOqVjK5RkDOpxCrb0M0sLWEufXEwhTNo0x5qH4FkteN8zaUAZfO8lD13I=,iv:jHQANXC1tamiphm33RpTKpEB3niLFx6IKzcdq8/KNCg=,tag:NwkkmUPOS9K0IKtUBVRWqg==,type:str] linkding: ENC[AES256_GCM,data:+diOqVjK5RkDOpxCrb0M0sLWEufXEwhTNo0x5qH4FkteN8zaUAZfO8lD13I=,iv:jHQANXC1tamiphm33RpTKpEB3niLFx6IKzcdq8/KNCg=,tag:NwkkmUPOS9K0IKtUBVRWqg==,type:str]
openwebui: ENC[AES256_GCM,data:QNeBJjdLIzoI2h9e3me6A/xZ03YpBeLpKK0Yya5Wi5vryv1ZwnHHZwOjmuA=,iv:qLpsxlwpE+V+qACkfOee/ExFIyVV+KsXzuT3VDiJ/w4=,tag:YsmBhmEkqYcSmIIX7R2nTw==,type:str]
kaneo: ENC[AES256_GCM,data:Q2rg4/0wAgWDzB8Aj5B6i851FZN3eGR7BbX2l3Y5+Ij47//TtKGzdq6cxNg=,iv:Mbp/S7Lc0tLJAT+QFMVB3a86S+mqoQmZDZQ7Agf8rU8=,tag:BkerNmfeeOicsQAwUo4dtQ==,type:str]
#ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] #ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment]
# #
# #
@@ -148,6 +150,7 @@ mariadb:
crt: ENC[AES256_GCM,data:aCrAXf5n6vIyEIbpwyrmVvjPa6gmVrAU213SnqHO3/Mem4MgfDjmh9Q1tQbvXKSJT4tcecp08sCY1Y+AKN26lvqFd1Y8b50dNVLXuBRd0saLDwyvi2/w7a9B5sinKLCtKfvOc2JOdoYVHQKcyQXG6uHxSpZPf2+j8s7KUrgTNAjKM6aOQaUYydbxcD0pQ9BgJxCCM88+que+HtX0FdLYr/ImF+dvpMRE5RlvWlWB4bae6A/xPRWH8ZHj4ZwO4uMSQARu5w3kWDngcE/CnEhC1JQvKLJ1gKzkaUF3/Vn8kkxIITGddw0+2kQyGCJ9a5ZXpzr+AJX1OuFE83YDZRRXbhmEuSZiNQmLaw7ImSjNI5uZ4MNyKUuq9t+xqSgBj64dhn4egHaAb9fzZo9lV9/2kAchnidWBzn9EtAacYsO0lf7i7OYuSwCjKaTBvO8DwPudSsWi3CYYx2NlQXP5PvElJi/7aasWcF3ohmhMUew0ATBdgxMKvl3MENzi66WuTb2uzcryMQf5M6lIceXYa3RLQQzJXkBUe4vBg5aQBiKAYqURvxmvgb+j/YMLHrXGj8bkkV/iTdiI5oyeE9+si6Tw+f6Gj/E++KUWSR8AIrNbAHgJ1dtPd/oHt8CW8X9USJd56L3tOAA0r5SDnHhnZ/sfumuyz3H34RYjPxfKu0FCQhsEofDrDc4DnypD6s1RgzJM+esvHRLb2k4+X1CN5Lf04pTis+GshRtWxRClycrwLjcyaCZ/JN5+4sAHazr2nEQ8C+RQ9NuVRK2enFVcfTCf+2Sk6pN8ltHcmrYH1Xx1B88gsAPL8eWmeGLiTbzCqRAkkZ2EvbOAu4qZteLC0qRan1jYQs3C8zjwfQKf8QpEY1EYqNYaNkqDPpZcxeRMoNX5vlS9n5W3qoX+qyBxUKKNl6O3WH6JNnODkJMwnFhaSE8HSTr7rbBjqc1XaM0,iv:lCxYy8LkovdX1dIIoS0NXe5YbUT/ctPK1eS1pSU1z9c=,tag:Pads/Okb8YVZH19ODUThHw==,type:str] crt: ENC[AES256_GCM,data:aCrAXf5n6vIyEIbpwyrmVvjPa6gmVrAU213SnqHO3/Mem4MgfDjmh9Q1tQbvXKSJT4tcecp08sCY1Y+AKN26lvqFd1Y8b50dNVLXuBRd0saLDwyvi2/w7a9B5sinKLCtKfvOc2JOdoYVHQKcyQXG6uHxSpZPf2+j8s7KUrgTNAjKM6aOQaUYydbxcD0pQ9BgJxCCM88+que+HtX0FdLYr/ImF+dvpMRE5RlvWlWB4bae6A/xPRWH8ZHj4ZwO4uMSQARu5w3kWDngcE/CnEhC1JQvKLJ1gKzkaUF3/Vn8kkxIITGddw0+2kQyGCJ9a5ZXpzr+AJX1OuFE83YDZRRXbhmEuSZiNQmLaw7ImSjNI5uZ4MNyKUuq9t+xqSgBj64dhn4egHaAb9fzZo9lV9/2kAchnidWBzn9EtAacYsO0lf7i7OYuSwCjKaTBvO8DwPudSsWi3CYYx2NlQXP5PvElJi/7aasWcF3ohmhMUew0ATBdgxMKvl3MENzi66WuTb2uzcryMQf5M6lIceXYa3RLQQzJXkBUe4vBg5aQBiKAYqURvxmvgb+j/YMLHrXGj8bkkV/iTdiI5oyeE9+si6Tw+f6Gj/E++KUWSR8AIrNbAHgJ1dtPd/oHt8CW8X9USJd56L3tOAA0r5SDnHhnZ/sfumuyz3H34RYjPxfKu0FCQhsEofDrDc4DnypD6s1RgzJM+esvHRLb2k4+X1CN5Lf04pTis+GshRtWxRClycrwLjcyaCZ/JN5+4sAHazr2nEQ8C+RQ9NuVRK2enFVcfTCf+2Sk6pN8ltHcmrYH1Xx1B88gsAPL8eWmeGLiTbzCqRAkkZ2EvbOAu4qZteLC0qRan1jYQs3C8zjwfQKf8QpEY1EYqNYaNkqDPpZcxeRMoNX5vlS9n5W3qoX+qyBxUKKNl6O3WH6JNnODkJMwnFhaSE8HSTr7rbBjqc1XaM0,iv:lCxYy8LkovdX1dIIoS0NXe5YbUT/ctPK1eS1pSU1z9c=,tag:Pads/Okb8YVZH19ODUThHw==,type:str]
password: password:
bookstack: ENC[AES256_GCM,data:kFQ47qbE+D89Pjr4B6WMAcCHQ8deCyzaPmNspI5TvqwvN5RIaKOWU1uT4jU=,iv:3OiYzBod2xxKMt/5Q9uazalld1NkUNun+DJipC3Ts+w=,tag:orCNWYMAZ/5yi7c6apXVJg==,type:str] bookstack: ENC[AES256_GCM,data:kFQ47qbE+D89Pjr4B6WMAcCHQ8deCyzaPmNspI5TvqwvN5RIaKOWU1uT4jU=,iv:3OiYzBod2xxKMt/5Q9uazalld1NkUNun+DJipC3Ts+w=,tag:orCNWYMAZ/5yi7c6apXVJg==,type:str]
groupoffice: ENC[AES256_GCM,data:s4jO9jrn15tPcStDYUTNLgvCJvtlfp641nU3EzYmPvIzk4sQIQZRuGsODaU=,iv:OBpzPkW0bMWL9da2/vL7GgOTqaKSvYamapaK5AMZeZE=,tag:CYuoBHO5Wt0VBL32YKrd5A==,type:str]
#ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] #ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment]
# #
# #
@@ -352,6 +355,33 @@ linkding:
#ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] #ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment]
# #
# #
#ENC[AES256_GCM,data:aFaqLiw+m7M5KEBCH06w2Cgasw==,iv:Yq1tY7lNxW6UuY+77OQEmWwzW9vnJpaZL/bI650wg18=,tag:e9Fozat8Pf3h/BXUNZ0fHQ==,type:comment]
openwebui:
secret_key: ENC[AES256_GCM,data:dAUmbxfOGo684M7Mk7VAv+mZTLT2qi83Nvf6fOR6M/Ale/2n1D4Q8jIl0C4=,iv:uNNQmXTIUACVyguvPBhLzv9ir3WXlPFacgqMhrNScKM=,tag:h3plKpikh7UqHIUO9fyZiQ==,type:str]
oidc:
secret: ENC[AES256_GCM,data:1PZgSzT28FMuClh7W21j9zjHn/YAPiY7iBF8Hid2FCDMzC2jHaObQdXM5H4=,iv:mhNxP0So9JjG1Hgtvt8OpdzIcfp7SZZGPiX0wj0ZP20=,tag:xvKiHjeVJDz0uSkZXpuKcA==,type:str]
hash: ENC[AES256_GCM,data:XIHS9JJVGy3qKFiBIPETou+79vMV4wjYXW9Grv4Hx1zqjqZ4onJ/JiMVcYmtBm5CAp2hsrouIdw9gjW344pnjVg5DORMz6P+/95Yu3OlYmOEj+Cl1iLqgN/q5qc3XAXzQaOylwdbp6UMR+z7PmqJzY+z1h/qMqlSIYGtRAVo4BgNBng=,iv:sK8HTn0z/kzYB8Db+zrKmlk2DxcH3DcoQkPRJtlIvSY=,tag:5hmbItxT4bRlGaM5z2kQkg==,type:str]
#ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment]
#
#
#ENC[AES256_GCM,data:AzckMweK6MUIX9VYy96juNpiGfU=,iv:4/j/5QgJy9OsxZ1dNN16TKR93/9mCmOuaqLrab3ymLo=,tag:4wrv96l2BI09Afc7HaQiRA==,type:comment]
groupoffice:
admin:
id: ENC[AES256_GCM,data:WafJHpvJ3MmpfA8=,iv:i9KbnMoMG8EnaryPAQ+3xYvUwFYKQDnzGmMkSOKllTs=,tag:uH/ksrBdK1M++rVyMvxcRQ==,type:str]
email: ENC[AES256_GCM,data:AfAQUHankHwMGutLM/LZWJEimCiDFzyDp+GsPg==,iv:atCWo/TTshDGGwtu42ZddILK45eMIg5oYy82m4TAzUU=,tag:0X+bhJ05/1b7DS9wb22A9w==,type:str]
password: ENC[AES256_GCM,data:V0CJ8KAoHguWrW9+/j12xjbqqn/5nKAqbYYHZ1UxqAubpD0fQDLTjrZRTuQ=,iv:MJinVDkLBaSNAwM8qCj0WaWl26vTYDoHQqcmdlyNjR4=,tag:7P1UQZd197aJlFREiJP2ow==,type:str]
#ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment]
#
#
#ENC[AES256_GCM,data:zEb2WvNmwVIqKcKesJk=,iv:G/CP+ZrjF9uJNTRC0CVY1cRM1gPL8Ud2GHAmHkkSM78=,tag:lXIH+YF2dCdIzI/ImhQRig==,type:comment]
kaneo:
secret_key: ENC[AES256_GCM,data:SeMvcW+SZABzQKnTsd1dOFbKklfqqTEDM6q9pqd1SS9hig/XuW4lDzS8q0k=,iv:3Djzn0VGasdxyoZo8zMx4G8sIOGBhRt/1cq3LuabUzA=,tag:FCbSB7A/PowGPz8URFm/eA==,type:str]
oidc:
secret: ENC[AES256_GCM,data:AFjQeEv3DMFZCpObgOR69i/Tavp3lcYUsaUqZ/X4pQYcXCNAE1gRHwkk//8=,iv:nEsQOT5AZnHHHMfrfcCIClw3Dwg+O2ykPFjIlcl8Wrc=,tag:J+E50bmw6pEXSVkAOwfuWQ==,type:str]
hash: ENC[AES256_GCM,data:47WzIRYnXn/Jz01BwSVjtff2cTl16vVWh1YypJLmg/GJjXNj+XcghAnw4j7hFf8YhIyyCgWHHm0wkgqxpjPXN03wDIP8Djxg3K5ubUPXZJZWYVTdwcc1Ebdi/3x/DijMLqSJxbRATYSXijBF1o6t1uv0GfGHXTt7M50vdsB8MNjBkds=,iv:MRj+G3gpr8tQdwAxxZlojknUeQ67WY17OAbu04v2sxY=,tag:avz6caNLAi/bGvkfheGJCw==,type:str]
#ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment]
#
#
#ENC[AES256_GCM,data:T4Wtn49AAxPd2QUFTR+q,iv:bH5goGWBDqumAat9dUv2OwfCUJUpuVqncTMqMBZUXhI=,tag:G+W6hHA+yftQ+4RJpXrxHg==,type:comment] #ENC[AES256_GCM,data:T4Wtn49AAxPd2QUFTR+q,iv:bH5goGWBDqumAat9dUv2OwfCUJUpuVqncTMqMBZUXhI=,tag:G+W6hHA+yftQ+4RJpXrxHg==,type:comment]
switch: switch:
admin: admin:
@@ -368,7 +398,7 @@ sops:
k+uyWPPi+enFkVaceSwMFrA66uaWWrwAj11sXEB7yzvGFPrnAGezjQ== k+uyWPPi+enFkVaceSwMFrA66uaWWrwAj11sXEB7yzvGFPrnAGezjQ==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age120wuwcmsm845ztsvsz46pswj5je53uc2n35vadklrfqudu6cxuusxetk7y recipient: age120wuwcmsm845ztsvsz46pswj5je53uc2n35vadklrfqudu6cxuusxetk7y
lastmodified: "2026-08-27T14:09:38Z" lastmodified: "2026-09-08T03:23:38Z"
mac: ENC[AES256_GCM,data:K+SwIrdo62opVGnsrMH7FysC8GtMN1RGPGmtUxRKLoNy/Yi/tKxjVH1xiu1ePCWXWqpGjuC7btJdel3SZmwDi4jMhN87DadOh1Kf0sIGDe8+Ff1LrLLl0I4+NWpu/RJicXwxOXkZUwdEZ0sqTW2Qm1S7aknPpSoD9vc44epv1AA=,iv:npBPZiCeb3+7s+xHd/XhOhEnxvAJzeZWGFzW98uFSK0=,tag:KhQcf6/SVDtbgpiZO/7VNA==,type:str] mac: ENC[AES256_GCM,data:+mTAe1NwSbQHsmaVxKrtIDeRyyKbDVHSRCkji5+hAByS/GS4OzK+SwdyGYa5Ihv+g2J/7Pechf44GY8mv0lirWcOTGxqzvMDiRSXNty+aQVzOn9sXbfCvWXBUqDLNOiNISN0RfF1eVlG12D8wNfFEXFbs6lv81IMIN9ctdvTN5c=,iv:CG6Px0p/EomUafLody2kJOR8AzdmPTqe1ry1DZUhNSc=,tag:ClSWU401Jt6PzhcSCNNvbg==,type:str]
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.13.2 version: 3.13.2
@@ -13,6 +13,7 @@ PublishPort={{ services['collabora']['ports']['http'] }}:9980/tcp
Environment="TZ={{ timezone }}" Environment="TZ={{ timezone }}"
Environment="aliasgroup1=https://{{ services['nextcloud']['domain']['public'] }}.{{ domain['public'] }}" Environment="aliasgroup1=https://{{ services['nextcloud']['domain']['public'] }}.{{ domain['public'] }}"
Environment="aliasgroup2=https://{{ services['groupoffice']['domain']['public'] }}.{{ domain['public'] }}"
# Environment="aliasgroup2=other_server_FQDN" # Environment="aliasgroup2=other_server_FQDN"
Environment="extra_params=--o:ssl.enable=false --o:ssl.termination=true --o:server_name={{ services['collabora']['domain']['public'] }}.{{ domain['public'] }} --o:admin_console.enable=false" Environment="extra_params=--o:ssl.enable=false --o:ssl.termination=true --o:server_name={{ services['collabora']['domain']['public'] }}.{{ domain['public'] }} --o:admin_console.enable=false"
@@ -0,0 +1,519 @@
<?php
namespace go\modules\community\imapauthenticator\model;
use Exception;
use go\core\auth\PrimaryAuthenticator;
use go\core\imap\Connection;
use go\core\model\User;
use GO\Email\Model\Account;
use function GO;
class Authenticator extends PrimaryAuthenticator
{
public static function id(): string
{
return "imap";
}
/**
* Resolve a login name into a Group-Office username and e-mail address.
*
* Full e-mail:
* foo@example.com
* -> username = foo
* -> email = foo@example.com
*
* Short username:
* foo
* -> allowed only when exactly one concrete IMAP domain is configured
* -> username = foo
* -> email = foo@configured-domain
*
* Wildcard (*) domains are not used for short-name inference.
*
* @param string $login
* @return array{username:string,email:string}|null
*/
private static function resolveIdentity(string $login): ?array
{
$login = trim($login);
if($login === '') {
return null;
}
$atPos = strpos($login, '@');
if($atPos !== false) {
if($atPos === 0 || $atPos === strlen($login) - 1) {
return null;
}
return [
'username' => substr($login, 0, $atPos),
'email' => $login
];
}
/*
* For a short username, infer the mail domain from the
* configured IMAP authenticator domains.
*
* This is only unambiguous when exactly one concrete
* domain is configured.
*/
$stmt = go()->getDbConnection()->query(
"SELECT DISTINCT `name`
FROM `imapauth_server_domain`
WHERE `name` <> '*'
AND `name` <> ''
ORDER BY `name`"
);
$domains = $stmt->fetchAll(\PDO::FETCH_COLUMN);
if(count($domains) !== 1) {
return null;
}
return [
'username' => $login,
'email' => $login . '@' . $domains[0]
];
}
public static function isAvailableFor(string $username): bool
{
$identity = static::resolveIdentity($username);
if(!$identity) {
return false;
}
/*
* Existing Group-Office users with a local password must be
* authenticated locally.
*
* This keeps local/break-glass administrator accounts such as
* "local_admin" out of the IMAP authentication flow.
*
* IMAP-managed users have their local password cleared after
* successful IMAP authentication, so they continue to use IMAP.
*/
$localUser = User::find()
->where([
'username' => $identity['username']
])
->single();
if($localUser && $localUser->hasPassword()) {
return false;
}
return static::findServer($identity['email']) !== false;
}
/**
* Find the IMAP authentication server for an e-mail address.
*
* @param string $email
* @return Server|boolean
*/
private static function findServer($email)
{
$atPos = strpos($email, '@');
if(!$atPos) {
return false;
}
$domain = substr($email, $atPos + 1);
return Server::find()
->join(
'imapauth_server_domain',
'd',
's.id = d.serverId'
)
->where([
'd.name' => $domain
])
->orWhere([
'd.name' => '*'
])
->single();
}
public function authenticate(
string $username,
string $password
): bool|User
{
$this->fireEvent(
'beforeauthenticate',
[$username]
);
$identity = static::resolveIdentity($username);
if(!$identity) {
return false;
}
$localUsername = $identity['username'];
$email = $identity['email'];
$server = static::findServer($email);
if(!$server) {
return false;
}
go()->debug(
"Attempting IMAP authentication on " .
$server->imapHostname
);
$connection = new Connection();
if(!$connection->connect(
$server->imapHostname,
$server->imapPort,
$server->imapEncryption == 'ssl',
10,
!$server->imapValidateCertificate
)) {
throw new Exception(
"Could not connect to IMAP server"
);
}
if($server->imapEncryption == 'tls') {
if(!$connection->startTLS()) {
throw new Exception(
"Could not enable TLS on " .
$server->imapHostname .
" at port " .
$server->imapPort
);
}
}
/*
* Username sent to the IMAP server.
*
* Remove domain from username = No
* -> foo@example.com
*
* Remove domain from username = Yes
* -> foo
*/
$imapUsername = $server->removeDomainFromUsername
? $localUsername
: $email;
if(!$connection->authenticate(
$imapUsername,
$password
)) {
return false;
}
/*
* Group-Office username is the local-part.
*/
$user = User::find()
->where([
'username' => $localUsername
])
->single();
/*
* Prevent the same short username from being associated
* with two different e-mail addresses.
*/
if(
$user &&
strcasecmp(
(string) $user->email,
$email
) !== 0
) {
throw new Exception(
"Username '" .
$localUsername .
"' already belongs to another e-mail address"
);
}
/*
* Migrate an account previously created by the stock
* IMAP authenticator:
*
* username = foo@example.com
* email = foo@example.com
*
* to:
*
* username = foo
* email = foo@example.com
*
* The existing User ID is preserved.
*/
if(!$user) {
$user = User::find()
->where([
'username' => $email
])
->single();
if(!$user) {
$user = User::find()
->where([
'email' => $email
])
->single();
}
if($user) {
$user->username = $localUsername;
}
}
if(!$user) {
$user = $this->createUser(
$localUsername,
$email
);
} elseif($user->hasPassword()) {
$user->clearPassword();
}
foreach($server->groups as $group) {
$user->addGroup($group->groupId);
}
if($user->isModified()) {
if(!$user->save()) {
throw new Exception(
"Could not save user"
);
}
}
/*
* Synchronize the user's actual Email Account with the
* global IMAP Authenticator server profile on every
* successful login.
*/
$this->setEmailAccount(
$imapUsername,
$password,
$email,
$server,
$user
);
return $user;
}
/**
* Create a new Group-Office user.
*
* @throws Exception
*/
private function createUser(
string $username,
string $email
): User
{
$user = new User();
$user->displayName = $username;
$user->username = $username;
$user->email = $email;
$user->recoveryEmail = $email;
if(!$user->save()) {
throw new Exception(
"Could not save user after successful IMAP login"
);
}
return $user;
}
/**
* Synchronize the user's Email Account with the global
* IMAP Authenticator server profile on every successful login.
*
* The following settings are authoritative:
*
* - IMAP host
* - IMAP port
* - IMAP encryption
* - IMAP certificate validation
* - IMAP username
* - IMAP password
* - SMTP host
* - SMTP port
* - SMTP encryption
* - SMTP certificate validation
* - SMTP authentication credentials
*
* User-specific mailbox, alias and other unrelated settings
* are preserved.
*
* @throws Exception
*/
private function setEmailAccount(
$username,
$password,
$email,
Server $server,
User $user
) {
if(!$user->hasModule('legacy', 'email')) {
return;
}
/*
* Find all existing e-mail accounts belonging to this user.
*
* Do not use the configured IMAP hostname as an identifier.
* The administrator may change the global hostname later,
* and in that case we want to update the existing account
* instead of creating a duplicate.
*/
$accounts = Account::model()
->findByAttributes([
'user_id' => $user->id
])
->fetchAll();
$account = null;
/*
* Prefer an account whose IMAP username matches either
* the current IMAP login username or the full e-mail
* address.
*/
foreach($accounts as $candidate) {
if(
$candidate->username === $username ||
$candidate->username === $email
) {
$account = $candidate;
break;
}
}
/*
* If this user has exactly one e-mail account, treat that
* account as the one managed by the IMAP authenticator.
*
* This allows changes to username/hostname configuration
* without accidentally creating a duplicate account.
*/
if(!$account && count($accounts) === 1) {
$account = $accounts[0];
}
$wasNew = false;
if(!$account) {
$account = new Account();
$account->user_id = $user->id;
$account->mbroot = "";
$wasNew = true;
}
/*
* IMAP Authenticator server profile is authoritative.
*
* Refresh IMAP configuration on every successful login.
*/
$account->host =
$server->imapHostname;
$account->port =
$server->imapPort;
$account->username =
$username;
$account->password =
$password;
$account->imap_encryption =
$server->imapEncryption ?? "";
$account->imap_allow_self_signed =
!$server->imapValidateCertificate;
/*
* Refresh SMTP configuration from the same global
* authentication server profile.
*/
$account->smtp_host =
$server->smtpHostname;
$account->smtp_port =
$server->smtpPort;
$account->smtp_encryption =
$server->smtpEncryption ?? "";
$account->smtp_allow_self_signed =
!$server->smtpValidateCertificate;
/*
* SMTP authentication.
*/
if($server->smtpUseUserCredentials) {
/*
* Use the same credentials that successfully
* authenticated against IMAP.
*/
$account->smtp_username =
$username;
$account->smtp_password =
$password;
} else {
/*
* Use static SMTP credentials configured in the
* global IMAP Authenticator server profile.
*/
$account->smtp_username =
$server->smtpUsername;
$account->smtp_password =
$server->getSmtpPassword();
}
/*
* Verify the resulting IMAP configuration while saving.
*/
$account->checkImapConnectionOnSave = true;
if(!$account->save(true)) {
throw new Exception(
"Could not save e-mail account: " .
implode(
"\n",
$account->getValidationErrors()
)
);
}
if($wasNew) {
$account->addAlias(
$email,
$user->displayName
);
}
}
}
@@ -0,0 +1,736 @@
<?php
namespace go\core\db;
use Exception;
use go\core\App;
use go\core\orm\Property;
use LogicException;
use PDO;
use PDOException;
use PDOStatement;
use Throwable;
/**
* The database connection object. It uses PDO to connect to the database.
*
* The app instance of this connection is available by calling:
*
* ````````````````````````````````````````````````````````````````````````````
* \go\core\App::get()->getDbConnection();
* ````````````````````````````````````````````````````````````````````````````
*
* @copyright (c) 2014, Intermesh BV http://www.intermesh.nl
* @author Merijn Schering <mschering@intermesh.nl>
* @license http://www.gnu.org/licenses/agpl-3.0.html AGPLv3
*/
class Connection {
private $dsn;
private $username;
private $password;
private $options;
/**
*
* @var PDO
*/
private $pdo;
/**
* Output all SQL to the debugger
*
* @var bool
*/
public $debug = false;
public function __construct($dsn, $username, $password) {
$initCmd = "SET character_set_client = utf8mb4,
character_set_connection = utf8mb4,
character_set_results = utf8mb4,
sql_mode = 'STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION',
time_zone = '+00:00',
lc_messages = 'en_US'";
$this->dsn = $dsn;
$this->username = $username;
$this->password = $password;
$this->options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_PERSISTENT => false, // Unit test on closing DB connection fails. We need this to work for long running processes
PDO::ATTR_EMULATE_PREPARES => false, //for native data types int, bool etc.
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
];
if(version_compare(PHP_VERSION, '8.5.0') >= 0) {
$this->options[PDO\Mysql::ATTR_USE_BUFFERED_QUERY] = true;
$this->options[PDO\Mysql::ATTR_INIT_COMMAND] = $initCmd;
} else {
$this->options[PDO::MYSQL_ATTR_USE_BUFFERED_QUERY] = true;
$this->options[PDO::MYSQL_ATTR_INIT_COMMAND] = $initCmd;
}
/*
* Database TLS
*
* SSL attributes must be passed when PDO is constructed.
*/
if (defined('GO_DB_SSL_CA') && GO_DB_SSL_CA !== '') {
$verifyServerCert =
defined('GO_DB_SSL_VERIFY_SERVER_CERT')
? GO_DB_SSL_VERIFY_SERVER_CERT
: true;
if(version_compare(PHP_VERSION, '8.5.0') >= 0) {
$this->options[PDO\Mysql::ATTR_SSL_CA] =
GO_DB_SSL_CA;
$this->options[PDO\Mysql::ATTR_SSL_VERIFY_SERVER_CERT] =
$verifyServerCert;
} else {
$this->options[PDO::MYSQL_ATTR_SSL_CA] =
GO_DB_SSL_CA;
$this->options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] =
$verifyServerCert;
}
}
}
public function getDsn() {
return $this->dsn;
}
/**
* Gets the global database connection object.
*
* {@link http://php.net/manual/en/pdo.construct.php}
*
* @return PDO Database connection object
*/
public function getPDO(): PDO
{
if (!isset($this->pdo)) {
$this->setPDO();
}
return $this->pdo;
}
private $database;
/**
* Get the database instance
*
* @return Database
*/
public function getDatabase(): Database
{
if(!isset($this->database)) {
$this->database = new Database($this);
}
return $this->database;
}
/**
* Close the database connection. Beware that all active PDO statements must be set to null too
* in the current scope.
*
* Weird things happen when using fsockopen. This test case leaves the conneciton open. When removing the fputs call it seems to work.
*
*
$settings = \GO\Sync\Model\Settings::model()->findForUser(\go\core\App::get()->user());
$account = \GO\Email\Model\Account::model()->findByPk($settings->account_id);
$handle = stream_socket_client("tcp://localhost:143");
$login = 'A1 LOGIN "admin@intermesh.dev" "admin"'."\r\n";
fputs($handle, $login);
fclose($handle);
$handle=null;
echo "Test\n";
\go\core\App::get()->unsetDbConnection();
sleep(10);
*/
public function disconnect(): void
{
$this->pdo = null;
Property::clearCachedRelationStmts();
Property::$lastDeleteStmt = null;
self::$cachedStatements = [];
}
/**
* Checks if connection still exists
*
* @param int $id
* @return bool
* @throws DbException
*/
public static function exists(int $id) : bool {
$dsn = go()->getDbConnection()->getDsn();
$config = go()->getConfig();
$watchConn = new Connection(
$dsn, $config['db_user'], $config['db_pass']
);
$processes = $watchConn->query("SHOW PROCESSLIST")->fetchAll(\PDO::FETCH_ASSOC);
foreach($processes as $process) {
if($process['Id'] == $id) {
return true;
}
}
return false;
}
private static array $cachedStatements = [];
/**
* Register a cached statement that will be unset when we want to disconnect from the database.
* If you don't unset these statement the connection will be kept open.
* @param string $name
* @param Statement $statement
* @return void
*/
public function cacheStatement(string $name, Statement $statement): void
{
self::$cachedStatements[$name] = $statement;
}
public function getCachedStatment(string $name) : ?Statement {
return self::$cachedStatements[$name] ?? null;
}
/**
* Set's a new PDO object base on the current connection settings
*/
private function setPDO() {
$this->pdo = null;
$this->pdo = new PDO($this->dsn, $this->username, $this->password, $this->options);
// go()->debug("PDO Driver: " . $this->pdo->getAttribute(PDO::ATTR_CLIENT_VERSION));
// if (strpos($this->pdo->getAttribute(PDO::ATTR_CLIENT_VERSION), 'mysqlnd') !== false) {
// echo 'PDO MySQLnd enabled!';
// }
}
/**
* Get MySQL connection ID
*
* @return int
*/
public function getId() : int {
return (int) $this->query("select connection_id()")->fetch(PDO::FETCH_COLUMN, 0);
}
/**
* Execute an SQL string
*
* Should be properly escaped!
* {@link http://php.net/manual/en/pdo.query.php}
*
* @param string $sql
* @return PDOStatement
* @throws DbException
*/
public function query(string $sql): PDOStatement
{
if($this->debug) {
go()->getDebugger()->debug($sql, 1 ,false);
}
try {
return $this->getPdo()->query($sql);
}
catch(PDOException $e) {
throw new DbException($e, $sql);
}
}
/**
* Execute an SQL statement and return the number of affected rows
* <p><b>PDO::exec()</b> executes an SQL statement in a single function call, returning the number of rows affected by the statement.</p><p><b>PDO::exec()</b> does not return results from a SELECT statement. For a SELECT statement that you only need to issue once during your program, consider issuing <code>PDO::query()</code>. For a statement that you need to issue multiple times, prepare a PDOStatement object with <code>PDO::prepare()</code> and issue the statement with <code>PDOStatement::execute()</code>.</p>
* @param string $sql <p>The SQL statement to prepare and execute.</p> <p>Data inside the query should be properly escaped.</p>
* @return int <p><b>PDO::exec()</b> returns the number of rows that were modified or deleted by the SQL statement you issued. If no rows were affected, <b>PDO::exec()</b> returns <i>0</i>.</p><p><b>Warning</b></p><p>This function may return Boolean <b><code>FALSE</code></b>, but may also return a non-Boolean value which evaluates to <b><code>FALSE</code></b>. Please read the section on Booleans for more information. Use the === operator for testing the return value of this function.</p><p>The following example incorrectly relies on the return value of <b>PDO::exec()</b>, wherein a statement that affected 0 rows results in a call to <code>die()</code>:</p> <code> &lt;&#63;php<br>$db-&gt;exec()&nbsp;or&nbsp;die(print_r($db-&gt;errorInfo(),&nbsp;true));&nbsp;//&nbsp;incorrect<br>&#63;&gt; </code>
* @link http://php.net/manual/en/pdo.exec.php
*
* @throws DbException
*/
public function exec(string $sql): int
{
if($this->debug) {
go()->getDebugger()->debug($sql, 1, true);
}
try {
return $this->getPdo()->exec($sql);
}
catch(PDOException $e) {
throw new DbException($e, $sql);
}
}
/**
* UNLOCK TABLES explicitly releases any table locks held by the current session
*/
public function unlockTables(): bool
{
return $this->getPdo()->exec("UNLOCK TABLES") !== false;
}
private $transactionSavePointLevel = 0;
/**
* Start a database transation
*
* @return boolean
*/
public function beginTransaction(): bool
{
if($this->transactionSavePointLevel == 0) {
$ret = $this->getPdo()->beginTransaction();
}else
{
$ret = true;
}
$this->transactionSavePointLevel++;
if($this->debug) {
go()->debug("beginTransaction " . $this->transactionSavePointLevel, 1);
}
return $ret;
}
private $resumeLevels = 0;
public function isPaused(): bool
{
return $this->resumeLevels > 0;
}
/**
* Commits the transaction but remembers the nesting of transaction nesting level. Must be resumed with resumeTransactions().
* This is used by custom fields that do database structure changes. MySQL commits transactions automatically when the database
* structure changes.
*
*/
public function pauseTransactions() {
if($this->isPaused()) {
return;
}
$this->resumeLevels = $this->transactionSavePointLevel;
while($this->transactionSavePointLevel > 0) {
$this->commit();
}
}
/**
* @see pauseTransactions()
*/
public function resumeTransactions() {
while($this->resumeLevels > 0) {
$this->beginTransaction();
$this->resumeLevels--;
}
}
/**
* Rollback the DB transaction
*
* @return boolean
*/
public function rollBack(): bool
{
if($this->transactionSavePointLevel == 0) {
throw new LogicException("Not in transaction!");
}
$this->transactionSavePointLevel--;
if($this->debug) {
go()->warn("ROLLBACK DB TRANSACTION " . $this->transactionSavePointLevel, 1);
}
if($this->transactionSavePointLevel == 0) {
return $this->getPdo()->rollBack();
}else
{
return true;
}
}
/**
* Commit the database transaction
*
* @return boolean
* @throws PDOException
*/
public function commit(): bool
{
if($this->transactionSavePointLevel == 0) {
throw new PDOException("Not in transaction!");
}
$this->transactionSavePointLevel--;
if($this->debug) {
go()->debug("COMMIT DB TRANSACTION " . $this->transactionSavePointLevel, 1);
}
if($this->transactionSavePointLevel == 0) {
return $this->getPdo()->commit();
}else
{
return true;
}
}
/**
* Check if a transaction is active
*
* @return boolean
*/
public function inTransaction(): bool
{
return $this->getPdo()->inTransaction();
}
/**
* Lock the table of this model and also for the 't' alias.
*
* The locks array should be indexed by model name and the value is an array with two optional values.
* THe first is a boolean that enables a write-lock and the second is a table alias.
*
* @param array $locks eg. [go\cores\Users\Model\User::tableName() => [true, 't']]
*
* @throws PDOException
*/
public function lock(array $locks)
{
$sql = "LOCK TABLES ";
foreach ($locks as $tableName => $lockInfo) {
$sql .= $tableName . " ";
if (isset($lockInfo[1])) {
$sql .= ' AS ' . $lockInfo[1] . ' ';
}
$sql .= empty($lockInfo[0]) ? 'READ' : 'WRITE';
$sql .= ', ';
}
$sql = rtrim($sql, ', ');
App::get()->getDbConnection()->exec($sql);
}
/**
* Create a delete statement
*
* @param string $tableName
* @param Query|array|string|null $query
* @return Statement
* @example
* ```
* $success = App::get()
* ->getDbConnection()
* ->delete("test_a", ['id' => 1])
* ->execute();
* ```
*/
public function delete(string $tableName, $query = null): Statement
{
$query = Query::normalize($query);
$queryBuilder = new QueryBuilder($this);
$build = $queryBuilder->buildDelete($tableName, $query);
return $this->createStatement($build);
}
/**
* Create an insert statement
*
* @param string $tableName
* @param array|Query $data Key value array, a numeric array with key value arrays or select query
* @param string[] $columns If $data is a query object then you can supply the
* selected columns with this parameter. If not given all columns must be
* selected in the correct order.
*
* @return Statement
* @throws PDOException
* @example Single record
* ```
* $data = [
* "propA" => "string 1",
* "createdAt" => new \DateTime(),
* "modifiedAt" => new \DateTime()
* ];
*
* $result = App::get()
* ->getDbConnection()
* ->insert("test_a", $data)
* ->execute();
* ```
*
* Get the ID if it has an auto increment column:
* ```
* $id = App::get()->getDbConnection()->getPDO()->lastInsertId();
* ```
*
*
* @example Multiple records
* ```
* $data = [[
* "propA" => "string 1",
* "createdAt" => new \DateTime(),
* "modifiedAt" => new \DateTime()
* ],[
* "propA" => "string 2",
* "createdAt" => new \DateTime(),
* "modifiedAt" => new \DateTime()
* ]];
*
* $result = App::get()
* ->getDbConnection()
* ->insert("test_a", $data)
* ->execute();
* ```
*
* Or with an expression:
*
* ```
* App::get()->getDbConnection()
* ->update("core_state", new \go\core\db\Expression("highestModSeq = highestModSeq + 1"), $query);
* ```
*
*/
public function insert(string $tableName, $data, array $columns = []): Statement
{
$queryBuilder = new QueryBuilder($this);
$build = $queryBuilder->buildInsert($tableName, $data, $columns);
return $this->createStatement($build);
}
/**
* Insert data in the database and ignore if the records exist
*
* @param string $tableName
* @param array|Query $data Key value array or select query
* @param string[] $columns If $data is a query object then you can supply the
* selected columns with this parameter. If not given all columns must be
* selected in the correct order.
*
* @return Statement
* @throws DbException
* @see insert()
*/
public function insertIgnore(string $tableName, Query|array $data, array $columns = []): Statement
{
$queryBuilder = new QueryBuilder($this);
$build = $queryBuilder->buildInsert($tableName, $data, $columns, "INSERT IGNORE");
return $this->createStatement($build);
}
/**
* Replace data in the database
*
* @param string $tableName
* @param array|Query $data Key value array, array of key value arrays for multi insert or select query
* @param string[] $columns If $data is a query object then you can supply the
* selected columns with this parameter. If not given all columns must be
* selected in the correct order.
*
* @return Statement
* @throws Throwable
* @see insert()
*/
public function replace(string $tableName, Query|array $data, array $columns = []): Statement
{
$queryBuilder = new QueryBuilder($this);
$build = $queryBuilder->buildInsert($tableName, $data, $columns, "REPLACE");
return $this->createStatement($build);
}
/**
* Create an update statement
*
* @param string $tableName
* @param $data
* @param Query|string|array|null $query {@see Query::normalize()}
* @return Statement
* @throws PDOException
* @example with join
* ```
* go()->getDbConnection()->update(
* 'core_acl',
* [
* 'acl.entityTypeId' => $entityTypeId,
* 'acl.entityId' => new Expression('ab.id')], // Use go\core\db\Expression for references to tables
* (new Query())
* ->tableAlias('acl') // set alias for core_acl table
* ->join('addressbook_addressbook, 'ab', 'ab.aclId = acl.id'))
* ->execute();
* ```
*
*
* @example
* ```
* $data = [
* "propA" => "string 3",
* "count" => new \go\core\db\Expression('count + 1'), //Example for expression
* ];
*
* $stmt = App::get()->getDbConnection()->update("test_a", $data, ['id' => 1]);
* $stmt->execute();
* ```
*
*/
public function update(string $tableName, $data, Criteria|array|string|null $query = null): Statement
{
$query = Query::normalize($query);
$queryBuilder = new QueryBuilder($this);
$build = $queryBuilder->buildUpdate($tableName, $data, $query);
return $this->createStatement($build);
}
/**
* Update but with ignore
*
* @param $tableName
* @param $data
* @param Criteria|array|string|null $query
* @return Statement
* @throws Exception
* @see update()
*/
public function updateIgnore($tableName, $data, Criteria|array|string|null $query = null): Statement
{
$query = Query::normalize($query);
$queryBuilder = new QueryBuilder($this);
$build = $queryBuilder->buildUpdate($tableName, $data, $query, "UPDATE IGNORE");
return $this->createStatement($build);
}
/**
* Create a select statement.
*
* @param string|string[] $select
* @return Query
* @example
* ```
* $query = go()->getDbConnection()
* ->select('*')
* ->from('test_a')
* ->where('id', '=', 1);
*
* $stmt = $query->execute();
*
* ```
*
* @see Query
*/
public function select(array|string $select = "*"): Query
{
$query = new Query();
return $query->setDbConnection($this)->select($select);
}
/**
* Select a single column or count(*) for example.
*
* Shortcut for:
* $query->fetchMode(\PDO::FETCH_COLUMN,0)->select($select)
*
* @param string $select
* @return Query
*/
public function selectSingleValue(string $select): Query
{
$query = new Query();
return $query->setDbConnection($this)->selectSingleValue($select);
}
/**
* Create a statement from a QueryBuilder result.
*
* For internal use of the API.
*
* @param array $build
* @return Statement
* @throws Exception
*/
public function createStatement(array &$build): Statement
{
try {
if($this->debug) {
$build['start'] = go()->getDebugger()->getMicroTime();
}
$pdoStmt = $this->getPDO()->prepare($build['sql']);
$stmt = new Statement($pdoStmt);
$stmt->setBuild($build);
for ($i =0, $l = count($build['params']); $i < $l; $i++) {
$stmt->bindValue($i + 1, $build['params'][$i]['value'], $build['params'][$i]['pdoType']);
}
return $stmt;
} catch(PDOException $e) {
throw new DbException($e, go()->getDebugger()->enabled ? QueryBuilder::debugBuild($build) : null);
} catch(Exception $e) {
if(go()->getDebugger()->enabled) {
go()->debug(QueryBuilder::debugBuild($build));
}
throw $e;
}
}
/**
* Get or set foreign key checks
*
* @param bool|null $value Provide to set new value
* @return bool The current or old value
* @throws DbException
*/
public function foreignKeyChecks(bool|null $value = null): bool
{
$stmt = $this->query("SELECT @@SESSION.foreign_key_checks");
$current = !!$stmt->fetch(PDO::FETCH_COLUMN, 0);
if(isset($value) && $value != $current) {
$this->exec("set foreign_key_checks = " . ($value ? "1" : "0") . ";");
}
return $current;
}
}
@@ -0,0 +1,36 @@
<?php
require "docker-config.php";
$config['db_port'] = 3306;
/*
* Database TLS
*/
$config['db_ssl_ca'] = "/etc/ssl/groupoffice/{{ root_cert_filename }}";
$config['db_ssl_verify_server_cert'] = true;
/*
* Connection.php is instantiated while the application is being initialized.
* Export DB TLS settings as constants to avoid calling App::get() recursively
* from the Connection constructor.
*/
if (!defined('GO_DB_SSL_CA')) {
define('GO_DB_SSL_CA', $config['db_ssl_ca']);
}
if (!defined('GO_DB_SSL_VERIFY_SERVER_CERT')) {
define(
'GO_DB_SSL_VERIFY_SERVER_CERT',
$config['db_ssl_verify_server_cert']
);
}
$config['file_storage_path'] = "/var/lib/groupoffice";
$config['tmpdir'] = "/tmp/groupoffice";
$config['debug'] = false;
$config['business'] = array (
'studio' =>
array (
'package' => 'studio',
)
);
@@ -0,0 +1,252 @@
<?php
require('../vendor/autoload.php');
use go\core\App;
use go\core\fs\Folder;
use go\core\util\IniFile;
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
$dbConnectError = null;
$pdo = null;
/**
*
* @global PDO $pdo
* @param type $config
* @return PDO
*/
function dbConnect($config){
global $pdo, $dbConnectError;
if(isset($pdo)) {
return $pdo;
}
$dsn = go()->createDsn($config['db_name'] ?? "groupoffice", $config);
/*
* Database TLS
*/
$options = [];
if (!empty($config['db_ssl_ca'])) {
$verifyServerCert =
$config['db_ssl_verify_server_cert'] ?? true;
if (version_compare(PHP_VERSION, '8.5.0') >= 0) {
$options[PDO\Mysql::ATTR_SSL_CA] =
$config['db_ssl_ca'];
$options[PDO\Mysql::ATTR_SSL_VERIFY_SERVER_CERT] =
$verifyServerCert;
} else {
$options[PDO::MYSQL_ATTR_SSL_CA] =
$config['db_ssl_ca'];
$options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] =
$verifyServerCert;
}
}
try {
$pdo = new PDO(
$dsn,
$config['db_user'],
$config['db_pass'],
$options
);
}
/*
* Database TLS
*/
catch(Exception $e){
$dbConnectError = "Could not connect to the database with DSN = '" . $dsn . "' with username '" . ($config['db_user'] ?? "groupoffice") . " and " . (empty($config['db_pass']) ? "NOT using password" : "USING a password") . ". The database returned this error:<br />".$e->getMessage();
}
if($pdo) {
$clientVersion = $pdo->getAttribute(PDO::ATTR_CLIENT_VERSION);
if (strpos($clientVersion, 'mysqlnd') === false) {
$dbConnectError = "PDO is not using the mysqlnd driver. Please make sure PDO uses mysqlnd. It's now using: " . $clientVersion;
$pdo = null;
}
}
return $pdo;
}
$configFile = App::findConfigFile();
if(!$configFile) {
$configFile = '/etc/groupoffice/config.php';
} else
{
require($configFile);
}
$mustSave = false;
if($_SERVER['REQUEST_METHOD'] != 'POST')
{
$config['file_storage_path'] = $config['file_storage_path'] ?? "/var/lib/groupoffice";
$config['tmpdir'] = $config['tmpdir'] ?? "/tmp/groupoffice";
//check if config.ini is already prefilled with usable data (Docker)
$tmpFolder = new Folder($config['tmpdir']);
$dataFolder = new Folder($config['file_storage_path']);
if(!$dataFolder->isWritable() && isset($_SERVER['DOCUMENT_ROOT'])) {
$dataFolder = new Folder(dirname($_SERVER['DOCUMENT_ROOT']) . '/groupoffice');
$mustSave = true;
}
if(dbConnect($config) && $dataFolder->isWritable() && $tmpFolder->isWritable() && !$mustSave) {
header("Location: install.php");
exit();
}
$_POST['dbPort'] = $config['db_port'] ?? 3306;
$_POST['dbHostname'] = $config['db_host'] ?? 'localhost';
$_POST['dbName'] = $config['db_name'] ?? "groupoffice";
$_POST['dbUsername'] = $config['db_user'] ?? "groupoffice";
//$_POST['dbPassword'] = $config['db']['password'];
} else
{
// $config['general']['dataPath'] = $_POST['dataPath'];
// $config['general']['tmpPath'] = $_POST['tmpPath'];
// $config['db']['dsn'] = 'mysql:host='.$_POST['dbHostname'].';port='.$_POST['dbPort'].';dbname='.$_POST['dbName'];
// $config['db']['username'] = $_POST['dbUsername'];
// $config['db']['password'] = $_POST['dbPassword'];
$config['file_storage_path'] = $_POST['dataPath'];
$config['tmpdir'] = $_POST['tmpPath'];
$config['db_host'] =$_POST['dbHostname'];
$config['db_name'] =$_POST['dbName'];
$config['db_user'] =$_POST['dbUsername'];
$config['db_pass'] =$_POST['dbPassword'];
$config['db_port'] =$_POST['dbPort'];
$tmpFolder = new Folder($config['tmpdir']);
$dataFolder = new Folder($config['file_storage_path']);
if($dataFolder->isWritable() && $tmpFolder->isWritable() && dbConnect($config)) {
$cFile = new \go\core\fs\File($configFile);
if(!$cFile->putContents("<?php\n\n\$config = ".var_export($config, true) .";\n\n")) {
$configError = "Could not write config.php";
} else
{
if(function_exists("opcache_invalidate")) {
opcache_invalidate($cFile->getPath());
}
header('Location: install.php');
exit();
}
}
}
require('header.php');
?>
<?php
try {
$isWritable = is_writable($configFile);
} catch(Throwable $e) {
//open basedir restriction
$isWritable = false;
}
if(!$isWritable):
?>
<section>
<fieldset>
<h2><?= go()->t("Config file"); ?></h2>
<p class="error">Please create a writeable config.php file here: <?= $configFile; ?>.</p>
</fieldset>
</section>
<?php endif ?>
<section>
<form method="POST" action="" onsubmit="submitButton.disabled = true;">
<fieldset>
<h2>Data storage</h2>
<p>
Please create a folder for storing Group-Office files and a temporary files folder and make sure the webserver user can write to these folders.
</p>
<?php
if(!$dataFolder->isWritable()) {
echo '<p class="error">Not writable</p>';
}
?>
<p>
<input type="text" name="dataPath" value="<?= htmlspecialchars($dataFolder->getPath()) ?>" placeholder="" required />
<label>Data folder</label>
</p>
<?php
if(!$tmpFolder->isWritable()) {
echo '<p class="error">Not writable</p>';
}
?>
<p>
<input type="text" name="tmpPath" value="<?= htmlspecialchars($config['tmpdir']); ?>" placeholder="Temp folder" required />
<label>Temp folder</label>
</p>
</fieldset>
<fieldset>
<h2>Configure the database</h2>
<?php
if(isset($config['db_host']) && !dbConnect($config)) {
echo '<p class="error">'.$dbConnectError.'</p>';
}
?>
<p>
<label>Database user</label>
<input type="text" name="dbUsername" value="<?= htmlspecialchars($_POST['dbUsername'] ?? "groupoffice"); ?>" required />
</p>
<p>
<label>Database password</label>
<input type="password" name="dbPassword" value="<?= htmlspecialchars($_POST['dbPassword'] ?? "");?>" required />
</p>
<p>
<label>Database name</label>
<input type="text" name="dbName" value="<?= htmlspecialchars($_POST['dbName'] ?? "groupoffice");?>" required />
</p>
<p>
<label>Database hostname</label>
<input type="text" name="dbHostname" value="<?= htmlspecialchars($_POST['dbHostname'] ?? "localhost");?>" required />
</p>
<p>
<label>Database port</label>
<input type="text" name="dbPort" value="<?= htmlspecialchars($_POST['dbPort'] ?? 3306); ?>" required />
</p>
<button class="primary right" name="submitButton"><?= go()->t('Continue'); ?></button>
</fieldset>
</form>
</section>
<?php require('footer.php'); ?>
@@ -0,0 +1 @@
openssl.cafile=/etc/ssl/groupoffice/{{ root_cert_filename }}
@@ -0,0 +1,41 @@
[Quadlet]
DefaultDependencies=false
[Unit]
Description=GroupOffice
[Container]
Image=docker.io/intermesh/groupoffice:{{ version['containers']['groupoffice'] }}
ContainerName=groupoffice
HostName=groupoffice
PublishPort={{ services['groupoffice']['ports']['http'] }}:80/tcp
Volume=%h/data/containers/groupoffice/data:/var/lib/groupoffice:rw
Volume=%h/containers/groupoffice/ssl/{{ root_cert_filename }}:/etc/ssl/groupoffice/{{ root_cert_filename }}:ro
# Keep the official image intact.
Volume=%h/containers/groupoffice/config/config.php:/etc/groupoffice/config.php:ro
Volume=%h/containers/groupoffice/config/groupoffice-ca.ini:/usr/local/etc/php/conf.d/99-groupoffice-ca.ini:ro
Volume=%h/containers/groupoffice/config/Authenticator.php:/usr/local/share/groupoffice/go/modules/community/imapauthenticator/model/Authenticator.php:ro
Volume=%h/containers/groupoffice/config/db_Connection.php:/usr/local/share/groupoffice/go/core/db/Connection.php:ro
Volume=%h/containers/groupoffice/config/configfile.php:/usr/local/share/groupoffice/install/configfile.php:ro
Environment="TZ={{ timezone }}"
# GroupOffice official Docker DB configuration
Environment="MYSQL_HOST={{ services['mariadb']['domain']['internal'] }}.{{ domain['internal'] }}"
Environment="MYSQL_DATABASE=groupoffice_db"
Environment="MYSQL_USER=groupoffice"
Secret=GROUPOFFICE_DB_PASSWORD,type=env,target=MYSQL_PASSWORD
[Service]
ExecStartPre=/usr/bin/nc -zv {{ services['mariadb']['domain']['internal'] }}.{{ domain['internal'] }} {{ services['mariadb']['ports']['tcp'] }}
ExecStartPre=/usr/bin/nc -zv {{ services['maddy']['domain']['internal'] }}.{{ domain['internal'] }} 993
ExecStartPost=-/usr/bin/podman exec -u www-data groupoffice php /usr/local/share/groupoffice/cli.php core/System/upgrade
Restart=always
RestartSec=10s
TimeoutStopSec=120
[Install]
WantedBy=default.target
@@ -0,0 +1,9 @@
[Unit]
Description=GroupOffice cron.php
Requires=groupoffice.service
After=groupoffice.service
[Service]
Type=oneshot
ExecStart=/usr/bin/podman exec -u www-data groupoffice \
php /usr/local/share/groupoffice/cron.php
@@ -0,0 +1,10 @@
[Unit]
Description=Run GroupOffice cron every 1 minutes
[Timer]
OnBootSec=1min
OnUnitActiveSec=1min
Unit=groupoffice-cron.service
[Install]
WantedBy=timers.target
@@ -1,6 +1,7 @@
{% set icon_map = { {% set icon_map = {
'ldap': 'lldap',
'tandoor': 'tandoor-recipes', 'tandoor': 'tandoor-recipes',
'ldap': 'lldap' 'openwebui': 'open-webui'
} %} } %}
- Services: - Services:
@@ -0,0 +1,48 @@
[Quadlet]
DefaultDependencies=false
[Unit]
Description=Kaneo
[Container]
Image=ghcr.io/usekaneo/kaneo:{{ version['containers']['kaneo'] }}
ContainerName=kaneo
HostName=kaneo
PublishPort={{ services['kaneo']['ports']['http'] }}:5173/tcp
Volume=%h/containers/kaneo/ssl:/etc/ssl/kaneo:ro
Environment="TZ={{ timezone }}"
Environment="DISABLE_PASSWORD_REGISTRATION=true"
Environment="DISABLE_REGISTRATION=false"
Environment="DISABLE_GUEST_ACCESS=true"
Environment="KANEO_CLIENT_URL=https://{{ services['kaneo']['domain']['public'] }}.{{ domain['public'] }}"
Secret=KANEO_AUTH_SECRET,type=env,target=AUTH_SECRET
# PostgreSQL
Environment="POSTGRES_HOST={{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }}"
Environment="POSTGRES_PORT={{ services['postgresql']['ports']['tcp'] }}"
Environment="POSTGRES_DB=kaneo_db"
Environment="POSTGRES_USER=kaneo"
Environment="PGSSLMODE=verify-full"
Environment="NODE_EXTRA_CA_CERTS=/etc/ssl/kaneo/{{ root_cert_filename }}"
Secret=KANEO_DB_PASSWORD,type=env,target=POSTGRES_PASSWORD
Environment="CUSTOM_OAUTH_CLIENT_ID=kaneo"
Environment="CUSTOM_OAUTH_DISCOVERY_URL=https://{{ services['authelia']['domain']['public'] }}.{{ domain['public'] }}/.well-known/openid-configuration"
Environment="CUSTOM_OAUTH_SCOPES=openid,profile,email"
Environment="CUSTOM_AUTH_PKCE=true"
Secret=KANEO_OIDC_CLIENT_SECRET,type=env,target=CUSTOM_OAUTH_CLIENT_SECRET
[Service]
ExecStartPre=/usr/bin/nc -zv {{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }} {{ services['postgresql']['ports']['tcp'] }}
Restart=always
RestartSec=10s
TimeoutStopSec=120
[Install]
WantedBy=default.target
@@ -0,0 +1,13 @@
version = 1
{% for models in (services['llama']['dependency']['models'] | dict2items | list) %}
{% if models['key'] == 'default' %}
[*]
{{ models['value'] }}
{% else %}
[{{ models['key'] }}]
{{ models['value'] }}
{% endif %}
{% endfor %}{{ '' }}
@@ -0,0 +1,34 @@
[Quadlet]
DefaultDependencies=false
[Unit]
Description=llama.cpp Model Server
[Container]
Image=ghcr.io/ggml-org/llama.cpp:{{ version['containers']['llama'] }}
ContainerName=llama
HostName=llama
PublishPort={{ services['llama']['ports']['http'] }}:9931/tcp
# iGPU access for Vulkan
AddDevice=/dev/dri:/dev/dri
PodmanArgs=--group-add keep-groups
Volume=%h/containers/llama/models:/models:ro
Volume=%h/containers/llama/config:/config:ro
Environment="TZ={{ timezone }}"
Exec=--models-preset /config/models.ini \
--models-max 2 \
--host 0.0.0.0 \
--port 9931
[Service]
Restart=always
RestartSec=10s
TimeoutStopSec=300
[Install]
WantedBy=default.target
@@ -0,0 +1,84 @@
[Quadlet]
DefaultDependencies=false
[Unit]
Description=Open WebUI
[Container]
Image=ghcr.io/open-webui/open-webui:{{ version['containers']['openwebui'] }}
ContainerName=openwebui
HostName=openwebui
PublishPort={{ services['openwebui']['ports']['http'] }}:8080/tcp
Volume=%h/data/containers/openwebui/data:/app/backend/data:rw
Volume=%h/containers/openwebui/ssl:/etc/ssl/openwebui:ro
# General
Environment="TZ={{ timezone }}"
Environment="WEBUI_URL=https://{{ services['openwebui']['domain']['public'] }}.{{ domain['public'] }}"
Environment="CORS_ALLOW_ORIGIN=https://{{ services['openwebui']['domain']['public'] }}.{{ domain['public'] }}"
Environment="ENABLE_PERSISTENT_CONFIG=false"
Environment="ENABLE_SIGNUP=false"
Environment="ENABLE_LOGIN_FORM=false"
Environment="ENABLE_PASSWORD_AUTH=false"
Environment="ENABLE_PASSWORD_CHANGE_FORM=false"
Secret=OPEN_WEBUI_SECRET_KEY,type=env,target=WEBUI_SECRET_KEY
# We use OpenAI-compatible backends, not Ollama
Environment="ENABLE_OLLAMA_API=false"
# PostgreSQL
Environment="DATABASE_TYPE=postgresql"
Environment="DATABASE_HOST={{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }}"
Environment="DATABASE_PORT={{ services['postgresql']['ports']['tcp'] }}"
Environment="DATABASE_NAME=openwebui_db"
Environment="DATABASE_USER=openwebui"
Environment="PGSSLMODE=verify-full"
Environment="PGSSLROOTCERT=/etc/ssl/openwebui/{{ root_cert_filename }}"
Environment="PGSSLCERTMODE=disable"
Secret=OPEN_WEBUI_POSTGRES_PASSWORD,type=env,target=DATABASE_PASSWORD
# OIDC
Environment="ENABLE_OAUTH_PERSISTENT_CONFIG=false"
Environment="ENABLE_OAUTH_SIGNUP=true"
Environment="OAUTH_AUTO_REDIRECT=false"
Environment="OAUTH_MERGE_ACCOUNTS_BY_EMAIL=true"
Environment="OAUTH_CLIENT_ID=openwebui"
Environment="OPENID_PROVIDER_URL=https://{{ services['authelia']['domain']['public'] }}.{{ domain['public'] }}/.well-known/openid-configuration"
Environment="OAUTH_PROVIDER_NAME=Authelia"
Environment="OAUTH_SCOPES=openid email profile groups"
Environment="ENABLE_OAUTH_ROLE_MANAGEMENT=true"
Environment="OAUTH_ALLOWED_ROLES=admins,users"
Environment="OAUTH_ADMIN_ROLES=admins"
Environment="OAUTH_ROLES_CLAIM=groups"
Environment="OAUTH_CODE_CHALLENGE_METHOD=S256"
Secret=OPEN_WEBUI_OAUTH_CLIENT_SECRET,type=env,target=OAUTH_CLIENT_SECRET
# AI backend server
# llama.cpp
Environment="OPENAI_API_BASE_URL=http://host.containers.internal:{{ services['llama']['ports']['http'] }}/v1"
Environment="OPENAI_API_KEY=none"
# stable-diffusion.cpp
Environment="ENABLE_IMAGE_GENERATION=true"
Environment="IMAGE_GENERATION_ENGINE=openai"
Environment="IMAGES_OPENAI_API_BASE_URL=http://host.containers.internal:{{ services['stablediffusion']['ports']['http'] }}/v1"
Environment="IMAGES_OPENAI_API_KEY=none"
Environment="IMAGE_GENERATION_MODEL=sd-cpp-local"
Environment="IMAGE_SIZE=1024x1024"
Environment="AIOHTTP_CLIENT_TIMEOUT=3600"
# Disable background LLM tasks
Environment="ENABLE_TITLE_GENERATION=false"
Environment="ENABLE_TAGS_GENERATION=false"
Environment="ENABLE_FOLLOW_UP_GENERATION=false"
Environment="ENABLE_AUTOCOMPLETE_GENERATION=false"
Environment="DEFAULT_MODEL_METADATA={\"builtinTools\":{\"user_input\":false}}"
[Service]
MemoryKSM=yes
ExecStartPre=/usr/bin/nc -zv {{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }} {{ services['postgresql']['ports']['tcp'] }}
Restart=always
RestartSec=10s
TimeoutStopSec=120
[Install]
WantedBy=default.target
@@ -0,0 +1,36 @@
[Quadlet]
DefaultDependencies=false
[Unit]
Description=stable-diffusion.cpp Image Generation Server
[Container]
Image=ghcr.io/leejet/stable-diffusion.cpp:{{ version['containers']['stablediffusion'] }}
ContainerName=stablediffusion
HostName=stablediffusion
PublishPort={{ services['stablediffusion']['ports']['http'] }}:1234/tcp
# iGPU access for container
AddDevice=/dev/dri:/dev/dri
PodmanArgs=--group-add keep-groups
Volume=%h/containers/stablediffusion/models:/models:ro
# General
Environment="TZ={{ timezone }}"
# Prevent Fence expiration time out f9r CYCL
Environment="SYCL_PI_LEVEL_ZERO_USE_IMMEDIATE_COMMANDLISTS=1"
Entrypoint=/sd-server
Exec=--listen-ip 0.0.0.0 \
--listen-port 1234 \
{{ services['stablediffusion']['dependency']['models'][stabeldiffusion_active_model] }}
[Service]
Restart=always
RestartSec=10s
TimeoutStopSec=300
[Install]
WantedBy=default.target
@@ -57,6 +57,17 @@ Environment="WEBAUTHN_ALLOWED_ORIGINS=https://{{ services['sure']['domain']['pub
Environment="EXCHANGE_RATE_PROVIDER=yahoo_finance" Environment="EXCHANGE_RATE_PROVIDER=yahoo_finance"
Environment="SECURITIES_PROVIDER=yahoo_finance" Environment="SECURITIES_PROVIDER=yahoo_finance"
# AI backend
Environment="ASSISTANT_TYPE=builtin"
Environment="OPENAI_URI_BASE=http://host.containers.internal:10000/v1"
Environment="OPENAI_ACCESS_TOKEN=local"
Environment="OPENAI_MODEL={{ services['llama']['dependency']['models'] | dict2items | rejectattr('key', 'equalto', 'default') | map(attribute='key') | last }}"
Environment="LLM_CONTEXT_WINDOW=8192"
Environment="OPENAI_REQUEST_TIMEOUT=600"
Environment="ASSISTANT_MAX_TOOL_CALL_ITERATIONS=2"
Environment="AI_RESPONSE_TIMEOUT=2400"
[Service] [Service]
MemoryKSM=yes MemoryKSM=yes
ExecStartPre=/usr/bin/nc -zv {{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }} {{ services['postgresql']['ports']['tcp'] }} ExecStartPre=/usr/bin/nc -zv {{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }} {{ services['postgresql']['ports']['tcp'] }}
@@ -33,6 +33,16 @@ Secret=SURE_POSTGRES_PASSWORD,type=env,target=POSTGRES_PASSWORD
# Redis # Redis
Environment="REDIS_URL=redis://host.containers.internal:{{ services['sure']['ports']['redis'] }}/1" Environment="REDIS_URL=redis://host.containers.internal:{{ services['sure']['ports']['redis'] }}/1"
# AI backend
Environment="ASSISTANT_TYPE=builtin"
Environment="OPENAI_URI_BASE=http://host.containers.internal:10000/v1"
Environment="OPENAI_ACCESS_TOKEN=local"
Environment="OPENAI_MODEL={{ services['llama']['dependency']['models'] | dict2items | rejectattr('key', 'equalto', 'default') | map(attribute='key') | last }}"
Environment="LLM_CONTEXT_WINDOW=8192"
Environment="OPENAI_REQUEST_TIMEOUT=600"
Environment="ASSISTANT_MAX_TOOL_CALL_ITERATIONS=2"
Environment="AI_RESPONSE_TIMEOUT=2400"
# Provider # Provider
## Currency ## Currency
Environment="EXCHANGE_RATE_PROVIDER=yahoo_finance" Environment="EXCHANGE_RATE_PROVIDER=yahoo_finance"
@@ -361,4 +361,48 @@ identity_providers:
- 'profile' - 'profile'
access_token_signed_response_alg: 'none' access_token_signed_response_alg: 'none'
userinfo_signed_response_alg: 'none' userinfo_signed_response_alg: 'none'
token_endpoint_auth_method: 'client_secret_post' token_endpoint_auth_method: 'client_secret_post'
# https://www.authelia.com/integration/openid-connect/clients/open-webui/
- client_id: 'openwebui'
client_name: 'openwebui'
consent_mode: 'implicit'
client_secret: '{{ hostvars['console']['openwebui']['oidc']['hash'] }}'
public: false
authorization_policy: 'one_factor'
require_pkce: true
pkce_challenge_method: 'S256'
redirect_uris:
- 'https://{{ services['openwebui']['domain']['public'] }}.{{ domain['public'] }}/oauth/oidc/callback'
scopes:
- 'openid'
- 'profile'
- 'groups'
- 'email'
response_types:
- 'code'
grant_types:
- 'authorization_code'
access_token_signed_response_alg: 'none'
userinfo_signed_response_alg: 'none'
token_endpoint_auth_method: 'client_secret_basic'
- client_id: 'kaneo'
client_name: 'kaneo'
consent_mode: 'implicit'
client_secret: '{{ hostvars['console']['kaneo']['oidc']['hash'] }}'
public: false
authorization_policy: 'one_factor'
require_pkce: true
pkce_challenge_method: 'S256'
redirect_uris:
- 'https://{{ services['kaneo']['domain']['public'] }}.{{ domain['public'] }}/api/auth/oauth2/callback/custom'
scopes:
- 'openid'
- 'profile'
- 'email'
response_types:
- 'code'
grant_types:
- 'authorization_code'
access_token_signed_response_alg: 'none'
userinfo_signed_response_alg: 'none'
token_endpoint_auth_method: 'client_secret_post'
@@ -60,6 +60,10 @@
import crowdsec_log import crowdsec_log
route { route {
crowdsec crowdsec
{% if service['route'] is defined and service['route']['dav'] is defined and service['route']['dav'] %}
redir /.well-known/caldav /dav 301
redir /.well-known/carddav /dav 301
{% endif %}
{% if web_type == 'auth-public' %} {% if web_type == 'auth-public' %}
reverse_proxy host.containers.internal:{{ service['ports']['http'] }} reverse_proxy host.containers.internal:{{ service['ports']['http'] }}
{% else %} {% else %}
@@ -0,0 +1,8 @@
services:
ovms:
ports:
http: "5000"
subuid: "104999"
version:
containers:
ovms: "2026.3-gpu"
@@ -0,0 +1,35 @@
[Quadlet]
DefaultDependencies=false
[Unit]
Description=OpenVINO Model Server
[Container]
Image=docker.io/openvino/model_server:{{ version['containers']['ovms'] }}
ContainerName=ovms
HostName=ovms
PublishPort={{ services['ovms']['ports']['http'] }}:8000/tcp
# iGPU access for OpenVINO
AddDevice=/dev/dri:/dev/dri
PodmanArgs=--group-add keep-groups
Volume=%h/containers/ovms/models:/models:ro
Volume=%h/containers/ovms/config:/config:ro
Volume=%h/containers/ovms/cache:/opt/cache:rw
Environment="TZ={{ timezone }}"
Exec=--rest_port 8000 \
--config_path /config/config.json \
--cache_dir /opt/cache \
--file_system_poll_wait_seconds 0
[Service]
Restart=always
RestartSec=10s
TimeoutStopSec=300
[Install]
WantedBy=default.target
@@ -0,0 +1,106 @@
---
- name: Add user in video, render group
ansible.builtin.user:
name: "{{ ansible_user }}"
state: "present"
groups: "video, render"
append: true
become: true
- name: Check model directory in data volume
ansible.builtin.find:
path: "{{ node['home_path'] }}/data/containers/ovms/models"
file_type: any
hidden: true
register: ovms_models
- name: Check config directory in data volume
ansible.builtin.stat:
path: "{{ node['home_path'] }}/data/containers/ovms/config/config.json"
register: ovms_config
- name: Failure when models directory or config file in data volume
ansible.builtin.fail:
msg: "There are no models or config file in data directory, please prepare the models and config before set the ovms"
when: (ovms_models.matched == 0) or (not ovms_config.stat.exists)
- name: Create ovms directory
ansible.builtin.file:
path: "{{ node['home_path'] }}/{{ item }}"
state: "directory"
owner: "{{ services['ovms']['subuid'] }}"
group: "{{ node['group'] }}"
mode: "0770"
loop:
- "containers/ovms"
- "containers/ovms/models"
- "containers/ovms/config"
- "containers/ovms/cache"
become: true
- name: Sync model data (--delete=true)
delegate_to: "{{ inventory_hostname }}"
ansible.posix.synchronize:
src: "{{ node['home_path'] }}/data/containers/ovms/models/"
dest: "{{ node['home_path'] }}/containers/ovms/models/"
owner: true
group: true
perms: true
delete: true
become: true
notify: "notification_restart_ovms"
- name: Copy configuration file
ansible.builtin.copy:
remote_src: true
src: "{{ node['home_path'] }}/data/containers/ovms/config/config.json"
dest: "{{ node['home_path'] }}/containers/ovms/config/config.json"
owner: "{{ services['ovms']['subuid'] }}"
group: "{{ node['group'] }}"
mode: "0640"
become: true
notify: "notification_restart_ovms"
- name: Pull image
containers.podman.podman_image:
name: "docker.io/openvino/model_server"
tag: "{{ version['containers']['ovms'] }}"
state: "present"
pull: true
- name: Deploy container file
ansible.builtin.template:
src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/ovms/ovms.container.j2"
dest: "{{ node['home_path'] }}/.config/containers/systemd/ovms.container"
owner: "{{ ansible_user }}"
group: "{{ node['group'] }}"
mode: "0644"
notify: "notification_restart_ovms"
- name: Enable ovms.service
ansible.builtin.systemd:
name: "ovms.service"
state: "started"
enabled: true
daemon_reload: true
scope: "user"
- name: Check container archive images
ansible.builtin.stat:
path: "{{ node['home_path'] }}/archives/containers/docker.io_openvino_model_server_{{ version['containers']['ovms'] }}.tar"
register: container_archive_images
- name: Save container archive images
containers.podman.podman_save:
image:
- "docker.io/openvino/model_server:{{ version['containers']['ovms'] }}"
dest: "{{ node['home_path'] }}/archives/containers/docker.io_openvino_model_server_{{ version['containers']['ovms'] }}.tar"
format: "oci-archive"
force: false
when: not container_archive_images.stat.exists
- name: Fetch container archive images
ansible.builtin.fetch:
src: "{{ node['home_path'] }}/archives/containers/docker.io_openvino_model_server_{{ version['containers']['ovms'] }}.tar"
dest: "{{ hostvars['console']['node']['data_path'] }}/images/containers/"
flat: true