Compare commits

...

14 commits

Author SHA1 Message Date
6e30232057
ref: Install authorized keys per user
Instead of installing authorized keys globally (same for everybody), we
pass in the authorized_keys variable per user, and thus the installation
also takes place per user.

This makes much more sense and works with minimal refactoring.
2025-11-19 22:13:11 +01:00
66ce16ce55
fix: Remove bootstrap python setup from check mode
Since we can't look into the variable `pythoncheck.rc`, as it doesn't
exist, we skip the `when` check for checkmode -- its installation cannot
run under any circumstance in the mode anyways.
2025-11-19 22:13:10 +01:00
2fc23d9774
feat: Set up timezone and users and groups on system host 2025-11-19 22:13:10 +01:00
b493485b90
feat: Add authorized ssh keys to host 2025-11-19 22:13:09 +01:00
8019fa9276
docs: Add organization roadmap to README 2025-11-19 22:13:08 +01:00
bb9de502ce
feat: Set up filesystems
Automatically set up btrfs root and data filesystem, as well as external
HDD.

This automation change assumes a layout exactly as in current bob to
function by default, can be changed to any btrfs layout with the
`btrfs_mounts` configuration option, however.
2025-11-19 22:13:08 +01:00
a217d65640
feat: Update incus installation role
Now uses simple external ansible galaxy role, and should install incus
from a pre-fixed seed.
2025-11-19 22:13:07 +01:00
d8ed04f4d1
ref: Add galaxy installed ansible roles into local directory 2025-11-19 22:13:07 +01:00
5f737ab4b5
feat: Set up ansible role install task for just 2025-11-19 22:13:06 +01:00
5257525c7e
ref: Add local ansible dir to gitignore
Will contain local ansible roles and perhaps more, set in ansible.cfg.
2025-11-19 22:13:06 +01:00
0de79fc1d2
ref: Remove vault-password static file from repo
Instead of having the file statically (and plain-text) in the repo
itself, we simply query `pass` for it instead.

Slightly cumbersome syntax since ansible (afaik) does not allow a
similar easy variable-enabled lookup as for become passwords, so we also
whipped it into a justfile to not have to type it each time.

The command line uses cat to receive the password as a 'file' on stdin.
2025-11-19 22:13:05 +01:00
e6194e35bf
docs: Add more stacks to readme 2025-11-19 22:13:05 +01:00
54b8404743
fix: Move incus template files into correct role
Moved from system role where they used to be required into the
(currently disabled) incus installation role.
2025-11-19 22:13:04 +01:00
66d1032869
ref: Create scratch dir for not to be commited items 2025-11-19 22:13:03 +01:00
19 changed files with 319 additions and 227 deletions

3
.gitignore vendored
View file

@ -1 +1,4 @@
vaultpass
/.ansible
/temp/

View file

@ -17,3 +17,60 @@ and media management applications jellyfin and audiobookshelf.
Media can be requested through Jellyseerr.
Served through homarr personal dashboard.
## Paperless stack
Hosts all my personal documents. This is an important stack which should be backed up accordingly.
## Grocy stack
Was an experimental stack which I may have used in my home for shopping lists, ingredient tracking,
and more.
After some consideration and experimentation, for the moment, I have decided against using grocy:
it provides comprehensive tracking but also requires comprehensive use to get the most out of it.
I get the feeling a badly implemented/maintained grocy setup is _worse_ than a simpler task-list and
e.g. Recipe KanBan board approach.
## Thoughts on organization
.
ansible
roles
system
infrastructure -> calls tofu role
arr
paperless
...
tofu
incus_machines
incus_networks?
incus_storage?
### Production IaC
- ansible:
host_roles:
system
filesystem
- terraform:
infrastructure (tofu)
- ansible:
instance_roles:
caddy
arr
paperless
### Testing
- terraform? ansible?
- create 'host' VM
- ensure connection to host vm as part of host group
- ansible:
host_roles: ...
- tf:
infra...
- ansible:
instance_roles:...

View file

@ -1,5 +1,4 @@
[defaults]
remote_tmp = /tmp
inventory = inventory
vault_password_file = vaultpass
roles_path = .ansible/roles:roles

5
justfile Normal file
View file

@ -0,0 +1,5 @@
install:
ansible-galaxy install -r requirements.yaml
deploy:
pass show hosting/ansible/bob/vault-password | ansible-playbook --vault-password-file=/bin/cat site.yaml

4
requirements.yaml Normal file
View file

@ -0,0 +1,4 @@
# Install and pre-configure incus
- src: gliech.incus
# install and set up docker
- src: geerlingguy.docker

View file

@ -0,0 +1,38 @@
filesystem
=========
Mounts the filesystem(s) required for bob.
Focused on correct `btrfs` layout and mounting,
but also mounts an external `ext4` HDD by default.
Requirements
------------
This role requires a btrfs filesystem _existing_ on _any device_ that is targeted with the role (using the 'btrfs_mounts') configuration option.
Optionally, an external HDD is required if the mount toggle is true.
Role Variables
--------------
`btrfs_mounts` can be used to set up the various (top-level) btrfs subvolumes, and their later mount points in the system.
Define an entry by giving the `uuid` of the targeted btrfs filesystem (or device), give the name of the `subvol` you intend to give, and define the `path` where it should ultimately be mounted by `fstab`.
You can additionally provide some `opts` which are given to `fstab` for the mount process as well.
Example:
```yaml
btrfs_mounts:
- path: "/home"
subvol: "@home"
uuid: "3204f33f-5fa7-4c11-bdd8-979c539fce91"
opts: "defaults,ssd,noatime,compress=zstd:3,space_cache=v2"
- path: "/srv/media"
subvol: "@media"
uuid: "2256bd23-7751-486a-bfc4-b216a6b0c4f4"
opts: "defaults,noatime,compress=zstd:3,space_cache=v2"
```
---
`should_mount_external_hdd` can be toggled to automatically mount a USB-connected HDD on boot, or not.

View file

@ -0,0 +1,30 @@
---
should_mount_external_hdd: true
should_reboot_machine: false
btrfs_mounts:
- path: "/"
subvol: "@rootfs"
uuid: "3204f33f-5fa7-4c11-bdd8-979c539fce91"
opts: "defaults,ssd,noatime,compress=zstd:3,space_cache=v2"
- path: "/home"
subvol: "@home"
uuid: "3204f33f-5fa7-4c11-bdd8-979c539fce91"
opts: "defaults,ssd,noatime,compress=zstd:3,space_cache=v2"
- path: "/srv/media"
subvol: "@media"
uuid: "2256bd23-7751-486a-bfc4-b216a6b0c4f4"
opts: "defaults,noatime,compress=zstd:3,space_cache=v2"
- path: "/srv/files"
subvol: "@files"
uuid: "2256bd23-7751-486a-bfc4-b216a6b0c4f4"
opts: "defaults,noatime,compress=zstd:3,space_cache=v2"
- path: "/srv/documents"
subvol: "@documents"
uuid: "2256bd23-7751-486a-bfc4-b216a6b0c4f4"
opts: "defaults,noatime,compress=zstd:3,space_cache=v2"
- path: "/srv/wolf"
subvol: "@wolf"
uuid: "2256bd23-7751-486a-bfc4-b216a6b0c4f4"
opts: "defaults,noatime,compress=zstd:1,space_cache=v2"

View file

@ -0,0 +1,7 @@
---
- name: Reboot machine
ansible.builtin.reboot:
connect_timeout: 600
post_reboot_delay: 10
when: "should_reboot_machine"

View file

@ -0,0 +1,30 @@
---
- name: Ensure btrfs ROOT layout
community.general.btrfs_subvolume:
name: "/{{ item.subvol }}"
# filesystem_device: /dev/sdb1
# fileystem_label: btrfs-root # only 1 of the 3 required
filesystem_uuid: "{{ item.uuid }}"
loop: "{{ btrfs_mounts }}"
become: true
- name: Ensure fstab contains btrfs mount entries
ansible.posix.mount:
path: "{{ item.path }}"
src: "UUID={{ item.uuid }}"
fstype: btrfs
opts: "{{ item.opts }},subvol={{ item.subvol }}"
state: present
loop: "{{ btrfs_mounts }}"
become: true
notify: Reboot machine
- name: Ensure external HDD is mounted
ansible.posix.mount:
path: /mnt/ext
src: "UUID=01b221f2-83a5-49e4-bdef-ee9ee9ac5310"
fstype: ext4
opts: "noatime"
state: present
become: true
when: "should_mount_external_hdd"

View file

@ -0,0 +1,2 @@
---
# vars file for btrfs

View file

@ -1,220 +0,0 @@
---
- name: Incus - Install packages and bootstrap
hosts: all
gather_facts: true
gather_subset:
- "default_ipv4"
- "default_ipv6"
- "distribution_release"
vars:
task_init: "{{ incus_init | default('{}') }}"
task_ip_address: "{{ incus_ip_address | default(ansible_default_ipv6['address'] | default(ansible_default_ipv4['address'])) }}"
task_name: "{{ incus_name | default('') }}"
task_roles: "{{ incus_roles | default(['ui', 'standalone']) }}"
task_ovn_northbound: "{{ lookup('template', '../files/ovn/ovn-central.servers.tpl') | from_yaml | map('regex_replace', '^(.*)$', 'ssl:[\\1]:6641') | join(',') }}"
task_servers: "{{ lookup('template', 'files/incus.servers.tpl') | from_yaml | sort }}"
any_errors_fatal: true
become: true
tasks:
- name: Install the Incus package (deb)
ansible.builtin.apt:
name:
- incus
install_recommends: no
state: present
register: install_deb
when: 'ansible_distribution in ("Debian", "Ubuntu") and task_roles | length > 0'
- name: Install the Incus package (rpm)
ansible.builtin.package:
name:
- incus
state: present
register: install_rpm
when: 'ansible_distribution == "CentOS" and task_roles | length > 0'
- name: Install the Incus UI package (deb)
ansible.builtin.apt:
name:
- incus-ui-canonical
install_recommends: no
state: present
when: 'ansible_distribution in ("Debian", "Ubuntu") and "ui" in task_roles'
# - name: Install btrfs tools
# ansible.builtin.package:
# name:
# - btrfs-progs
# state: present
# when: "task_roles | length > 0 and 'btrfs' in task_init['storage'] | dict2items | json_query('[].value.driver')"
#
# - name: Install ceph tools
# ansible.builtin.package:
# name:
# - ceph-common
# state: present
# when: "task_roles | length > 0 and 'ceph' in task_init['storage'] | dict2items | json_query('[].value.driver')"
#
# - name: Install LVM tools
# ansible.builtin.package:
# name:
# - lvm2
# state: present
# when: "task_roles | length > 0 and 'lvm' in task_init['storage'] | dict2items | json_query('[].value.driver')"
#
# - name: Install ZFS dependencies
# ansible.builtin.package:
# name:
# - zfs-dkms
# state: present
# when: "task_roles | length > 0 and 'zfs' in task_init['storage'] | dict2items | json_query('[].value.driver') and ansible_distribution == 'Debian'"
#
# - name: Install ZFS tools
# ansible.builtin.package:
# name:
# - zfsutils-linux
# state: present
# when: "task_roles | length > 0 and 'zfs' in task_init['storage'] | dict2items | json_query('[].value.driver')"
- name: Set uid allocation
ansible.builtin.shell:
cmd: "usermod root --add-subuids 10000000-1009999999"
when: '(install_deb.changed or install_rpm.changed) and ansible_distribution == "CentOS"'
- name: Set gid allocation
ansible.builtin.shell:
cmd: "usermod root --add-subgids 10000000-1009999999"
when: '(install_deb.changed or install_rpm.changed) and ansible_distribution == "CentOS"'
- name: Enable incus socket unit
ansible.builtin.systemd:
enabled: true
name: incus.socket
state: started
when: "install_deb.changed or install_rpm.changed"
- name: Enable incus service unit
ansible.builtin.systemd:
enabled: true
name: incus.service
state: started
when: "install_deb.changed or install_rpm.changed"
- name: Enable incus startup unit
ansible.builtin.systemd:
enabled: true
name: incus-startup.service
state: started
when: "install_deb.changed or install_rpm.changed"
- name: Set client listen address
ansible.builtin.shell:
cmd: "incus --force-local config set core.https_address {{ task_ip_address }}"
when: '(install_deb.changed or install_rpm.changed) and ("standalone" in task_roles or ("cluster" in task_roles and task_servers[0] == inventory_hostname))'
- name: Set cluster listen address
ansible.builtin.shell:
cmd: "incus --force-local config set cluster.https_address {{ task_ip_address }}"
when: '(install_deb.changed or install_rpm.changed) and "cluster" in task_roles and task_servers[0] == inventory_hostname'
# - name: Set OVN NorthBound database
# shell:
# cmd: "incus --force-local config set network.ovn.northbound_connection={{ task_ovn_northbound }} network.ovn.client_cert=\"{{ lookup('file', '../data/ovn/'+ovn_name+'/'+inventory_hostname+'.crt') }}\" network.ovn.client_key=\"{{ lookup('file', '../data/ovn/'+ovn_name+'/'+inventory_hostname+'.key') }}\" network.ovn.ca_cert=\"{{ lookup('file', '../data/ovn/'+ovn_name+'/ca.crt') }}\""
# notify: Restart Incus
# when: '(install_deb.changed or install_rpm.changed) and task_ovn_northbound and ("standalone" in task_roles or ("cluster" in task_roles and task_servers[0] == inventory_hostname))'
- name: Add networks
ansible.builtin.shell:
cmd: "incus network create {{ item.key }} --type={{ item.value.type }}{% for k in item.value.local_config | default([]) %} {{ k }}={{ item.value.local_config[k] }}{% endfor %}{% for k in item.value.config | default([]) %} {{ k }}={{ item.value.config[k] }}{% endfor %}"
loop: "{{ task_init['network'] | dict2items }}"
when: '(install_deb.changed or install_rpm.changed) and ("standalone" in task_roles or ("cluster" in task_roles and task_servers[0] == inventory_hostname))'
- name: Set network description
ansible.builtin.shell:
cmd: 'incus network set --property {{ item.key }} description="{{ item.value.description }}"'
loop: "{{ task_init['network'] | dict2items }}"
when: '(install_deb.changed or install_rpm.changed) and ("standalone" in task_roles or ("cluster" in task_roles and task_servers[0] == inventory_hostname)) and item.value.description | default(None)'
- name: Add storage pools
ansible.builtin.shell:
cmd: "incus storage create {{ item.key }} {{ item.value.driver }}{% for k in item.value.local_config | default([]) %} {{ k }}={{ item.value.local_config[k] }}{% endfor %}{% for k in item.value.config | default([]) %} {{ k }}={{ item.value.config[k] }}{% endfor %}"
loop: "{{ task_init['storage'] | dict2items }}"
when: '(install_deb.changed or install_rpm.changed) and ("standalone" in task_roles or ("cluster" in task_roles and task_servers[0] == inventory_hostname))'
- name: Set storage pool description
ansible.builtin.shell:
cmd: 'incus storage set --property {{ item.key }} description="{{ item.value.description }}"'
loop: "{{ task_init['storage'] | dict2items }}"
when: '(install_deb.changed or install_rpm.changed) and ("standalone" in task_roles or ("cluster" in task_roles and task_servers[0] == inventory_hostname)) and item.value.description | default(None)'
- name: Add storage pool to default profile
ansible.builtin.shell:
cmd: "incus profile device add default root disk path=/ pool={{ item }}"
loop: "{{ task_init['storage'] | dict2items | json_query('[?value.default].key') }}"
when: '(install_deb.changed or install_rpm.changed) and ("standalone" in task_roles or ("cluster" in task_roles and task_servers[0] == inventory_hostname))'
- name: Add network to default profile
ansible.builtin.shell:
cmd: "incus profile device add default eth0 nic network={{ item }} name=eth0"
loop: "{{ task_init['network'] | dict2items | json_query('[?value.default].key') }}"
when: '(install_deb.changed or install_rpm.changed) and ("standalone" in task_roles or ("cluster" in task_roles and task_servers[0] == inventory_hostname))'
- name: Bootstrap the cluster
ansible.builtin.shell:
cmd: "incus --force-local cluster enable {{ inventory_hostname }}"
when: '(install_deb.changed or install_rpm.changed) and "cluster" in task_roles and task_servers[0] == inventory_hostname'
- name: Create join tokens
delegate_to: "{{ task_servers[0] }}"
ansible.builtin.shell:
cmd: "incus --force-local --quiet cluster add {{ inventory_hostname }}"
register: cluster_add
when: '(install_deb.changed or install_rpm.changed) and "cluster" in task_roles and task_servers[0] != inventory_hostname'
- name: Wait 5s to avoid token use before valid
ansible.builtin.wait_for:
timeout: 5
delegate_to: localhost
when: "cluster_add.changed"
- name: Join the cluster
throttle: 1
ansible.builtin.shell:
cmd: "incus --force-local admin init --preseed"
stdin: |-
cluster:
enabled: true
cluster_address: "{{ task_ip_address }}"
cluster_token: "{{ cluster_add.stdout }}"
server_address: "{{ task_ip_address }}"
member_config: {% for pool in task_init.storage %}{% for key in task_init.storage[pool].local_config | default([]) %}
- entity: storage-pool
name: {{ pool }}
key: {{ key }}
value: {{ task_init.storage[pool].local_config[key] }}{% endfor %}{% endfor %}{% for network in task_init.network %}{% for key in task_init.network[network].local_config | default([]) %}
- entity: network
name: {{ network }}
key: {{ key }}
value: {{ task_init.network[network].local_config[key] }}{% endfor %}{% endfor %}
when: "cluster_add.changed"
- name: Apply additional configuration
ansible.builtin.shell:
cmd: 'incus config set {{ item.key }}="{{ item.value }}"'
loop: "{{ task_init['config'] | default({}) | dict2items }}"
when: '(install_deb.changed or install_rpm.changed) and ("standalone" in task_roles or ("cluster" in task_roles and task_servers[0] == inventory_hostname))'
- name: Load client certificates
ansible.builtin.shell:
cmd: 'incus config trust add-certificate --name "{{ item.key }}" --type={{ item.value.type | default(''client'') }} -'
stdin: "{{ item.value.certificate }}"
loop: "{{ task_init['clients'] | default({}) | dict2items }}"
when: '(install_deb.changed or install_rpm.changed) and ("standalone" in task_roles or ("cluster" in task_roles and task_servers[0] == inventory_hostname))'
handlers:
- name: Restart Incus
ansible.builtin.systemd:
name: incus.service
state: restarted

View file

@ -0,0 +1,70 @@
# required by gliech.incus role
- name: Ensure lxc conf directory exists
ansible.builtin.file:
state: directory
path: /etc/lxc
become: true
- name: Install incus
ansible.builtin.import_role:
name: gliech.incus
vars:
incus_config:
config:
core.https_address: "[::]:8443"
networks:
- config:
ipv4.address: 10.172.89.1/24
ipv4.firewall: "true"
ipv4.nat: "true"
ipv6.address: fd42:c9d2:6e9f:be57::1/64
ipv6.nat: "true"
description: ""
name: incusbr0
type: bridge
project: default
storage_pools:
- config:
source: /var/lib/incus/storage-pools/default
volatile.initial_source: /var/lib/incus/storage-pools/default
description: ""
name: default
driver: btrfs
- config:
size: 50GiB
source: /var/lib/incus/disks/docker_store.img
description: ""
name: docker_store
driver: btrfs
storage_volumes: []
profiles:
- config: {}
description: Default Incus profile
devices:
eth0:
name: eth0
network: incusbr0
type: nic
root:
path: /
pool: default
type: disk
name: default
project: ""
projects:
- config:
features.images: "true"
features.networks: "true"
features.networks.zones: "true"
features.profiles: "true"
features.storage.buckets: "true"
features.storage.volumes: "true"
restricted: "false"
description: NAS
name: default
certificates: []
cluster_groups: []
# # TODO: Should presumably be split
# - name: "Install and bootstrap incus"
# ansible.builtin.include_tasks: bootstrap.yaml
# when: ansible_distribution == "Debian" and ansible_distribution_release == "bookworm"

View file

@ -1,7 +1,9 @@
---
- name: "Add incus repository to system"
## for bookworm only
- name: "Add incus repository to bookworm system"
ansible.builtin.include_tasks: add-repo.yaml
when: ansible_distribution == "Debian" and ansible_distribution_release == "bookworm"
# TODO: Should presumably be split
- name: "Install and bootstrap incus"
ansible.builtin.include_tasks: bootstrap.yaml
# TODO: there might be remaining issues on other OSes like centos, etc
- name: Install incus
ansible.builtin.include_tasks: install.yaml

View file

@ -0,0 +1,16 @@
---
system_timezone: "Europe/Berlin"
system_users:
- name: marty
groups:
- marty
- data
- incus-admin
authorized_keys:
- "{{ lookup('file', '~/.ssh/keys/bob.pub') }}"
- name: data
groups:
- data
create_home: false
shell: /sbin/nologin

View file

@ -36,3 +36,38 @@
- packages
become: true
- name: Set correct timezone
community.general.timezone:
name: "{{ system_timezone }}"
when: "system_timezone"
become: true
- name: Create necessary groups
ansible.builtin.group:
name: "{{ item }}"
state: present
loop: "{{ system_users | map(attribute='groups') | flatten | unique }}"
when: "system_users"
become: true
- name: Set up system users
ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
append: "{{ item.append | default(true) }}"
create_home: "{{ item.create_home | default(false) }}"
shell: "{{ item.shell | default('/bin/bash') }}"
loop: "{{ system_users }}"
when: "system_users"
become: true
- name: Add authorized SSH keys
ansible.posix.authorized_key:
user: "{{ item.name }}"
state: present
key: "{{ item.authorized_keys }}"
loop: "{{ system_users }}"
when: system_users is defined and item.authorized_keys is defined
tags:
- ssh
become: true

View file

@ -15,16 +15,30 @@
register: pythoncheck
- name: install debian python
ansible.builtin.raw: apt-get update && apt-get install python3 -y
when: pythoncheck.rc == 127
when: not ansible_check_mode and pythoncheck.rc == 127
- name: pretend installing debian python for check mode
ansible.builtin.debug:
msg: Pretending to install python...
when: ansible_check_mode
- name: Prepare incus server host
hosts: host_system
tasks:
- name: Prepare host filesystems
ansible.builtin.import_role:
name: filesystem
tags: filesystem
- name: Prepare system
ansible.builtin.import_role:
name: system
tags: system
- name: Set up incus
ansible.builtin.import_role:
name: incus-install
tags: incus
- name: Set up nfs shares
ansible.builtin.import_role:
name: nfs