mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-09-08 03:07:14 +02:00
Compare commits
3 Commits
6a1a83432f
...
1b9775ccb5
Author | SHA1 | Date | |
---|---|---|---|
1b9775ccb5 | |||
45d9da3125 | |||
8ccfb1dfbe |
39
filter_plugins/merge_with_defaults.py
Normal file
39
filter_plugins/merge_with_defaults.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
def merge_with_defaults(defaults, customs):
|
||||||
|
"""
|
||||||
|
Recursively merge two dicts (customs into defaults).
|
||||||
|
For each top-level key in customs, ensure all dict keys from defaults are present (at least empty dict).
|
||||||
|
Customs always take precedence.
|
||||||
|
"""
|
||||||
|
def merge_dict(d1, d2):
|
||||||
|
# Recursively merge d2 into d1, d2 wins
|
||||||
|
result = dict(d1) if d1 else {}
|
||||||
|
for k, v in (d2 or {}).items():
|
||||||
|
if k in result and isinstance(result[k], dict) and isinstance(v, dict):
|
||||||
|
result[k] = merge_dict(result[k], v)
|
||||||
|
else:
|
||||||
|
result[k] = v
|
||||||
|
return result
|
||||||
|
|
||||||
|
merged = {}
|
||||||
|
# Union of all app-keys
|
||||||
|
all_keys = set(defaults or {}).union(set(customs or {}))
|
||||||
|
for app_key in all_keys:
|
||||||
|
base = (defaults or {}).get(app_key, {})
|
||||||
|
override = (customs or {}).get(app_key, {})
|
||||||
|
|
||||||
|
# Step 1: merge override into base
|
||||||
|
result = merge_dict(base, override)
|
||||||
|
|
||||||
|
# Step 2: ensure all dict keys from base exist in result (at least {})
|
||||||
|
for k, v in (base or {}).items():
|
||||||
|
if isinstance(v, dict) and k not in result:
|
||||||
|
result[k] = {}
|
||||||
|
merged[app_key] = result
|
||||||
|
return merged
|
||||||
|
|
||||||
|
class FilterModule(object):
|
||||||
|
'''Custom merge filter for CyMaIS: merge_with_defaults'''
|
||||||
|
def filters(self):
|
||||||
|
return {
|
||||||
|
'merge_with_defaults': merge_with_defaults,
|
||||||
|
}
|
@@ -49,7 +49,7 @@ ports:
|
|||||||
web-app-akaunting: 8025
|
web-app-akaunting: 8025
|
||||||
web-app-moodle: 8026
|
web-app-moodle: 8026
|
||||||
taiga: 8027
|
taiga: 8027
|
||||||
friendica: 8028
|
web-app-friendica: 8028
|
||||||
web-app-port-ui: 8029
|
web-app-port-ui: 8029
|
||||||
bluesky_api: 8030
|
bluesky_api: 8030
|
||||||
bluesky_web: 8031
|
bluesky_web: 8031
|
||||||
|
@@ -18,7 +18,7 @@ defaults_networks:
|
|||||||
subnet: 192.168.101.48/28
|
subnet: 192.168.101.48/28
|
||||||
bluesky:
|
bluesky:
|
||||||
subnet: 192.168.101.64/28
|
subnet: 192.168.101.64/28
|
||||||
friendica:
|
web-app-friendica:
|
||||||
subnet: 192.168.101.80/28
|
subnet: 192.168.101.80/28
|
||||||
funkwhale:
|
funkwhale:
|
||||||
subnet: 192.168.101.96/28
|
subnet: 192.168.101.96/28
|
||||||
|
36
roles/desk-copyq/README.md
Normal file
36
roles/desk-copyq/README.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# CopyQ Role for Ansible
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This role installs the CopyQ clipboard manager on Pacman-based systems (e.g. Arch Linux) and ensures it is started automatically for the current user.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
- Ansible 2.9 or higher
|
||||||
|
- Pacman package manager (Arch Linux or derivative)
|
||||||
|
- X11/Wayland desktop environment (for GUI)
|
||||||
|
|
||||||
|
## Role Variables
|
||||||
|
No additional role variables are required.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
No external dependencies.
|
||||||
|
|
||||||
|
## Example Playbook
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- hosts: all
|
||||||
|
roles:
|
||||||
|
- desk-copyq
|
||||||
|
```
|
||||||
|
|
||||||
|
## Further Resources
|
||||||
|
|
||||||
|
- [CopyQ official site](https://hluk.github.io/CopyQ/)
|
||||||
|
- [Arch Wiki: Clipboard](https://wiki.archlinux.org/title/Clipboard)
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome. Please follow best practices.
|
||||||
|
|
||||||
|
## Other Resources
|
||||||
|
|
||||||
|
This role was created as part of a larger playbook. For more context on this role, you can refer to the related ChatGPT conversation [here](https://chat.openai.com/share/ae168ca0-5191-4bec-96a0-ffcfabca0024).
|
27
roles/desk-copyq/meta/main.yml
Normal file
27
roles/desk-copyq/meta/main.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
galaxy_info:
|
||||||
|
author: "Kevin Veen-Birchenbach"
|
||||||
|
description: "Installs CopyQ clipboard manager on Pacman-based systems and configures autostart for the current user."
|
||||||
|
license: "CyMaIS NonCommercial License (CNCL)"
|
||||||
|
license_url: "https://s.veen.world/cncl"
|
||||||
|
company: |
|
||||||
|
Kevin Veen-Birchenbach
|
||||||
|
Consulting & Coaching Solutions
|
||||||
|
https://www.veen.world
|
||||||
|
galaxy_tags:
|
||||||
|
- copyq
|
||||||
|
- clipboard
|
||||||
|
- manager
|
||||||
|
- gui
|
||||||
|
- cli
|
||||||
|
logo:
|
||||||
|
class: fa fa-clipboard
|
||||||
|
repository: "https://github.com/kevinveenbirkenbach/cymais"
|
||||||
|
issue_tracker_url: "https://github.com/kevinveenbirkenbach/cymais/issues"
|
||||||
|
documentation: "https://github.com/kevinveenbirkenbach/cymais/tree/main/roles/desk-copyq"
|
||||||
|
min_ansible_version: "2.9"
|
||||||
|
platforms:
|
||||||
|
- name: Archlinux
|
||||||
|
versions:
|
||||||
|
- all
|
||||||
|
dependencies: []
|
27
roles/desk-copyq/tasks/main.yml
Normal file
27
roles/desk-copyq/tasks/main.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
- name: Install CopyQ clipboard manager
|
||||||
|
community.general.pacman:
|
||||||
|
name:
|
||||||
|
- copyq
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Ensure autostart directory exists
|
||||||
|
file:
|
||||||
|
path: "{{ ansible_env.HOME }}/.config/autostart"
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
become: false
|
||||||
|
|
||||||
|
- name: Add CopyQ to user autostart
|
||||||
|
copy:
|
||||||
|
dest: "{{ ansible_env.HOME }}/.config/autostart/copyq.desktop"
|
||||||
|
content: |
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Exec=copyq
|
||||||
|
Hidden=false
|
||||||
|
NoDisplay=false
|
||||||
|
X-GNOME-Autostart-enabled=true
|
||||||
|
Name=CopyQ Clipboard Manager
|
||||||
|
Comment=Advanced clipboard manager with searchable and editable history
|
||||||
|
mode: '0644'
|
||||||
|
become: false
|
1
roles/desk-copyq/vars/main.yml
Normal file
1
roles/desk-copyq/vars/main.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
application_id: desk-copyq
|
9
roles/web-app-friendica/tasks/01_ldap.yml
Normal file
9
roles/web-app-friendica/tasks/01_ldap.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
- name: "create {{ friendica_host_ldap_config }}"
|
||||||
|
template:
|
||||||
|
src: "ldapauth.config.php.j2"
|
||||||
|
dest: "{{ friendica_host_ldap_config }}"
|
||||||
|
mode: '644'
|
||||||
|
owner: root
|
||||||
|
group: 33
|
||||||
|
force: yes
|
||||||
|
notify: docker compose up
|
34
roles/web-app-friendica/tasks/02_database.yml
Normal file
34
roles/web-app-friendica/tasks/02_database.yml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
- name: flush handlers to ensure that friendica is up before friendica addon configuration
|
||||||
|
meta: flush_handlers
|
||||||
|
- name: Check if Friendica local.config.php exists
|
||||||
|
command: docker exec --user {{ friendica_user }} {{ friendica_container }} test -f {{ friendica_config_file }}
|
||||||
|
register: friendica_config_exists
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Patch Friendica local.config.php with updated DB credentials
|
||||||
|
when: friendica_config_exists.rc == 0
|
||||||
|
block:
|
||||||
|
- name: Update DB host
|
||||||
|
command: >
|
||||||
|
docker exec --user {{ friendica_user }} {{ friendica_container }}
|
||||||
|
sed -i "s/'hostname' => .*/'hostname' => '{{ database_host }}:{{ database_port }}',/" {{ friendica_config_file }}
|
||||||
|
notify: docker compose up
|
||||||
|
|
||||||
|
- name: Update DB name
|
||||||
|
command: >
|
||||||
|
docker exec --user {{ friendica_user }} {{ friendica_container }}
|
||||||
|
sed -i "s/'database' => .*/'database' => '{{ database_name }}',/" {{ friendica_config_file }}
|
||||||
|
notify: docker compose up
|
||||||
|
|
||||||
|
- name: Update DB user
|
||||||
|
command: >
|
||||||
|
docker exec --user {{ friendica_user }} {{ friendica_container }}
|
||||||
|
sed -i "s/'username' => .*/'username' => '{{ database_username }}',/" {{ friendica_config_file }}
|
||||||
|
notify: docker compose up
|
||||||
|
|
||||||
|
- name: Update DB password
|
||||||
|
command: >
|
||||||
|
docker exec --user {{ friendica_user }} {{ friendica_container }}
|
||||||
|
sed -i "s/'password' => .*/'password' => '{{ database_password }}',/" {{ friendica_config_file }}
|
||||||
|
notify: docker compose up
|
35
roles/web-app-friendica/tasks/03_addons.yml
Normal file
35
roles/web-app-friendica/tasks/03_addons.yml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
- name: flush handlers to ensure that friendica is up before friendica addon configuration
|
||||||
|
meta: flush_handlers
|
||||||
|
|
||||||
|
- name: Build friendica_addons based on features
|
||||||
|
set_fact:
|
||||||
|
friendica_addons: >-
|
||||||
|
{{
|
||||||
|
friendica_addons | default([])
|
||||||
|
+ [{
|
||||||
|
'name': item.key,
|
||||||
|
'enabled': (
|
||||||
|
applications | get_app_conf(application_id, 'features.oidc', True)
|
||||||
|
if item.key == 'keycloakpassword'
|
||||||
|
else applications | get_app_conf(application_id, 'features.ldap', True)
|
||||||
|
if item.key == 'ldapauth'
|
||||||
|
else (item.value.enabled if item.value is mapping and 'enabled' in item.value else False)
|
||||||
|
)
|
||||||
|
}]
|
||||||
|
}}
|
||||||
|
loop: "{{ applications | get_app_conf(application_id, 'addons', True) | dict2items }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.key }}"
|
||||||
|
|
||||||
|
- name: Ensure Friendica addons are in sync
|
||||||
|
command: >
|
||||||
|
docker compose exec --user {{ friendica_user }}
|
||||||
|
application
|
||||||
|
bin/console addon
|
||||||
|
{{ 'enable' if item.enabled else 'disable' }}
|
||||||
|
{{ item.name }}
|
||||||
|
args:
|
||||||
|
chdir: "{{ docker_compose.directories.instance }}"
|
||||||
|
loop: "{{ friendica_addons }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.name }}"
|
@@ -3,50 +3,12 @@
|
|||||||
include_role:
|
include_role:
|
||||||
name: cmp-db-docker-proxy
|
name: cmp-db-docker-proxy
|
||||||
|
|
||||||
- name: "create {{ friendica_host_ldap_config }}"
|
- name: Integrate LDAP
|
||||||
template:
|
include_tasks: 01_ldap.yml
|
||||||
src: "ldapauth.config.php.j2"
|
|
||||||
dest: "{{ friendica_host_ldap_config }}"
|
|
||||||
mode: '644'
|
|
||||||
owner: root
|
|
||||||
group: 33
|
|
||||||
force: yes
|
|
||||||
notify: docker compose up
|
|
||||||
when: applications | get_app_conf(application_id, 'features.ldap', False)
|
when: applications | get_app_conf(application_id, 'features.ldap', False)
|
||||||
|
|
||||||
- name: Build friendica_addons based on features
|
- name: Update Friendica DB credentials
|
||||||
set_fact:
|
include_tasks: 02_database.yml
|
||||||
friendica_addons: >-
|
|
||||||
{{
|
|
||||||
friendica_addons | default([])
|
|
||||||
+ [{
|
|
||||||
'name': item.key,
|
|
||||||
'enabled': (
|
|
||||||
applications | get_app_conf(application_id, 'features.oidc', True)
|
|
||||||
if item.key == 'keycloakpassword'
|
|
||||||
else applications | get_app_conf(application_id, 'features.ldap', True)
|
|
||||||
if item.key == 'ldapauth'
|
|
||||||
else (item.value.enabled if item.value is mapping and 'enabled' in item.value else False)
|
|
||||||
)
|
|
||||||
}]
|
|
||||||
}}
|
|
||||||
loop: "{{ applications | get_app_conf(application_id, 'addons', True) | dict2items }}"
|
|
||||||
loop_control:
|
|
||||||
label: "{{ item.key }}"
|
|
||||||
|
|
||||||
- name: flush handlers to ensure that friendica is up before friendica addon configuration
|
|
||||||
meta: flush_handlers
|
|
||||||
|
|
||||||
- name: Ensure Friendica addons are in sync
|
|
||||||
command: >
|
|
||||||
docker compose exec --user www-data
|
|
||||||
application
|
|
||||||
bin/console addon
|
|
||||||
{{ 'enable' if item.enabled else 'disable' }}
|
|
||||||
{{ item.name }}
|
|
||||||
args:
|
|
||||||
chdir: "{{ docker_compose.directories.instance }}"
|
|
||||||
loop: "{{ friendica_addons }}"
|
|
||||||
loop_control:
|
|
||||||
label: "{{ item.name }}"
|
|
||||||
|
|
||||||
|
- name: Add Friendica Add Ons
|
||||||
|
include_tasks: 03_addons.yml
|
@@ -1,8 +1,11 @@
|
|||||||
application_id: "friendica"
|
application_id: "web-app-friendica"
|
||||||
database_type: "mariadb"
|
database_type: "mariadb"
|
||||||
|
friendica_container: "application"
|
||||||
friendica_no_validation: "{{ applications | get_app_conf(application_id, 'features.oidc', True) }}" # Email validation is not neccessary if OIDC is active
|
friendica_no_validation: "{{ applications | get_app_conf(application_id, 'features.oidc', True) }}" # Email validation is not neccessary if OIDC is active
|
||||||
friendica_application_base: "/var/www/html"
|
friendica_application_base: "/var/www/html"
|
||||||
friendica_docker_ldap_config: "{{friendica_application_base}}/config/ldapauth.config.php"
|
friendica_docker_ldap_config: "{{friendica_application_base}}/config/ldapauth.config.php"
|
||||||
friendica_host_ldap_config: "{{ docker_compose.directories.volumes }}ldapauth.config.php"
|
friendica_host_ldap_config: "{{ docker_compose.directories.volumes }}ldapauth.config.php"
|
||||||
|
friendica_config_dir: "{{ friendica_application_base }}/config"
|
||||||
|
friendica_config_file: "{{ friendica_config_dir }}/local.config.php"
|
||||||
|
friendica_user: "www-data"
|
||||||
|
|
||||||
|
@@ -19,7 +19,7 @@ galaxy_info:
|
|||||||
issue_tracker_url: "https://github.com/kevinveenbirkenbach/meta-infinite-graph/issues"
|
issue_tracker_url: "https://github.com/kevinveenbirkenbach/meta-infinite-graph/issues"
|
||||||
documentation: "https://github.com/kevinveenbirkenbach/meta-infinite-graph/"
|
documentation: "https://github.com/kevinveenbirkenbach/meta-infinite-graph/"
|
||||||
logo:
|
logo:
|
||||||
class: ""
|
class: "fa-solid fa-infinity"
|
||||||
run_after: []
|
run_after: []
|
||||||
dependencies:
|
dependencies:
|
||||||
- sys-cli
|
- sys-cli
|
||||||
|
107
tests/unit/filter_plugins/test_merge_with_defaults.py
Normal file
107
tests/unit/filter_plugins/test_merge_with_defaults.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import unittest
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Allow import from project filter_plugins directory
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../filter_plugins')))
|
||||||
|
|
||||||
|
from merge_with_defaults import merge_with_defaults
|
||||||
|
|
||||||
|
class TestMergeWithDefaultsFilter(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_basic_merge(self):
|
||||||
|
defaults = {
|
||||||
|
"app1": {
|
||||||
|
"docker": {
|
||||||
|
"network": "default",
|
||||||
|
"services": {"foo": "bar"},
|
||||||
|
"volumes": {"data": "/mnt"}
|
||||||
|
},
|
||||||
|
"features": {"ldap": True, "sso": False},
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customs = {
|
||||||
|
"app1": {
|
||||||
|
"docker": {
|
||||||
|
"network": "customnet"
|
||||||
|
},
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"app2": {
|
||||||
|
"docker": {
|
||||||
|
"network": "other"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"app1": {
|
||||||
|
"docker": {
|
||||||
|
"network": "customnet",
|
||||||
|
"services": {"foo": "bar"},
|
||||||
|
"volumes": {"data": "/mnt"}
|
||||||
|
},
|
||||||
|
"features": {"ldap": True, "sso": False},
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"app2": {
|
||||||
|
"docker": {
|
||||||
|
"network": "other"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = merge_with_defaults(defaults, customs)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_keys_from_defaults_only(self):
|
||||||
|
defaults = {
|
||||||
|
"foo": {"docker": {"a": 1, "b": 2}, "features": {"x": True}},
|
||||||
|
}
|
||||||
|
customs = {
|
||||||
|
"foo": {},
|
||||||
|
}
|
||||||
|
expected = {
|
||||||
|
"foo": {"docker": {"a": 1, "b": 2}, "features": {"x": True}}
|
||||||
|
}
|
||||||
|
result = merge_with_defaults(defaults, customs)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_custom_overrides_nested_dict(self):
|
||||||
|
defaults = {"x": {"docker": {"bar": 1, "baz": 2}}}
|
||||||
|
customs = {"x": {"docker": {"bar": 99}}}
|
||||||
|
expected = {"x": {"docker": {"bar": 99, "baz": 2}}}
|
||||||
|
result = merge_with_defaults(defaults, customs)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_only_defaults_present(self):
|
||||||
|
defaults = {"only": {"value": 1}}
|
||||||
|
customs = {}
|
||||||
|
expected = {"only": {"value": 1}}
|
||||||
|
result = merge_with_defaults(defaults, customs)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_only_customs_present(self):
|
||||||
|
defaults = {}
|
||||||
|
customs = {"x": {"foo": 42}}
|
||||||
|
expected = {"x": {"foo": 42}}
|
||||||
|
result = merge_with_defaults(defaults, customs)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
def test_deep_merge_multiple_levels(self):
|
||||||
|
defaults = {
|
||||||
|
"a": {"outer": {"mid": {"inner": 1, "keep": True}}, "plain": "test"}
|
||||||
|
}
|
||||||
|
customs = {
|
||||||
|
"a": {"outer": {"mid": {"inner": 99}}, "plain": "changed"}
|
||||||
|
}
|
||||||
|
expected = {
|
||||||
|
"a": {"outer": {"mid": {"inner": 99, "keep": True}}, "plain": "changed"}
|
||||||
|
}
|
||||||
|
result = merge_with_defaults(defaults, customs)
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
Reference in New Issue
Block a user