[WIP] Perform installation of HomeAssistant host #71

Draft
noby wants to merge 13 commits from noby/ansible:homeassistant into master
33 changed files with 1905 additions and 0 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
site.retry
ansible.log
*.swp
*.pyc

View File

@ -9,3 +9,40 @@ root_keys_host:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC0Wq37DP89UO6MiJvvRbsXEcEV9d5/JJb7K2R0WHsHa sct39667@m-mob-062"
uau_reboot: "false"
mosquitto_listeners:
# Listeners for Mosquitto MQTT Broker
- name: "default"
listener: "1883"
protocol: "mqtt"
use_username_as_clientid: "false"
allow_zero_length_clientid: "true"
allow_anonymous: "false"
users:
- username: admin
password: "{{ vault_mosquitto_arwen_admin_passwd }}"
acl:
- permissions: readwrite
topic: "#"
- username: homeassistant
password: "{{ vault_mosquitto_arwen_homeassistant_passwd }}"
acl:
- permissions: readwrite
topic: "#"
mosquitto_bridges:
- connection: pizza
address: 172.23.4.6:1883
topics:
- topic: "# out 0"
- topic: "# in 0"
ha_pg_db_pass: "{{ vault_ha_pg_db_pass }}"
pgadmin4_db_password: "{{ vault_pgadmin4_db_password }}"
pgadmin4_initial_user_email: noby@binary-kitchen.de
pgadmin4_initial_user_password: "{{ vault_pgadmin4_initial_user_password }}"
ha_pg_grafana_db_pass: "{{ vault_ha_pg_grafana_db_pass }}"
ha_domains:
- lasagne.binary.kitchen

View File

@ -0,0 +1,22 @@
---
# Python version required for home assistant
ha_python_version: '3.12'
# The location of the config directory
ha_conf_dir: /etc/homeassistant
# The location of the installatin directory
ha_venv_dir: "/opt/homeassistant"
# The default user
ha_user: homeassistant
ha_pg_db_version: 15
ha_pg_db_name: homeassistant
ha_pg_db_user: homeassistant
ha_pg_db_pass: xxxxx
ha_pg_grafana_db_name: grafana
ha_pg_grafana_db_user: grafana
ha_pg_grafana_db_pass: xxxxx

View File

@ -0,0 +1,21 @@
---
- name: Restart postgresql
ansible.builtin.service:
name: postgresql
state: restarted
- name: Restart homeassistant
ansible.builtin.service:
name: home-assistant
state: restarted
- name: Restart grafana
ansible.builtin.service:
name: grafana-server
state: restarted
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restarted

View File

@ -0,0 +1,14 @@
---
galaxy_info:
author: Thomas Basler
description: Install HomeAssistant environment
license: None
platforms:
- name: Debian
min_ansible_version: "2.4"
dependencies:
- { role: mosquitto }
- { role: pgadmin4 }
- { role: nginx, nginx_ssl: false }

View File

@ -0,0 +1,77 @@
---
- name: Grafana | add GPG signing key
become: true
ansible.builtin.apt_key:
url: "https://apt.grafana.com/gpg.key"
state: present
validate_certs: true
- name: Grafana | add official repository
become: true
ansible.builtin.apt_repository:
repo: "deb https://apt.grafana.com stable main"
state: present
filename: grafana
update_cache: true
tags: install
- name: Grafana | establish dependencies
become: true
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop: ["grafana"]
tags: install
- name: Grafana | Configure PostgreSQL database
community.general.postgresql_db:
name: "{{ ha_pg_grafana_db_name }}"
template: template0
encoding: utf8
become: true
become_user: postgres
- name: Grafana | Configure PostgreSQL user
community.general.postgresql_user:
db: "{{ ha_pg_grafana_db_name }}"
name: "{{ ha_pg_grafana_db_user }}"
password: "{{ ha_pg_grafana_db_pass }}"
become: true
become_user: postgres
- name: Grafana | GRANT ALL PRIVILEGES ON SCHEMA public TO {{ pgadmin4_db_user }}
community.postgresql.postgresql_privs:
db: "{{ ha_pg_grafana_db_name }}"
privs: ALL
type: schema
objs: public
role: "{{ ha_pg_grafana_db_user }}"
become: true
become_user: postgres
- name: GRANT SELECT PRIVILEGES ON DATABASE {{ ha_pg_db_name }} TO {{ ha_pg_grafana_db_user }}
community.general.postgresql_privs:
db: "{{ ha_pg_db_name }}"
privs: SELECT
type: table
objs: statistics,statistics_meta
role: "{{ ha_pg_grafana_db_user }}"
become: true
become_user: postgres
ignore_errors: true
- name: Grafana | install config file
ansible.builtin.template:
src: grafana.ini.j2
dest: "/etc/grafana/grafana.ini"
owner: root
group: root
mode: "0644"
notify: Restart grafana
- name: Grafana | Start service
ansible.builtin.service:
name: grafana-server
state: started
enabled: true

View File

@ -0,0 +1,33 @@
---
- name: Install defined version of Home Assistant
ansible.builtin.pip:
name:
- wheel
- psycopg2
- packaging
- uv
- netifaces
- homeassistant=={{ ha_version }}
virtualenv: '{{ ha_venv_dir }}'
virtualenv_command: 'python{{ ha_python_version }} -m venv'
when: ha_version is defined
become: true
become_user: "{{ ha_user }}"
notify: Restart homeassistant
- name: Install latest version of Home Assistant
ansible.builtin.pip:
name:
- wheel
- psycopg2
- packaging
- uv
- homeassistant
extra_args: "--upgrade"
virtualenv: "{{ ha_venv_dir }}"
virtualenv_command: 'python{{ ha_python_version }} -m venv'
when: ha_version is undefined
become: true
become_user: "{{ ha_user }}"
notify: Restart homeassistant

View File

@ -0,0 +1,15 @@
---
- name: Install python if required
ansible.builtin.include_tasks: python_312.yml
when: ha_python_version == '3.12'
- name: Include sub-tasks
ansible.builtin.include_tasks: '{{ item }}'
loop:
- preparation.yml
- postgres.yml
- systemd.yml
- installation.yml
- grafana.yml
- nginx.yml

View File

@ -0,0 +1,15 @@
---
- name: Configure vhost
ansible.builtin.template:
src: vhost.j2
dest: /etc/nginx/sites-available/homeassistant
mode: "0644"
notify: Restart nginx
- name: Enable vhost
ansible.builtin.file:
src: /etc/nginx/sites-available/homeassistant
dest: /etc/nginx/sites-enabled/homeassistant
state: link
notify: Restart nginx

View File

@ -0,0 +1,54 @@
---
- name: Postgres | establish dependencies
ansible.builtin.package:
name: "{{ item }}"
state: present
loop:
- postgresql-{{ ha_pg_db_version }}
- libpq-dev
- python3-psycopg2
- name: Postgres | Configure PostgreSQL database
community.general.postgresql_db:
name: "{{ ha_pg_db_name }}"
template: template0
encoding: utf8
become: true
become_user: postgres
- name: Postgres | Configure PostgreSQL user
community.general.postgresql_user:
db: "{{ ha_pg_db_name }}"
name: "{{ ha_pg_db_user }}"
password: "{{ ha_pg_db_pass }}"
become: true
become_user: postgres
- name: Postgres | GRANT ALL PRIVILEGES ON SCHEMA public TO {{ ha_pg_db_user }}
community.postgresql.postgresql_privs:
db: "{{ ha_pg_db_user }}"
privs: ALL
type: schema
objs: public
role: "{{ ha_pg_db_user }}"
become: true
become_user: postgres
- name: Postgres | Grant all users access to all dbs
community.general.postgresql_pg_hba:
dest: /etc/postgresql/{{ ha_pg_db_version }}/main/pg_hba.conf
contype: host
users: all
databases: all
method: scram-sha-256
source: 0.0.0.0/0
notify: Restart postgresql
- name: Postgres | Listen to external interfaces
community.general.postgresql_set:
name: listen_addresses
value: "*"
become: true
become_user: postgres
notify: Restart postgresql

View File

@ -0,0 +1,41 @@
---
- name: Install commonly-named packages
ansible.builtin.package:
name: "{{ item }}"
state: present
loop:
- python3
- python3-dev
- python3-venv
- python3-pip
- libffi-dev
- libssl-dev
- libjpeg-dev
- zlib1g-dev
- autoconf
- build-essential
- libopenjp2-7
- libtiff6
- libturbojpeg0
- tzdata
- git
- ffmpeg
- name: Create user
ansible.builtin.user:
name: "{{ ha_user }}"
comment: "Home Assistant"
system: true
shell: "/sbin/nologin"
- name: Create directory
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "02775"
owner: "{{ ha_user }}"
group: "{{ ha_user }}"
loop:
- "{{ ha_conf_dir }}"
- "{{ ha_venv_dir }}"

View File

@ -0,0 +1,26 @@
---
- name: Python 3.12 | add GPG signing key
become: true
ansible.builtin.apt_key:
url: "https://pascalroeleven.nl/deb-pascalroeleven.gpg"
state: present
validate_certs: true
tags: install
- name: Python 3.12 | add official repository
become: true
ansible.builtin.apt_repository:
repo: "deb http://deb.pascalroeleven.nl/python3.12 bookworm-backports main"
state: present
filename: python312
update_cache: true
tags: install
- name: Python 3.12 | establish dependencies
become: true
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop: "{{ python312_dependencies }}"
tags: install

View File

@ -0,0 +1,17 @@
---
- name: Install systemd unit file
ansible.builtin.template:
src: home-assistant.service.j2
dest: "/etc/systemd/system/home-assistant.service"
owner: root
group: root
mode: "0644"
notify: Restart homeassistant
- name: Enable home assistant service
ansible.builtin.systemd:
name: home-assistant
daemon_reload: true
enabled: true
notify: Restart homeassistant

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,14 @@
[Unit]
Description=Home Assistant
After=network.target postgresql.service
[Service]
Type=simple
User={{ ha_user }}
Environment="PATH=/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:{{ ha_venv_dir }}/bin"
ExecStart={{ ha_venv_dir }}/bin/hass --config {{ ha_conf_dir }}
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,41 @@
{{ ansible_managed | comment }}
server {
listen 80;
listen [::]:80;
server_name {{ ha_domains | join(' ') }};
proxy_buffering off;
location / {
proxy_pass http://127.0.0.1:8123;
proxy_set_header Host $host;
proxy_redirect http:// https://;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /api/websocket {
proxy_pass http://127.0.0.1:8123;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /grafana {
client_max_body_size 1024M;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_pass http://localhost:3000;
}
location = /pgadmin4 { rewrite ^ /pgadmin4/; }
location /pgadmin4 { try_files $uri @pgadmin4; }
location @pgadmin4 {
include uwsgi_params;
uwsgi_pass unix:/run/pgadmin4/pgadmin4.sock;
}
}

View File

@ -0,0 +1,6 @@
---
python312_dependencies:
- python3.12
- python3.12-venv
- python3.12-dev

View File

@ -0,0 +1,4 @@
Ansible Role: Mosquitto
=========
Install and configure [Mosquitto](https://mosquitto.org/) MQTT message broker.

View File

@ -0,0 +1,46 @@
---
mosquitto_packages:
- mosquitto
- mosquitto-clients
mosquitto_listeners:
# Listeners for Mosquitto MQTT Broker
- name: "default"
listener: "1883 localhost"
protocol: "mqtt"
use_username_as_clientid: "true"
allow_zero_length_clientid: "true"
allow_anonymous: "false"
users: []
# Users for Mosquitto MQTT Broker
# Type: Arrays of Objects with following parameters defined:
# - username: <string>
# password: <string>
# acl: <Array> of Objects as follows:
# - permissions: <string> Acceptable Value: either `read`, `readwrite`, `write`, `deny`
# - topic: <string> Acceptable Value: your/mqtt/topic (wildcards `+`, and `*` allowed)
auth_anonymous: []
# Topics which are accessable with anonymous access
# Example
# - "topic read topic_name"
auth_patterns: []
# %c to match the client id of the client
# %u to match the username of the client
# Example
# - "pattern write $SYS/broker/connection/%c/state"
mosquitto_bridges: []
# Bridges for Mosquitto MQTT Broker
# Type: Arrays of Objects with following parameters defined:
# - connection: <string>
# address: <string>
# bridge_insecure: <boolean>
# bridge_capath: <string>
# remote_password: <string>
# remote_username: <string>
# remote_clientid: <string>
# try_private: <boolean>
# topics:
# - topic: # in 0 down/ to-level/02/line/
# - topic: # out 0 up/ from-level/02/line/

View File

@ -0,0 +1,34 @@
# mosquitto_passwd.py: Custom Jinja2 filter plugin to generate valid PBKDF2_SHA512
# hash digests for plain-text passwords in `users` file for
# Eclipse Mosquitto Broker
from ansible.errors import AnsibleError
def mosquitto_passwd(passwd):
try:
import passlib.hash
except Exception as e:
raise AnsibleError(
'mosquitto_passlib custom filter requires the passlib pip package installed')
SALT_SIZE = 12
ITERATIONS = 101
salt = passwd[:SALT_SIZE]
salt = bytes(salt, 'utf-8')
salt += b"0" * (SALT_SIZE - len(salt))
digest = passlib.hash.pbkdf2_sha512.using(salt_size=SALT_SIZE, rounds=ITERATIONS, salt=salt) \
.hash(passwd) \
.replace("pbkdf2-sha512", "7") \
.replace(".", "+")
return digest + "=="
class FilterModule(object):
def filters(self):
return {
'mosquitto_passwd': mosquitto_passwd,
}

View File

@ -0,0 +1,6 @@
---
- name: Restart Mosquitto
ansible.builtin.service:
name: mosquitto
state: restarted

View File

@ -0,0 +1,11 @@
---
galaxy_info:
author: Thomas Basler
description: Install Mosquitto
license: None
platforms:
- name: Debian
min_ansible_version: "2.4"
dependencies: []

View File

@ -0,0 +1,35 @@
---
- name: Mosquitto | Install Mosquitto packages
ansible.builtin.apt:
name: "{{ item }}"
state: present
with_items: "{{ mosquitto_packages }}"
notify: Restart Mosquitto
- name: Mosquitto | Generating Configuration File
ansible.builtin.template:
src: mosquitto.conf.j2
dest: /etc/mosquitto/conf.d/mosquitto.conf
mode: "0755"
notify: Restart Mosquitto
- name: Mosquitto | Generating Authentication Users File
ansible.builtin.template:
src: users.j2
dest: "/etc/mosquitto/users_{{ item.name }}"
mode: "0755"
vars:
mosquitto_users: "{{ item.users }}"
with_items: "{{ mosquitto_listeners }}"
notify: Restart Mosquitto
- name: Mosquitto | Generating Access Control List File
ansible.builtin.template:
src: acl.j2
dest: "/etc/mosquitto/acl_{{ item.name }}"
mode: "0755"
vars:
listener: "{{ item }}"
with_items: "{{ mosquitto_listeners }}"
notify: Restart Mosquitto

View File

@ -0,0 +1,28 @@
{{ ansible_managed | comment }}
{% for entry in listener.auth_anonymous | default([]) %}
{% if loop.first %}
# Anonymous access
{% endif %}
{{ entry }}
{% endfor %}
{% for user in listener.users %}
{% if loop.first %}
# User access
{% endif %}
user {{ user.username }}
{% for access_list in user.acl | default([]) %}
topic {{ access_list.permissions }} {{ access_list.topic }}
{% if loop.last %}
{% endif %}
{% endfor %}
{% endfor %}
{% for entry in listener.auth_patterns | default([]) %}
{% if loop.first %}
# Global patterns
{% endif %}
{{ entry }}
{% endfor %}

View File

@ -0,0 +1,36 @@
{{ ansible_managed | comment }}
# Logging Configuration
log_timestamp true
log_type all
# Listener
per_listener_settings true
{% for elem in mosquitto_listeners %}
### Listener '{{ elem.name }}'
listener {{ elem.listener }}
{% for key, value in elem | dictsort %}
{% if key not in ["listener", "name", "users", "auth_anonymous", "auth_patterns"] %}
{{ key }} {{ value }}
{% endif %}
{% endfor %}
password_file /etc/mosquitto/users_{{ elem.name }}
acl_file /etc/mosquitto/acl_{{ elem.name }}
{% endfor %}
{% for elem in mosquitto_bridges %}
{% if loop.first %}
# Bridges
{% endif %}
connection {{ elem.connection }}
{% for key, value in elem | dictsort %}
{% if key not in ["connection", "topics"] %}
{{ key }} {{ value }}
{% endif %}
{% endfor %}
{% for topic in elem.topics %}
topic {{ topic.topic }}
{% endfor %}
{% endfor %}

View File

@ -0,0 +1,5 @@
{{ ansible_managed | comment }}
{% for user in mosquitto_users %}
{{ user.username }}:{{ user.password | mosquitto_passwd }}
{% endfor %}

View File

@ -0,0 +1,10 @@
---
pgadmin4_user: pgadmin4
pgadmin4_db_database: pgadmin4
pgadmin4_db_user: pgadmin4
pgadmin4_db_password: xxxxx
pgadmin4_conf_dir: /etc/pgadmin
pgadmin4_initial_user_email: admin@admin.com
pgadmin4_initial_user_password: admin42

View File

@ -0,0 +1,6 @@
---
- name: Restart pgadmin4
ansible.builtin.service:
name: pgadmin4
state: restarted

View File

@ -0,0 +1,11 @@
---
galaxy_info:
author: Thomas Basler
description: Install PgAdmin4
license: None
platforms:
- name: Debian
min_ansible_version: "2.4"
dependencies: []

View File

@ -0,0 +1,119 @@
---
- name: PgAdmin 4 | add GPG signing key
become: true
ansible.builtin.apt_key:
url: "https://www.pgadmin.org/static/packages_pgadmin_org.pub"
state: present
validate_certs: true
tags: install
- name: PgAdmin 4 | add official repository
become: true
ansible.builtin.apt_repository:
repo: "deb https://ftp.postgresql.org/pub/pgadmin/pgadmin4/apt/bookworm pgadmin4 main"
state: present
filename: pgadmin4
update_cache: true
tags: install
- name: PgAdmin 4 | establish dependencies
become: true
ansible.builtin.apt:
name: "{{ item }}"
state: present
tags: install
loop: ["pgadmin4-server", "uwsgi-core", "uwsgi-plugin-python3", "python3-pexpect"]
- name: PgAdmin 4 | Configure PostgreSQL database
community.general.postgresql_db:
name: "{{ pgadmin4_db_database }}"
template: template0
encoding: utf8
become: true
become_user: postgres
register: pgadmin4_db
- name: PgAdmin 4 | Configure PostgreSQL user
community.general.postgresql_user:
db: "{{ pgadmin4_db_database }}"
name: "{{ pgadmin4_db_user }}"
password: "{{ pgadmin4_db_password }}"
become: true
become_user: postgres
- name: PgAdmin 4 | Configure PostgreSQL user privileges
community.postgresql.postgresql_privs:
database: "{{ pgadmin4_db_database }}"
state: present
privs: ALL
type: database
role: "{{ pgadmin4_db_user }}"
become: true
become_user: postgres
- name: PgAdmin 4 | GRANT ALL PRIVILEGES ON SCHEMA public TO {{ pgadmin4_db_user }}
community.postgresql.postgresql_privs:
db: "{{ pgadmin4_db_database }}"
privs: ALL
type: schema
objs: public
role: "{{ pgadmin4_db_user }}"
become: true
become_user: postgres
- name: Create user
ansible.builtin.user:
name: "{{ pgadmin4_user }}"
comment: "pgAdmin 4"
createhome: false
system: true
shell: "/sbin/nologin"
- name: PgAdmin 4 | create config directory
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "02775"
owner: "root"
group: "root"
with_items:
- "{{ pgadmin4_conf_dir }}"
- name: PgAdmin 4 | install config file
ansible.builtin.template:
src: config_system.py.j2
dest: "{{ pgadmin4_conf_dir }}/config_system.py"
owner: root
group: root
mode: "0644"
notify: Restart pgadmin4
- name: PgAdmin 4 | install systemd unit file
ansible.builtin.template:
src: pgadmin4.service.j2
dest: "/etc/systemd/system/pgadmin4.service"
owner: root
group: root
mode: "0644"
notify: Restart pgadmin4
- name: PgAdmin 4 | enable service
ansible.builtin.service:
name: pgadmin4
enabled: true
- name: PgAdmin 4 | setup pgadmin # noqa: no-handler
ansible.builtin.expect:
command: /bin/bash -c "/usr/pgadmin4/venv/bin/python3 /usr/pgadmin4/web/setup.py setup-db"
chdir: /usr/pgadmin4/web/
echo: true
timeout: 300
responses:
'Email\ address:': "{{ pgadmin4_initial_user_email | trim }}"
'Password:': "{{ pgadmin4_initial_user_password | trim }}"
'Retype\ password:': "{{ pgadmin4_initial_user_password | trim }}"
'Do\ you\ wish\ to\ continue\ \(y/n\)\?': "y"
'Would\ you\ like\ to\ continue\ \(y/n\)\?': "y"
when: pgadmin4_db.changed
notify: Restart pgadmin4

View File

@ -0,0 +1,4 @@
LOG_FILE = '/var/log/pgadmin/pgadmin4.log'
CONFIG_DATABASE_URI = 'postgresql://{{ pgadmin4_db_user }}:{{ pgadmin4_db_password }}@localhost:5432/{{ pgadmin4_db_database }}'
SESSION_DB_PATH = '/var/lib/pgadmin/sessions'
STORAGE_DIR = '/var/lib/pgadmin/storage'

View File

@ -0,0 +1,29 @@
[Unit]
Description = PgAdmin4 uwsgi Service
After = network.target network-online.target
Wants = network-online.target
[Service]
User={{ pgadmin4_user }}
StateDirectory=pgadmin
RuntimeDirectory=pgadmin4
LogsDirectory=pgadmin
ExecStart=uwsgi \
--socket /run/pgadmin4/pgadmin4.sock --chmod-socket=666 \
--plugin python3 \
-H /usr/pgadmin4/venv \
--processes 1 \
--threads 25 \
--chdir /usr/pgadmin4/web/ \
--manage-script-name \
--mount /pgadmin4=pgAdmin4:app
ExecReload=/bin/kill -HUP $MAINPID
ExecStop=/bin/kill -INT $MAINPID
Restart=always
Type=notify
StandardError=syslog
NotifyAccess=all
KillSignal=SIGQUIT
[Install]
WantedBy = multi-user.target

View File

@ -172,3 +172,8 @@
hosts: barium.binary-kitchen.net
roles:
- workadventure
- name: Setup HomeAssistant server
hosts: lasagne.binary.kitchen
roles:
- homeassistant