57 Commits

Author SHA1 Message Date
jakub 8f14ec2e69 Rename hellsos/jim to hellsoslocal/jimlocal
Carry forward the local-account rename from the direct edit on
initial_setup.yml into the new canonical users list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 14:55:48 +02:00
jakub 6f73b83bc0 Centralize users list in group_vars and rename baseline playbook
Move the canonical user list to group_vars/all/users.yml so both
setup_linux.yml (renamed from initial_setup.yml) and the
initial_install users role consume the same source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 14:55:38 +02:00
jakub b5cb064bd8 Update initial_setup.yml 2026-05-23 12:18:57 +00:00
jakub ef49608ccc Add hostname play and hellsos SSH keys to initial_setup
Adds an opt-in (tags: never,hostname) play that sets the system
hostname to inventory_hostname, and fills in both real authorized
keys for the hellsos user.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 14:13:23 +02:00
jakub a23492d9d1 Update inv_linuxes 2026-05-23 11:28:47 +00:00
jakub b7f4ba6502 Add dockhand role to initial_install
Tagged never,dockhand_install so it only runs when explicitly requested.
Installs docker.io + docker-compose-v2, templates a compose file for
fnsys/dockhand:latest at /docker/dockhand, and wires a oneshot systemd
unit that brings the stack up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 15:51:14 +02:00
jakub 54e111338d Encrypt borg repos with repokey-blake2 + shared passphrase
borg_passphrase is required (Semaphore secret, same across hosts).
The role writes it to /etc/borgmatic/passphrase (0600 root) and
configures borgmatic to use BORG_PASSCOMMAND=cat /etc/borgmatic/passphrase,
and runs `borg init --encryption=repokey-blake2` with BORG_PASSPHRASE in
the env. no_log on the tasks that touch the passphrase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:58:14 +02:00
jakub 885a617388 Fix borg SSH URI and auto-init the remote repo
The URI was wrong — BorgWarehouse uses a single shared SSH user
(`borgwarehouse`) and routes by the repo id in the path, so the form
is `ssh://<borgSshHost>/./<repo.id>` (not per-repo user with /./repos).

Role now also trusts the borg server's SSH host key in root's
known_hosts and runs `borg init --encryption=none` (idempotent — treats
"already exists" as success) so first backups don't need manual prep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:45:06 +02:00
jakub 0a97f00356 Auto-register borg repo on the controller per host
backup role now logs into borgcontroller and creates (or looks up) a
repository with alias=inventory_hostname, registering root's pubkey and
the requested storageSize. The resulting SSH URI is injected into the
borgmatic config so each host gets a remote-managed repo without manual
config.

backup_hosts entries gain a `storage_size_gb` field (stripped before
templating) and lose the manual `repositories` entry — the role fills it.
borgcontroller_{username,password} are expected from Semaphore secrets.

Also gitignores .claude/ local state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:37:10 +02:00
jakub 4275f2e8fe Enable borgmatic timer and generate root SSH key for borg server
The role now ensures the systemd timer is running (so backups actually
fire on the schedule borgmatic ships by default) and generates an
ed25519 key for root that can be authorized on the remote borg server.

Also adds a testipaclient entry to backup_hosts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:49:58 +02:00
jakub 0d027a2c73 Add .gitignore for venv and mikrotik backup output
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:41:48 +02:00
jakub e43c3aaae3 Add backup role and manage_ssh_keys role
- Borgmatic backup role driven by per-host config in group_vars/all/backup.yml
- manage_ssh_keys role with add/remove paths; remove_ssh_key_playbook.yml uses it

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:38:11 +02:00
jakub f540af580f Update inv_linuxes 2026-05-06 14:40:39 +00:00
jakub 34edead9b4 Update initial_install/roles/freeipa_client/tasks/main.yml 2026-04-24 20:44:46 +00:00
jakub 4987003c4e Update initial_install/roles/baseline_sudo/tasks/main.yml 2026-04-24 20:18:37 +00:00
jakub 22413beb25 Update initial_install/roles/users/tasks/main.yml 2026-04-24 20:16:31 +00:00
jakub 04ce966b20 Update initial_install/roles/users/tasks/main.yml 2026-04-24 20:12:19 +00:00
jakub 6368edcb67 Update initial_install/roles/users/tasks/main.yml 2026-04-24 20:09:02 +00:00
jakub 949940c730 Update initial_install/roles/freeipa_client/tasks/main.yml 2026-04-24 15:00:33 +00:00
jakub 9cc587cda5 Update initial_install/roles/users/tasks/main.yml 2026-04-24 14:53:48 +00:00
jakub 65dc887749 Update initial_install/roles/freeipa_client/tasks/main.yml 2026-04-24 14:51:35 +00:00
jakub e2015fe03e Update initial_install/roles/freeipa_client/tasks/main.yml 2026-04-24 14:48:14 +00:00
jakub ef3b293977 Update initial_install/roles/freeipa_client/tasks/main.yml 2026-04-24 14:45:40 +00:00
jakub 9d9695a7b3 Update initial_install/roles/freeipa_client/tasks/main.yml 2026-04-24 14:43:37 +00:00
jakub becc21ff9e Update initial_install/roles/freeipa_client/tasks/main.yml 2026-04-24 14:36:32 +00:00
jakub 22deb79b46 Update initial_install/roles/users/tasks/main.yml 2026-04-24 14:30:08 +00:00
jakub 9c2f0e577b Update initial_install/roles/baseline_sudo/tasks/main.yml 2026-04-24 12:20:39 +00:00
jakub 50b4bfa6fc Add initial_install/collections/requirements.yml 2026-04-24 12:09:57 +00:00
jakub 873af66079 Update inv_linuxes 2026-04-24 12:08:29 +00:00
jakub 504dc88756 Add initial_install/roles/freeipa_client/handlers/main.yml 2026-04-24 11:58:18 +00:00
jakub a2393f7f44 Update initial_install/roles/freeipa_client/tasks/main.yml 2026-04-24 11:57:32 +00:00
jakub 0583f5c85f Add initial_install/roles/freeipa_client/tasks/main.yml 2026-04-24 11:56:42 +00:00
jakub 7af44add30 Add initial_install/roles/ssh_hardening/handlers/main.yml 2026-04-24 11:56:22 +00:00
jakub ce17a4b00e Add initial_install/roles/ssh_hardening/tasks/main.yml 2026-04-24 11:56:01 +00:00
jakub cc679c5953 Add initial_install/roles/users/tasks/main.yml 2026-04-24 11:55:36 +00:00
jakub 26ae15cd46 Add initial_install/roles/baseline_sudo/tasks/main.yml 2026-04-24 11:54:41 +00:00
jakub e4cabaf2d5 Add initial_install/playbook.yml 2026-04-24 11:54:03 +00:00
jakub c22c566fa2 Update test_sms.yml 2026-04-13 16:50:08 +00:00
jakub 6de6fde4cd Update mikrotikbackup_clean.yml 2026-03-25 10:50:50 +00:00
jakub 5ba549a052 Update mikrotikbackup_clean.yml 2026-03-24 15:11:28 +00:00
jakub 699d7ef089 Update mikrotikbackup_clean.yml 2026-03-24 15:03:43 +00:00
jakub ce1ba9cf97 Update mikrotikbackup_clean.yml 2026-03-24 15:00:40 +00:00
jakub 724e954828 Update mikrotikbackup_clean.yml 2026-03-24 14:59:02 +00:00
jakub b0f5825d8a Update mikrotikbackup_clean.yml 2026-03-24 14:56:42 +00:00
jakub 2779970324 Update mikrotikbackup_clean.yml 2026-03-24 14:54:06 +00:00
jakub 93e8d5abb4 Update mikrotikbackup_clean.yml 2026-03-24 14:52:22 +00:00
jakub 7ab8b46e3e Update mikrotikbackup_clean.yml 2026-03-24 14:50:17 +00:00
jakub 482a298fdb Update mikrotikbackup_clean.yml 2026-03-24 14:46:32 +00:00
jakub 069579aa5a Update mikrotikbackup_clean.yml 2026-03-24 14:43:42 +00:00
jakub c8dbc4c518 Update mikrotikbackup_clean.yml 2026-03-24 14:41:22 +00:00
jakub 33a2d86910 Update mikrotikbackup_clean.yml 2026-03-24 14:39:19 +00:00
jakub 2fa716624e Update mikrotikbackup_clean.yml 2026-03-24 14:35:26 +00:00
jakub d65ac431e5 Update inv_linuxes 2026-03-23 17:11:03 +00:00
jakub 5d17ba7d6b Update initial_setup.yml 2026-03-23 17:09:47 +00:00
jakub daf198ea82 Update initial_setup.yml 2026-03-23 17:08:37 +00:00
jakub 3563c69d54 Update inv_linuxes 2026-03-23 16:25:22 +00:00
jakub b727d51dfd Update mikrotikbackup_clean.yml 2026-03-21 18:45:03 +00:00
27 changed files with 679 additions and 44 deletions
+10
View File
@@ -0,0 +1,10 @@
# Python virtual environments
.venv/
venv/
# MikroTik backup output
mikrotik/backups/
mikrotik/output/
# Claude Code local state
.claude/
+8
View File
@@ -0,0 +1,8 @@
---
- name: Install borgmatic and deploy per-host config
hosts: all
become: true
tags: never,backup
roles:
- role: backup
+18
View File
@@ -0,0 +1,18 @@
---
# Borg Controller — auto-creates a repo per host on a BorgWarehouse-backed server.
# borgcontroller_username / borgcontroller_password come from Semaphore secrets.
borgcontroller_url: https://borgcontroller.internet-master.cz
# Per-host borgmatic config. Hosts not listed here are skipped by the `backup` role.
# `storage_size_gb` is stripped before rendering and used to size the controller-side
# repo. `repositories` is auto-filled from the controller — don't set it manually.
# Other keys are passed through verbatim to borgmatic, see
# https://torsion.org/borgmatic/docs/reference/configuration/
backup_hosts:
testipaclient:
storage_size_gb: 10
source_directories:
- /home/jakub
keep_daily: 7
keep_weekly: 4
keep_monthly: 6
+22
View File
@@ -0,0 +1,22 @@
---
# Canonical user list — consumed by both setup_linux.yml and
# initial_install/roles/users.
users:
- name: automation
shell: /bin/bash
sudo_nopasswd: true
ssh_keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEx+ltCKNIEM7F4PzGLv22cIu7N0Fpn5gxwV02xq0GS9 automation@internet-master.cz"
- name: hellsoslocal
shell: /bin/bash
sudo_nopasswd: true
ssh_keys:
- "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB+wC3T4/HSs1q5jf34sCqicSQOb05k+bxfmNMMKEGzRrGT3BfCG428F19OBIswvcuBPC0Q4TpPz84BkiATCx2o1JUH1xIOFcHzxxXbyzHAhjwto1wOr1DkwZWAvDPbdnJ39OsC0EdmrAHSXut93q4vzOsLlS34bOWP1THGY9nBKOHwJUQmS5tLw6dqbhKA886TrPXJDR9euEC+SYaytMyDUPYEa6dlDyRp77eII/uI/hf/6e+34wm9XyFyGMiMrQeO0u6Gq5NhsoBlhrCW3ds0To+DBZ/YKNzpzcN+uPKM1+r9nN8KwdwjRkQEwSdB4osz/UeGTiXB0jwb0+ftFthBFdOil86cd1OiAMmuKB/19QHv0NsVhs2JocP5JcrAgx8ktQzLIkOM4lt6Kt9rjHv1KNfdsZdiHqlOwrDv9B2Ei44qEUAsWlFSzEi7R3mOED4F04N3FeQ9TkrRRH6SE733t1Kum2VAz6wr4BSNyYxQOCnXoANy/JyoM5e5tQ7pht7tuxX78zhFlC7pAmVu0dnCQimSsIsXNlYGM7DQ7QMva8mKu49V3B7tU0ChghSRJUb2Sg0tkTAxzCR+WOOf8kAnkXNOV5vExQ3h5Xmb52A+az37Hyex379ryKqpffaf9RTx9pBagQf1XbUtA4KazuEi3fMmqCQjz+xFZJAZQ== hellsos@hellsos-PC"
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKhfQt1VNQo8EbIog4yjU5VEF3mTyMEC7o1Qe95X4JwG jan@rabcan.cz"
- name: jimlocal
shell: /bin/bash
sudo_nopasswd: true
ssh_keys:
- "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPFS4fsqMjMMu/Bi/884bw7yJBqvWusDRESvanH6Owco jakub@jimbuntu"
@@ -0,0 +1,2 @@
collections:
- name: freeipa.ansible_freeipa
+47
View File
@@ -0,0 +1,47 @@
---
- name: Baseline system setup
hosts: all
become: true
roles:
- role: baseline_sudo
tags: sudo
- role: users
tags: users
# ==============================
# FREEIPA / SSSD (optional)
# ==============================
- name: FreeIPA client setup
hosts: all
become: true
tags: never,sssd
roles:
- role: freeipa_client
# ==============================
# DOCKHAND (optional)
# ==============================
- name: Install dockhand
hosts: all
become: true
tags: never,dockhand_install
roles:
- role: dockhand
# ==============================
# SSH HARDENING (run last!)
# ==============================
- name: SSH hardening
hosts: all
become: true
tags: never,hardening
roles:
- role: ssh_hardening
@@ -0,0 +1,23 @@
---
- name: Ensure sudo package is installed
ansible.builtin.package:
name: sudo
state: present
- name: Ensure automation user has passwordless sudo
ansible.builtin.copy:
dest: /etc/sudoers.d/automation
owner: root
group: root
mode: '0440'
content: |
automation ALL=(ALL:ALL) NOPASSWD: ALL
validate: 'visudo -cf %s'
- name: Ensure sudo binary has correct permissions
ansible.builtin.file:
path: /usr/bin/sudo
owner: root
group: root
mode: '4755'
when: ansible_facts.os_family in ["Debian", "RedHat"]
@@ -0,0 +1,9 @@
---
- name: Reload systemd
ansible.builtin.systemd:
daemon_reload: true
- name: Restart dockhand
ansible.builtin.systemd:
name: dockhand
state: restarted
@@ -0,0 +1,46 @@
---
- name: Install Docker and Compose
ansible.builtin.package:
name:
- docker.io
- docker-compose-v2
state: present
- name: Ensure Docker is running
ansible.builtin.systemd:
name: docker
enabled: true
state: started
- name: Ensure /docker/dockhand exists
ansible.builtin.file:
path: /docker/dockhand
state: directory
owner: root
group: root
mode: '0755'
- name: Deploy dockhand docker-compose.yml
ansible.builtin.template:
src: docker-compose.yml.j2
dest: /docker/dockhand/docker-compose.yml
owner: root
group: root
mode: '0644'
notify: Restart dockhand
- name: Deploy dockhand systemd unit
ansible.builtin.template:
src: dockhand.service.j2
dest: /etc/systemd/system/dockhand.service
owner: root
group: root
mode: '0644'
notify: Reload systemd
- name: Enable and start dockhand
ansible.builtin.systemd:
name: dockhand
enabled: true
state: started
daemon_reload: true
@@ -0,0 +1,14 @@
# Managed by Ansible — do not edit by hand.
services:
dockhand:
image: fnsys/dockhand:latest
container_name: dockhand
restart: unless-stopped
ports:
- "3000:3000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- dockhand_data:/app/data
volumes:
dockhand_data:
@@ -0,0 +1,16 @@
[Unit]
Description=dockhand (docker compose stack)
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/docker/dockhand
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=300
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,5 @@
---
- name: Restart SSSD
ansible.builtin.service:
name: sssd
state: restarted
@@ -0,0 +1,57 @@
---
- name: Install FreeIPA client packages
ansible.builtin.package:
name:
- freeipa-client
- sssd
- sssd-tools
- oddjob
- oddjob-mkhomedir
state: present
- name: Set hostname FQDN
ansible.builtin.hostname:
name: "{{ inventory_hostname }}.im.lab"
- name: Check if FreeIPA client is already configured
ansible.builtin.stat:
path: /etc/ipa/default.conf
register: ipa_client_conf
- name: Enroll to FreeIPA
ansible.builtin.command:
argv:
- ipa-client-install
- --domain=im.lab
- --realm=IM.LAB
- --server=ipa.im.lab
- "--hostname={{ inventory_hostname }}.im.lab"
- --mkhomedir
- --principal=admin
- --password={{ ipa_admin_password }}
- --unattended
- --force-join
no_log: false
when: not ipa_client_conf.stat.exists
- name: Enable mkhomedir
ansible.builtin.command:
argv:
- authselect
- enable-feature
- with-mkhomedir
register: authselect_mkhomedir
changed_when: "'already enabled' not in authselect_mkhomedir.stdout"
failed_when: false
- name: Enable and start oddjobd
ansible.builtin.service:
name: oddjobd
state: started
enabled: true
- name: Enable and start SSSD
ansible.builtin.service:
name: sssd
state: started
enabled: true
@@ -0,0 +1,8 @@
---
- name: Restart SSH
ansible.builtin.service:
name: "{{ 'sshd'
if ansible_facts.os_family in
['RedHat','Rocky','AlmaLinux','Fedora','OracleLinux','Suse']
else 'ssh' }}"
state: restarted
@@ -0,0 +1,29 @@
---
- name: Detect if system is Proxmox
ansible.builtin.stat:
path: /usr/bin/pveversion
register: proxmox_check
- name: Ensure sshd_config.d exists
ansible.builtin.file:
path: /etc/ssh/sshd_config.d
state: directory
- name: Deploy SSH hardening config
ansible.builtin.copy:
dest: /etc/ssh/sshd_config.d/99-ansible-hardening.conf
mode: '0644'
content: |
PasswordAuthentication no
ChallengeResponseAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
UsePAM yes
{% if not proxmox_check.stat.exists %}
PermitRootLogin no
{% else %}
PermitRootLogin prohibit-password
{% endif %}
validate: 'sshd -t -f %s'
notify: Restart SSH
@@ -0,0 +1,39 @@
---
# `users` comes from group_vars/all/users.yml
- name: Ensure users exist
ansible.builtin.user:
name: "{{ item.name }}"
shell: "{{ item.shell }}"
create_home: true
loop: "{{ users }}"
# --------------------------------------------------
# Configure passwordless sudo safely
# --------------------------------------------------
- name: Configure passwordless sudo
ansible.builtin.copy:
dest: "/etc/sudoers.d/{{ item.name }}"
mode: '0440'
owner: root
group: root
content: |
{{ item.name }} ALL=(ALL:ALL) NOPASSWD: ALL
validate: 'visudo -cf %s'
loop: "{{ users }}"
when: item.sudo_nopasswd | default(false)
# --------------------------------------------------
# Install SSH keys
# --------------------------------------------------
- name: Install authorized SSH keys
ansible.builtin.authorized_key:
user: "{{ item.name }}"
key: "{{ item.ssh_keys | join('\n') }}"
exclusive: true
loop: "{{ users }}"
# --------------------------------------------------
# Reset connection so sudo rules take effect immediately
# --------------------------------------------------
- name: Reset SSH connection
meta: reset_connection
+14 -3
View File
@@ -1,9 +1,20 @@
[linux_servers]
[linux_servers_jim]
jimbuntu ansible_host=192.168.19.4
jim_storage ansible_host=192.168.19.7
portainernode2_jim ansible_host=192.168.19.8
galera3 ansible_host=192.168.19.92
galera2 ansible_host=192.168.19.91
testipaclient ansible_host=192.168.19.98
testclient ansible_host=192.168.19.115
portainer1_jim.im.lab ansible_host=192.168.19.7
[linux_servers_hellsos]
portainer2_hellsos ansible_host=192.168.52.9
portainernode_hellsos ansible_host=192.168.52.21
portainernode2_jim ansible_host=192.168.19.8
[linux_servers:children]
linux_servers_jim
linux_servers_hellsos
[local]
localhost ansible_connection=local
+54 -16
View File
@@ -41,20 +41,20 @@
state: directory
mode: "0755"
delegate_to: localhost
tags: backup
tags: [backup, never]
- name: Export router config
community.routeros.command:
commands: /export terse show-sensitive
register: export_cfg
tags: backup
tags: [backup, never]
- name: Save export locally
ansible.builtin.copy:
content: "{{ export_cfg.stdout[0] }}"
dest: "{{ backup_dir }}/{{ router_name }}-{{ ts }}.rsc"
delegate_to: localhost
tags: backup
tags: [backup, never]
# ----------------------------
# Upgrade (tag: upgrade)
@@ -63,46 +63,84 @@
community.routeros.command:
commands: /system package update check-for-updates
register: update_check
tags: upgrade
tags: [upgrade, never]
- name: Normalize update output
set_fact:
_update_text: "{{ update_check.stdout[0] | replace('\r', '') }}"
tags: [upgrade, never]
# ⬇️ Add this to see exactly what RouterOS returns before parsing
- name: Debug raw update output
ansible.builtin.debug:
msg: "{{ _update_text }}"
tags: [upgrade, never]
- name: Parse installed and latest versions
set_fact:
installed_version: "{{ update_check.stdout[0] | regex_search('installed-version: ([\\d.]+)', '\\1') | first }}"
latest_version: "{{ update_check.stdout[0] | regex_search('latest-version: ([\\d.]+)', '\\1') | first }}"
tags: upgrade
installed_version: >-
{{
(_update_text | regex_findall('(?:installed|current)-version:[ ]*([0-9A-Za-z.]+)'))[0]
if (_update_text | regex_findall('(?:installed|current)-version:[ ]*([0-9A-Za-z.]+)'))
else 'unknown'
}}
latest_version: >-
{{
(_update_text | regex_findall('(?:latest|newest)-version:[ ]*([0-9A-Za-z.]+)'))[0]
if (_update_text | regex_findall('(?:latest|newest)-version:[ ]*([0-9A-Za-z.]+)'))
else 'unknown'
}}
tags: [upgrade, never]
- name: Fail if versions could not be parsed
ansible.builtin.fail:
msg: >
Could not parse versions from update output.
Raw text was: {{ _update_text }}
when: installed_version == 'unknown' or latest_version == 'unknown'
tags: [upgrade, never]
- name: Debug parsed versions
ansible.builtin.debug:
msg:
- "Installed: {{ installed_version }}"
- "Latest: {{ latest_version }}"
tags: [upgrade, never]
- name: Skip upgrade if already on latest
ansible.builtin.debug:
msg: "Router {{ router_name }} is already on latest version {{ installed_version }}. Skipping upgrade."
when: installed_version == latest_version
tags: upgrade
tags: [upgrade, never]
- name: Trigger package download and install
community.routeros.command:
commands: /system package update install
register: upgrade_result
when: installed_version != latest_version
tags: upgrade
tags: [upgrade, never]
- name: Wait for router to come back online after reboot
ansible.builtin.wait_for_connection:
delay: 180
timeout: 300
sleep: 10
community.routeros.command:
commands: /system resource print
register: reboot_wait
until: reboot_wait is not failed
retries: 30
delay: 15
when:
- installed_version != latest_version
- upgrade_result is not failed
tags: upgrade
tags: [upgrade, never]
- name: Confirm upgraded version
community.routeros.command:
commands: /system resource print
register: post_upgrade_info
when: installed_version != latest_version
tags: upgrade
tags: [upgrade, never]
- name: Show post-upgrade RouterOS version
ansible.builtin.debug:
msg: "{{ post_upgrade_info.stdout[0] | regex_search('version: .+') }}"
when: installed_version != latest_version
tags: upgrade
tags: [upgrade, never]
+6
View File
@@ -0,0 +1,6 @@
- name: Remove SSH key
hosts: all
become: yes
roles:
- role: manage_ssh_keys
remove_user: true
+131
View File
@@ -0,0 +1,131 @@
---
- name: Login to borg controller
ansible.builtin.uri:
url: "{{ borgcontroller_url }}/api/auth/login"
method: POST
body_format: json
body:
username: "{{ borgcontroller_username }}"
password: "{{ borgcontroller_password }}"
status_code: 200
delegate_to: localhost
become: false
register: _bc_login
no_log: true
- name: Get borg server SSH endpoint
ansible.builtin.uri:
url: "{{ borgcontroller_url }}/api/config"
method: GET
headers:
Cookie: "{{ _bc_login.cookies_string }}"
delegate_to: localhost
become: false
register: _bc_config
- name: List repositories
ansible.builtin.uri:
url: "{{ borgcontroller_url }}/api/repositories"
method: GET
headers:
Cookie: "{{ _bc_login.cookies_string }}"
delegate_to: localhost
become: false
register: _bc_repos
- name: Find existing repository for this host
ansible.builtin.set_fact:
_bc_existing: >-
{{ _bc_repos.json | selectattr('alias', 'eq', inventory_hostname) | list }}
- name: Create repository if missing
ansible.builtin.uri:
url: "{{ borgcontroller_url }}/api/repositories"
method: POST
body_format: json
body:
alias: "{{ inventory_hostname }}"
sshPublicKey: "{{ root_ssh.ssh_public_key }}"
storageSize: "{{ backup_hosts[inventory_hostname].storage_size_gb | int }}"
headers:
Cookie: "{{ _bc_login.cookies_string }}"
status_code: [200, 201]
delegate_to: localhost
become: false
when: _bc_existing | length == 0
- name: Update repository SSH key if root's key changed
ansible.builtin.uri:
url: "{{ borgcontroller_url }}/api/repositories/{{ _bc_existing[0].id }}"
method: PATCH
body_format: json
body:
sshPublicKey: "{{ root_ssh.ssh_public_key }}"
headers:
Cookie: "{{ _bc_login.cookies_string }}"
status_code: 200
delegate_to: localhost
become: false
when:
- _bc_existing | length > 0
- _bc_existing[0].sshPublicKey != root_ssh.ssh_public_key
- name: Re-list repositories after possible create/update
ansible.builtin.uri:
url: "{{ borgcontroller_url }}/api/repositories"
method: GET
headers:
Cookie: "{{ _bc_login.cookies_string }}"
delegate_to: localhost
become: false
register: _bc_repos_after
- name: Resolve repository for this host
ansible.builtin.set_fact:
_bc_repo: >-
{{ (_bc_repos_after.json | selectattr('alias', 'eq', inventory_hostname) | list)[0] }}
- name: Build borg SSH URI
ansible.builtin.set_fact:
borgcontroller_repo_uri: "ssh://{{ _bc_config.json.borgSshHost }}/./{{ _bc_repo.id }}"
_bc_borg_host: "{{ _bc_config.json.borgSshHost.split('@')[1].split(':')[0] }}"
_bc_borg_port: "{{ _bc_config.json.borgSshHost.split('@')[1].split(':')[1] | default('22') }}"
- name: Ensure /root/.ssh exists
ansible.builtin.file:
path: /root/.ssh
state: directory
owner: root
group: root
mode: '0700'
- name: Scan borg server SSH host key
ansible.builtin.command: ssh-keyscan -p {{ _bc_borg_port }} {{ _bc_borg_host }}
register: _bc_keyscan
changed_when: false
check_mode: false
- name: Trust borg server SSH host key (root known_hosts)
ansible.builtin.lineinfile:
path: /root/.ssh/known_hosts
line: "{{ item }}"
create: true
owner: root
group: root
mode: '0600'
loop: "{{ _bc_keyscan.stdout_lines }}"
when:
- item | length > 0
- not item.startswith('#')
- name: Initialize borg repository (no-op if already initialized)
ansible.builtin.command:
cmd: borg init --encryption=repokey-blake2 {{ borgcontroller_repo_uri }}
environment:
BORG_PASSPHRASE: "{{ borg_passphrase }}"
register: _borg_init
changed_when: _borg_init.rc == 0
failed_when:
- _borg_init.rc != 0
- "'already exists' not in (_borg_init.stderr | default(''))"
no_log: true
+82
View File
@@ -0,0 +1,82 @@
---
- name: Skip hosts without backup config
ansible.builtin.debug:
msg: "No entry in backup_hosts for {{ inventory_hostname }}; skipping backup role."
when: inventory_hostname not in (backup_hosts | default({}))
- name: Configure borgmatic
when: inventory_hostname in (backup_hosts | default({}))
block:
- name: Ensure borg_passphrase is set (Semaphore secret)
ansible.builtin.assert:
that:
- borg_passphrase is defined
- borg_passphrase | length > 0
fail_msg: "borg_passphrase must be defined (provided by Semaphore secrets)"
- name: Install borgmatic
ansible.builtin.package:
name: borgmatic
state: present
- name: Ensure /etc/borgmatic exists
ansible.builtin.file:
path: /etc/borgmatic
state: directory
owner: root
group: root
mode: '0750'
- name: Write borg passphrase file
ansible.builtin.copy:
dest: /etc/borgmatic/passphrase
content: "{{ borg_passphrase }}"
owner: root
group: root
mode: '0600'
no_log: true
- name: Ensure root has an SSH key for the borg server
ansible.builtin.user:
name: root
generate_ssh_key: true
ssh_key_type: ed25519
ssh_key_file: .ssh/id_ed25519
ssh_key_comment: "borgmatic@{{ inventory_hostname }}"
register: root_ssh
- name: Register / look up repository on borg controller
ansible.builtin.include_tasks: borgcontroller.yml
when:
- borgcontroller_username is defined
- borgcontroller_password is defined
- name: Build borgmatic config (strip controller-only keys, inject repository + passcommand)
ansible.builtin.set_fact:
_borgmatic_config: >-
{{
(backup_hosts[inventory_hostname]
| dict2items
| rejectattr('key', 'in', ['storage_size_gb'])
| items2dict)
| combine({'encryption_passcommand': 'cat /etc/borgmatic/passphrase'})
| combine(
{'repositories': [{'path': borgcontroller_repo_uri, 'label': inventory_hostname}]}
if borgcontroller_repo_uri is defined else {}
)
}}
- name: Deploy borgmatic config
ansible.builtin.template:
src: borgmatic.yaml.j2
dest: /etc/borgmatic/config.yaml
owner: root
group: root
mode: '0640'
- name: Enable and start borgmatic timer
ansible.builtin.systemd:
name: borgmatic.timer
enabled: true
state: started
+3
View File
@@ -0,0 +1,3 @@
#jinja2: trim_blocks: True, lstrip_blocks: True
# Managed by Ansible — do not edit by hand.
{{ _borgmatic_config | to_nice_yaml(indent=2, width=1000) }}
@@ -0,0 +1,4 @@
- name: Add user and authorized key
authorized_keys:
user: "{{ user }}"
key: "{{ key }}"
+5
View File
@@ -0,0 +1,5 @@
- include_tasks: add_ssh_key.yml
when: add_user | default(false)
- include_tasks: remove_ssh_key.yml
when: remove_user | default(false)
@@ -0,0 +1,10 @@
- name: Remove authorized key
authorized_keys:
user: "{{ user }}"
key: "{{ key }}"
state: absent
- name: Ensure user is absent
user:
name: "{{ user }}"
state: absent
+15 -23
View File
@@ -3,29 +3,6 @@
hosts: all
become: true
vars:
users:
- name: automation
shell: /bin/bash
groups: []
sudo_nopasswd: true
ssh_keys:
- "ssh-ed25519 AAAAC3..."
- name: hellsos
shell: /bin/bash
groups: []
sudo_nopasswd: true
ssh_keys:
- "ssh-ed25519 AAAAC3..."
- name: jim
shell: /bin/bash
groups: []
sudo_nopasswd: true
ssh_keys:
- "ssh-ed25519 AAAAC3..."
tasks:
- name: Pick sudo group per distro
@@ -109,3 +86,18 @@
['RedHat','Rocky','AlmaLinux','Fedora','OracleLinux','Suse']
else 'ssh' }}"
state: restarted
# ==============================
# THIRD PLAY: HOSTNAME
# ==============================
- name: Set hostname from inventory
hosts: all
become: true
tags: never,hostname
tasks:
- name: Set system hostname to inventory_hostname
ansible.builtin.hostname:
name: "{{ inventory_hostname }}"
+1 -1
View File
@@ -7,7 +7,7 @@
sms_username: "mikrotik"
sms_password_send: "jdkotzHJIOPWhjtr32D"
sms_password_recv: "jdkotzHJIOPWhjtr32D"
sms_wait_seconds: 120 # Wait 2 minutes for delivery
sms_wait_seconds: 15 # Wait 15s for delivery
tasks:
- name: Generate random test string