Compare commits

..
6 Commits
26 changed files with 572 additions and 29 deletions
+12
View File
@@ -0,0 +1,12 @@
---
- name: Upgrade Arch systems
hosts: firewall
roles:
- arch_update
- dotfiles
- name: Process any reboots
hosts: "firewall, !control"
roles:
- reboot
+29
View File
@@ -0,0 +1,29 @@
---
- name: Upgrade Arch systems
hosts: arch
#strategy: free
roles:
- arch_update
- dotfiles
- name: Upgrade Debian systems
hosts: debian
roles:
- debian_update
- dotfiles
- name: Update Docker Stacks
hosts: docker
roles:
- docker_update
- name: Update mailcow Stacks
hosts: mailcow
roles:
- mailcow_update
- name: Process any reboots
hosts: "arch, debian, !control"
roles:
- reboot
+5
View File
@@ -0,0 +1,5 @@
---
- name: Deploy node_exporter
hosts: arch,debian,barbican
roles:
- node_exporter
+2
View File
@@ -0,0 +1,2 @@
---
aur_helper: pikaur
+162
View File
@@ -0,0 +1,162 @@
#!/usr/bin/env python3
import urllib.request
import urllib.parse
import json
import subprocess
import os
import re
import tempfile
import sys
import shutil
def run_cmd(cmd, cwd=None):
return subprocess.run(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
def get_local_foreign_packages():
res = run_cmd(['pacman', '-Qm'])
if res.returncode != 0:
return {}
pkgs = {}
for line in res.stdout.strip().split('\n'):
if line:
parts = line.split()
if len(parts) == 2:
pkgs[parts[0]] = parts[1]
return pkgs
def get_aur_info(pkgnames):
if not pkgnames:
return {}
results = {}
chunk_size = 100
pkgs = list(pkgnames)
for i in range(0, len(pkgs), chunk_size):
chunk = pkgs[i:i+chunk_size]
url = "https://aur.archlinux.org/rpc/v5/info?" + "&".join([f"arg[]={urllib.parse.quote(p)}" for p in chunk])
try:
req = urllib.request.urlopen(url, timeout=10)
data = json.loads(req.read().decode('utf-8'))
if data.get('type') == 'multiinfo':
for item in data.get('results', []):
results[item['Name']] = item
except Exception:
pass
return results
def needs_upgrade(local_ver, aur_ver):
res = run_cmd(['vercmp', local_ver, aur_ver])
try:
return int(res.stdout.strip()) < 0
except Exception:
return False
def extract_ver(content):
pkgver, pkgrel = None, None
for line in content.split('\n'):
line = line.strip()
if line.startswith('pkgver='):
pkgver = line.split('=', 1)[1].strip('"\'')
elif line.startswith('pkgrel='):
pkgrel = line.split('=', 1)[1].strip('"\'')
return pkgver, pkgrel
def strip_volatile_fields(content):
content = re.sub(r'^pkgver=.*$', '', content, flags=re.MULTILINE)
content = re.sub(r'^pkgrel=.*$', '', content, flags=re.MULTILINE)
content = re.sub(r'^arch=\(.*?\)', '', content, flags=re.MULTILINE | re.DOTALL)
source_vars = set()
source_matches = re.finditer(r'^source(?:_[a-zA-Z0-9_]+)?=\((.*?)\)', content, flags=re.MULTILINE | re.DOTALL)
for match in source_matches:
source_content = match.group(1)
vars_in_source = re.findall(r'\$(?:\{(_[a-zA-Z0-9_]+)\}|(_[a-zA-Z0-9_]+))', source_content)
for v1, v2 in vars_in_source:
if v1: source_vars.add(v1)
if v2: source_vars.add(v2)
for var in source_vars:
content = re.sub(r'^' + re.escape(var) + r'=.*$', '', content, flags=re.MULTILINE)
sum_patterns = [
r'md5sums', r'sha1sums', r'sha224sums', r'sha256sums',
r'sha384sums', r'sha512sums', r'b2sums', r'cksums'
]
for sp in sum_patterns:
pattern = r'^' + sp + r'(?:_[a-zA-Z0-9_]+)?=\(.*?\)'
content = re.sub(pattern, '', content, flags=re.MULTILINE | re.DOTALL)
lines = [line.rstrip() for line in content.split('\n') if line.strip()]
return '\n'.join(lines)
def main():
local_pkgs = get_local_foreign_packages()
if not local_pkgs:
sys.exit(0)
aur_info = get_aur_info(list(local_pkgs.keys()))
upgrades = []
for pkg, local_ver in local_pkgs.items():
if pkg in aur_info:
aur_ver = aur_info[pkg]['Version']
if needs_upgrade(local_ver, aur_ver):
upgrades.append((pkg, local_ver, aur_ver))
if not upgrades:
sys.exit(0)
print(f"Checking PKGBUILD diffs for {len(upgrades)} upgrades...")
temp_dir = tempfile.mkdtemp()
try:
for pkg, local_ver, aur_ver in upgrades:
print(f"Checking {pkg} ({local_ver} -> {aur_ver})")
repo_dir = os.path.join(temp_dir, pkg)
res = run_cmd(['git', 'clone', '--quiet', f'https://aur.archlinux.org/{pkg}.git', repo_dir])
if res.returncode != 0:
print(f"Failed to clone {pkg}", file=sys.stderr)
sys.exit(1)
res = run_cmd(['git', 'log', '--pretty=format:%H'], cwd=repo_dir)
commits = res.stdout.strip().split('\n')
old_pkgbuild_content = None
new_pkgbuild_content = None
with open(os.path.join(repo_dir, 'PKGBUILD'), 'r', encoding='utf-8', errors='ignore') as f:
new_pkgbuild_content = f.read()
local_ver_no_epoch = local_ver.split(':', 1)[-1]
parts = local_ver_no_epoch.rsplit('-', 1)
expected_ver, expected_rel = parts if len(parts) == 2 else (parts[0], "")
for commit in commits:
res = run_cmd(['git', 'show', f"{commit}:PKGBUILD"], cwd=repo_dir)
if res.returncode == 0:
content = res.stdout
p_ver, p_rel = extract_ver(content)
if p_ver == expected_ver and p_rel == expected_rel:
old_pkgbuild_content = content
break
if old_pkgbuild_content is None:
print(f"ERROR: Could not find old PKGBUILD for {pkg} version {local_ver}. Cannot safely verify diff.", file=sys.stderr)
sys.exit(1)
old_stripped = strip_volatile_fields(old_pkgbuild_content)
new_stripped = strip_volatile_fields(new_pkgbuild_content)
if old_stripped != new_stripped:
print(f"ERROR: Unsafe changes detected in PKGBUILD for {pkg}!", file=sys.stderr)
with open(os.path.join(temp_dir, 'old'), 'w') as f:
f.write(old_stripped)
with open(os.path.join(temp_dir, 'new'), 'w') as f:
f.write(new_stripped)
subprocess.run(['diff', '-u', os.path.join(temp_dir, 'old'), os.path.join(temp_dir, 'new')])
sys.exit(1)
finally:
shutil.rmtree(temp_dir)
if __name__ == '__main__':
main()
+6 -3
View File
@@ -2,13 +2,16 @@
ansible.builtin.shell:
cmd:
comm -12 <(pactree -lrud1 {{ package_pattern }} | sort -u) <(pacman -Qqm | sort -u)
executable: /bin/bash
register: aur_packages
changed_when: false
failed_when: false
- name: Rebuild AUR Python packages
aur:
use: "{{ aur_helper }}"
name: '{{ item }}'
aur_only: true
extra_args: --rebuild
loop: '{{ aur_packages.stdout.split() }}'
loop: '{{ aur_packages.stdout_lines | default([]) }}'
when: aur_packages.stdout_lines | default([]) | length > 0
+4
View File
@@ -1,4 +1,8 @@
---
- name: Check AUR package diffs for poisoning
ansible.builtin.script: aur_diff_check.py
changed_when: false
- name: AUR upgrade
aur:
use: "{{ aur_helper }}"
+26 -1
View File
@@ -5,8 +5,33 @@
recurse: yes
register: pacnew_files
- name: Ensure pacnew.logs directory exists locally
ansible.builtin.file:
path: "{{ playbook_dir }}/pacnew.logs"
state: directory
mode: '0755'
delegate_to: localhost
run_once: true
become: no
- name: Save .pacnew list to file
ansible.builtin.copy:
content: "{{ pacnew_files.files | map(attribute='path') | join('\n') }}\n"
dest: "{{ playbook_dir }}/pacnew.logs/{{ inventory_hostname }}.pacnew"
mode: '0644'
delegate_to: localhost
become: no
when: pacnew_files.matched > 0
- name: Clean up old .pacnew list file if no .pacnew files exist
ansible.builtin.file:
path: "{{ playbook_dir }}/pacnew.logs/{{ inventory_hostname }}.pacnew"
state: absent
delegate_to: localhost
become: no
when: pacnew_files.matched == 0
- name: Alert if .pacnew files exist
ansible.builtin.debug:
msg: "Warning: The following .pacnew files require merging: {{ pacnew_files.files | map(attribute='path') | list }}"
when: pacnew_files.matched > 0
+4 -6
View File
@@ -6,12 +6,10 @@
upgrade: true
extra_args: "--noconfirm"
register: arch_upgrade_result
#- name: Debug full Arch upgrade output
# ansible.builtin.debug:
# var: arch_upgrade_result
failed_when:
- arch_upgrade_result.failed == true
# We ignore the failure if it's just 'nothing to do',
# # but otherwise, we let it fail so you can step in.
- "'Nothing to upgrade' not in arch_upgrade_result.msg"
- "'there is nothing to do' not in (arch_upgrade_result.stdout | default('') | lower)"
- name: Debug full Arch upgrade output
ansible.builtin.debug:
var: arch_upgrade_result
@@ -1,5 +0,0 @@
- name: Full system upgrade
become: true
ansible.builtin.apt:
update_cache: true
upgrade: full
+42
View File
@@ -0,0 +1,42 @@
---
- name: Check for pending dpkg/ucf config files
ansible.builtin.find:
paths: /etc
patterns:
- "*.dpkg-dist"
- "*.dpkg-new"
- "*.ucf-dist"
- "*.ucf-new"
recurse: yes
register: debnew_files
- name: Ensure debnew.logs directory exists locally
ansible.builtin.file:
path: "{{ playbook_dir }}/debnew.logs"
state: directory
mode: '0755'
delegate_to: localhost
run_once: true
become: no
- name: Save debnew config list to file
ansible.builtin.copy:
content: "{{ debnew_files.files | map(attribute='path') | join('\n') }}\n"
dest: "{{ playbook_dir }}/debnew.logs/{{ inventory_hostname }}.debnew"
mode: '0644'
delegate_to: localhost
become: no
when: debnew_files.matched > 0
- name: Clean up old debnew config list file if none exist
ansible.builtin.file:
path: "{{ playbook_dir }}/debnew.logs/{{ inventory_hostname }}.debnew"
state: absent
delegate_to: localhost
become: no
when: debnew_files.matched == 0
- name: Alert if debnew config files exist
ansible.builtin.debug:
msg: "Warning: The following Debian config files require merging: {{ debnew_files.files | map(attribute='path') | list }}"
when: debnew_files.matched > 0
+9 -2
View File
@@ -1,2 +1,9 @@
- name: Perform official repository updates
ansible.builtin.import_tasks: apt_upgrade.yaml
---
- name: Full system upgrade
become: true
ansible.builtin.apt:
update_cache: true
upgrade: full
- name: Report on any Debian config files that need to be merged
ansible.builtin.include_tasks: debnew.yaml
+19 -3
View File
@@ -1,10 +1,26 @@
---
- name: Pull latest images and update Docker Compose stacks
- name: Pre-build images to ensure base images are pulled
become: true
ansible.builtin.command:
cmd: docker compose build --pull
chdir: "{{ item }}"
loop: "{{ docker_compose_dirs | default([]) }}"
changed_when: false
register: _build_result
- name: Pull latest non-buildable service images
become: true
ansible.builtin.command:
cmd: docker compose pull --ignore-buildable --ignore-pull-failures
chdir: "{{ item }}"
loop: "{{ docker_compose_dirs | default([]) }}"
changed_when: false
- name: Update Docker Compose stacks
become: true
community.docker.docker_compose_v2:
project_src: "{{ item }}"
state: present
pull: always
pull: never
build: always
loop: "{{ docker_compose_dirs | default([]) }}"
+12
View File
@@ -0,0 +1,12 @@
# Gruvbox colors for zsh (fallback)
export MATUGEN_PRIMARY="#83a598"
export MATUGEN_ON_PRIMARY="#282828"
export MATUGEN_SECONDARY="#8ec07c"
export MATUGEN_ON_SECONDARY="#282828"
export MATUGEN_TERTIARY="#d3869b"
export MATUGEN_ON_TERTIARY="#282828"
export MATUGEN_BACKGROUND="#282828"
export MATUGEN_FOREGROUND="#ebdbb2"
export MATUGEN_ERROR="#fb4934"
export MATUGEN_OUTLINE="#928374"
export MATUGEN_SURFACE_CONTAINER="#3c3836"
+20
View File
@@ -0,0 +1,20 @@
# Tmux colors generated by Matugen (Gruvbox fallback)
set -g status-justify "left"
set -g status "on"
set -g status-left-style "none"
set -g message-command-style "fg=#282828,bg=#83a598"
set -g status-right-style "none"
set -g pane-active-border-style "fg=#83a598"
set -g status-style "none,bg=#3c3836"
set -g message-style "fg=#282828,bg=#83a598"
set -g pane-border-style "fg=#504945"
set -g status-right-length "100"
set -g status-left-length "100"
setw -g window-status-activity-style "none"
setw -g window-status-separator ""
setw -g window-status-style "none,fg=#ebdbb2,bg=#3c3836"
set -g status-left "#[fg=#282828,bg=#83a598] #S #[fg=#83a598,bg=#3c3836,nobold,nounderscore,noitalics]"
set -g status-right "#[fg=#504945,bg=#3c3836,nobold,nounderscore,noitalics]#[fg=#ebdbb2,bg=#504945] %Y-%m-%d  %H:%M #[fg=#83a598,bg=#504945,nobold,nounderscore,noitalics]#[fg=#282828,bg=#83a598] #h "
setw -g window-status-format "#[fg=#ebdbb2,bg=#3c3836] #I #[fg=#ebdbb2,bg=#3c3836] #W "
setw -g window-status-current-format "#[fg=#3c3836,bg=#504945,nobold,nounderscore,noitalics]#[fg=#ebdbb2,bg=#504945] #I #[fg=#ebdbb2,bg=#504945] #W #[fg=#504945,bg=#3c3836,nobold,nounderscore,noitalics]"
+94
View File
@@ -0,0 +1,94 @@
---
- name: "Ensure target config directories exist for {{ target_user.user }}"
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: '0755'
owner: "{{ target_user.user }}"
loop:
- "{{ target_user.home }}/.config/tmux"
- "{{ target_user.home }}/.config/zsh"
- "{{ target_user.home }}/.vim"
become: yes
- name: "Sync tmux config directory for {{ target_user.user }}"
ansible.posix.synchronize:
src: /home/trey/.config/tmux/
dest: "{{ target_user.home }}/.config/tmux/"
archive: yes
delete: no
rsync_opts:
- "--exclude=matugen.conf"
become: yes
become_user: "{{ target_user.user }}"
- name: "Sync zsh config directory for {{ target_user.user }}"
ansible.posix.synchronize:
src: /home/trey/.config/zsh/
dest: "{{ target_user.home }}/.config/zsh/"
archive: yes
delete: no
rsync_opts:
- "--exclude=colors.zsh"
become: yes
become_user: "{{ target_user.user }}"
- name: "Sync vim directory for {{ target_user.user }}"
ansible.posix.synchronize:
src: /home/trey/.vim/
dest: "{{ target_user.home }}/.vim/"
archive: yes
delete: no
rsync_opts:
- "--exclude=.git/"
- "--exclude=.netrwhist"
- "--exclude=swap/"
- "--exclude=vimrc"
become: yes
become_user: "{{ target_user.user }}"
- name: "Deploy static fallback gruvbox colors for zsh for {{ target_user.user }}"
ansible.builtin.copy:
src: colors.zsh
dest: "{{ target_user.home }}/.config/zsh/colors.zsh"
mode: '0644'
owner: "{{ target_user.user }}"
become: yes
- name: "Deploy static fallback gruvbox colors for tmux for {{ target_user.user }}"
ansible.builtin.copy:
src: matugen.conf
dest: "{{ target_user.home }}/.config/tmux/matugen.conf"
mode: '0644'
owner: "{{ target_user.user }}"
become: yes
- name: "Copy .zshrc and modify prompt for root user"
ansible.builtin.copy:
content: |
{% set zshrc_content = lookup('file', '/home/trey/.zshrc') %}
{% if target_user.user == 'root' %}
{{ zshrc_content | replace("export PS1='%F{$MATUGEN_PRIMARY}%n%F{$MATUGEN_SECONDARY}@%F{$MATUGEN_TERTIARY}%m%f", "export PS1='%F{red}%m%f") }}
{% else %}
{{ zshrc_content }}
{% endif %}
dest: "{{ target_user.home }}/.zshrc"
mode: '0644'
owner: "{{ target_user.user }}"
become: yes
- name: "Copy vimrc without matugen for {{ target_user.user }}"
ansible.builtin.copy:
content: "{{ lookup('file', '/home/trey/.vim/vimrc') | regex_replace('(?m)^colorscheme matugen', 'colorscheme gruvbox') }}"
dest: "{{ target_user.home }}/.vim/vimrc"
mode: '0644'
owner: "{{ target_user.user }}"
become: yes
- name: "Ensure .vimrc symlink exists for {{ target_user.user }}"
ansible.builtin.file:
src: "{{ target_user.home }}/.vim/vimrc"
dest: "{{ target_user.home }}/.vimrc"
state: link
owner: "{{ target_user.user }}"
become: yes
+10
View File
@@ -0,0 +1,10 @@
---
- name: Deploy dotfiles for users
ansible.builtin.include_tasks: deploy_user.yml
loop:
- user: "{{ 'admin' if inventory_hostname == 'bastion' else 'trey' }}"
home: "{{ '/home/admin' if inventory_hostname == 'bastion' else '/home/trey' }}"
- user: 'root'
home: '/root'
loop_control:
loop_var: target_user
+15
View File
@@ -0,0 +1,15 @@
---
node_exporter_base_flags: >-
--collector.systemd
--collector.processes
--collector.tcpstat
--no-collector.infiniband
--no-collector.tapestats
--no-collector.zfs
# Disable ARP collector by default unless this is the bastion router in barbican
node_exporter_disable_arp: "{{ not (inventory_hostname == 'bastion' and 'barbican' in group_names) }}"
node_exporter_flags: >-
{{ node_exporter_base_flags }}
{% if node_exporter_disable_arp %}--no-collector.arp{% endif %}
+6
View File
@@ -0,0 +1,6 @@
---
- name: Restart node_exporter
ansible.builtin.systemd:
name: prometheus-node-exporter
state: restarted
become: true
+59
View File
@@ -0,0 +1,59 @@
---
- name: Install node_exporter (Debian)
ansible.builtin.apt:
name: prometheus-node-exporter
state: present
when: ansible_facts["os_family"] == "Debian"
become: true
- name: Install node_exporter (Arch)
community.general.pacman:
name: prometheus-node-exporter
state: present
when: ansible_facts["os_family"] == "Archlinux"
become: true
- name: Configure node_exporter flags (Debian)
ansible.builtin.copy:
dest: /etc/default/prometheus-node-exporter
content: |
ARGS="{{ node_exporter_flags }}"
owner: root
group: root
mode: '0644'
when: ansible_facts["os_family"] == "Debian"
become: true
notify: Restart node_exporter
- name: Configure node_exporter flags (Arch)
ansible.builtin.copy:
dest: /etc/conf.d/prometheus-node-exporter
content: |
NODE_EXPORTER_ARGS="{{ node_exporter_flags }}"
owner: root
group: root
mode: '0644'
when: ansible_facts["os_family"] == "Archlinux"
become: true
notify: Restart node_exporter
- name: Enable and start node_exporter service
ansible.builtin.systemd:
name: prometheus-node-exporter
state: started
enabled: true
become: true
- name: Populate service facts
ansible.builtin.service_facts:
- name: Open port 9100 for prometheus scraping
ansible.posix.firewalld:
port: 9100/tcp
permanent: true
state: enabled
immediate: true
become: true
when: >
ansible_facts.services['firewalld.service'] is defined and
ansible_facts.services['firewalld.service'].state == 'running'
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env zsh
#!/usr/bin/env bash
# Exit code 0 = Reboot required
# Exit code 1 = System is up to date / No reboot needed
+1
View File
@@ -5,6 +5,7 @@
name: mollyguard.service
state: stopped
listen: Reboot system
failed_when: false
- name: Execute System Reboot
become: true
+1 -1
View File
@@ -1,6 +1,6 @@
---
- name: Check if kernel or microcode update requires reboot
ansible.builtin.script: check_reboot.zsh # Your script placed in files/
ansible.builtin.script: check_reboot.sh # Your script placed in files/
register: reboot_check
# Prevent Ansible from failing if the script returns false (exit code 1)
failed_when: false
+21
View File
@@ -0,0 +1,21 @@
---
- name: Install smartctl_exporter (Debian)
ansible.builtin.apt:
name: prometheus-smartctl-exporter
state: present
when: ansible_facts["os_family"] == "Debian"
become: true
- name: Install smartctl_exporter (Arch)
aur:
name: prometheus-smartctl-exporter
use: "{{ aur_helper }}"
state: present
when: ansible_facts["os_family"] == "Archlinux"
- name: Enable and start smartctl_exporter service
ansible.builtin.systemd:
name: prometheus-smartctl-exporter
state: started
enabled: true
become: true
+5
View File
@@ -0,0 +1,5 @@
---
- name: Deploy smartctl_exporter
hosts: smartd
roles:
- smartctl_exporter
+1 -1
View File
@@ -22,6 +22,6 @@
- name: Create override
ansible.builtin.shell:
cmd: |
printf {{ passphrase }} | (echo "[Service]"; systemd-creds encrypt --name={{ creds_name }} --pretty - -) >> /etc/systemd/system/shared.d/00-systemd-creds.conf
printf '%s' {{ passphrase | quote }} | (echo "[Service]"; systemd-creds encrypt --name={{ creds_name }} --pretty - -) >> /etc/systemd/system/shared.d/00-systemd-creds.conf
printf "Environment=%s=%%d/%s\n" {{ creds_var }} {{ creds_name }} >> /etc/systemd/system/shared.d/00-systemd-creds.conf
#creates: /etc/systemd/system/shared.d/00-systemd-creds.conf