Refactor playbooks: servers/workstations, split monitoring, improve shell
All checks were successful
CI / skip-ci-check (pull_request) Successful in 1m18s
CI / lint-and-test (pull_request) Successful in 1m21s
CI / ansible-validation (pull_request) Successful in 2m43s
CI / secret-scanning (pull_request) Successful in 1m19s
CI / dependency-scan (pull_request) Successful in 1m23s
CI / sast-scan (pull_request) Successful in 2m28s
CI / license-check (pull_request) Successful in 1m20s
CI / vault-check (pull_request) Successful in 2m21s
CI / playbook-test (pull_request) Successful in 2m19s
CI / container-scan (pull_request) Successful in 1m48s
CI / sonar-analysis (pull_request) Successful in 1m26s
CI / workflow-summary (pull_request) Successful in 1m17s

This commit is contained in:
ilia 2025-12-31 23:13:03 -05:00
parent 572af82852
commit 3415340e26
80 changed files with 1564 additions and 2841 deletions

View File

@ -1,7 +1,14 @@
# Ansible Lint Configuration
--- ---
# Exclude patterns # ansible-lint configuration
#
# We exclude inventory host/group vars because many contain vault-encrypted content
# that cannot be parsed without vault secrets in CI/dev environments.
exclude_paths: exclude_paths:
- inventories/production/host_vars/
- inventories/production/group_vars/all/vault.yml
- inventories/production/group_vars/all/vault.example.yml
# Exclude patterns
- .cache/ - .cache/
- .github/ - .github/
- .ansible/ - .ansible/

View File

@ -0,0 +1,33 @@
## Project rules (Ansible infrastructure repo)
### Canonical documentation
- Start here: `project-docs/index.md`
- Architecture: `project-docs/architecture.md`
- Standards: `project-docs/standards.md`
- Workflow: `project-docs/workflow.md`
- Decisions: `project-docs/decisions.md`
### Repo structure (high level)
- **Inventory**: `inventories/production/`
- **Playbooks**: `playbooks/`
- `playbooks/servers.yml`: server baseline
- `playbooks/workstations.yml`: workstation baseline + desktop apps on `desktop` group only
- `playbooks/app/*`: Proxmox app-project suite
- **Roles**: `roles/*` (standard Ansible role layout)
### Key standards to follow
- **YAML**: 2-space indentation; tasks must have `name:`
- **Modules**: prefer native modules; use FQCN (e.g., `ansible.builtin.*`, `community.general.*`)
- **Idempotency**: no “always-changed” shell tasks; use `changed_when:` / `creates:` / `removes:`
- **Secrets**: never commit plaintext; use Ansible Vault with `vault_`-prefixed vars
- **Makefile-first**: prefer `make ...` targets over raw `ansible-playbook`
### Architectural decisions (must not regress)
- Editor/IDE installation is **out of scope** for Ansible roles/playbooks.
- Monitoring is split: `monitoring_server` vs `monitoring_desktop`.
- Desktop applications run only for `desktop` group (via workstations playbook).

View File

@ -1,7 +1,7 @@
--- ---
name: CI name: CI
on: "on":
push: push:
branches: [master] branches: [master]
pull_request: pull_request:

View File

@ -1,4 +1,4 @@
.PHONY: help bootstrap lint test check dev datascience inventory inventory-all local clean status tailscale tailscale-check tailscale-dev tailscale-status create-vault create-vm monitoring .PHONY: help bootstrap lint test check dev datascience inventory inventory-all local servers workstations clean status tailscale tailscale-check tailscale-dev tailscale-status create-vault create-vm monitoring
.DEFAULT_GOAL := help .DEFAULT_GOAL := help
## Colors for output ## Colors for output
@ -13,6 +13,8 @@ RESET := \033[0m
PLAYBOOK_SITE := playbooks/site.yml PLAYBOOK_SITE := playbooks/site.yml
PLAYBOOK_DEV := playbooks/development.yml PLAYBOOK_DEV := playbooks/development.yml
PLAYBOOK_LOCAL := playbooks/local.yml PLAYBOOK_LOCAL := playbooks/local.yml
PLAYBOOK_SERVERS := playbooks/servers.yml
PLAYBOOK_WORKSTATIONS := playbooks/workstations.yml
PLAYBOOK_MAINTENANCE := playbooks/maintenance.yml PLAYBOOK_MAINTENANCE := playbooks/maintenance.yml
PLAYBOOK_TAILSCALE := playbooks/tailscale.yml PLAYBOOK_TAILSCALE := playbooks/tailscale.yml
PLAYBOOK_PROXMOX := playbooks/infrastructure/proxmox-vm.yml PLAYBOOK_PROXMOX := playbooks/infrastructure/proxmox-vm.yml
@ -251,6 +253,28 @@ local: ## Run the local playbook on localhost
@echo "$(YELLOW)Applying local playbook...$(RESET)" @echo "$(YELLOW)Applying local playbook...$(RESET)"
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_LOCAL) -K $(ANSIBLE_PLAYBOOK) $(PLAYBOOK_LOCAL) -K
servers: ## Run baseline server playbook (usage: make servers [GROUP=services] [HOST=host1])
@echo "$(YELLOW)Applying server baseline...$(RESET)"
@EXTRA=""; \
if [ -n "$(HOST)" ]; then \
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_SERVERS) --limit $(HOST); \
elif [ -n "$(GROUP)" ]; then \
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_SERVERS) -e target_group=$(GROUP); \
else \
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_SERVERS); \
fi
workstations: ## Run workstation baseline (usage: make workstations [GROUP=dev] [HOST=dev01])
@echo "$(YELLOW)Applying workstation baseline...$(RESET)"
@EXTRA=""; \
if [ -n "$(HOST)" ]; then \
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_WORKSTATIONS) --limit $(HOST); \
elif [ -n "$(GROUP)" ]; then \
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_WORKSTATIONS) -e target_group=$(GROUP); \
else \
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_WORKSTATIONS); \
fi
# Host-specific targets # Host-specific targets
dev: ## Run on specific host (usage: make dev HOST=dev01) dev: ## Run on specific host (usage: make dev HOST=dev01)
ifndef HOST ifndef HOST
@ -381,7 +405,7 @@ shell-all: ## Configure shell on all shell_hosts (usage: make shell-all)
apps: ## Install applications only apps: ## Install applications only
@echo "$(YELLOW)Installing applications...$(RESET)" @echo "$(YELLOW)Installing applications...$(RESET)"
$(ANSIBLE_PLAYBOOK) $(PLAYBOOK_DEV) --tags apps $(ANSIBLE_PLAYBOOK) $(PLAYBOOK_WORKSTATIONS) --tags apps
# Connectivity targets # Connectivity targets
ping: auto-fallback ## Ping hosts with colored output (usage: make ping [GROUP=dev] [HOST=dev01]) ping: auto-fallback ## Ping hosts with colored output (usage: make ping [GROUP=dev] [HOST=dev01])

View File

@ -78,3 +78,4 @@ ansible/
- **Guides**: `docs/guides/` - **Guides**: `docs/guides/`
- **Reference**: `docs/reference/` - **Reference**: `docs/reference/`
- **Project docs (architecture/standards/workflow)**: `project-docs/index.md`

View File

@ -5,5 +5,3 @@
- name: Configure app project guests - name: Configure app project guests
import_playbook: playbooks/app/configure_app.yml import_playbook: playbooks/app/configure_app.yml

View File

@ -0,0 +1,28 @@
# Custom roles guide
This repo is designed to be extended by adding new roles under `roles/`.
## Role structure
Follow the standard Ansible role layout:
```
roles/<role_name>/
├── defaults/main.yml
├── handlers/main.yml
├── tasks/main.yml
├── templates/
├── files/
└── README.md
```
## Where to wire a new role
- Add it to the relevant playbook under `playbooks/` (or create a new playbook if its a major concern).
- Prefer tagging the role at inclusion time so `make <feature>` targets can use `--tags`.
## Standards (canonical)
- `project-docs/standards.md`
- `project-docs/decisions.md` (add an ADR entry for significant changes)

22
docs/guides/monitoring.md Normal file
View File

@ -0,0 +1,22 @@
# Monitoring guide
Monitoring is split by host type:
- **Servers**: `roles/monitoring_server/` (includes `fail2ban`, sysstat tooling)
- **Desktops/workstations**: `roles/monitoring_desktop/` (desktop-oriented tooling)
## Run monitoring only
```bash
# Dry-run
make monitoring CHECK=true
# Apply
make monitoring
```
## Notes
- Desktop apps are installed only on the `desktop` group via `playbooks/workstations.yml`.
- If you need packet analysis tools, keep them opt-in (see `docs/reference/applications.md`).

31
docs/guides/security.md Normal file
View File

@ -0,0 +1,31 @@
# Security hardening guide
This repos “security” work is primarily implemented via roles and inventory defaults.
## What runs where
- **SSH hardening + firewall**: `roles/ssh/`
- **Baseline packages/security utilities**: `roles/base/`
- **Monitoring + intrusion prevention (servers)**: `roles/monitoring_server/` (includes `fail2ban`)
- **Secrets**: Ansible Vault in `inventories/production/group_vars/all/vault.yml`
## Recommended flow
```bash
# Dry-run first
make check
# Apply only security-tagged roles
make security
```
## Secrets / Vault
Use vault for anything sensitive:
- Guide: `docs/guides/vault.md`
## Canonical standards
- `project-docs/standards.md`

13
docs/project-docs.md Normal file
View File

@ -0,0 +1,13 @@
# Project docs (canonical)
Canonical project documentation lives in `project-docs/` (repo root):
- Index: `project-docs/index.md`
- Overview: `project-docs/overview.md`
- Architecture: `project-docs/architecture.md`
- Standards: `project-docs/standards.md`
- Workflow: `project-docs/workflow.md`
- Decisions (ADRs): `project-docs/decisions.md`
This file exists so users browsing `docs/` can quickly find the canonical project documentation.

View File

@ -54,10 +54,7 @@ Complete inventory of applications and tools deployed by Ansible playbooks.
| zsh | Z shell | apt | shell | | zsh | Z shell | apt | shell |
| tmux | Terminal multiplexer | apt | shell | | tmux | Terminal multiplexer | apt | shell |
| fzf | Fuzzy finder | apt | shell | | fzf | Fuzzy finder | apt | shell |
| oh-my-zsh | Zsh framework | git | shell | | zsh aliases | Minimal alias set (sourced from ~/.zshrc) | file | shell |
| powerlevel10k | Zsh theme | git | shell |
| zsh-syntax-highlighting | Syntax highlighting | git | shell |
| zsh-autosuggestions | Command suggestions | git | shell |
### 📊 Monitoring Tools ### 📊 Monitoring Tools
| Package | Description | Source | Role | | Package | Description | Source | Role |
@ -84,16 +81,34 @@ Complete inventory of applications and tools deployed by Ansible playbooks.
### 🖱️ Desktop Applications ### 🖱️ Desktop Applications
| Package | Description | Source | Role | | Package | Description | Source | Role |
|---------|-------------|--------|------| |---------|-------------|--------|------|
| brave-browser | Privacy-focused browser | brave | applications | | copyq | Clipboard manager (history/search) | apt | applications |
| libreoffice | Office suite | apt | applications |
| evince | PDF viewer | apt | applications | | evince | PDF viewer | apt | applications |
| redshift | Blue light filter | apt | applications | | redshift | Blue light filter | apt | applications |
### 📝 Code Editors ## Nice-to-have apps (not installed by default)
| Package | Description | Source | Role |
|---------|-------------|--------|------| These are good add-ons depending on how you use your workstations. Keep them opt-in to avoid bloating baseline installs.
| code | Visual Studio Code | snap | snap |
| cursor | AI-powered editor | snap | snap | ### Desktop / UX
- **flameshot**: screenshots + annotation
- **keepassxc**: local password manager (or use your preferred)
- **syncthing**: peer-to-peer file sync (if you want self-hosted sync)
- **remmina**: RDP/VNC client
- **mpv**: lightweight media player
### Developer workstation helpers
- **direnv**: per-project env var loading
- **shellcheck**: shell script linting
- **jq** / **yq**: JSON/YAML CLI tooling (already in base here, but listing for completeness)
- **ripgrep** / **fd-find**: fast search/find (already in base here)
### Networking / diagnostics
- **wireshark** (GUI) or **wireshark-common**: packet analysis (only if you need it)
- **iperf3**: bandwidth testing
- **dnsutils**: dig/nslookup tools
## Installation by Playbook ## Installation by Playbook
@ -103,7 +118,6 @@ Installs all roles for development machines:
- Development environment - Development environment
- Docker platform - Docker platform
- Shell configuration - Shell configuration
- Desktop applications
- Monitoring tools - Monitoring tools
- Tailscale VPN - Tailscale VPN
@ -112,7 +126,11 @@ Installs for local machine management:
- Core system tools - Core system tools
- Shell environment - Shell environment
- Development basics - Development basics
- Selected applications
### `playbooks/workstations.yml`
Installs baseline for `dev:desktop:local`, and installs desktop apps only for the `desktop` group:
- Workstation baseline (dev + desktop + local)
- Desktop applications (desktop group only)
### `playbooks/maintenance.yml` ### `playbooks/maintenance.yml`
Maintains existing installations: Maintains existing installations:
@ -135,7 +153,6 @@ Maintains existing installations:
| snap | Snap packages | snapd daemon | | snap | Snap packages | snapd daemon |
| docker | Docker repository | Docker GPG key + repo | | docker | Docker repository | Docker GPG key + repo |
| tailscale | Tailscale repository | Tailscale GPG key + repo | | tailscale | Tailscale repository | Tailscale GPG key + repo |
| brave | Brave browser repository | Brave GPG key + repo |
| git | Git repositories | Direct clone | | git | Git repositories | Direct clone |
## Services Enabled ## Services Enabled

View File

@ -1,263 +1,10 @@
# Architecture Overview # Architecture (canonical doc moved)
Technical architecture and design of the Ansible infrastructure management system. The canonical architecture document is now:
## System Architecture - `project-docs/architecture.md`
``` This `docs/reference/architecture.md` file is kept as a pointer to avoid maintaining two competing sources of truth.
┌─────────────────────────────────────────────────────────────┐
│ Control Machine |
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Ansible │ │ Makefile │ │ Vault │ │
│ │ Engine │ │ Automation │ │ Secrets │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────┬───────────────────────────────────────┘
│ SSH + Tailscale VPN
┌─────────────────┴──────────────────────────────┐
│ │
┌───▼────────┐ ┌──────────────┐ ┌─────────────────▼────────┐
│ Dev │ │ Service │ │ Infrastructure │
│ Machines │ │ VMs │ │ VMs │
├────────────┤ ├──────────────┤ ├──────────────────────────┤
│ • dev01 │ │ • giteaVM │ │ • Proxmox Controller │
│ • bottom │ │ • portainerVM│ │ • Future VMs │
│ • desktop │ │ • homepageVM │ │ │
└────────────┘ └──────────────┘ └──────────────────────────┘
```
## Network Topology
### Physical Network
- **LAN**: 192.168.1.0/24 (typical home/office network)
- **Proxmox Host**: Hypervisor for VM management
- **Physical Machines**: Direct network access
### Tailscale Overlay Network
- **Mesh VPN**: Secure peer-to-peer connections
- **100.x.x.x**: Tailscale IP range
- **Zero-config**: Automatic NAT traversal
- **End-to-end encryption**: WireGuard protocol
## Host Groups
### Development (`dev`)
**Purpose**: Developer workstations and environments
- **Hosts**: dev01, bottom, debianDesktopVM
- **OS**: Debian/Ubuntu
- **Roles**: Full development stack
### Services
**Purpose**: Self-hosted services and applications
#### Gitea (`gitea`)
- **Host**: giteaVM
- **OS**: Alpine Linux (lightweight)
- **Service**: Git repository hosting
#### Portainer (`portainer`)
- **Host**: portainerVM
- **OS**: Alpine Linux
- **Service**: Container management UI
#### Homepage (`homepage`)
- **Host**: homepageVM
- **OS**: Debian
- **Service**: Service dashboard
### Infrastructure (`ansible`)
**Purpose**: Ansible control and automation
- **Host**: Ansible controller VM
- **OS**: Ubuntu Server
- **Service**: Infrastructure automation
### Local (`local`)
**Purpose**: Local machine management
- **Host**: localhost
- **Connection**: Local (no SSH)
## Playbook Architecture
### Core Playbooks
```yaml
playbooks/development.yml # Development environment setup
├── roles/maintenance # System updates
├── roles/base # Core packages
├── roles/ssh # SSH hardening
├── roles/user # User management
├── roles/development # Dev tools
├── roles/shell # Shell config
├── roles/docker # Container platform
├── roles/applications # Desktop apps
├── roles/snap # Snap packages
├── roles/tailscale # VPN setup
├── roles/monitoring # Monitoring tools
playbooks/local.yml # Local machine
├── roles/base
├── roles/shell
├── roles/development
└── roles/tailscale
playbooks/maintenance.yml # System maintenance
└── roles/maintenance
playbooks/tailscale.yml # VPN deployment
└── roles/tailscale
playbooks/infrastructure/proxmox-vm.yml # KVM VM provisioning (controller VM, etc.)
└── roles/proxmox_vm
playbooks/app/site.yml # Proxmox app stack (LXC-first)
├── playbooks/app/provision_vms.yml # Proxmox API provisioning (LXC/KVM)
└── playbooks/app/configure_app.yml # Guest OS + app configuration over SSH
```
### Role Dependencies
```
base
├── Required by: all other roles
├── Provides: core utilities, security tools
└── Dependencies: none
ssh
├── Required by: secure access
├── Provides: hardened SSH, firewall
└── Dependencies: base
user
├── Required by: system access
├── Provides: user accounts, sudo
└── Dependencies: base
development
├── Required by: coding tasks
├── Provides: git, nodejs, python
└── Dependencies: base
docker
├── Required by: containerization
├── Provides: Docker CE, compose
└── Dependencies: base
tailscale
├── Required by: secure networking
├── Provides: mesh VPN
└── Dependencies: base
```
## Data Flow
### Configuration Management
1. **Variables**`inventories/production/group_vars/all/main.yml`
2. **Secrets**`inventories/production/group_vars/all/vault.yml` (encrypted)
3. **Host Config**`inventories/production/host_vars/<hostname>.yml`
4. **Role Defaults** → roles/*/defaults/main.yml
5. **Tasks** → roles/*/tasks/main.yml
6. **Templates** → roles/*/templates/*.j2
7. **Handlers** → roles/*/handlers/main.yml
### Execution Flow
```
make command
Makefile target
ansible-playbook
Inventory + Variables
Role execution
Task processing
Handler notification
Result reporting
```
## Security Architecture
### Defense in Depth
#### Network Layer
- **Tailscale VPN**: Encrypted mesh network
- **UFW Firewall**: Default deny, explicit allow
- **SSH Hardening**: Key-only, rate limiting
#### Application Layer
- **Fail2ban**: Intrusion prevention
- **Package signing**: GPG verification
- **Service isolation**: Docker containers
#### Data Layer
- **Ansible Vault**: Encrypted secrets
- **SSH Keys**: Ed25519 cryptography
### Access Control
```
User → SSH Key → Jump Host → Tailscale → Target Host
↓ ↓ ↓ ↓
Ed25519 Bastion WireGuard Firewall
Encryption Rules
```
## Storage Architecture
### Configuration Storage
```
/etc/ # System configuration
/opt/ # Application data
/usr/local/ # Custom scripts
/var/log/ # Logs and audit trails
```
## Monitoring Architecture
### System Monitoring
- **btop/htop**: Process monitoring
- **iotop**: I/O monitoring
- **nethogs**: Network per-process
- **Custom dashboards**: sysinfo, netinfo
### Log Management
- **logwatch**: Daily summaries
- **journald**: System logs
- **fail2ban**: Security logs
## Scalability Considerations
### Horizontal Scaling
- Add hosts to inventory groups
- Parallel execution with ansible forks
- Role reusability across environments
### Vertical Scaling
- Proxmox VM resource adjustment
- Docker resource limits
- Service-specific tuning
## Technology Stack
### Core Technologies
- **Ansible**: 2.9+ (Configuration management)
- **Python**: 3.x (Ansible runtime)
- **Jinja2**: Templating engine
- **YAML**: Configuration format
### Target Platforms
- **Debian**: 11+ (Bullseye, Bookworm)
- **Ubuntu**: 20.04+ (Focal, Jammy, Noble)
- **Alpine**: 3.x (Lightweight containers)
### Service Technologies
- **Docker**: Container runtime
- **Tailscale**: Mesh VPN
- **SystemD**: Service management
- **UFW**: Firewall management
## Best Practices ## Best Practices

21
docs/reference/network.md Normal file
View File

@ -0,0 +1,21 @@
# Network reference
## Overview
This repo manages hosts reachable over your LAN and optionally over a Tailscale overlay network.
## Physical network
- Typical LAN: `192.168.1.0/24` (adjust for your environment)
- Inventory host addressing is defined in `inventories/production/hosts`
## Tailscale overlay
- Tailscale provides a mesh VPN (WireGuard-based) with `100.x.y.z` addresses.
- The repo installs/configures it via `playbooks/tailscale.yml` + `roles/tailscale/`.
## References
- Tailscale guide: `docs/guides/tailscale.md`
- Canonical architecture: `project-docs/architecture.md`

View File

@ -0,0 +1,281 @@
# Playbooks & Tags Map
This repo is organized around playbooks in `playbooks/` (plus a few thin wrapper playbooks in the repo root).
This reference gives you:
- **Execution paths**: where each playbook “goes” (imports → roles → included tasks).
- **Tag paths**: what each tag actually selects, including Makefile shortcuts.
---
## Playbook entrypoints (paths)
### `site.yml` (wrapper)
`site.yml` is a wrapper that delegates to `playbooks/site.yml`.
```mermaid
flowchart TD
A[site.yml] --> B[playbooks/site.yml]
```
### `playbooks/site.yml` (dispatcher)
This is a pure dispatcher: it imports other playbooks and assigns **top-level tags** per import.
```mermaid
flowchart TD
S[playbooks/site.yml] -->|tags: maintenance| M[playbooks/maintenance.yml]
S -->|tags: development| D[playbooks/development.yml]
S -->|tags: tailscale| T[playbooks/tailscale.yml]
S -->|tags: app| A[playbooks/app/site.yml]
```
### `playbooks/maintenance.yml`
```mermaid
flowchart TD
P[playbooks/maintenance.yml] --> R[role: maintenance]
```
- **Notes**:
- `pre_tasks`/`post_tasks` in the playbook are **untagged**; if you run with
`--tags maintenance`, only the `maintenance` role runs (the untagged pre/post tasks are skipped).
### `playbooks/development.yml`
Targets: `hosts: dev`
```mermaid
flowchart TD
P[playbooks/development.yml] --> R1[role: maintenance]
P --> R2[role: base]
P --> R3[role: user]
P --> R4[role: ssh]
P --> R5[role: shell]
P --> R6[role: development]
P --> R7[role: datascience]
P --> R8[role: docker]
P --> R9[role: monitoring_desktop]
%% role-internal paths that matter
R5 --> S1[roles/shell/tasks/main.yml]
S1 --> S2[include_tasks: roles/shell/tasks/configure_user_shell.yml]
R8 --> D1[roles/docker/tasks/main.yml]
D1 --> D2[include_tasks: roles/docker/tasks/setup_gpg_key.yml]
D1 --> D3[include_tasks: roles/docker/tasks/setup_repo_*.yml]
```
- **Notes**:
- `pre_tasks` is **untagged**; if you run with tag filters, that apt-cache update is skipped unless you include
`--tags all` or also run untagged tasks (Ansible has `--skip-tags`/`--tags` behavior to be aware of).
### `playbooks/local.yml`
Targets: `hosts: localhost`, `connection: local`
This is basically the same role stack as `playbooks/development.yml` (minus `datascience`), but applied locally.
```mermaid
flowchart TD
P[playbooks/local.yml] --> R1[role: maintenance]
P --> R2[role: base]
P --> R3[role: user]
P --> R4[role: ssh]
P --> R5[role: shell]
P --> R6[role: development]
P --> R7[role: docker]
P --> R8[role: monitoring_desktop]
```
### `playbooks/servers.yml`
Targets by default: `services:qa:ansible:tailscale` (override via `-e target_group=...`).
```mermaid
flowchart TD
P[playbooks/servers.yml] --> R1[role: maintenance]
P --> R2[role: base]
P --> R3[role: user]
P --> R4[role: ssh]
P --> R5[role: shell]
P --> R6[role: docker]
P --> R7[role: monitoring_server]
```
### `playbooks/workstations.yml`
Two plays:
- Workstation baseline for `dev:desktop:local`
- Desktop applications only for the `desktop` group
```mermaid
flowchart TD
W[playbooks/workstations.yml] --> B1[play: workstation baseline]
B1 --> R1[role: maintenance]
B1 --> R2[role: base]
B1 --> R3[role: user]
B1 --> R4[role: ssh]
B1 --> R5[role: shell]
B1 --> R6[role: development]
B1 --> R7[role: datascience]
B1 --> R8[role: docker]
B1 --> R9[role: monitoring_desktop]
W --> B2[play: desktop apps]
B2 --> A1[role: applications]
```
### `playbooks/shell.yml`
```mermaid
flowchart TD
P[playbooks/shell.yml] --> R[role: shell]
R --> S1[roles/shell/tasks/main.yml]
S1 --> S2[include_tasks: roles/shell/tasks/configure_user_shell.yml]
```
### `playbooks/tailscale.yml`
```mermaid
flowchart TD
P[playbooks/tailscale.yml] --> R[role: tailscale]
R --> T1[roles/tailscale/tasks/main.yml]
T1 -->|Debian| T2[include_tasks: roles/tailscale/tasks/debian.yml]
T1 -->|Alpine| T3[include_tasks: roles/tailscale/tasks/alpine.yml]
```
### `playbooks/infrastructure/proxmox-vm.yml`
Creates an Ansible controller VM on Proxmox (local connection) via `role: proxmox_vm`.
```mermaid
flowchart TD
P[playbooks/infrastructure/proxmox-vm.yml] --> R[role: proxmox_vm]
R --> M1[roles/proxmox_vm/tasks/main.yml]
M1 -->|proxmox_guest_type=lxc| L1[include_tasks: roles/proxmox_vm/tasks/lxc.yml]
M1 -->|else| K1[include_tasks: roles/proxmox_vm/tasks/kvm.yml]
```
### App project suite (`playbooks/app/*`)
#### `playbooks/app/site.yml` (app dispatcher)
```mermaid
flowchart TD
S[playbooks/app/site.yml] -->|tags: app,provision| P[playbooks/app/provision_vms.yml]
S -->|tags: app,configure| C[playbooks/app/configure_app.yml]
```
#### `playbooks/app/provision_vms.yml` (provision app guests)
High-level loop: `project_key``env_item` → provision guest → add to dynamic inventory groups.
```mermaid
flowchart TD
P[playbooks/app/provision_vms.yml] --> T1[include_tasks: playbooks/app/provision_one_guest.yml]
T1 --> T2[include_tasks: playbooks/app/provision_one_env.yml]
T2 --> R[include_role: proxmox_vm]
R --> M1[roles/proxmox_vm/tasks/main.yml]
M1 --> L1[roles/proxmox_vm/tasks/lxc.yml]
T2 --> H[add_host groups]
H --> G1[app_all]
H --> G2[app_${project}_all]
H --> G3[app_${project}_${env}]
```
#### `playbooks/app/configure_app.yml` (configure app guests)
Two phases:
1) **localhost** builds a dynamic inventory from `app_projects` (static IPs)
2) **app_all** (or `app_${project}_all`) configures each host
```mermaid
flowchart TD
A[play: localhost build inventory] --> H[add_host groups]
H --> G1[app_all / app_${project}_*]
B[play: app_all configure] --> OS[include_role: base_os]
B --> POTE[include_role: pote (only when app_project == 'pote')]
B --> APP[include_role: app_setup (when app_project != 'pote')]
```
#### `playbooks/app/proxmox_info.yml`
Single local play that queries Proxmox and prints a filtered summary.
#### `playbooks/app/ssh_client_config.yml`
Single local play that optionally manages `~/.ssh/config` (gated by `manage_ssh_config`).
---
## Tags map (what each tag hits)
### Top-level dispatcher tags (`playbooks/site.yml`)
- **maintenance**: runs `playbooks/maintenance.yml`
- **development**: runs `playbooks/development.yml`
- **tailscale**: runs `playbooks/tailscale.yml`
- **app**: runs `playbooks/app/site.yml` (and therefore app provision + configure)
### Dev/local role tags (`playbooks/development.yml`, `playbooks/local.yml`)
These playbooks tag roles directly:
- **maintenance**`role: maintenance`
- **base**`role: base`
- **security**`role: base` + `role: ssh` (both are tagged `security` at role-inclusion)
- **user**`role: user`
- **ssh**`role: ssh`
- **shell**`role: shell`
- **development** / **dev**`role: development`
- **docker**`role: docker`
- **monitoring**`role: monitoring_desktop`
- **datascience** / **conda** / **jupyter** / **r**`role: datascience` (development playbook only)
- **tailscale** / **vpn** → (currently commented out in dev/local)
### Workstation + desktop apps tags (`playbooks/workstations.yml`)
- **apps** / **applications**`role: applications` (desktop group only)
### App suite tags
From `playbooks/app/site.yml` imports:
- **app**: everything in the app suite
- **provision**: `playbooks/app/provision_vms.yml` only
- **configure**: `playbooks/app/configure_app.yml` only
Standalone app playbook tags (so `--tags ...` works when running them directly):
- `playbooks/app/provision_vms.yml`: **app**, **provision**
- `playbooks/app/configure_app.yml`: **app**, **configure**
- `playbooks/app/proxmox_info.yml`: **app**, **proxmox**, **info**
- `playbooks/app/ssh_client_config.yml`: **app**, **ssh-config**
### Role-internal tags (task/block level)
These are tags inside role task files (useful for targeting parts of a role even if the role itself isnt included with that tag):
- `roles/datascience/tasks/main.yml`:
- **conda**
- **jupyter**
- **r**, **rstats**
### Makefile tag shortcuts
Make targets that apply `--tags`:
- `make datascience HOST=...``--tags datascience`
- `make security``--tags security`
- `make docker``--tags docker`
- `make shell``--tags shell`
- `make apps``--tags apps`
- `make monitoring``--tags monitoring`
---
## Tag-filtering gotchas (important)
- If you run with `--tags X`, **untagged** `pre_tasks`/`tasks`/`post_tasks` in a playbook are skipped.
- Example: `playbooks/maintenance.yml` has untagged `pre_tasks` and `post_tasks`.
`--tags maintenance` runs only the `maintenance` role, not the surrounding reporting steps.

View File

@ -0,0 +1,28 @@
# Security reference
## Overview
Security in this repo is implemented via:
- hardened SSH + firewall defaults (`roles/ssh/`)
- baseline system configuration (`roles/base/`)
- monitoring/intrusion prevention on servers (`roles/monitoring_server/`)
- secrets handled via Ansible Vault (`inventories/production/group_vars/all/vault.yml`)
## Recommended execution
```bash
# Dry-run first
make check
# Apply security-tagged tasks
make security
```
## Vault
- Vault guide: `docs/guides/vault.md`
## Canonical standards
- `project-docs/standards.md`

View File

@ -1,3 +1,4 @@
---
ansible_become_password: root ansible_become_password: root
ansible_python_interpreter: /usr/bin/python3 ansible_python_interpreter: /usr/bin/python3
@ -21,10 +22,4 @@ jupyter_bind_all_interfaces: true
# R configuration # R configuration
install_r: true install_r: true
# Cursor IDE configuration # IDE/editor tooling is intentionally not managed by Ansible in this repo.
install_cursor_extensions: true
# Cursor extension groups to enable
install_python: true # Python development
install_docs: true # Markdown/documentation

View File

@ -1,3 +1,4 @@
---
# Configure sudo path for git-ci-01 # Configure sudo path for git-ci-01
# Sudo may not be in PATH for non-interactive shells # Sudo may not be in PATH for non-interactive shells
ansible_become_exe: /usr/bin/sudo ansible_become_exe: /usr/bin/sudo
@ -5,4 +6,3 @@ ansible_become_method: sudo
# Alternative: if sudo is in a different location, update this # Alternative: if sudo is in a different location, update this
# ansible_become_exe: /usr/local/bin/sudo # ansible_become_exe: /usr/local/bin/sudo

View File

@ -7,4 +7,3 @@ ansible_become_method: sudo
# Configure shell for ladmin user # Configure shell for ladmin user
shell_users: shell_users:
- ladmin - ladmin

View File

@ -12,6 +12,7 @@
hosts: localhost hosts: localhost
connection: local connection: local
gather_facts: false gather_facts: false
tags: ['app', 'configure']
vars: vars:
selected_projects: >- selected_projects: >-
{{ {{
@ -61,6 +62,7 @@
}} }}
become: true become: true
gather_facts: true gather_facts: true
tags: ['app', 'configure']
tasks: tasks:
- name: Build project/env effective variables - name: Build project/env effective variables
@ -130,5 +132,3 @@
app_env_vars: "{{ env_def.env_vars | default({}) }}" app_env_vars: "{{ env_def.env_vars | default({}) }}"
when: app_project != 'pote' when: app_project != 'pote'

View File

@ -233,3 +233,5 @@
ansible_user: root ansible_user: root
app_project: "{{ project_key }}" app_project: "{{ project_key }}"
app_env: "{{ env_name }}" app_env: "{{ env_name }}"
# EOF

View File

@ -19,3 +19,5 @@
loop: "{{ project_def.envs | dict2items }}" loop: "{{ project_def.envs | dict2items }}"
loop_control: loop_control:
loop_var: env_item loop_var: env_item
# EOF

View File

@ -12,6 +12,7 @@
hosts: localhost hosts: localhost
connection: local connection: local
gather_facts: false gather_facts: false
tags: ['app', 'provision']
vars: vars:
selected_projects: >- selected_projects: >-
{{ {{
@ -32,5 +33,3 @@
loop: "{{ selected_projects }}" loop: "{{ selected_projects }}"
loop_control: loop_control:
loop_var: project_key loop_var: project_key

View File

@ -14,6 +14,7 @@
hosts: localhost hosts: localhost
connection: local connection: local
gather_facts: false gather_facts: false
tags: ['app', 'proxmox', 'info']
vars: vars:
selected_projects: >- selected_projects: >-
{{ {{
@ -95,5 +96,3 @@
{% for g in (filtered_guests | sort(attribute='vmid')) %} {% for g in (filtered_guests | sort(attribute='vmid')) %}
- vmid={{ g.vmid }} type={{ g.id.split('/')[0] if g.id is defined else 'unknown' }} name={{ g.name | default('') }} node={{ g.node | default('') }} status={{ g.status | default('') }} - vmid={{ g.vmid }} type={{ g.id.split('/')[0] if g.id is defined else 'unknown' }} name={{ g.name | default('') }} node={{ g.node | default('') }} status={{ g.status | default('') }}
{% endfor %} {% endfor %}

View File

@ -11,5 +11,3 @@
- name: Configure guests - name: Configure guests
import_playbook: configure_app.yml import_playbook: configure_app.yml
tags: ['app', 'configure'] tags: ['app', 'configure']

View File

@ -13,6 +13,7 @@
hosts: localhost hosts: localhost
connection: local connection: local
gather_facts: false gather_facts: false
tags: ['app', 'ssh-config']
vars: vars:
manage_ssh_config: "{{ manage_ssh_config | default(false) }}" manage_ssh_config: "{{ manage_ssh_config | default(false) }}"
ssh_config_path: "{{ lookup('ansible.builtin.env', 'HOME') + '/.ssh/config' }}" ssh_config_path: "{{ lookup('ansible.builtin.env', 'HOME') + '/.ssh/config' }}"
@ -47,5 +48,3 @@
- app_projects[item.0] is defined - app_projects[item.0] is defined
- app_projects[item.0].envs[item.1] is defined - app_projects[item.0].envs[item.1] is defined
- (app_projects[item.0].envs[item.1].ip | default('')) | length > 0 - (app_projects[item.0].envs[item.1].ip | default('')) | length > 0

View File

@ -2,32 +2,18 @@
- name: Configure development environment - name: Configure development environment
hosts: dev hosts: dev
become: true become: true
strategy: free
roles: roles:
- {role: maintenance, tags: ['maintenance']} - {role: maintenance, tags: ['maintenance']}
- {role: base, tags: ['base', 'security']} - {role: base, tags: ['base', 'security']}
- {role: user, tags: ['user']} - {role: user, tags: ['user']}
- {role: ssh, tags: ['ssh', 'security']} - {role: ssh, tags: ['ssh', 'security']}
- {role: shell, tags: ['shell']} - {role: shell, tags: ['shell'], shell_mode: full, shell_set_default_shell: true}
- {role: development, tags: ['development', 'dev']} - {role: development, tags: ['development', 'dev']}
- {role: datascience, tags: ['datascience', 'conda', 'jupyter', 'r']} - {role: datascience, tags: ['datascience', 'conda', 'jupyter', 'r']}
- {role: docker, tags: ['docker']} - {role: docker, tags: ['docker']}
- {role: applications, tags: ['applications', 'apps']}
# - {role: tailscale, tags: ['tailscale', 'vpn']} # - {role: tailscale, tags: ['tailscale', 'vpn']}
- {role: monitoring, tags: ['monitoring']} - {role: monitoring_desktop, tags: ['monitoring']}
pre_tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
ignore_errors: true
register: apt_update_result
- name: Display apt update status
ansible.builtin.debug:
msg: "Apt cache update: {{ 'Success' if apt_update_result is succeeded else 'Failed - continuing anyway' }}"
when: ansible_debug_output | default(false) | bool
tasks: tasks:
# Additional tasks can be added here if needed # Additional tasks can be added here if needed

View File

@ -12,14 +12,8 @@
- {role: shell, tags: ['shell']} - {role: shell, tags: ['shell']}
- {role: development, tags: ['development', 'dev']} - {role: development, tags: ['development', 'dev']}
- {role: docker, tags: ['docker']} - {role: docker, tags: ['docker']}
- {role: applications, tags: ['applications', 'apps']}
# - {role: tailscale, tags: ['tailscale', 'vpn']} # - {role: tailscale, tags: ['tailscale', 'vpn']}
- {role: monitoring, tags: ['monitoring']} - {role: monitoring_desktop, tags: ['monitoring']}
pre_tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
tasks: tasks:
- name: Display completion message - name: Display completion message

View File

@ -22,12 +22,6 @@
Group: {{ group_names | join(', ') }} Group: {{ group_names | join(', ') }}
Skip reboot: {{ skip_reboot | default(false) | bool }} Skip reboot: {{ skip_reboot | default(false) | bool }}
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
when: maintenance_update_cache | bool
roles: roles:
- {role: maintenance, tags: ['maintenance']} - {role: maintenance, tags: ['maintenance']}

27
playbooks/servers.yml Normal file
View File

@ -0,0 +1,27 @@
---
# Playbook: servers.yml
# Purpose: Baseline configuration for servers (no desktop apps, no IDE install)
# Targets: services + qa + ansible + tailscale (override with -e target_group=...)
# Tags: maintenance, base, security, user, ssh, shell, docker, monitoring
# Usage:
# ansible-playbook -i inventories/production playbooks/servers.yml
# ansible-playbook -i inventories/production playbooks/servers.yml -e target_group=services
# ansible-playbook -i inventories/production playbooks/servers.yml --limit jellyfin
- name: Configure servers baseline
hosts: "{{ target_group | default('services:qa:ansible:tailscale') }}"
become: true
roles:
- {role: maintenance, tags: ['maintenance']}
- {role: base, tags: ['base', 'security']}
- {role: user, tags: ['user']}
- {role: ssh, tags: ['ssh', 'security']}
- {role: shell, tags: ['shell']}
- {role: docker, tags: ['docker']}
- {role: monitoring_server, tags: ['monitoring']}
tasks:
- name: Display completion message
ansible.builtin.debug:
msg: "Server baseline configuration completed successfully!"

View File

@ -1,6 +1,6 @@
--- ---
# Playbook: shell.yml # Playbook: shell.yml
# Purpose: Configure shell environment (zsh, oh-my-zsh, plugins) # Purpose: Configure shell environment (minimal zsh + managed aliases)
# Targets: all hosts # Targets: all hosts
# Tags: shell # Tags: shell
# Usage: make shell-all # Usage: make shell-all
@ -8,25 +8,12 @@
- name: Configure shell environment - name: Configure shell environment
hosts: all hosts: all
become: true become: true
strategy: free
ignore_errors: true ignore_errors: true
ignore_unreachable: true ignore_unreachable: true
roles: roles:
- {role: shell, tags: ['shell']} - {role: shell, tags: ['shell']}
pre_tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
ignore_errors: true
register: apt_update_result
- name: Display apt update status
ansible.builtin.debug:
msg: "Apt cache update: {{ 'Success' if apt_update_result is succeeded else 'Failed - continuing anyway' }}"
when: ansible_debug_output | default(false) | bool
tasks: tasks:
- name: Display completion message - name: Display completion message
ansible.builtin.debug: ansible.builtin.debug:

View File

@ -9,12 +9,6 @@
# Override here if needed or pass via: --extra-vars "tailscale_auth_key=your_key" # Override here if needed or pass via: --extra-vars "tailscale_auth_key=your_key"
tailscale_auth_key: "{{ vault_tailscale_auth_key | default('') }}" tailscale_auth_key: "{{ vault_tailscale_auth_key | default('') }}"
pre_tasks:
- name: Update package cache (Debian/Ubuntu)
ansible.builtin.apt:
update_cache: true
when: ansible_os_family == "Debian"
roles: roles:
- {role: tailscale, tags: ['tailscale', 'vpn']} - {role: tailscale, tags: ['tailscale', 'vpn']}

View File

@ -0,0 +1,42 @@
---
# Playbook: workstations.yml
# Purpose: Workstation baseline (dev boxes + desktops). Desktop apps are applied only to the `desktop` group.
# Targets: dev + desktop + local (override with -e target_group=...)
# Tags: maintenance, base, security, user, ssh, shell, development, dev, datascience, docker, monitoring, apps
#
# Usage:
# ansible-playbook -i inventories/production playbooks/workstations.yml
# ansible-playbook -i inventories/production playbooks/workstations.yml -e target_group=dev
# ansible-playbook -i inventories/production playbooks/workstations.yml --tags apps
- name: Configure workstation baseline
hosts: "{{ target_group | default('dev:desktop:local') }}"
become: true
roles:
- {role: maintenance, tags: ['maintenance']}
- {role: base, tags: ['base', 'security']}
- {role: user, tags: ['user']}
- {role: ssh, tags: ['ssh', 'security']}
- {role: shell, tags: ['shell'], shell_mode: full, shell_set_default_shell: true}
- {role: development, tags: ['development', 'dev']}
- {role: datascience, tags: ['datascience', 'conda', 'jupyter', 'r']}
- {role: docker, tags: ['docker']}
- {role: monitoring_desktop, tags: ['monitoring']}
tasks:
- name: Display completion message
ansible.builtin.debug:
msg: "Workstation baseline configuration completed successfully!"
- name: Install desktop applications (desktop group only)
hosts: desktop
become: true
roles:
- {role: applications, tags: ['applications', 'apps']}
tasks:
- name: Display completion message
ansible.builtin.debug:
msg: "Desktop applications installed successfully!"

View File

@ -0,0 +1,63 @@
## Architecture
### High-level map (modules and relationships)
- **Inventory**: `inventories/production/`
- `hosts`: groups like `dev`, `desktop`, `services`, `qa`, `ansible`, `tailscale`, `local`
- `group_vars/all/main.yml`: shared configuration (including `app_projects`)
- `group_vars/all/vault.yml`: encrypted secrets (Ansible Vault)
- `host_vars/*`: per-host overrides (some encrypted)
- **Playbooks**: `playbooks/`
- `playbooks/site.yml`: dispatcher (imports other playbooks)
- `playbooks/servers.yml`: baseline for servers (`services:qa:ansible:tailscale`)
- `playbooks/workstations.yml`: baseline for `dev:desktop:local` + desktop apps for `desktop` group only
- `playbooks/development.yml`: dev machines baseline (no desktop apps)
- `playbooks/local.yml`: localhost baseline (no desktop apps)
- `playbooks/app/*`: Proxmox app-project provisioning/configuration suite
- **Roles**: `roles/*`
- Baseline/security: `base`, `user`, `ssh`
- Dev tooling: `development`, `datascience`, `docker`
- Shell: `shell` (minimal aliases-only)
- Monitoring split:
- `monitoring_server` (fail2ban + sysstat)
- `monitoring_desktop` (desktop-oriented monitoring tooling)
- Proxmox guests: `proxmox_vm`
- App guest configuration: `base_os`, `app_setup`, `pote`
### Proxmox “app projects” flow (data model + execution)
- **Data model**: `app_projects` in `inventories/production/group_vars/all/main.yml`
- Defines projects and per-env (`dev/qa/prod`) guest parameters (ip, branch, vmid, etc.)
- **Provision**: `playbooks/app/provision_vms.yml`
- Loops `app_projects` → envs → calls `role: proxmox_vm` to create LXC guests
- Adds dynamic inventory groups:
- `app_all`
- `app_<project>_all`
- `app_<project>_<env>`
- **Configure**: `playbooks/app/configure_app.yml`
- Builds a dynamic inventory from `app_projects` (so it can run standalone)
- Applies:
- `role: base_os` (baseline OS for app guests)
- `role: app_setup` (deploy + systemd) or `role: pote` for the POTE project
### Boundaries
- **Inventory/vars** define desired state and credentials.
- **Playbooks** define “what path to run” (role ordering, target groups, tags).
- **Roles** implement actual host configuration (idempotent tasks, handlers).
### External dependencies
- **Ansible collections**: `collections/requirements.yml`
- **Ansible Vault**: `inventories/production/group_vars/all/vault.yml`
- **Proxmox API**: used by `community.proxmox.*` modules in provisioning
### References
- Playbook execution graphs and tags: `docs/reference/playbooks-and-tags.md`
- Legacy pointer (do not update): `docs/reference/architecture.md``project-docs/architecture.md`

35
project-docs/decisions.md Normal file
View File

@ -0,0 +1,35 @@
## Decisions (ADR-style)
### 2025-12-31 — Do not manage IDE/editor installs in Ansible
- **Context**: IDEs/editors are interactive, fast-moving, and often user-preference-driven.
- **Decision**: Keep editor installation (Cursor, VS Code, etc.) out of Ansible roles/playbooks.
- **Consequences**:
- Faster, more stable provisioning runs
- Less drift caused by UI tooling changes
- Editor setup is handled separately (manual or via dedicated tooling)
### 2025-12-31 — Split monitoring into server vs workstation roles
- **Context**: Servers and workstations have different needs (e.g., fail2ban/sysstat are server-centric; wireshark-common is workstation-centric).
- **Decision**: Create `monitoring_server` and `monitoring_desktop` roles and wire them into `servers.yml` / workstation playbooks.
- **Consequences**:
- Smaller install footprint on servers
- Clearer intent and faster runs
### 2025-12-31 — Desktop applications are installed only on the `desktop` group
- **Context**: Desktop apps should not be installed on headless servers or dev VMs by default.
- **Decision**: Run `role: applications` only in a `desktop`-scoped play (workstations playbook).
- **Consequences**:
- Reduced unnecessary package installs
- Less attack surface and fewer updates on non-desktop hosts
### 2025-12-31 — Minimal shell role (aliases-only)
- **Context**: Oh-my-zsh/theme/plugin cloning is slow and overwriting `.zshrc` is risky.
- **Decision**: `role: shell` now manages a small alias file and ensures its sourced; it does not overwrite `.zshrc`.
- **Consequences**:
- Much faster shell configuration
- Safer for servers and multi-user systems

33
project-docs/index.md Normal file
View File

@ -0,0 +1,33 @@
## Project docs index
Last updated: **2025-12-31**
### Documents
- **`project-docs/overview.md`** (updated 2025-12-31)
High-level goals, scope, and primary users for this Ansible infrastructure repo.
- **`project-docs/architecture.md`** (updated 2025-12-31)
Architecture map: inventories, playbooks, roles, and the Proxmox app-project flow.
- **`project-docs/standards.md`** (updated 2025-12-31)
Conventions for Ansible YAML, role structure, naming, vault usage, and linting.
- **`project-docs/workflow.md`** (updated 2025-12-31)
How to run common tasks via `Makefile`, how to lint/test, and how to apply safely.
- **`project-docs/decisions.md`** (updated 2025-12-31)
Short ADR-style notes for important architectural decisions.
### Related docs (existing)
- **Playbooks/tags map**: `docs/reference/playbooks-and-tags.md`
- **Applications inventory**: `docs/reference/applications.md`
- **Makefile reference**: `docs/reference/makefile.md`
- **Proxmox app project guides**:
- `docs/guides/app_stack_proxmox.md`
- `docs/guides/app_stack_execution_flow.md`
Legacy pointers:
- `docs/reference/architecture.md``project-docs/architecture.md`

27
project-docs/overview.md Normal file
View File

@ -0,0 +1,27 @@
## Overview
This repository manages infrastructure automation using **Ansible** for:
- Development machines (`dev`)
- Desktop machines (`desktop`)
- Service hosts (`services`, `qa`, `ansible`, `tailscale`)
- Proxmox-managed guests for “app projects” (LXC-first, with a KVM path)
Primary entrypoint is the **Makefile** (`Makefile`) and playbooks under `playbooks/`.
### Goals
- **Predictable, repeatable provisioning** of hosts and Proxmox guests
- **Safe defaults**: avoid destructive automation; prefer guardrails and idempotency
- **Clear separation** between server vs workstation responsibilities
- **Secrets handled via Ansible Vault** (never commit plaintext credentials)
### Non-goals
- Automated decommission/destroy playbooks for infrastructure or guests
- Managing interactive IDE/editor installs (kept out of Ansible by design)
### Target users
- You (and collaborators) operating a small homelab / Proxmox environment
- Contributors extending roles/playbooks in a consistent style

49
project-docs/standards.md Normal file
View File

@ -0,0 +1,49 @@
## Standards
### Ansible + YAML conventions
- **Indentation**: 2 spaces (no tabs)
- **Task naming**: every task should include a clear `name:`
- **Play-level privilege**: prefer `become: true` at play level when most tasks need sudo
- **Modules**:
- Prefer native modules over `shell`/`command`
- Use **fully qualified collection names** (FQCN), e.g. `ansible.builtin.apt`, `community.general.ufw`
- **Handlers**: use handlers for restarts/reloads
- **Idempotency**:
- If `shell`/`command` is unavoidable, set `changed_when:` / `creates:` / `removes:` appropriately
### Role structure
Roles should follow:
```
roles/<role_name>/
├── defaults/main.yml
├── handlers/main.yml
├── tasks/main.yml
├── templates/
├── files/
└── README.md
```
### Variable naming
- **snake_case** everywhere
- Vault-backed variables are prefixed with **`vault_`**
### Secrets / Vault
- Never commit plaintext secrets.
- Use Ansible Vault for credentials:
- `inventories/production/group_vars/all/vault.yml` (encrypted)
- Local vault password file is expected at `~/.ansible-vault-pass`.
### Makefile-first workflow
- Prefer `make ...` targets over direct `ansible-playbook` commands for consistency.
### Linting
- `ansible-lint` is the primary linter.
- `.ansible-lint` excludes vault-containing inventory paths to keep linting deterministic without vault secrets.

86
project-docs/workflow.md Normal file
View File

@ -0,0 +1,86 @@
## Workflow
### Setup
- Install dependencies (Python requirements, Node deps for docs, Ansible collections):
```bash
make bootstrap
```
- Edit vault secrets:
```bash
make edit-group-vault
```
### Validate (safe, local)
- Syntax checks:
```bash
make test-syntax
```
- Lint:
```bash
make lint
```
### Common apply flows
- **Servers baseline** (services + qa + ansible + tailscale):
```bash
make servers
make servers GROUP=services
make servers HOST=jellyfin
```
- **Workstations baseline** (dev + desktop + local; desktop apps only on `desktop` group):
```bash
make workstations
make workstations GROUP=dev
make apps
```
### Proxmox app projects
End-to-end:
```bash
make app PROJECT=projectA
```
Provision only / configure only:
```bash
make app-provision PROJECT=projectA
make app-configure PROJECT=projectA
```
Inspect Proxmox guests:
```bash
make proxmox-info PROJECT=projectA
make proxmox-info ALL=true
make proxmox-info TYPE=lxc
```
### Safety checks
- Prefer `--check --diff` first:
```bash
make check
```
### Debugging
```bash
make debug
make verbose
```

View File

@ -5,5 +5,3 @@
- name: Provision app project guests - name: Provision app project guests
import_playbook: playbooks/app/provision_vms.yml import_playbook: playbooks/app/provision_vms.yml

View File

@ -37,4 +37,3 @@ app_frontend_start_cmd: "npm start"
# Arbitrary environment variables for the env file # Arbitrary environment variables for the env file
app_env_vars: {} app_env_vars: {}

View File

@ -4,5 +4,3 @@
- name: Reload systemd - name: Reload systemd
ansible.builtin.systemd: ansible.builtin.systemd:
daemon_reload: true daemon_reload: true

View File

@ -80,5 +80,3 @@
enabled: true enabled: true
state: started state: started
when: app_enable_frontend | bool when: app_enable_frontend | bool

View File

@ -1,7 +1,7 @@
# Role: applications # Role: applications
## Description ## Description
Installs desktop applications for development and productivity including browsers, office suites, and utilities. Installs a small set of desktop GUI applications (desktop group only via `playbooks/workstations.yml`).
## Requirements ## Requirements
- Ansible 2.9+ - Ansible 2.9+
@ -9,8 +9,7 @@ Installs desktop applications for development and productivity including browser
- Internet access for package downloads - Internet access for package downloads
## Installed Applications ## Installed Applications
- **Brave Browser**: Privacy-focused web browser - **CopyQ**: Clipboard manager (history, search, scripting)
- **LibreOffice**: Complete office suite
- **Evince**: PDF document viewer - **Evince**: PDF document viewer
- **Redshift**: Blue light filter for eye comfort - **Redshift**: Blue light filter for eye comfort
@ -18,10 +17,7 @@ Installs desktop applications for development and productivity including browser
| Variable | Default | Description | | Variable | Default | Description |
|----------|---------|-------------| |----------|---------|-------------|
| `applications_install_brave` | `true` | Install Brave browser | | `applications_desktop_packages` | `['copyq','evince','redshift']` | Desktop packages to install |
| `applications_install_libreoffice` | `true` | Install LibreOffice suite |
| `applications_install_evince` | `true` | Install PDF viewer |
| `applications_install_redshift` | `true` | Install blue light filter |
## Dependencies ## Dependencies
- `base` role (for package management) - `base` role (for package management)
@ -31,16 +27,13 @@ Installs desktop applications for development and productivity including browser
```yaml ```yaml
- hosts: desktop - hosts: desktop
roles: roles:
- { role: applications, applications_install_brave: false } - role: applications
``` ```
## Tags ## Tags
- `applications`: All application installations - `applications`: All application installations
- `apps`: Alias for applications - `apps`: Alias for applications
- `browser`: Browser installation only
- `office`: Office suite installation only
## Notes ## Notes
- Adds external repositories for Brave browser
- Requires desktop environment for GUI applications - Requires desktop environment for GUI applications
- Applications are installed system-wide - Applications are installed system-wide

View File

@ -1 +1,7 @@
--- ---
# Desktop GUI applications to install (desktop group only via playbooks/workstations.yml)
applications_desktop_packages:
- copyq
- evince
- redshift

View File

@ -3,109 +3,25 @@
ansible.builtin.package_facts: ansible.builtin.package_facts:
manager: apt manager: apt
- name: Check if Brave browser is installed
ansible.builtin.command: brave-browser --version
register: applications_brave_check
ignore_errors: true
changed_when: false
failed_when: false
no_log: true
- name: Set installation conditions - name: Set installation conditions
ansible.builtin.set_fact: ansible.builtin.set_fact:
applications_desktop_apps_needed: "{{ ['redshift', 'libreoffice', 'evince'] | difference(ansible_facts.packages.keys()) | length > 0 }}" applications_desktop_apps_needed: >-
applications_brave_needs_install: "{{ applications_brave_check.rc != 0 or 'brave-browser' not in ansible_facts.packages }}" {{
(applications_desktop_packages | default([]))
- name: Check if Brave GPG key exists and is correct | difference(ansible_facts.packages.keys())
ansible.builtin.shell: | | length > 0
if [ -f /usr/share/keyrings/brave-browser-archive-keyring.gpg ]; then }}
if file /usr/share/keyrings/brave-browser-archive-keyring.gpg | grep -q "PGP"; then
echo "correct_key"
else
echo "wrong_key"
fi
else
echo "not_exists"
fi
register: brave_key_check
failed_when: false
when: applications_brave_needs_install
- name: Check if Brave repository exists and is correct
ansible.builtin.shell: |
if [ -f /etc/apt/sources.list.d/brave-browser.list ]; then
if grep -q "deb \[signed-by=/usr/share/keyrings/brave-browser-archive-keyring.gpg\]" /etc/apt/sources.list.d/brave-browser.list; then
echo "correct_config"
else
echo "wrong_config"
fi
else
echo "not_exists"
fi
register: brave_repo_check
failed_when: false
when: applications_brave_needs_install
- name: Clean up duplicate Brave repository files
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /etc/apt/sources.list.d/brave-browser.list
- /etc/apt/sources.list.d/brave-browser-release.sources
become: true
failed_when: false
when:
- applications_brave_needs_install
- brave_repo_check.stdout == "wrong_config"
- name: Remove incorrect Brave GPG key
ansible.builtin.file:
path: /usr/share/keyrings/brave-browser-archive-keyring.gpg
state: absent
become: true
when:
- applications_brave_needs_install
- brave_key_check.stdout == "wrong_key"
- name: Install desktop applications - name: Install desktop applications
ansible.builtin.apt: ansible.builtin.apt:
name: name: "{{ applications_desktop_packages }}"
- redshift
- libreoffice
- evince
state: present state: present
when: applications_desktop_apps_needed when: applications_desktop_apps_needed
- name: Brave browser installation
when: applications_brave_needs_install
block:
- name: Download Brave APT key only if needed
ansible.builtin.get_url:
url: https://brave-browser-apt-release.s3.brave.com/brave-browser-archive-keyring.gpg
dest: /usr/share/keyrings/brave-browser-archive-keyring.gpg
mode: '0644'
when: brave_key_check.stdout in ["not_exists", "wrong_key"]
- name: Add Brave repository only if needed
ansible.builtin.apt_repository:
repo: "deb [signed-by=/usr/share/keyrings/brave-browser-archive-keyring.gpg] https://brave-browser-apt-release.s3.brave.com/ stable main"
filename: brave-browser
state: present
when: brave_repo_check.stdout in ["not_exists", "wrong_config"]
- name: Install Brave browser
ansible.builtin.apt:
name: brave-browser
state: present
- name: Display application status - name: Display application status
ansible.builtin.debug: ansible.builtin.debug:
msg: msg:
- "Desktop apps needed: {{ applications_desktop_apps_needed }}" - "Desktop apps needed: {{ applications_desktop_apps_needed }}"
- "Brave needed: {{ applications_brave_needs_install }}"
- "Redshift: {{ 'Installed' if 'redshift' in ansible_facts.packages else 'Missing' }}" - "Redshift: {{ 'Installed' if 'redshift' in ansible_facts.packages else 'Missing' }}"
- "LibreOffice: {{ 'Installed' if 'libreoffice' in ansible_facts.packages else 'Missing' }}"
- "Evince: {{ 'Installed' if 'evince' in ansible_facts.packages else 'Missing' }}" - "Evince: {{ 'Installed' if 'evince' in ansible_facts.packages else 'Missing' }}"
- "Brave: {{ applications_brave_check.stdout if applications_brave_check.rc == 0 else 'Not installed' }}"
when: ansible_debug_output | default(false) | bool when: ansible_debug_output | default(false) | bool

View File

@ -1,4 +1,10 @@
--- ---
- name: Update apt cache (shared baseline)
ansible.builtin.apt:
update_cache: true
cache_valid_time: "{{ apt_cache_valid_time | default(3600) }}"
when: ansible_os_family == "Debian"
- name: Ensure Ansible remote_tmp directory exists with correct permissions - name: Ensure Ansible remote_tmp directory exists with correct permissions
ansible.builtin.file: ansible.builtin.file:
path: /root/.ansible/tmp path: /root/.ansible/tmp

View File

@ -29,4 +29,3 @@ base_os_user_ssh_public_key: "{{ appuser_ssh_public_key | default('') }}"
# If true, create passwordless sudo for base_os_user. # If true, create passwordless sudo for base_os_user.
base_os_passwordless_sudo: true base_os_passwordless_sudo: true

View File

@ -4,5 +4,3 @@
- name: Reload ufw - name: Reload ufw
ansible.builtin.command: ufw reload ansible.builtin.command: ufw reload
changed_when: false changed_when: false

View File

@ -38,28 +38,26 @@
when: base_os_passwordless_sudo | bool when: base_os_passwordless_sudo | bool
- name: Ensure UFW allows SSH - name: Ensure UFW allows SSH
ansible.builtin.ufw: community.general.ufw:
rule: allow rule: allow
port: "{{ base_os_allow_ssh_port }}" port: "{{ base_os_allow_ssh_port }}"
proto: tcp proto: tcp
- name: Ensure UFW allows backend port - name: Ensure UFW allows backend port
ansible.builtin.ufw: community.general.ufw:
rule: allow rule: allow
port: "{{ base_os_backend_port }}" port: "{{ base_os_backend_port }}"
proto: tcp proto: tcp
when: base_os_enable_backend | bool when: base_os_enable_backend | bool
- name: Ensure UFW allows frontend port - name: Ensure UFW allows frontend port
ansible.builtin.ufw: community.general.ufw:
rule: allow rule: allow
port: "{{ base_os_frontend_port }}" port: "{{ base_os_frontend_port }}"
proto: tcp proto: tcp
when: base_os_enable_frontend | bool when: base_os_enable_frontend | bool
- name: Enable UFW (deny incoming by default) - name: Enable UFW (deny incoming by default)
ansible.builtin.ufw: community.general.ufw:
state: enabled state: enabled
policy: deny policy: deny

View File

@ -17,4 +17,3 @@ r_packages:
- r-base - r-base
- r-base-dev - r-base-dev
- r-recommended - r-recommended

View File

@ -5,4 +5,3 @@
state: restarted state: restarted
daemon_reload: true daemon_reload: true
become: true become: true

View File

@ -1,4 +1,3 @@
--- ---
dependencies: dependencies:
- role: base - role: base

View File

@ -200,4 +200,3 @@
- name: Display R version - name: Display R version
ansible.builtin.debug: ansible.builtin.debug:
msg: "R version installed: {{ r_version.stdout_lines[0] if r_version.stdout_lines | length > 0 else 'Not checked in dry-run mode' }}" msg: "R version installed: {{ r_version.stdout_lines[0] if r_version.stdout_lines | length > 0 else 'Not checked in dry-run mode' }}"

View File

@ -23,42 +23,12 @@ Installs core development tools and utilities for software development. This rol
- **npm**: Node package manager (included with Node.js) - **npm**: Node package manager (included with Node.js)
- Configured from official NodeSource repository - Configured from official NodeSource repository
### Code Editors
- **Cursor IDE**: AI-powered code editor (AppImage)
- Installed to `/usr/local/bin/cursor`
- Latest stable version from cursor.com
## Variables ## Variables
### Core Settings ### Core Settings
| Variable | Default | Description | | Variable | Default | Description |
|----------|---------|-------------| |----------|---------|-------------|
| `install_cursor` | `true` | Install Cursor IDE | | `development_packages` | See defaults | Base packages installed by the role |
| `install_cursor_extensions` | `false` | Install Cursor extensions |
### Extension Groups
Enable specific extension groups based on your development needs:
| Variable | Default | Extensions Included |
|----------|---------|-------------------|
| `install_python` | `false` | Python, Pylance, Black, isort, Flake8, Ruff |
| `install_jupyter` | `false` | Jupyter notebooks, keybindings, renderers |
| `install_web` | `false` | Prettier, ESLint, Tailwind, Vue, Svelte |
| `install_playwright` | `false` | Playwright testing framework |
| `install_devops` | `false` | Go, Rust, YAML, Docker, Ansible |
| `install_r` | `false` | R language support and pack development |
| `install_docs` | `false` | Markdown tools and linter |
### Base Extensions (Always Installed)
When `install_cursor_extensions: true`, these are always installed:
- ErrorLens (better error highlighting)
- GitLens (Git supercharged)
- Git Graph (visualization)
- Code Spell Checker
- EditorConfig support
- Material Icon Theme
- GitHub Copilot (if licensed)
- Copilot Chat
## Dependencies ## Dependencies
- `base` role (for core utilities) - `base` role (for core utilities)
@ -72,61 +42,17 @@ When `install_cursor_extensions: true`, these are always installed:
- role: development - role: development
``` ```
### Python Data Science Machine ### Customize packages
```yaml ```yaml
- hosts: datascience - hosts: developers
roles: roles:
- role: development - role: development
vars: vars:
install_cursor_extensions: true development_packages:
install_python: true - git
install_jupyter: true - build-essential
install_docs: true - python3
``` - python3-pip
### Web Development Machine
```yaml
- hosts: webdevs
roles:
- role: development
vars:
install_cursor_extensions: true
install_web: true
install_playwright: true
install_docs: true
```
### Full Stack with DevOps
```yaml
- hosts: fullstack
roles:
- role: development
vars:
install_cursor_extensions: true
install_python: true
install_web: true
install_devops: true
install_docs: true
```
### Custom Extension List
You can also override the extension list completely in `host_vars`:
```yaml
# host_vars/myhost.yml
install_cursor_extensions: true
cursor_extensions:
- ms-python.python
- golang.go
- hashicorp.terraform
# ... your custom list
```
### With Cursor disabled
```yaml
- hosts: servers
roles:
- role: development
install_cursor: false
``` ```
## Usage ## Usage
@ -141,7 +67,6 @@ ansible-playbook playbooks/development.yml --limit dev01 --tags development
## Tags ## Tags
- `development`, `dev`: All development tasks - `development`, `dev`: All development tasks
- `cursor`, `ide`: Cursor IDE installation only
## Post-Installation ## Post-Installation
@ -151,7 +76,6 @@ git --version
node --version node --version
npm --version npm --version
python3 --version python3 --version
cursor --version
``` ```
### Node.js Usage ### Node.js Usage
@ -163,28 +87,17 @@ npm install -g <package>
node --version # Should show v22.x node --version # Should show v22.x
``` ```
### Cursor IDE Usage
```bash
# Launch Cursor (if using X11/Wayland)
cursor
# For root users, use the aliased version from .zshrc:
cursor # Automatically adds --no-sandbox flags
```
## Performance Notes ## Performance Notes
### Installation Time ### Installation Time
- **Base packages**: 1-2 minutes - **Base packages**: 1-2 minutes
- **Node.js**: 1-2 minutes - **Node.js**: 1-2 minutes
- **Cursor IDE**: 2-5 minutes (~200MB download) - **Total**: ~3-5 minutes
- **Total**: ~5-10 minutes
### Disk Space ### Disk Space
- **Node.js + npm**: ~100MB - **Node.js + npm**: ~100MB
- **Cursor IDE**: ~200MB
- **Build tools**: ~50MB - **Build tools**: ~50MB
- **Total**: ~350MB - **Total**: ~150MB
## Integration ## Integration
@ -217,17 +130,9 @@ apt-get remove nodejs
# Re-run playbook # Re-run playbook
``` ```
### Cursor Won't Launch
For root users, use the alias that adds required flags:
```bash
# Check alias in .zshrc
alias cursor="cursor --no-sandbox --disable-gpu-sandbox..."
```
## Notes ## Notes
- Node.js 22 is the current LTS version - Node.js 22 is the current LTS version
- NodeSource repository is configured for automatic updates - NodeSource repository is configured for automatic updates
- Cursor IDE is installed as AppImage for easy updates
- Build tools (gcc, make) are essential for npm native modules - Build tools (gcc, make) are essential for npm native modules
- Python 3 is included for development scripts - Python 3 is included for development scripts
- All installations are idempotent (safe to re-run) - All installations are idempotent (safe to re-run)
@ -239,11 +144,10 @@ alias cursor="cursor --no-sandbox --disable-gpu-sandbox..."
| Git | ✅ | - | | Git | ✅ | - |
| Node.js | ✅ | - | | Node.js | ✅ | - |
| Build Tools | ✅ | - | | Build Tools | ✅ | - |
| Cursor IDE | ✅ | - |
| Anaconda | ❌ | ✅ | | Anaconda | ❌ | ✅ |
| Jupyter | ❌ | ✅ | | Jupyter | ❌ | ✅ |
| R Language | ❌ | ✅ | | R Language | ❌ | ✅ |
| Install Time | ~10 min | ~30-60 min | | Install Time | ~10 min | ~30-60 min |
| Disk Space | ~350MB | ~3GB | | Disk Space | ~150MB | ~3GB |
**Recommendation**: Use `development` role for general coding. Add `datascience` role only when needed for data analysis/ML work. **Recommendation**: Use `development` role for general coding. Add `datascience` role only when needed for data analysis/ML work.

View File

@ -1,87 +1,9 @@
--- ---
# Development role defaults # Development role defaults (IDEs intentionally not managed here).
# Node.js is installed by default from NodeSource # Base packages for a lightweight dev foundation.
# No additional configuration needed development_packages:
- git
# Cursor IDE - lightweight IDE installation - build-essential
install_cursor: true - python3
install_cursor_extensions: false - python3-pip
# Base Cursor extensions (always good to have)
cursor_extensions_base:
- usernamehw.errorlens # Better error highlighting
- eamodio.gitlens # Git supercharged
- mhutchie.git-graph # Git graph visualization
- streetsidesoftware.code-spell-checker # Spell checker
- EditorConfig.EditorConfig # EditorConfig support
- PKief.material-icon-theme # Better file icons
# Python/Data Science extensions
cursor_extensions_python:
- ms-python.python # Python language support
- ms-python.vscode-pylance # Python IntelliSense
- ms-python.black-formatter # Black formatter
- ms-python.isort # Import sorter
- ms-python.flake8 # Linter
- charliermarsh.ruff # Fast Python linter
# Jupyter/Data Science extensions
cursor_extensions_jupyter:
- ms-toolsai.jupyter # Jupyter notebooks
- ms-toolsai.jupyter-keymap # Jupyter keybindings
- ms-toolsai.jupyter-renderers # Jupyter renderers
# Web Development extensions
cursor_extensions_web:
- esbenp.prettier-vscode # Code formatter
- dbaeumer.vscode-eslint # ESLint
- bradlc.vscode-tailwindcss # Tailwind CSS
- vue.volar # Vue 3
- svelte.svelte-vscode # Svelte
# Testing extensions
cursor_extensions_testing:
- ms-playwright.playwright # Playwright testing
# Systems/DevOps extensions
cursor_extensions_devops:
- golang.go # Go language
- rust-lang.rust-analyzer # Rust language
- redhat.vscode-yaml # YAML support
- ms-azuretools.vscode-docker # Docker support
- redhat.ansible # Ansible support
# R language extensions
cursor_extensions_r:
- REditorSupport.r # R language support
- Ikuyadeu.r-pack # R package development
# Markdown/Documentation extensions
cursor_extensions_docs:
- yzhang.markdown-all-in-one # Markdown tools
- DavidAnson.vscode-markdownlint # Markdown linter
# Default combined list (customize per host in host_vars)
cursor_extensions: >-
{{
[
cursor_extensions_base,
(cursor_extensions_python if install_python | default(false) else []),
(cursor_extensions_jupyter if install_jupyter | default(false) else []),
(cursor_extensions_web if install_web | default(false) else []),
(cursor_extensions_testing if install_playwright | default(false) else []),
(cursor_extensions_devops if install_devops | default(false) else []),
(cursor_extensions_r if install_r | default(false) else []),
(cursor_extensions_docs if install_docs | default(false) else [])
] | flatten
}}
# Feature flags to enable extension groups
install_python: false
install_jupyter: false
install_web: false
install_playwright: false
install_devops: false
install_r: false
install_docs: false

View File

@ -1,13 +1,7 @@
--- ---
- name: Install basic development packages - name: Install basic development packages
ansible.builtin.apt: ansible.builtin.apt:
name: name: "{{ development_packages }}"
# Development tools
- git
# Build tools
- build-essential
- python3
- python3-pip
state: present state: present
become: true become: true
@ -17,34 +11,41 @@
failed_when: false failed_when: false
changed_when: false changed_when: false
- name: Check if NodeSource repository exists and is correct - name: Check NodeSource repository file presence
ansible.builtin.shell: | ansible.builtin.stat:
if [ -f /etc/apt/sources.list.d/nodesource.list ]; then path: /etc/apt/sources.list.d/nodesource.list
if grep -q "deb \[signed-by=/etc/apt/keyrings/nodesource.gpg\] https://deb.nodesource.com/node_22.x nodistro main" /etc/apt/sources.list.d/nodesource.list; then register: nodesource_list_stat
echo "correct_config"
else
echo "wrong_config"
fi
else
echo "not_exists"
fi
register: nodesource_repo_check
failed_when: false
when: node_version_check.rc != 0 or not node_version_check.stdout.startswith('v22') when: node_version_check.rc != 0 or not node_version_check.stdout.startswith('v22')
- name: Check if NodeSource GPG key exists and is correct - name: Read NodeSource repository file
ansible.builtin.shell: | ansible.builtin.slurp:
if [ -f /etc/apt/keyrings/nodesource.gpg ]; then src: /etc/apt/sources.list.d/nodesource.list
if file /etc/apt/keyrings/nodesource.gpg | grep -q "PGP"; then register: nodesource_list_slurp
echo "correct_key" when:
else - node_version_check.rc != 0 or not node_version_check.stdout.startswith('v22')
echo "wrong_key" - nodesource_list_stat.stat.exists | default(false)
fi
else - name: Set NodeSource repository state
echo "not_exists" ansible.builtin.set_fact:
fi nodesource_repo_state: >-
register: nodesource_key_check {{
failed_when: false 'not_exists'
if not (nodesource_list_stat.stat.exists | default(false))
else (
'correct_config'
if (
(nodesource_list_slurp.content | b64decode)
is search('^deb \\[signed-by=/etc/apt/keyrings/nodesource\\.gpg\\] https://deb\\.nodesource\\.com/node_22\\.x nodistro main', multiline=True)
)
else 'wrong_config'
)
}}
when: node_version_check.rc != 0 or not node_version_check.stdout.startswith('v22')
- name: Check NodeSource GPG key presence
ansible.builtin.stat:
path: /etc/apt/keyrings/nodesource.gpg
register: nodesource_key_stat
when: node_version_check.rc != 0 or not node_version_check.stdout.startswith('v22') when: node_version_check.rc != 0 or not node_version_check.stdout.startswith('v22')
- name: Remove incorrect NodeSource repository - name: Remove incorrect NodeSource repository
@ -54,16 +55,7 @@
become: true become: true
when: when:
- node_version_check.rc != 0 or not node_version_check.stdout.startswith('v22') - node_version_check.rc != 0 or not node_version_check.stdout.startswith('v22')
- nodesource_repo_check.stdout == "wrong_config" - nodesource_repo_state == "wrong_config"
- name: Remove incorrect NodeSource key
ansible.builtin.file:
path: /etc/apt/keyrings/nodesource.gpg
state: absent
become: true
when:
- node_version_check.rc != 0 or not node_version_check.stdout.startswith('v22')
- nodesource_key_check.stdout == "wrong_key"
- name: Create keyrings directory - name: Create keyrings directory
ansible.builtin.file: ansible.builtin.file:
@ -73,7 +65,7 @@
become: true become: true
when: when:
- node_version_check.rc != 0 or not node_version_check.stdout.startswith('v22') - node_version_check.rc != 0 or not node_version_check.stdout.startswith('v22')
- nodesource_key_check.stdout in ["not_exists", "wrong_key"] - not (nodesource_key_stat.stat.exists | default(false))
- name: Add NodeSource GPG key only if needed - name: Add NodeSource GPG key only if needed
ansible.builtin.get_url: ansible.builtin.get_url:
@ -84,7 +76,7 @@
become: true become: true
when: when:
- node_version_check.rc != 0 or not node_version_check.stdout.startswith('v22') - node_version_check.rc != 0 or not node_version_check.stdout.startswith('v22')
- nodesource_key_check.stdout in ["not_exists", "wrong_key"] - not (nodesource_key_stat.stat.exists | default(false))
- name: Add NodeSource repository only if needed - name: Add NodeSource repository only if needed
ansible.builtin.apt_repository: ansible.builtin.apt_repository:
@ -94,7 +86,7 @@
become: true become: true
when: when:
- node_version_check.rc != 0 or not node_version_check.stdout.startswith('v22') - node_version_check.rc != 0 or not node_version_check.stdout.startswith('v22')
- nodesource_repo_check.stdout in ["not_exists", "wrong_config"] - nodesource_repo_state in ["not_exists", "wrong_config"]
- name: Install Node.js 22 from NodeSource - name: Install Node.js 22 from NodeSource
ansible.builtin.apt: ansible.builtin.apt:
@ -111,92 +103,3 @@
- name: Display Node.js version - name: Display Node.js version
ansible.builtin.debug: ansible.builtin.debug:
msg: "Node.js version installed: {{ final_node_version.stdout if final_node_version.stdout is defined else 'Not checked in dry-run mode' }}" msg: "Node.js version installed: {{ final_node_version.stdout if final_node_version.stdout is defined else 'Not checked in dry-run mode' }}"
# Cursor IDE installation (using AppImage)
# Downloads the latest version from cursor.com API
- name: Install Cursor IDE block
tags: ['cursor', 'ide']
block:
- name: Install libfuse2 dependency for AppImage
ansible.builtin.apt:
name: libfuse2
state: present
update_cache: false
become: true
when: ansible_os_family == "Debian"
- name: Check if Cursor is already installed at /usr/local/bin
ansible.builtin.stat:
path: /usr/local/bin/cursor
register: cursor_bin_check
- name: Get Cursor download URL from API and download AppImage
ansible.builtin.shell: |
DOWNLOAD_URL=$(curl -sL "https://www.cursor.com/api/download?platform=linux-x64&releaseTrack=stable" | grep -o '"downloadUrl":"[^"]*' | cut -d'"' -f4)
wget --timeout=60 --tries=3 -O /tmp/cursor.AppImage "$DOWNLOAD_URL"
args:
creates: /tmp/cursor.AppImage
when: not cursor_bin_check.stat.exists
register: cursor_download
retries: 2
delay: 5
until: cursor_download.rc == 0
- name: Make Cursor AppImage executable
ansible.builtin.file:
path: /tmp/cursor.AppImage
mode: '0755'
when:
- not cursor_bin_check.stat.exists
- cursor_download is defined
- cursor_download.rc is defined
- cursor_download.rc == 0
- name: Install Cursor to /usr/local/bin
ansible.builtin.copy:
src: /tmp/cursor.AppImage
dest: /usr/local/bin/cursor
mode: '0755'
remote_src: true
when:
- not cursor_bin_check.stat.exists
- cursor_download is defined
- cursor_download.rc is defined
- cursor_download.rc == 0
become: true
- name: Clean up Cursor download
ansible.builtin.file:
path: /tmp/cursor.AppImage
state: absent
when:
- cursor_download is defined
- cursor_download.rc is defined
- cursor_download.rc == 0
- name: Display Cursor installation status
ansible.builtin.debug:
msg: "{{ 'Cursor already installed' if cursor_bin_check.stat.exists else ('Cursor installed successfully' if (cursor_download is defined and cursor_download.rc is defined and cursor_download.rc == 0) else 'Cursor installation failed - download manually from cursor.com') }}"
# Cursor extensions installation
- name: Install Cursor extensions block
when:
- install_cursor | default(true) | bool
- install_cursor_extensions | default(false) | bool
- cursor_extensions is defined
- cursor_extensions | length > 0
tags: ['cursor', 'extensions']
block:
- name: Install Cursor extensions
ansible.builtin.shell: |
cursor --install-extension {{ item }} --force --user-data-dir={{ ansible_env.HOME }}/.cursor-root 2>/dev/null || true
loop: "{{ cursor_extensions }}"
register: cursor_ext_install
changed_when: "'successfully installed' in cursor_ext_install.stdout.lower()"
failed_when: false
become: true
become_user: "{{ ansible_user }}"
- name: Display Cursor extensions status
ansible.builtin.debug:
msg: "Installed {{ cursor_extensions | length }} Cursor extensions"

View File

@ -12,6 +12,7 @@
fi fi
register: docker_key_check register: docker_key_check
failed_when: false failed_when: false
changed_when: false
- name: Remove incorrect Docker GPG key - name: Remove incorrect Docker GPG key
ansible.builtin.file: ansible.builtin.file:
@ -43,4 +44,3 @@
path: /tmp/docker.gpg path: /tmp/docker.gpg
state: absent state: absent
when: docker_key_check.stdout in ["not_exists", "wrong_key"] when: docker_key_check.stdout in ["not_exists", "wrong_key"]

View File

@ -12,6 +12,7 @@
fi fi
register: docker_repo_check register: docker_repo_check
failed_when: false failed_when: false
changed_when: false
- name: Remove incorrect Docker repository - name: Remove incorrect Docker repository
ansible.builtin.file: ansible.builtin.file:
@ -26,4 +27,3 @@
state: present state: present
update_cache: true update_cache: true
when: docker_repo_check.stdout in ["not_exists", "wrong_config"] when: docker_repo_check.stdout in ["not_exists", "wrong_config"]

View File

@ -20,6 +20,7 @@
fi fi
register: docker_repo_check register: docker_repo_check
failed_when: false failed_when: false
changed_when: false
- name: Remove incorrect Docker repository - name: Remove incorrect Docker repository
ansible.builtin.file: ansible.builtin.file:
@ -34,4 +35,3 @@
state: present state: present
update_cache: true update_cache: true
when: docker_repo_check.stdout in ["not_exists", "wrong_config"] when: docker_repo_check.stdout in ["not_exists", "wrong_config"]

View File

@ -12,6 +12,7 @@
fi fi
register: docker_repo_check register: docker_repo_check
failed_when: false failed_when: false
changed_when: false
- name: Remove incorrect Docker repository - name: Remove incorrect Docker repository
ansible.builtin.file: ansible.builtin.file:
@ -26,4 +27,3 @@
state: present state: present
update_cache: true update_cache: true
when: docker_repo_check.stdout in ["not_exists", "wrong_config"] when: docker_repo_check.stdout in ["not_exists", "wrong_config"]

View File

@ -0,0 +1,5 @@
---
# Monitoring (desktop/workstation) role defaults
monitoring_desktop_install_btop: true
monitoring_desktop_install_wireshark_common: true
monitoring_desktop_create_scripts: true

View File

@ -0,0 +1,131 @@
---
- name: Install monitoring packages (desktop/workstation)
ansible.builtin.apt:
name:
# System monitoring
- htop
- iotop
- nethogs
- iftop
- ncdu
- dstat
# Network monitoring
- nmap
- tcpdump
# Performance monitoring
- atop
# Desktop extras
- "{{ 'wireshark-common' if monitoring_desktop_install_wireshark_common | bool else omit }}"
state: present
- name: Check if btop is available in apt
ansible.builtin.command: apt-cache policy btop
register: monitoring_desktop_btop_apt_check
changed_when: false
failed_when: false
when: monitoring_desktop_install_btop | bool
- name: Install btop from apt if available (Debian 12+)
ansible.builtin.apt:
name: btop
state: present
update_cache: false
when:
- monitoring_desktop_install_btop | bool
- monitoring_desktop_btop_apt_check.rc == 0
- "'Candidate:' in monitoring_desktop_btop_apt_check.stdout"
- "'(none)' not in monitoring_desktop_btop_apt_check.stdout"
failed_when: false
- name: Install btop from binary if apt not available
when:
- monitoring_desktop_install_btop | bool
- monitoring_desktop_btop_apt_check.rc != 0 or "(none)" in monitoring_desktop_btop_apt_check.stdout
block:
- name: Download btop binary
ansible.builtin.get_url:
url: https://github.com/aristocratos/btop/releases/latest/download/btop-x86_64-linux-musl.tbz
dest: /tmp/btop.tbz
mode: '0644'
failed_when: false
- name: Extract btop
ansible.builtin.unarchive:
src: /tmp/btop.tbz
dest: /tmp/
remote_src: true
failed_when: false
- name: Install btop binary
ansible.builtin.copy:
src: /tmp/btop/bin/btop
dest: /usr/local/bin/btop
mode: '0755'
remote_src: true
failed_when: false
- name: Clean up btop download
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /tmp/btop.tbz
- /tmp/btop
failed_when: false
- name: Create monitoring scripts directory
ansible.builtin.file:
path: /usr/local/bin/monitoring
state: directory
mode: '0755'
when: monitoring_desktop_create_scripts | bool
- name: Deploy system monitoring script
ansible.builtin.copy:
content: |
#!/bin/bash
# System monitoring dashboard
echo "=== System Overview ==="
echo "Hostname: $(hostname)"
echo "Uptime: $(uptime -p)"
echo "Load: $(uptime | awk -F'load average:' '{print $2}')"
echo ""
echo "=== Memory ==="
free -h
echo ""
echo "=== Disk Usage ==="
df -h / /home 2>/dev/null | grep -v tmpfs
echo ""
echo "=== Top Processes ==="
ps aux --sort=-%cpu | head -6
echo ""
echo "=== Network Connections ==="
ss -tuln | head -10
echo ""
if command -v tailscale >/dev/null; then
echo "=== Tailscale Status ==="
tailscale status --peers=false 2>/dev/null || echo "Not connected"
fi
dest: /usr/local/bin/monitoring/sysinfo
mode: '0755'
when: monitoring_desktop_create_scripts | bool
- name: Deploy network monitoring script
ansible.builtin.copy:
content: |
#!/bin/bash
# Network monitoring script
echo "=== Network Interface Status ==="
ip addr show | grep -E "(inet |state )" | grep -v 127.0.0.1
echo ""
echo "=== Route Table ==="
ip route show
echo ""
echo "=== DNS Configuration ==="
cat /etc/resolv.conf | grep nameserver
echo ""
echo "=== Open Ports ==="
ss -tuln | grep LISTEN | sort
dest: /usr/local/bin/monitoring/netinfo
mode: '0755'
when: monitoring_desktop_create_scripts | bool

View File

@ -0,0 +1,5 @@
---
# Monitoring (server) role defaults
monitoring_server_install_btop: true
monitoring_server_enable_sysstat: true
monitoring_server_create_scripts: true

View File

@ -0,0 +1,11 @@
---
- name: restart fail2ban
ansible.builtin.systemd:
name: fail2ban
state: restarted
- name: restart sysstat
ansible.builtin.systemd:
name: sysstat
state: restarted
enabled: true

View File

@ -0,0 +1,148 @@
---
- name: Install monitoring packages (server)
ansible.builtin.apt:
name:
# System monitoring
- htop
- iotop
- nethogs
- iftop
- ncdu
- dstat
# Log monitoring / security
- logwatch
- fail2ban
# Network monitoring
- nmap
- tcpdump
# Performance monitoring
- sysstat
- atop
state: present
- name: Check if btop is available in apt
ansible.builtin.command: apt-cache policy btop
register: monitoring_server_btop_apt_check
changed_when: false
failed_when: false
when: monitoring_server_install_btop | bool
- name: Install btop from apt if available (Debian 12+)
ansible.builtin.apt:
name: btop
state: present
update_cache: false
when:
- monitoring_server_install_btop | bool
- monitoring_server_btop_apt_check.rc == 0
- "'Candidate:' in monitoring_server_btop_apt_check.stdout"
- "'(none)' not in monitoring_server_btop_apt_check.stdout"
failed_when: false
- name: Install btop from binary if apt not available
when:
- monitoring_server_install_btop | bool
- monitoring_server_btop_apt_check.rc != 0 or "(none)" in monitoring_server_btop_apt_check.stdout
block:
- name: Download btop binary
ansible.builtin.get_url:
url: https://github.com/aristocratos/btop/releases/latest/download/btop-x86_64-linux-musl.tbz
dest: /tmp/btop.tbz
mode: '0644'
failed_when: false
- name: Extract btop
ansible.builtin.unarchive:
src: /tmp/btop.tbz
dest: /tmp/
remote_src: true
failed_when: false
- name: Install btop binary
ansible.builtin.copy:
src: /tmp/btop/bin/btop
dest: /usr/local/bin/btop
mode: '0755'
remote_src: true
failed_when: false
- name: Clean up btop download
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /tmp/btop.tbz
- /tmp/btop
failed_when: false
- name: Configure fail2ban
ansible.builtin.template:
src: jail.local.j2
dest: /etc/fail2ban/jail.local
mode: '0644'
notify: restart fail2ban
- name: Enable sysstat data collection
ansible.builtin.lineinfile:
path: /etc/default/sysstat
regexp: '^ENABLED='
line: 'ENABLED="true"'
notify: restart sysstat
when: monitoring_server_enable_sysstat | bool
- name: Create monitoring scripts directory
ansible.builtin.file:
path: /usr/local/bin/monitoring
state: directory
mode: '0755'
when: monitoring_server_create_scripts | bool
- name: Deploy system monitoring script
ansible.builtin.copy:
content: |
#!/bin/bash
# System monitoring dashboard
echo "=== System Overview ==="
echo "Hostname: $(hostname)"
echo "Uptime: $(uptime -p)"
echo "Load: $(uptime | awk -F'load average:' '{print $2}')"
echo ""
echo "=== Memory ==="
free -h
echo ""
echo "=== Disk Usage ==="
df -h / /home 2>/dev/null | grep -v tmpfs
echo ""
echo "=== Top Processes ==="
ps aux --sort=-%cpu | head -6
echo ""
echo "=== Network Connections ==="
ss -tuln | head -10
echo ""
if command -v tailscale >/dev/null; then
echo "=== Tailscale Status ==="
tailscale status --peers=false 2>/dev/null || echo "Not connected"
fi
dest: /usr/local/bin/monitoring/sysinfo
mode: '0755'
when: monitoring_server_create_scripts | bool
- name: Deploy network monitoring script
ansible.builtin.copy:
content: |
#!/bin/bash
# Network monitoring script
echo "=== Network Interface Status ==="
ip addr show | grep -E "(inet |state )" | grep -v 127.0.0.1
echo ""
echo "=== Route Table ==="
ip route show
echo ""
echo "=== DNS Configuration ==="
cat /etc/resolv.conf | grep nameserver
echo ""
echo "=== Open Ports ==="
ss -tuln | grep LISTEN | sort
dest: /usr/local/bin/monitoring/netinfo
mode: '0755'
when: monitoring_server_create_scripts | bool

View File

@ -0,0 +1,34 @@
[DEFAULT]
# Ban hosts for 1 hour
bantime = 3600
# Check for repeated failures for 10 minutes
findtime = 600
# Allow 3 failures before banning
maxretry = 3
# Email notifications (uncomment and configure if needed)
destemail = idobkin@gmail.com
sender = idobkin@gmail.com
action = %(action_mwl)s
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
[apache]
enabled = false
port = http,https
filter = apache-auth
logpath = /var/log/apache2/error.log
maxretry = 3
[nginx-http-auth]
enabled = false
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 3

View File

@ -78,5 +78,3 @@
Storage: {{ vm_storage }}:{{ vm_disk_size }} Storage: {{ vm_storage }}:{{ vm_disk_size }}
Network: {{ vm_network_bridge }} Network: {{ vm_network_bridge }}
Status: {{ vm_creation_result.msg | default('Created') }} Status: {{ vm_creation_result.msg | default('Created') }}

View File

@ -1,7 +1,10 @@
### Role: shell ### Role: shell
## Description ## Description
Configures modern shell environment with zsh, Oh My Zsh, Powerlevel10k theme, and useful plugins. Can be configured for multiple users on the same host. Configures shell in one of two modes:
- **minimal**: aliases-only (safe for servers; does not overwrite `~/.zshrc`)
- **full**: installs Oh My Zsh + Powerlevel10k + plugins and deploys a managed `~/.zshrc` (intended for developer machines)
## Requirements ## Requirements
- Ansible 2.9+ - Ansible 2.9+
@ -12,25 +15,23 @@ Configures modern shell environment with zsh, Oh My Zsh, Powerlevel10k theme, an
### Shell Environment ### Shell Environment
- **zsh**: Z shell - **zsh**: Z shell
- **Oh My Zsh**: Zsh configuration framework
- **Powerlevel10k**: Modern, feature-rich theme
- **tmux**: Terminal multiplexer - **tmux**: Terminal multiplexer
- **fzf**: Fuzzy finder - **fzf**: Fuzzy finder
- **oh-my-zsh / powerlevel10k**: only in `shell_mode=full`
### Zsh Plugins
- **zsh-syntax-highlighting**: Syntax highlighting for commands
- **zsh-autosuggestions**: Fish-like autosuggestions
### Configuration Files ### Configuration Files
- `.zshrc`: Custom zsh configuration with conda support - `~/.zsh_aliases_ansible`: Managed aliases file (sourced from `~/.zshrc`)
- `.p10k.zsh`: Powerlevel10k theme configuration - `~/.zshrc`: appended in `minimal` mode; fully managed in `full` mode
- `~/.p10k.zsh`: only in `shell_mode=full`
## Variables ## Variables
| Variable | Default | Description | | Variable | Default | Description |
|----------|---------|-------------| |----------|---------|-------------|
| `shell_users` | `[ansible_user]` | List of users to configure zsh for | | `shell_users` | `[ansible_user]` | List of users to configure zsh for |
| `zsh_plugins` | See defaults/main.yml | List of zsh plugins to install | | `shell_packages` | `['zsh','tmux','fzf']` | Packages installed by the role |
| `shell_mode` | `minimal` | `minimal` (aliases-only) or `full` (oh-my-zsh + p10k + managed zshrc) |
| `shell_set_default_shell` | `false` | If true, set login shell to `/usr/bin/zsh` |
## Dependencies ## Dependencies
None None
@ -44,6 +45,16 @@ None
- role: shell - role: shell
``` ```
### Full Zsh for developer machines
```yaml
- hosts: dev
roles:
- role: shell
vars:
shell_mode: full
shell_set_default_shell: true
```
### Configure Multiple Users ### Configure Multiple Users
```yaml ```yaml
- hosts: servers - hosts: servers
@ -90,19 +101,14 @@ make dev HOST=devGPU --tags shell
## Post-Installation ## Post-Installation
### For Each Configured User ### For Each Configured User
The shell configuration is immediately active. Users can: The aliases are immediately available in new shells. Users can:
1. **Customize Powerlevel10k**: 1. **Reload Configuration**:
```bash
p10k configure
```
2. **Reload Configuration**:
```bash ```bash
source ~/.zshrc source ~/.zshrc
``` ```
3. **View Available Aliases**: 2. **View Available Aliases**:
```bash ```bash
alias # List all aliases alias # List all aliases
``` ```
@ -110,27 +116,12 @@ The shell configuration is immediately active. Users can:
## Features ## Features
### Custom Aliases ### Custom Aliases
The `.zshrc` includes aliases for: The role installs a small, server-safe alias set in `~/.zsh_aliases_ansible`.
- Git operations (`gs`, `ga`, `gc`, etc.)
- Docker (`dps`, `dex`, `dlogs`)
- System (`ll`, `la`, `update`, `sysinfo`)
- Networking (`ports`, `myip`)
- Development (`serve`, `mkcd`)
- Data Science (`jup`, `conda-list`, `r-studio`)
### Conda Integration
If Anaconda is installed (via datascience role), conda is automatically initialized in zsh.
### Root User Support
Includes special aliases for root users (IDEs with `--no-sandbox` flags).
## Notes ## Notes
- Zsh is set as the default shell for all configured users
- Oh My Zsh is installed in each user's home directory
- The role is idempotent - safe to run multiple times - The role is idempotent - safe to run multiple times
- Existing `.zshrc` files are overwritten - Existing `.zshrc` files are **not** overwritten
- Users must log out and back in for shell changes to take effect
- The role skips users that don't exist on the system - The role skips users that don't exist on the system
## Troubleshooting ## Troubleshooting
@ -143,16 +134,13 @@ User username not found, skipping shell configuration
Solution: Ensure the user exists or remove from `shell_users` list. Solution: Ensure the user exists or remove from `shell_users` list.
### Oh My Zsh Installation Fails ### Oh My Zsh Installation Fails
Check user has a valid home directory and write permissions. If `shell_mode=full`, ensure the host has outbound internet access to fetch the installer and clone git repos.
### Powerlevel10k Not Loading ### Powerlevel10k Not Loading
Verify the theme is cloned: Only applies to `shell_mode=full`. Verify `~/.p10k.zsh` exists and the theme repo is present under `~/.oh-my-zsh/custom/themes/powerlevel10k`.
```bash
ls ~/.oh-my-zsh/custom/themes/powerlevel10k
```
### Conda Not Initialized ### Conda Not Initialized
The datascience role must be run to install Anaconda. The shell role only adds the initialization code to `.zshrc`. `shell_mode=full` includes a minimal “initialize conda if present” block. Conda installation is still handled by the `datascience` role.
## Integration ## Integration
@ -174,29 +162,21 @@ roles:
## Security Considerations ## Security Considerations
- The `.zshrc` is deployed from a template - review before deploying to production - Only an aliases file is managed; no remote scripts/themes are downloaded.
- Root aliases include `--no-sandbox` flags for IDEs (required for root but less secure)
- Consider limiting which users get shell configuration on production servers - Consider limiting which users get shell configuration on production servers
## Performance ## Performance
- Installation time: ~2-3 minutes per user - Installation time: seconds per user (copy + lineinfile)
- Disk space: ~10MB per user (Oh My Zsh + plugins + theme) - Disk space: negligible
- First shell launch: ~1-2 seconds (Powerlevel10k initialization)
- Subsequent launches: <0.5 seconds
## Customization ## Customization
### Adding Custom Aliases ### Adding Custom Aliases
Edit `roles/shell/files/.zshrc` and add your aliases. Edit `roles/shell/files/ansible_aliases.zsh` and re-run the role.
### Adding More Plugins ### Adding More Plugins
Update `roles/shell/defaults/main.yml`: Out of scope for this role (keep it fast/minimal).
```yaml
zsh_plugins:
- name: "my-plugin"
repo: "https://github.com/user/my-plugin.git"
```
### Custom Theme ### Custom Theme
Replace Powerlevel10k in tasks if desired, or users can run `p10k configure` to customize. Out of scope for this role.

View File

@ -15,7 +15,40 @@ shell_users:
# - ladmin # - ladmin
shell_additional_users: [] shell_additional_users: []
# Zsh plugins to install # Shell configuration mode:
# - minimal: aliases-only (safe for servers; does not overwrite ~/.zshrc)
# - full: install oh-my-zsh + powerlevel10k + plugins and deploy managed ~/.zshrc
shell_mode: minimal
# Packages installed for all modes.
shell_packages_common:
- zsh
- tmux
- fzf
# Extra packages for full mode.
shell_packages_full_extra:
- git
# Effective package list.
shell_packages: "{{ shell_packages_common + (shell_packages_full_extra if shell_mode == 'full' else []) }}"
# If true, change users' login shell to zsh.
shell_set_default_shell: false
# Path (relative to the user's home) for the managed aliases file.
shell_aliases_filename: ".zsh_aliases_ansible"
# Line added to ~/.zshrc to source the managed aliases file.
shell_zshrc_source_line: '[ -f "$HOME/{{ shell_aliases_filename }}" ] && source "$HOME/{{ shell_aliases_filename }}"'
# Full mode settings
shell_install_oh_my_zsh: "{{ shell_mode == 'full' }}"
shell_install_powerlevel10k: "{{ shell_mode == 'full' }}"
shell_install_plugins: "{{ shell_mode == 'full' }}"
shell_deploy_managed_zshrc: "{{ shell_mode == 'full' }}"
# Zsh plugins cloned into oh-my-zsh custom plugins (full mode only).
zsh_plugins: zsh_plugins:
- name: "zsh-syntax-highlighting" - name: "zsh-syntax-highlighting"
repo: "https://github.com/zsh-users/zsh-syntax-highlighting.git" repo: "https://github.com/zsh-users/zsh-syntax-highlighting.git"

File diff suppressed because it is too large Load Diff

View File

@ -1,224 +0,0 @@
typeset -g POWERLEVEL9K_INSTANT_PROMPT=quiet
# Enable Powerlevel10k instant prompt. Should stay close to the top of ~/.zshrc.
# Initialization code that may require console input (password prompts, [y/n]
# confirmations, etc.) must go above this block; everything else may go below.
if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then
source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh"
fi
# If you come from bash you might have to change your $PATH.
# export PATH=$HOME/bin:$HOME/.local/bin:/usr/local/bin:$PATH
# Path to your Oh My Zsh installation.
export ZSH="$HOME/.oh-my-zsh"
# Set name of the theme to load --- if set to "random", it will
# load a random theme each time Oh My Zsh is loaded, in which case,
# to know which specific one was loaded, run: echo $RANDOM_THEME
# See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes
ZSH_THEME="powerlevel10k/powerlevel10k"
# Set list of themes to pick from when loading at random
# Setting this variable when ZSH_THEME=random will cause zsh to load
# a theme from this variable instead of looking in $ZSH/themes/
# If set to an empty array, this variable will have no effect.
# ZSH_THEME_RANDOM_CANDIDATES=( "robbyrussell" "agnoster" )
# Uncomment the following line to use case-sensitive completion.
# CASE_SENSITIVE="true"
# Uncomment the following line to use hyphen-insensitive completion.
# Case-sensitive completion must be off. _ and - will be interchangeable.
# HYPHEN_INSENSITIVE="true"
# Uncomment one of the following lines to change the auto-update behavior
# zstyle ':omz:update' mode disabled # disable automatic updates
# zstyle ':omz:update' mode auto # update automatically without asking
# zstyle ':omz:update' mode reminder # just remind me to update when it's time
# Uncomment the following line to change how often to auto-update (in days).
# zstyle ':omz:update' frequency 13
# Uncomment the following line if pasting URLs and other text is messed up.
# DISABLE_MAGIC_FUNCTIONS="true"
# Uncomment the following line to disable colors in ls.
# DISABLE_LS_COLORS="true"
# Uncomment the following line to disable auto-setting terminal title.
# DISABLE_AUTO_TITLE="true"
# Uncomment the following line to enable command auto-correction.
# ENABLE_CORRECTION="true"
# Uncomment the following line to display red dots whilst waiting for completion.
# You can also set it to another string to have that shown instead of the default red dots.
# e.g. COMPLETION_WAITING_DOTS="%F{yellow}waiting...%f"
# Caution: this setting can cause issues with multiline prompts in zsh < 5.7.1 (see #5765)
# COMPLETION_WAITING_DOTS="true"
# Uncomment the following line if you want to disable marking untracked files
# under VCS as dirty. This makes repository status check for large repositories
# much, much faster.
# DISABLE_UNTRACKED_FILES_DIRTY="true"
# Uncomment the following line if you want to change the command execution time
# stamp shown in the history command output.
# You can set one of the optional three formats:
# "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd"
# or set a custom format using the strftime function format specifications,
# see 'man strftime' for details.
# HIST_STAMPS="mm/dd/yyyy"
# Would you like to use another custom folder than $ZSH/custom?
# ZSH_CUSTOM=/path/to/new-custom-folder
# Which plugins would you like to load?
# Standard plugins can be found in $ZSH/plugins/
# Custom plugins may be added to $ZSH_CUSTOM/plugins/
# Example format: plugins=(rails git textmate ruby lighthouse)
# Add wisely, as too many plugins slow down shell startup.
plugins=(git sudo z colored-man-pages fzf zsh-syntax-highlighting zsh-autosuggestions web-search copypath)
source $ZSH/oh-my-zsh.sh
# User configuration
# export MANPATH="/usr/local/man:$MANPATH"
# You may need to manually set your language environment
# export LANG=en_US.UTF-8
# Preferred editor for local and remote sessions
# if [[ -n $SSH_CONNECTION ]]; then
# export EDITOR='vim'
# else
# export EDITOR='nvim'
# fi
# Compilation flags
# export ARCHFLAGS="-arch $(uname -m)"
# Set personal aliases, overriding those provided by Oh My Zsh libs,
# plugins, and themes. Aliases can be placed here, though Oh My Zsh
# users are encouraged to define aliases within a top-level file in
# the $ZSH_CUSTOM folder, with .zsh extension. Examples:
# - $ZSH_CUSTOM/aliases.zsh
# - $ZSH_CUSTOM/macos.zsh
# For a full list of active aliases, run `alias`.
#
# Example aliases
# alias zshconfig="mate ~/.zshrc"
# alias ohmyzsh="mate ~/.oh-my-zsh"
# To customize prompt, run `p10k configure` or edit ~/.p10k.zsh.
[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh
[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh
alias reload="source ~/.zshrc && echo 'ZSH config reloaded from ~/.zshrc'"
alias editrc="nano ~/.zshrc"
alias c="clear"
alias ls="ls --color=auto"
alias ..="cd .."
alias ...="cd ../.."
alias ....="cd ../../.."
alias cd..="cd .."
alias h="cd ~"
alias dc="cd ~/Documents/code"
# System information
alias df="df -h" # disk usage human readable
alias du="du -h" # directory usage human readable
alias free="free -h" # memory usage human readable
alias sysinfo="/usr/local/bin/monitoring/sysinfo 2>/dev/null || echo 'sysinfo script not found'"
# Process management
alias ps="ps aux"
alias cpu="lscpu"
alias top="btop"
alias mem="free -m"
alias ports="ss -tulpn" # open ports
# Network information
alias myip="curl -s http://ipecho.net/plain; echo"
alias localip="ip route get 1.2.3.4 | awk '{print $7}'"
alias netinfo="/usr/local/bin/monitoring/netinfo 2>/dev/null || echo 'netinfo script not found'"
# Software inventory - show what's installed on this system
alias showapps="$HOME/.local/bin/showapps"
alias apps="showapps"
# Python
alias py="python3"
alias pip="pip3"
alias venv="python3 -m venv"
alias activate="source venv/bin/activate"
# Docker
alias d="docker"
alias dc="docker-compose"
alias dcu="docker-compose up -d"
alias dcd="docker-compose down"
alias dcb="docker-compose build"
alias dps="docker ps"
alias di="docker images"
# IDE - suppress root warnings
alias code="code --no-sandbox --user-data-dir=/root/.vscode-root"
alias cursor="cursor --no-sandbox --disable-gpu-sandbox --appimage-extract-and-run --user-data-dir=/root/.cursor-root"
# Date and time
alias now="date +'%Y-%m-%d %H:%M:%S'"
alias today="date +'%Y-%m-%d'"
# Package management (Debian/Ubuntu)
alias update="sudo apt update && sudo apt upgrade -y"
alias install="sudo apt install"
alias remove="sudo apt remove"
alias search="apt search"
# Permissions and ownership
alias chmox="chmod +x"
alias own="sudo chown -R $USER:$USER"
alias nfresh="rm -rf node_modules/ package-lock.json && npm install"
# SSH aliases for Ansible hosts
alias ssh-gitea="ssh gitea@10.0.30.169"
alias ssh-portainer="ssh ladmin@10.0.30.69"
alias ssh-homepage="ssh homepage@10.0.30.12"
alias ssh-vaultwarden="ssh root@100.100.19.11"
alias ssh-vaultwarden-fallback="ssh root@10.0.10.142"
alias ssh-dev01="ssh ladmin@10.0.30.105"
alias ssh-bottom="ssh beast@10.0.10.156"
alias ssh-debian="ssh user@10.0.10.206"
alias ssh-devGPU="ssh root@10.0.30.63"
alias ssh-ansible="ssh master@10.0.10.157"
alias ssh-tailscale="ssh ladmin@100.66.218.53"
alias ssh-caddy="ssh root@100.117.106.18"
alias ssh-caddy-fallback="ssh root@10.0.10.50"
alias ssh-jellyfin="ssh root@100.104.109.45"
alias ssh-jellyfin-fallback="ssh root@10.0.10.232"
alias ssh-listmonk="ssh root@100.73.190.115"
alias ssh-listmonk-fallback="ssh root@10.0.10.149"
alias ssh-desktop="ssh beast@100.117.34.106"
# >>> conda initialize >>>
# !! Contents within this block are managed by 'conda init' !!
if [ -f "$HOME/anaconda3/bin/conda" ]; then
__conda_setup="$('$HOME/anaconda3/bin/conda' 'shell.zsh' 'hook' 2> /dev/null)"
if [ $? -eq 0 ]; then
eval "$__conda_setup"
else
if [ -f "$HOME/anaconda3/etc/profile.d/conda.sh" ]; then
. "$HOME/anaconda3/etc/profile.d/conda.sh"
else
export PATH="$HOME/anaconda3/bin:$PATH"
fi
fi
unset __conda_setup
fi
# <<< conda initialize <<<

View File

@ -0,0 +1,38 @@
# Managed by Ansible (role: shell)
#
# This file is intentionally small and safe for servers.
# It is sourced from ~/.zshrc by the shell role.
alias reload="source ~/.zshrc && echo 'ZSH config reloaded from ~/.zshrc'"
alias editrc="nano ~/.zshrc"
alias c="clear"
alias ls="ls --color=auto"
alias ..="cd .."
alias ...="cd ../.."
alias ....="cd ../../.."
alias cd..="cd .."
alias h="cd ~"
# System information
alias df="df -h"
alias du="du -h"
alias free="free -h"
alias ports="ss -tulpn"
alias myip="curl -s http://ipecho.net/plain; echo"
# Package management (Debian/Ubuntu)
alias update="sudo apt update && sudo apt upgrade -y"
alias install="sudo apt install"
alias remove="sudo apt remove"
alias search="apt search"
# Docker (if installed)
alias d="docker"
alias dps="docker ps"
alias di="docker images"
# Python
alias py="python3"
alias pip="pip3"

View File

@ -0,0 +1,19 @@
# Minimal Powerlevel10k config (lean-ish). Users can customize later via `p10k configure`.
typeset -g POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(
dir
vcs
newline
prompt_char
)
typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(
status
command_execution_time
background_jobs
time
)
typeset -g POWERLEVEL9K_MODE='nerdfont-complete'
typeset -g POWERLEVEL9K_PROMPT_ON_NEWLINE=true

View File

@ -36,7 +36,7 @@ for tool in conda jupyter R; do
done done
echo -e "\n${YELLOW}Editors:${RESET}" echo -e "\n${YELLOW}Editors:${RESET}"
for tool in cursor code vim nvim nano; do for tool in vim nvim nano; do
command -v $tool >/dev/null 2>&1 && printf " ${GREEN}${RESET} %s\n" "$tool" command -v $tool >/dev/null 2>&1 && printf " ${GREEN}${RESET} %s\n" "$tool"
done done

View File

@ -0,0 +1,42 @@
typeset -g POWERLEVEL9K_INSTANT_PROMPT=quiet
# Enable Powerlevel10k instant prompt. Should stay close to the top of ~/.zshrc.
if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then
source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh"
fi
# Path to your Oh My Zsh installation.
export ZSH="$HOME/.oh-my-zsh"
# Theme
ZSH_THEME="powerlevel10k/powerlevel10k"
# Plugins
plugins=(git sudo z colored-man-pages fzf zsh-syntax-highlighting zsh-autosuggestions web-search copypath)
source $ZSH/oh-my-zsh.sh
# Powerlevel10k config
[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh
# fzf integration (if installed by user)
[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh
# Source Ansible-managed aliases (safe, small)
[ -f "$HOME/.zsh_aliases_ansible" ] && source "$HOME/.zsh_aliases_ansible"
# Conda auto-init (only if conda exists)
if [ -f "$HOME/anaconda3/bin/conda" ]; then
__conda_setup="$('$HOME/anaconda3/bin/conda' 'shell.zsh' 'hook' 2> /dev/null)"
if [ $? -eq 0 ]; then
eval "$__conda_setup"
else
if [ -f "$HOME/anaconda3/etc/profile.d/conda.sh" ]; then
. "$HOME/anaconda3/etc/profile.d/conda.sh"
else
export PATH="$HOME/anaconda3/bin:$PATH"
fi
fi
unset __conda_setup
fi

View File

@ -22,11 +22,40 @@
- name: Configure shell environment - name: Configure shell environment
when: user_info.ansible_facts.getent_passwd[current_user] is defined when: user_info.ansible_facts.getent_passwd[current_user] is defined
block: block:
- name: "Set zsh as default shell: {{ current_user }}" - name: "Optionally set zsh as default shell: {{ current_user }}"
ansible.builtin.user: ansible.builtin.user:
name: "{{ current_user }}" name: "{{ current_user }}"
shell: /usr/bin/zsh shell: /usr/bin/zsh
become: true become: true
when: shell_set_default_shell | bool
- name: "Install managed zsh aliases file: {{ current_user }}"
ansible.builtin.copy:
src: files/ansible_aliases.zsh
dest: "{{ user_home }}/{{ shell_aliases_filename }}"
owner: "{{ current_user }}"
group: "{{ current_user }}"
mode: "0644"
become: true
- name: "Ensure ~/.zshrc exists (do not overwrite): {{ current_user }}"
ansible.builtin.file:
path: "{{ user_home }}/.zshrc"
state: touch
owner: "{{ current_user }}"
group: "{{ current_user }}"
mode: "0644"
become: true
when: not (shell_deploy_managed_zshrc | bool)
- name: "Ensure ~/.zshrc sources managed aliases: {{ current_user }}"
ansible.builtin.lineinfile:
path: "{{ user_home }}/.zshrc"
line: "{{ shell_zshrc_source_line }}"
state: present
insertafter: EOF
become: true
when: not (shell_deploy_managed_zshrc | bool)
- name: "Install Oh My Zsh: {{ current_user }}" - name: "Install Oh My Zsh: {{ current_user }}"
become: true become: true
@ -34,6 +63,8 @@
ansible.builtin.shell: sh -c "$(wget https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh -O -)" "" --unattended ansible.builtin.shell: sh -c "$(wget https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh -O -)" "" --unattended
args: args:
creates: "{{ user_home }}/.oh-my-zsh" creates: "{{ user_home }}/.oh-my-zsh"
changed_when: false
when: shell_install_oh_my_zsh | bool
- name: "Clone Powerlevel10k theme: {{ current_user }}" - name: "Clone Powerlevel10k theme: {{ current_user }}"
ansible.builtin.git: ansible.builtin.git:
@ -44,6 +75,9 @@
update: false update: false
become: true become: true
become_user: "{{ current_user }}" become_user: "{{ current_user }}"
when:
- shell_install_powerlevel10k | bool
- shell_install_oh_my_zsh | bool
- name: "Install zsh plugins: {{ current_user }}" - name: "Install zsh plugins: {{ current_user }}"
ansible.builtin.git: ansible.builtin.git:
@ -55,24 +89,33 @@
become: true become: true
become_user: "{{ current_user }}" become_user: "{{ current_user }}"
loop: "{{ zsh_plugins }}" loop: "{{ zsh_plugins }}"
when:
- shell_install_plugins | bool
- shell_install_oh_my_zsh | bool
- name: "Deploy .zshrc: {{ current_user }}" - name: "Deploy managed .zshrc (full mode): {{ current_user }}"
ansible.builtin.copy: ansible.builtin.copy:
src: files/.zshrc src: files/zshrc.full
dest: "{{ user_home }}/.zshrc" dest: "{{ user_home }}/.zshrc"
owner: "{{ current_user }}" owner: "{{ current_user }}"
group: "{{ current_user }}" group: "{{ current_user }}"
mode: '0644' mode: "0644"
backup: true
become: true become: true
when: shell_deploy_managed_zshrc | bool
- name: "Deploy Powerlevel10k configuration: {{ current_user }}" - name: "Deploy Powerlevel10k config (full mode): {{ current_user }}"
ansible.builtin.copy: ansible.builtin.copy:
src: files/.p10k.zsh src: files/p10k.zsh
dest: "{{ user_home }}/.p10k.zsh" dest: "{{ user_home }}/.p10k.zsh"
owner: "{{ current_user }}" owner: "{{ current_user }}"
group: "{{ current_user }}" group: "{{ current_user }}"
mode: '0644' mode: "0644"
backup: true
become: true become: true
when:
- shell_install_powerlevel10k | bool
- shell_deploy_managed_zshrc | bool
- name: "Ensure .local/bin directory exists: {{ current_user }}" - name: "Ensure .local/bin directory exists: {{ current_user }}"
ansible.builtin.file: ansible.builtin.file:
@ -96,9 +139,8 @@
ansible.builtin.debug: ansible.builtin.debug:
msg: msg:
- "=== Shell Configuration Complete for {{ current_user }} ===" - "=== Shell Configuration Complete for {{ current_user }} ==="
- "NOTE: Zsh has been set as the default shell." - "Aliases installed: {{ user_home }}/{{ shell_aliases_filename }}"
- "To activate immediately, choose one of:" - >-
- " 1. Log out and back in (recommended)" Mode: {{ shell_mode | default('minimal') }} ({{ 'managed ~/.zshrc deployed' if (shell_deploy_managed_zshrc | bool) else 'aliases-only appended to ~/.zshrc' }})
- " 2. Run: exec zsh" - "If you want zsh as default login shell, set: shell_set_default_shell=true"
- " 3. Or simply run: zsh"
- "==========================================" - "=========================================="

View File

@ -1,11 +1,7 @@
--- ---
- name: Install shell packages - name: Install shell packages
ansible.builtin.apt: ansible.builtin.apt:
name: name: "{{ shell_packages }}"
- zsh
- tmux
- fzf
- git
state: present state: present
become: true become: true

View File

@ -18,6 +18,7 @@
fi fi
register: tailscale_key_check register: tailscale_key_check
failed_when: false failed_when: false
changed_when: false
when: tailscale_version_check.rc != 0 when: tailscale_version_check.rc != 0
- name: Check if Tailscale repository exists and is correct - name: Check if Tailscale repository exists and is correct
@ -33,6 +34,7 @@
fi fi
register: tailscale_repo_check register: tailscale_repo_check
failed_when: false failed_when: false
changed_when: false
when: tailscale_version_check.rc != 0 when: tailscale_version_check.rc != 0
- name: Remove incorrect Tailscale GPG key - name: Remove incorrect Tailscale GPG key

View File

@ -30,7 +30,6 @@ get_version() {
R) R --version 2>/dev/null | head -1 | awk '{print $3}' ;; R) R --version 2>/dev/null | head -1 | awk '{print $3}' ;;
yq) yq --version 2>/dev/null | awk '{print $NF}' ;; yq) yq --version 2>/dev/null | awk '{print $NF}' ;;
btop) btop --version 2>/dev/null | head -1 | awk '{print $3}' ;; btop) btop --version 2>/dev/null | head -1 | awk '{print $3}' ;;
cursor) echo "installed" ;;
*) echo "unknown" ;; *) echo "unknown" ;;
esac esac
fi fi
@ -65,16 +64,6 @@ else
echo -e " ${RED}${RESET} Jupyter service not running" echo -e " ${RED}${RESET} Jupyter service not running"
fi fi
# IDEs & Editors
echo -e "\n${YELLOW}IDEs & Editors:${RESET}"
for tool in cursor code; do
if cmd_exists $tool; then
printf " ${GREEN}${RESET} %-15s installed\n" "$tool"
else
printf " ${RED}${RESET} %-15s not installed\n" "$tool"
fi
done
# Container Platform # Container Platform
echo -e "\n${YELLOW}Container Platform:${RESET}" echo -e "\n${YELLOW}Container Platform:${RESET}"
version=$(get_version docker) version=$(get_version docker)

View File

@ -7,5 +7,3 @@
- name: Main site - name: Main site
import_playbook: playbooks/site.yml import_playbook: playbooks/site.yml