2026-SEP-09
This commit is contained in:
@@ -160,6 +160,84 @@ services:
|
||||
http: "3000" # app
|
||||
dependency:
|
||||
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:
|
||||
domain:
|
||||
public: "webmail"
|
||||
@@ -212,6 +290,28 @@ services:
|
||||
dependency:
|
||||
web: "app-public"
|
||||
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:
|
||||
domain:
|
||||
public: "nextcloud"
|
||||
@@ -318,9 +418,9 @@ version:
|
||||
# infra
|
||||
ca: "0.30.2"
|
||||
## Postgresql
|
||||
postgresql: "18.4"
|
||||
postgresql: "18.6"
|
||||
## Mariadb
|
||||
mariadb: "12.3.2"
|
||||
mariadb: "12.3.3"
|
||||
### For immich - https://github.com/immich-app/base-images/blob/main/postgres/versions.yaml
|
||||
vectorchord: "1.1.1"
|
||||
## lldap
|
||||
@@ -328,25 +428,34 @@ version:
|
||||
## maddy
|
||||
maddy: "0.9.5"
|
||||
x509-exporter: "4.1.0"
|
||||
prometheus: "v3.13.2"
|
||||
loki: "3.7.4"
|
||||
grafana: "13.1.1"
|
||||
prometheus: "v3.14.0"
|
||||
loki: "3.7.7"
|
||||
grafana: "13.2.1"
|
||||
# Auth
|
||||
authelia: "4.39.20"
|
||||
# App
|
||||
vaultwarden: "1.37.1"
|
||||
gitea: "1.27.2"
|
||||
redis: "8.10.0"
|
||||
vaultwarden: "1.37.2"
|
||||
gitea: "1.27.3"
|
||||
redis: "8.10.1"
|
||||
immich: "v3.1.0"
|
||||
nextcloud: "34.0.3"
|
||||
collabora: "26.04.3.1.1"
|
||||
sure: "0.7.3"
|
||||
outline: "1.9.2"
|
||||
collabora: "26.04.3.2.1"
|
||||
sure: "0.7.4"
|
||||
outline: "1.10.0"
|
||||
homebox: "0.26.2"
|
||||
homepage: "v2.0.0"
|
||||
snappymail: "v2.38.2"
|
||||
mastodon: "v4.6.6"
|
||||
mastodon: "v4.6.7"
|
||||
papra: "26.6.1-rootless"
|
||||
tandoor: "2.6.13"
|
||||
bookstack: "26.5.3"
|
||||
bookstack: "26.5.4"
|
||||
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"
|
||||
|
||||
@@ -16,7 +16,7 @@ vm:
|
||||
<vcpupin vcpu='1' cpuset='7'/>
|
||||
<emulatorpin cpuset='6-7'/>
|
||||
shares: 1024
|
||||
memory: 2
|
||||
memory: 1
|
||||
storage: 32
|
||||
lan_mac: "0a:49:6e:4d:02:00"
|
||||
lan_net: "lan-net"
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
- "netcat-openbsd"
|
||||
- "dbus-user-session"
|
||||
- "ksmtuned"
|
||||
- "rsync"
|
||||
state: "present"
|
||||
become: true
|
||||
tags: ["init", "site", "install-packages"]
|
||||
@@ -216,16 +217,24 @@
|
||||
name: "app"
|
||||
tasks_from: "services/set_immich"
|
||||
apply:
|
||||
tags: ["site", "immich"]
|
||||
tags: ["site", "immich"]
|
||||
tags: ["site", "immich", "redis"]
|
||||
tags: ["site", "immich", "redis"]
|
||||
|
||||
- name: Set nextcloud
|
||||
ansible.builtin.include_role:
|
||||
name: "app"
|
||||
tasks_from: "services/set_nextcloud"
|
||||
apply:
|
||||
tags: ["site", "nextcloud"]
|
||||
tags: ["site", "nextcloud"]
|
||||
tags: ["site", "nextcloud", "redis"]
|
||||
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
|
||||
ansible.builtin.include_role:
|
||||
@@ -235,21 +244,29 @@
|
||||
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
|
||||
ansible.builtin.include_role:
|
||||
name: "app"
|
||||
tasks_from: "services/set_sure"
|
||||
apply:
|
||||
tags: ["site", "sure"]
|
||||
tags: ["site", "sure"]
|
||||
tags: ["site", "sure", "redis"]
|
||||
tags: ["site", "sure", "redis"]
|
||||
|
||||
- name: Set outline
|
||||
ansible.builtin.include_role:
|
||||
name: "app"
|
||||
tasks_from: "services/set_outline"
|
||||
apply:
|
||||
tags: ["site", "outline"]
|
||||
tags: ["site", "outline"]
|
||||
tags: ["site", "outline", "redis"]
|
||||
tags: ["site", "outline", "redis"]
|
||||
|
||||
- name: Set homebox
|
||||
ansible.builtin.include_role:
|
||||
@@ -264,8 +281,8 @@
|
||||
name: "app"
|
||||
tasks_from: "services/set_mastodon"
|
||||
apply:
|
||||
tags: ["site", "mastodon"]
|
||||
tags: ["site", "mastodon"]
|
||||
tags: ["site", "mastodon", "redis"]
|
||||
tags: ["site", "mastodon", "redis"]
|
||||
|
||||
- name: Set papra
|
||||
ansible.builtin.include_role:
|
||||
@@ -299,6 +316,30 @@
|
||||
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
|
||||
ansible.builtin.include_role:
|
||||
name: "app"
|
||||
@@ -320,10 +361,19 @@
|
||||
state: "present"
|
||||
become: true
|
||||
notify:
|
||||
- "notification_add_user_in_group"
|
||||
- "notification_update_initramfs"
|
||||
- "notification_reboot_app"
|
||||
tags: ["init"]
|
||||
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
|
||||
ansible.builtin.command:
|
||||
update-initramfs -u
|
||||
|
||||
@@ -183,3 +183,58 @@
|
||||
changed_when: false
|
||||
listen: "notification_restart_linkding"
|
||||
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"
|
||||
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
|
||||
ansible.builtin.file:
|
||||
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
|
||||
@@ -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 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
|
||||
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_CLIENT jump client comment "set CLIENT interface rules"
|
||||
|
||||
@@ -139,6 +139,8 @@ postgresql:
|
||||
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]
|
||||
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]
|
||||
#
|
||||
#
|
||||
@@ -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]
|
||||
password:
|
||||
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]
|
||||
#
|
||||
#
|
||||
@@ -352,6 +355,33 @@ linkding:
|
||||
#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]
|
||||
switch:
|
||||
admin:
|
||||
@@ -368,7 +398,7 @@ sops:
|
||||
k+uyWPPi+enFkVaceSwMFrA66uaWWrwAj11sXEB7yzvGFPrnAGezjQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age120wuwcmsm845ztsvsz46pswj5je53uc2n35vadklrfqudu6cxuusxetk7y
|
||||
lastmodified: "2026-08-27T14:09:38Z"
|
||||
mac: ENC[AES256_GCM,data:K+SwIrdo62opVGnsrMH7FysC8GtMN1RGPGmtUxRKLoNy/Yi/tKxjVH1xiu1ePCWXWqpGjuC7btJdel3SZmwDi4jMhN87DadOh1Kf0sIGDe8+Ff1LrLLl0I4+NWpu/RJicXwxOXkZUwdEZ0sqTW2Qm1S7aknPpSoD9vc44epv1AA=,iv:npBPZiCeb3+7s+xHd/XhOhEnxvAJzeZWGFzW98uFSK0=,tag:KhQcf6/SVDtbgpiZO/7VNA==,type:str]
|
||||
lastmodified: "2026-09-08T03:23:38Z"
|
||||
mac: ENC[AES256_GCM,data:+mTAe1NwSbQHsmaVxKrtIDeRyyKbDVHSRCkji5+hAByS/GS4OzK+SwdyGYa5Ihv+g2J/7Pechf44GY8mv0lirWcOTGxqzvMDiRSXNty+aQVzOn9sXbfCvWXBUqDLNOiNISN0RfF1eVlG12D8wNfFEXFbs6lv81IMIN9ctdvTN5c=,iv:CG6Px0p/EomUafLody2kJOR8AzdmPTqe1ry1DZUhNSc=,tag:ClSWU401Jt6PzhcSCNNvbg==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.2
|
||||
|
||||
@@ -13,6 +13,7 @@ PublishPort={{ services['collabora']['ports']['http'] }}:9980/tcp
|
||||
|
||||
Environment="TZ={{ timezone }}"
|
||||
Environment="aliasgroup1=https://{{ services['nextcloud']['domain']['public'] }}.{{ domain['public'] }}"
|
||||
Environment="aliasgroup2=https://{{ services['groupoffice']['domain']['public'] }}.{{ domain['public'] }}"
|
||||
# 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"
|
||||
|
||||
|
||||
@@ -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> <?php<br>$db->exec() or die(print_r($db->errorInfo(), true)); // incorrect<br>?> </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 = {
|
||||
'ldap': 'lldap',
|
||||
'tandoor': 'tandoor-recipes',
|
||||
'ldap': 'lldap'
|
||||
'openwebui': 'open-webui'
|
||||
} %}
|
||||
|
||||
- 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="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]
|
||||
MemoryKSM=yes
|
||||
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
|
||||
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
|
||||
## Currency
|
||||
Environment="EXCHANGE_RATE_PROVIDER=yahoo_finance"
|
||||
|
||||
@@ -361,4 +361,48 @@ identity_providers:
|
||||
- 'profile'
|
||||
access_token_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
|
||||
route {
|
||||
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' %}
|
||||
reverse_proxy host.containers.internal:{{ service['ports']['http'] }}
|
||||
{% 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
|
||||
Reference in New Issue
Block a user