/
/
/
This repo is destined for my server automations and setup.
1---
2# ============================================================================
3# Audio Client Deployment Playbook
4# ============================================================================
5#
6# This playbook deploys audio clients to HiFi nodes in two stages:
7# 1. Generic server setup on all hifi-nodes (system, docker, security, user)
8# 2. Audio client configuration with individual parameters per node
9#
10# Supports Snapcast and Sendspin backends via audio_client_type variable.
11#
12# Usage:
13# ansible-playbook hifi-nodes.yml
14#
15# ============================================================================
16
17# STAGE 1: Generic server setup on all hifi-nodes
18- name: "Generic Server Setup - HiFi Nodes Group"
19 hosts: hifi_nodes
20 become: true
21 gather_facts: true
22
23 # Load configuration from group_vars and host_vars
24 # Override with command line variables as needed
25
26 pre_tasks:
27 - name: Display deployment information
28 debug:
29 msg: |
30 ============================================================================
31 Generic Server Setup Starting - Stage 1
32 ============================================================================
33 Target Host: {{ inventory_hostname }}
34 Target IP: {{ ansible_default_ipv4.address | default('Unknown') }}
35 OS: {{ ansible_distribution | default('Unknown') }} {{ ansible_distribution_version | default('') }}
36 Architecture: {{ ansible_architecture | default('Unknown') }}
37 Server Type: {{ server_type | default('Unknown') }}
38 Node Type: {{ 'Kitchen' if inventory_hostname == 'hifi-node-kitchen' else 'Dining' }}
39 ============================================================================
40 tags: always
41
42 - name: Update apt cache
43 apt:
44 update_cache: yes
45 cache_valid_time: 3600
46 when: ansible_os_family == "Debian"
47
48 # ============================================================================
49 # GENERIC SERVER SETUP (System + User + Docker)
50 # ============================================================================
51
52 roles:
53 # System configuration (hostname, packages, sudo, etc.)
54 - role: system
55 tags: [system, base, configuration]
56
57 # Docker installation via geerlingguy.docker
58 - role: geerlingguy.docker
59 tags: [docker, containers]
60
61 # Docker directory framework (proper /docker permissions)
62 - role: docker-framework
63 tags: [docker, framework]
64
65 # Security hardening via geerlingguy.security
66 - role: geerlingguy.security
67 tags: [security, hardening]
68
69 # System monitoring
70 - role: monitoring
71 tags: [monitoring, glances]
72
73 tasks:
74 - name: Configure user with proper group memberships
75 include_role:
76 name: user
77 vars:
78 my_user_username: yannick
79 my_user_groups: "{{ user_groups | default(['docker', 'sudo']) }}"
80 my_user_ssh_public_key: "{{ user_ssh_keys[0] | default('') if user_ssh_keys is defined and user_ssh_keys|length > 0 else '' }}"
81 tags: [user, setup, configuration]
82
83 # ============================================================================
84 # POST-INSTALLATION VALIDATION
85 # ============================================================================
86
87 post_tasks:
88 - name: System validation
89 block:
90 - name: Check hostname
91 command: "hostname"
92 register: hostname_check
93 changed_when: false
94
95 - name: Check Docker installation
96 command: "docker --version"
97 register: docker_check
98 failed_when: false
99 changed_when: false
100
101 - name: Check Docker service status
102 systemd:
103 name: docker
104 register: docker_service
105 ignore_errors: yes
106
107 - name: Check SSH configuration
108 command: "sshd -t"
109 register: ssh_config_check
110 changed_when: false
111
112 - name: Check user in docker group
113 command: "groups yannick"
114 register: user_groups_check
115 changed_when: false
116
117 tags: [validation, system]
118
119 - name: Display configuration summary
120 debug:
121 msg: |
122 ============================================================================
123 Generic Server Setup Complete! - Stage 1 Finished
124 ============================================================================
125
126 System Information:
127 Hostname: {{ hostname_check.stdout | default('Unknown') }}
128 Target Host: {{ inventory_hostname }}
129 IP Address: {{ ansible_default_ipv4.address | default('Unknown') }}
130 OS: {{ ansible_distribution | default('Unknown') }} {{ ansible_distribution_version | default('') }}
131 Architecture: {{ ansible_architecture | default('Unknown') }}
132 Server Type: {{ server_type | default('Unknown') }}
133
134 Docker Configuration:
135 Docker Version: {{ docker_check.stdout | default('Not installed') }}
136 Docker Service: {{ 'Running' if docker_service is defined and docker_service.status.ActiveState == 'active' else 'Not running' }}
137 Docker Users: yannick
138
139 Security Configuration:
140 SSH Port: {{ security_ssh_port | default(22) }}
141 Password Auth: {{ security_ssh_password_authentication | default('yes') }}
142 Root Login: {{ security_ssh_permit_root_login | default('yes') }}
143 SSH Config: {{ 'Valid' if ssh_config_check is defined and ssh_config_check.rc == 0 else 'Skipped' if ssh_config_check is not defined else 'Invalid' }}
144
145 User Configuration:
146 Primary User: yannick
147 User Groups: {{ user_groups_check.stdout | default('Skipped') }}
148 Sudo Access: {{ 'Passwordless' if security_sudoers_passwordless is defined else 'Standard' }}
149
150 Validation Results:
151 Hostname Set: {{ 'OK' if hostname_check is defined and hostname_check.rc == 0 else 'Skipped' if hostname_check is not defined else 'Failed' }}
152 Docker Installed: {{ 'OK' if docker_check is defined and docker_check.rc == 0 else 'Skipped' if docker_check is not defined else 'Failed' }}
153 Docker Running: {{ 'OK' if docker_service is defined and docker_service.status.ActiveState == 'active' else 'Skipped' if docker_service is not defined else 'Not running' }}
154 SSH Config Valid: {{ 'OK' if ssh_config_check is defined and ssh_config_check.rc == 0 else 'Skipped' if ssh_config_check is not defined else 'Invalid' }}
155
156 ============================================================================
157 tags: [always]
158
159# STAGE 2: Audio client configuration with individual parameters
160- name: "Audio Client Configuration - Individual Node Setup"
161 hosts: hifi_nodes
162 become: true
163 gather_facts: false # No need to gather facts again
164
165 pre_tasks:
166 - name: Display audio client configuration information
167 debug:
168 msg: |
169 ============================================================================
170 Audio Client Configuration - Stage 2
171 ============================================================================
172 Target Host: {{ inventory_hostname }}
173 Audio Backend: {{ audio_client_type | default('snapcast') }}
174 Node Type: {{ 'Kitchen (mono mixing)' if inventory_hostname == 'hifi-node-kitchen' else 'Dining (stereo)' }}
175 Audio Device: {{ 'hw:1,0' if inventory_hostname == 'hifi-node-kitchen' else 'hw:0,0' }}
176 Mono Mixing: {{ 'Enabled' if inventory_hostname == 'hifi-node-kitchen' else 'Disabled' }}
177 ============================================================================
178 tags: always
179
180 roles:
181 - role: audio-client
182 tags: [audio, hifi]
183 vars:
184 # Configure based on node type
185 audio_merge_to_mono: "{{ inventory_hostname == 'hifi-node-kitchen' }}"
186 manage_asound_conf: "{{ inventory_hostname == 'hifi-node-kitchen' }}"
187
188 # Device configuration
189 audio_hw_device: "{{ 'hw:1,0' if inventory_hostname == 'hifi-node-kitchen' else 'hw:0,0' }}"
190 audio_ctl_card: "{{ 1 if inventory_hostname == 'hifi-node-kitchen' else 0 }}"
191 snapcast_device: "{{ 'hw:0,0' if inventory_hostname == 'hifi-node-dining' }}"
192
193 # Server configuration using inventory lookup
194 snapcast_server_host: "homeassistant.home"
195 snapcast_client_name: "{{ inventory_hostname }}"
196
197 # The dining node is the table. It takes its sound card for the length
198 # of a session and gives it back afterwards, so the D&D atmosphere has
199 # its own path and the kitchen keeps playing music throughout.
200 dnd_atmosphere_enabled: "{{ inventory_hostname == 'hifi-node-dining' }}"
201
202 # Disable Avahi for explicit server configuration
203 enable_avahi: false
204
205 # Room ambient light. Only where a sensor is physically wired -- the dining
206 # node has a BH1750 on its header, across the room from the photo frame, so
207 # it measures the light falling on the viewing position rather than what the
208 # panel is putting out. Enabled per host in host_vars.
209 - role: ambient-light-sensor
210 tags: [ambient, light, sensor]
211 when: ambient_light_enabled | default(false) | bool
212
213 post_tasks:
214 - name: Validate audio client installation
215 block:
216 - name: Check snapclient service status
217 systemd:
218 name: snapclient
219 register: snapclient_service
220 when: audio_client_type | default('snapcast') == "snapcast"
221 ignore_errors: true
222
223 - name: Check sendspin service status
224 systemd:
225 name: sendspin
226 register: sendspin_service
227 when: audio_client_type | default('snapcast') == "sendspin"
228 ignore_errors: true
229
230 tags: [validation, audio]
231
232 - name: Validate ambient light sensing
233 block:
234 - name: Check ambient-light service status
235 systemd:
236 name: ambient-light
237 register: ambient_light_service
238 ignore_errors: true
239
240 # Proves the whole path end to end -- bus, sensor and endpoint -- in a
241 # way that a running unit alone does not: the reader stays up and
242 # serving even when the sensor cannot be read.
243 - name: Check the ambient light endpoint answers
244 ansible.builtin.uri:
245 url: "http://127.0.0.1:{{ ambient_light_port | default(9030) }}/lux"
246 return_content: true
247 # 503 is the endpoint's own "reading is stale", which is a real
248 # answer worth reporting rather than a failure to reach it.
249 status_code: [200, 503]
250 register: ambient_light_probe
251 ignore_errors: true
252
253 when: ambient_light_enabled | default(false) | bool
254 tags: [validation, ambient]
255
256 - name: Display audio client configuration summary
257 debug:
258 msg: |
259 ============================================================================
260 Audio Client Configuration Complete! - Stage 2 Finished
261 ============================================================================
262
263 Audio Configuration:
264 Client Name: {{ inventory_hostname }}
265 Backend: {{ audio_client_type | default('snapcast') }}
266 Audio Device: {{ 'hw:1,0 (mono mixed)' if inventory_hostname == 'hifi-node-kitchen' else 'hw:0,0 (stereo)' }}
267 Service Status: {{ 'Running' if (snapclient_service is defined and snapclient_service.status is defined and snapclient_service.status.ActiveState == 'active') or (sendspin_service is defined and sendspin_service.status is defined and sendspin_service.status.ActiveState == 'active') else 'Not running / Skipped' }}
268
269 Ambient Light:
270 Enabled: {{ ambient_light_enabled | default(false) }}
271 Service: {{ 'Running' if (ambient_light_service is defined and ambient_light_service.status is defined and ambient_light_service.status.ActiveState == 'active') else 'Not running / Skipped' }}
272 Endpoint: {{ ('http://' ~ inventory_hostname ~ ':' ~ (ambient_light_port | default(9030)) ~ '/lux') if (ambient_light_enabled | default(false) | bool) else 'n/a' }}
273 Reading: {{ (ambient_light_probe.json.lux ~ ' lx' ~ (' (STALE)' if ambient_light_probe.json.stale else '')) if (ambient_light_probe is defined and ambient_light_probe.json is defined) else 'Skipped' }}
274
275 ============================================================================
276 tags: [always]
277