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-moodle: 8026
|
||||
taiga: 8027
|
||||
friendica: 8028
|
||||
web-app-friendica: 8028
|
||||
web-app-port-ui: 8029
|
||||
bluesky_api: 8030
|
||||
bluesky_web: 8031
|
||||
|
@@ -18,7 +18,7 @@ defaults_networks:
|
||||
subnet: 192.168.101.48/28
|
||||
bluesky:
|
||||
subnet: 192.168.101.64/28
|
||||
friendica:
|
||||
web-app-friendica:
|
||||
subnet: 192.168.101.80/28
|
||||
funkwhale:
|
||||
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:
|
||||
name: cmp-db-docker-proxy
|
||||
|
||||
- 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
|
||||
- name: Integrate LDAP
|
||||
include_tasks: 01_ldap.yml
|
||||
when: applications | get_app_conf(application_id, 'features.ldap', False)
|
||||
|
||||
- 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: 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: Update Friendica DB credentials
|
||||
include_tasks: 02_database.yml
|
||||
|
||||
- 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"
|
||||
|
||||
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_docker_ldap_config: "{{friendica_application_base}}/config/ldapauth.config.php"
|
||||
friendica_host_ldap_config: "{{ docker_compose.directories.volumes }}ldapauth.config.php"
|
||||
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_application_base: "/var/www/html"
|
||||
friendica_docker_ldap_config: "{{friendica_application_base}}/config/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"
|
||||
documentation: "https://github.com/kevinveenbirkenbach/meta-infinite-graph/"
|
||||
logo:
|
||||
class: ""
|
||||
class: "fa-solid fa-infinity"
|
||||
run_after: []
|
||||
dependencies:
|
||||
- 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