diff --git a/.gitignore b/.gitignore index b784e05..383de38 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ vaultpass + +/.ansible +/temp/ diff --git a/README.md b/README.md index e64469b..462261a 100644 --- a/README.md +++ b/README.md @@ -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:... diff --git a/ansible.cfg b/ansible.cfg index 168509f..8734d30 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -1,5 +1,4 @@ [defaults] remote_tmp = /tmp inventory = inventory - -vault_password_file = vaultpass +roles_path = .ansible/roles:roles diff --git a/justfile b/justfile new file mode 100644 index 0000000..5afc29f --- /dev/null +++ b/justfile @@ -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 diff --git a/requirements.yaml b/requirements.yaml new file mode 100644 index 0000000..22c5e84 --- /dev/null +++ b/requirements.yaml @@ -0,0 +1,4 @@ +# Install and pre-configure incus +- src: gliech.incus +# install and set up docker +- src: geerlingguy.docker diff --git a/roles/filesystem/README.md b/roles/filesystem/README.md new file mode 100644 index 0000000..c80df74 --- /dev/null +++ b/roles/filesystem/README.md @@ -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. diff --git a/roles/filesystem/defaults/main.yaml b/roles/filesystem/defaults/main.yaml new file mode 100644 index 0000000..0c5589e --- /dev/null +++ b/roles/filesystem/defaults/main.yaml @@ -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" diff --git a/roles/filesystem/handlers/main.yaml b/roles/filesystem/handlers/main.yaml new file mode 100644 index 0000000..b00e076 --- /dev/null +++ b/roles/filesystem/handlers/main.yaml @@ -0,0 +1,7 @@ +--- + +- name: Reboot machine + ansible.builtin.reboot: + connect_timeout: 600 + post_reboot_delay: 10 + when: "should_reboot_machine" diff --git a/roles/filesystem/tasks/main.yaml b/roles/filesystem/tasks/main.yaml new file mode 100644 index 0000000..0e85f23 --- /dev/null +++ b/roles/filesystem/tasks/main.yaml @@ -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" diff --git a/roles/filesystem/vars/main.yaml b/roles/filesystem/vars/main.yaml new file mode 100644 index 0000000..664bc9a --- /dev/null +++ b/roles/filesystem/vars/main.yaml @@ -0,0 +1,2 @@ +--- +# vars file for btrfs diff --git a/roles/system/files/incus.servers.tpl b/roles/incus-install/files/incus.servers.tpl similarity index 100% rename from roles/system/files/incus.servers.tpl rename to roles/incus-install/files/incus.servers.tpl diff --git a/roles/system/files/incus.sources.tpl b/roles/incus-install/files/incus.sources.tpl similarity index 100% rename from roles/system/files/incus.sources.tpl rename to roles/incus-install/files/incus.sources.tpl diff --git a/roles/system/files/zabbly-key.asc b/roles/incus-install/files/zabbly-key.asc similarity index 100% rename from roles/system/files/zabbly-key.asc rename to roles/incus-install/files/zabbly-key.asc diff --git a/roles/incus-install/tasks/bootstrap.yaml b/roles/incus-install/tasks/bootstrap.yaml deleted file mode 100644 index c6e1dfa..0000000 --- a/roles/incus-install/tasks/bootstrap.yaml +++ /dev/null @@ -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 diff --git a/roles/incus-install/tasks/install.yaml b/roles/incus-install/tasks/install.yaml new file mode 100644 index 0000000..2b5f4ab --- /dev/null +++ b/roles/incus-install/tasks/install.yaml @@ -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" diff --git a/roles/incus-install/tasks/main.yaml b/roles/incus-install/tasks/main.yaml index c465a5f..6623f4f 100644 --- a/roles/incus-install/tasks/main.yaml +++ b/roles/incus-install/tasks/main.yaml @@ -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 diff --git a/roles/system/defaults/main.yaml b/roles/system/defaults/main.yaml new file mode 100644 index 0000000..7100009 --- /dev/null +++ b/roles/system/defaults/main.yaml @@ -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 diff --git a/roles/system/tasks/main.yaml b/roles/system/tasks/main.yaml index 068c2e0..4ce6c5a 100644 --- a/roles/system/tasks/main.yaml +++ b/roles/system/tasks/main.yaml @@ -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 diff --git a/site.yaml b/site.yaml index ce1b5c6..0f89754 100644 --- a/site.yaml +++ b/site.yaml @@ -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