Compare commits
10 commits
f1d7b3059c
...
12001abac6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12001abac6 | ||
|
|
3fef7ba9e5 | ||
|
|
bdfde1644c | ||
|
|
0721ecc34c | ||
|
|
ad2c00f84a | ||
|
|
5c04b3405b | ||
|
|
3abb937a03 | ||
|
|
be9ac7f78b | ||
|
|
66a1aaad69 | ||
|
|
7731f98f15 |
26 changed files with 1542 additions and 0 deletions
9
.ansible-lint
Normal file
9
.ansible-lint
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
profile: production
|
||||||
|
skip_list:
|
||||||
|
- line-length
|
||||||
|
- no-changed-when
|
||||||
|
exclude_paths:
|
||||||
|
- .venv/
|
||||||
|
- backups/
|
||||||
|
- group_vars/mikrotik.vault.yml
|
||||||
9
.envrc
Normal file
9
.envrc
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Create .venv automatically if it doesn't exist
|
||||||
|
if [ ! -d .venv ]; then
|
||||||
|
python3 -m venv .venv
|
||||||
|
.venv/bin/python -m pip install -U pip setuptools wheel
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Activate the environment manually (avoids Python 3.13 deprecation warning)
|
||||||
|
export VIRTUAL_ENV=$PWD/.venv
|
||||||
|
PATH_add .venv/bin
|
||||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
.venv/
|
||||||
|
*.retry
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.DS_Store
|
||||||
12
.yamllint
Normal file
12
.yamllint
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
extends: default
|
||||||
|
rules:
|
||||||
|
line-length: disable
|
||||||
|
comments:
|
||||||
|
min-spaces-from-content: 1
|
||||||
|
truthy:
|
||||||
|
allowed-values: ["true", "false", "yes", "no"]
|
||||||
|
ignore: |
|
||||||
|
.venv/
|
||||||
|
backups/
|
||||||
|
*.vault.yml
|
||||||
49
CLAUDE.md
Normal file
49
CLAUDE.md
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# MakerFLOSS_Mikrotik
|
||||||
|
|
||||||
|
Ansible IaC for one **MikroTik CRS310-8G+2S+IN** switch (RouterOS 7) at the makerspace,
|
||||||
|
managed over SSH with `community.routeros`. Sibling project to AnsibleBaobabV4 (whose
|
||||||
|
conventions this repo copies); independent repo on `forgejo.makerfloss.eu`.
|
||||||
|
|
||||||
|
## Tech stack
|
||||||
|
|
||||||
|
- Ansible 10.x / ansible-core 2.17, `community.routeros` 3.x + `ansible.netcommon`
|
||||||
|
- Connection: `ansible.netcommon.network_cli`, `ansible_network_os: community.routeros.routeros`, SSH **key** auth
|
||||||
|
- Vault identity **`makerfloss`** (`~/.ansible/vault-keys/makerfloss.txt`)
|
||||||
|
- Lint: `ansible-lint` (profile: production), `yamllint`
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
- `inventories/prod/hosts.yml` — group `mikrotik`, host `crs310-maker`
|
||||||
|
- `group_vars/mikrotik.yml` — connection vars + `switch_*_enabled` flags
|
||||||
|
- `group_vars/mikrotik.vault.yml` — encrypted password (excluded from linters)
|
||||||
|
- `host_vars/crs310-maker.yml` — device facts, real addressing, VLAN/port map
|
||||||
|
- `roles/makerfloss.mikrotik_switch/` — one role, per-domain task files gated by flags
|
||||||
|
- `play_switch.yml` (day-2), `play_bootstrap.yml` / `play_backup.yml` (to implement)
|
||||||
|
- `docs/` — field guide, design spec, implementation plan
|
||||||
|
|
||||||
|
## Essential commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yamllint . && ansible-lint && ansible-playbook play_switch.yml --syntax-check
|
||||||
|
ansible-playbook play_switch.yml # day-2 (key auth)
|
||||||
|
ansible-playbook play_switch.yml --tags vlans # one domain
|
||||||
|
ansible-vault view group_vars/mikrotik.vault.yml # read a secret
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- **Idempotency:** RouterOS tasks use `community.routeros.command` with `:if [find]`
|
||||||
|
guards. Run every device-touching play **twice**; the second run must report no changes.
|
||||||
|
- **Lockout safety:** keep an independent recovery channel (serial/WinBox-MAC) when
|
||||||
|
touching mgmt/services/VLANs; enable `vlan-filtering` **last**.
|
||||||
|
- **All real values go in `host_vars`;** the role holds only mechanism + placeholders.
|
||||||
|
- **Secrets** go to the `makerfloss` vault, never plaintext. Encrypt with
|
||||||
|
`ansible-vault encrypt --encrypt-vault-id makerfloss <file>`.
|
||||||
|
- **New work:** branch first, implement, verify (lint + syntax + run-twice), then merge.
|
||||||
|
|
||||||
|
## Status / next
|
||||||
|
|
||||||
|
Bootstrap is done (user `sjat` + key + identity `crs310-maker`, RouterOS 7.19.6 pinned).
|
||||||
|
The per-domain task files are **stubs**; implement them per
|
||||||
|
`docs/superpowers/plans/2026-06-07-mikrotik-crs310-ansible.md` (Tasks 5–9), reading the
|
||||||
|
"carry-over notes" at the end of that plan first.
|
||||||
87
README.md
Normal file
87
README.md
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
# MakerFLOSS_Mikrotik
|
||||||
|
|
||||||
|
Infrastructure-as-Code for the makerspace's **MikroTik CRS310-8G+2S+IN** switch
|
||||||
|
(8× 2.5GbE + 2× SFP+ 10G, RouterOS 7). Configuration is managed declaratively with
|
||||||
|
Ansible over SSH using the `community.routeros` collection — identity, management
|
||||||
|
access, users/keys, VLAN switching, backups, and firmware — so the switch can be
|
||||||
|
rebuilt from this repo instead of by hand in WinBox.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
| Area | State |
|
||||||
|
|---|---|
|
||||||
|
| Repo scaffolding, role skeleton, vault | ✅ done |
|
||||||
|
| On-site device prep + **bootstrap** (named user + SSH key + identity) | ✅ done (2026-06-08) |
|
||||||
|
| Day-2 config: `identity` / `users` / `vlans` / `backup` / `firmware` tasks | ⏳ **stubs** — to implement (see `docs/superpowers/plans/`) |
|
||||||
|
|
||||||
|
The switch is reachable today by key auth as user `sjat`; the per-domain task files
|
||||||
|
still need their real RouterOS logic written and idempotency-tested.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
inventories/prod/hosts.yml # group `mikrotik` -> the switch host
|
||||||
|
group_vars/mikrotik.yml # connection vars (network_cli + community.routeros) + enable-flags
|
||||||
|
group_vars/mikrotik.vault.yml # encrypted admin/user password (makerfloss vault id)
|
||||||
|
host_vars/crs310-maker.yml # device facts + real addressing + VLAN/port map
|
||||||
|
roles/makerfloss.mikrotik_switch/ # the role: defaults + per-domain task files
|
||||||
|
play_switch.yml # day-2 run (key auth), applies all enabled domains
|
||||||
|
docs/makerspace-switch-fieldguide.md # on-site, printable prep checklist
|
||||||
|
docs/superpowers/specs|plans/ # design spec + implementation plan
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup (control node)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
direnv allow # or: python3 -m venv .venv && . .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
ansible-galaxy collection install -r requirements.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
**Vault:** secrets use a dedicated vault identity `makerfloss`, keyed by
|
||||||
|
`~/.ansible/vault-keys/makerfloss.txt` (referenced in `ansible.cfg`, kept outside the
|
||||||
|
repo). View a secret with `ansible-vault view group_vars/mikrotik.vault.yml`.
|
||||||
|
|
||||||
|
## Connectivity
|
||||||
|
|
||||||
|
The role connects with `ansible.netcommon.network_cli` + `ansible_network_os:
|
||||||
|
community.routeros.routeros`, authenticating with the operator SSH key
|
||||||
|
(`~/.ssh/id_ed25519`). Day-2 needs no password.
|
||||||
|
|
||||||
|
> **Bench note:** while the switch sits on an isolated bench reachable only through a
|
||||||
|
> jump host, Ansible's paramiko transport won't traverse `ProxyJump`. Run Ansible from a
|
||||||
|
> host on the switch's network, or forward the port:
|
||||||
|
> `ssh -J <jump> <user>@<jump-lan> -L 2222:192.168.88.1:22 -N` then set
|
||||||
|
> `ansible_host=127.0.0.1 ansible_port=2222`. In production (switch directly reachable)
|
||||||
|
> this is a non-issue.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Validate
|
||||||
|
yamllint . && ansible-lint && ansible-playbook play_switch.yml --syntax-check
|
||||||
|
|
||||||
|
# First contact on a fresh/reset device (password auth, one time)
|
||||||
|
ansible-playbook play_bootstrap.yml --ask-pass # (play to be implemented)
|
||||||
|
|
||||||
|
# Day-2 configuration (key auth, idempotent)
|
||||||
|
ansible-playbook play_switch.yml
|
||||||
|
ansible-playbook play_switch.yml --tags vlans # one domain
|
||||||
|
ansible-playbook play_switch.yml --limit crs310-maker
|
||||||
|
|
||||||
|
# Backup config into the repo
|
||||||
|
ansible-playbook play_backup.yml # (play to be implemented)
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ Lockout safety
|
||||||
|
|
||||||
|
When changing management, services, or VLAN/bridge settings, keep an independent
|
||||||
|
recovery channel open (serial console, or WinBox MAC-telnet) and enable
|
||||||
|
`vlan-filtering` **last**, after the management path is proven. RouterOS config tasks
|
||||||
|
use `:if [find]` guards for idempotency; **run every device-touching play twice** and
|
||||||
|
confirm the second run reports no changes.
|
||||||
|
|
||||||
|
## Preparing a switch on-site
|
||||||
|
|
||||||
|
See **`docs/makerspace-switch-fieldguide.md`** — a printable checklist for what to do
|
||||||
|
physically at the makerspace before Ansible takes over.
|
||||||
19
ansible.cfg
Normal file
19
ansible.cfg
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
[defaults]
|
||||||
|
inventory = inventories/prod/hosts.yml
|
||||||
|
roles_path = roles:~/.ansible/roles
|
||||||
|
collections_path = ~/.ansible/collections
|
||||||
|
host_key_checking = False
|
||||||
|
retry_files_enabled = False
|
||||||
|
interpreter_python = auto_silent
|
||||||
|
nocows = 1
|
||||||
|
timeout = 30
|
||||||
|
stdout_callback = yaml
|
||||||
|
bin_ansible_callbacks = True
|
||||||
|
vault_identity_list = makerfloss@~/.ansible/vault-keys/makerfloss.txt
|
||||||
|
|
||||||
|
[persistent_connection]
|
||||||
|
command_timeout = 60
|
||||||
|
connect_timeout = 60
|
||||||
|
|
||||||
|
[ssh_connection]
|
||||||
|
pipelining = True
|
||||||
165
docs/makerspace-switch-fieldguide.md
Normal file
165
docs/makerspace-switch-fieldguide.md
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
# Makerspace Field Guide — Preparing the CRS310 Switch
|
||||||
|
|
||||||
|
**Print this and bring it.** This is the exact, on-site procedure to get the MikroTik
|
||||||
|
**CRS310-8G+2S+IN** ready so Ansible can take over. Total time: ~30–45 min (most of it
|
||||||
|
the firmware upgrade). Work on a **bench/isolated network** — do **not** plug the switch
|
||||||
|
into the live makerspace network until VLANs are configured later (avoids loops and
|
||||||
|
DHCP/IP conflicts).
|
||||||
|
|
||||||
|
When you're done, you'll have: the switch on a known firmware, wiped to a clean slate,
|
||||||
|
reachable over SSH at a temporary IP, and a few facts written down for me to drop into
|
||||||
|
`host_vars`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bring with you
|
||||||
|
|
||||||
|
- [ ] The CRS310 + its PSU.
|
||||||
|
- [ ] A laptop with **WinBox** (download from mikrotik.com/download) — or just a browser for WebFig.
|
||||||
|
- [ ] One Ethernet cable (laptop ↔ a 2.5G port).
|
||||||
|
- [ ] Internet for the switch during the upgrade (a cable from an existing LAN/uplink, **temporarily**, with DHCP — unplug it again before the final steps).
|
||||||
|
- [ ] The **SFP+ module or DAC** for the 10G uplink (to fit physically; we don't cable the real uplink yet).
|
||||||
|
- [ ] This guide + something to write the recorded facts on (or a phone note).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1 — Power on and get in
|
||||||
|
|
||||||
|
1. Power the switch. Wait ~1 min for it to boot RouterOS.
|
||||||
|
2. Connect your laptop to **ether1** (a 2.5G port).
|
||||||
|
3. Open **WinBox → Neighbors tab**. The switch appears (by IP `192.168.88.1` and/or by MAC).
|
||||||
|
- **Tip:** click the **MAC address** (not the IP) to connect — this works even when the
|
||||||
|
switch has no IP, which matters in Step 5.
|
||||||
|
4. Log in: user `admin`, password **blank** (just press Enter). RouterOS 7 may ask you to
|
||||||
|
set a password — you can set a temporary one or skip; Ansible will set the real one later.
|
||||||
|
|
||||||
|
> No WinBox? Browse to `http://192.168.88.1` (WebFig) instead. The CLI commands below are
|
||||||
|
> typed in **WinBox/WebFig → New Terminal**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2 — Confirm it's running RouterOS (not SwOS)
|
||||||
|
|
||||||
|
The CRS310 can dual-boot SwOS, but we need **RouterOS** for VLAN filtering + Ansible.
|
||||||
|
|
||||||
|
- In terminal: `/system/routerboard/print`
|
||||||
|
- It should report RouterOS. If the device booted **SwOS** (different, simpler web UI),
|
||||||
|
switch the boot OS: in SwOS go to the **System** page and set boot to RouterOS, or use
|
||||||
|
the reset/boot-OS toggle, then reboot. (You want the full RouterOS interface.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3 — Upgrade and pin the firmware
|
||||||
|
|
||||||
|
This needs internet for the switch. Plug a DHCP uplink into **ether8** temporarily.
|
||||||
|
|
||||||
|
1. Give the switch internet briefly: it should pull a DHCP lease on the uplink port, or in
|
||||||
|
terminal: `/ip/dhcp-client/add interface=ether8 disabled=no`
|
||||||
|
2. Update RouterOS:
|
||||||
|
```
|
||||||
|
/system/package/update/set channel=stable
|
||||||
|
/system/package/update/check-for-updates
|
||||||
|
/system/package/update/download
|
||||||
|
/system/reboot
|
||||||
|
```
|
||||||
|
(Or WinBox: **System → Packages → Check For Updates → Download & Install**.)
|
||||||
|
3. After reboot, upgrade the bootloader (RouterBOOT) to match:
|
||||||
|
```
|
||||||
|
/system/routerboard/upgrade
|
||||||
|
/system/reboot
|
||||||
|
```
|
||||||
|
4. **Write down the final version:** `/system/resource/print` → the `version` line.
|
||||||
|
➜ **Record as `RouterOS version: ______`** (this becomes `switch_firmware_target`).
|
||||||
|
5. **Unplug the temporary internet uplink** and remove the DHCP client:
|
||||||
|
`/ip/dhcp-client/remove [find]`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4 — Record the device facts
|
||||||
|
|
||||||
|
Run `/system/routerboard/print` and `/system/resource/print` and write down:
|
||||||
|
|
||||||
|
- [ ] **Model:** ____________________ (should be CRS310-8G+2S+IN)
|
||||||
|
- [ ] **Serial:** ____________________ (also on the sticker underneath)
|
||||||
|
- [ ] **Base MAC:** ____________________
|
||||||
|
- [ ] **RouterOS version:** ____________________ (from Step 3.4)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5 — Wipe to a clean slate (no default config)
|
||||||
|
|
||||||
|
This makes Ansible the single owner of the whole configuration.
|
||||||
|
|
||||||
|
1. In terminal:
|
||||||
|
```
|
||||||
|
/system/reset-configuration no-defaults=yes skip-backup=yes
|
||||||
|
```
|
||||||
|
(Or WinBox: **System → Reset Configuration** → tick **No Default Configuration** and
|
||||||
|
**Do Not Backup** → **Reset**.)
|
||||||
|
2. The switch reboots. It now has **no IP and no services** — WinBox-by-IP won't find it.
|
||||||
|
3. Reconnect using **WinBox → Neighbors → click the MAC address** (this is why we use MAC).
|
||||||
|
Log in as `admin` with a **blank** password.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 6 — Give it a temporary IP + enable SSH (so Ansible can reach it)
|
||||||
|
|
||||||
|
In the terminal (laptop still on **ether1**):
|
||||||
|
|
||||||
|
```
|
||||||
|
/ip/address/add address=192.168.88.1/24 interface=ether1
|
||||||
|
/ip/service/enable ssh
|
||||||
|
/ip/service/print
|
||||||
|
```
|
||||||
|
|
||||||
|
Then on your laptop, set a static IP `192.168.88.2` / `255.255.255.0` and confirm SSH:
|
||||||
|
|
||||||
|
```
|
||||||
|
ssh admin@192.168.88.1
|
||||||
|
```
|
||||||
|
|
||||||
|
If that logs in, **you're done** — leave the switch powered and on the bench.
|
||||||
|
|
||||||
|
> ⚠️ Keep a **WinBox MAC session** open as your lifeline whenever you change network
|
||||||
|
> settings. If you ever lock yourself out, MAC-telnet/WinBox-by-MAC still works; a full
|
||||||
|
> **Netinstall** (mikrotik.com/download) is the last-resort recovery.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 7 — Decide the real addressing (write it down for me)
|
||||||
|
|
||||||
|
I need these to fill in `host_vars/crs310-maker.yml`. Decide with whatever the makerspace
|
||||||
|
network plan is (or we can finalize together):
|
||||||
|
|
||||||
|
- [ ] **Management IP + mask** (real, not the temp one): ____________________
|
||||||
|
- [ ] **Management VLAN ID:** ____________________
|
||||||
|
- [ ] **Default gateway:** ____________________
|
||||||
|
- [ ] **Upstream uplink port** (which SFP+ / port goes to the OPNsense/router): ____________________
|
||||||
|
- [ ] **DNS / NTP server IP** (usually the gateway): ____________________
|
||||||
|
|
||||||
|
(If the makerspace VLAN plan isn't settled yet, that's fine — we ship a placeholder and
|
||||||
|
fill these in later. The switch just needs to be reachable per Step 6.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 8 — Physical finish
|
||||||
|
|
||||||
|
- [ ] Fit the **SFP+ module/DAC** into `sfp-sfpplus1` (don't cable the live uplink yet).
|
||||||
|
- [ ] Mount/label the switch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When you're back
|
||||||
|
|
||||||
|
Bring me:
|
||||||
|
1. The recorded facts (Step 4) and addressing decisions (Step 7).
|
||||||
|
2. Confirmation that `ssh admin@192.168.88.1` (or your temp IP) works.
|
||||||
|
|
||||||
|
Then I'll: create the empty `MakerFLOSS_Mikrotik` repo on `forgejo.makerfloss.eu`, drop
|
||||||
|
your facts into `host_vars`, and run **`play_bootstrap.yml`** — which creates your named
|
||||||
|
admin user, imports your SSH key, and hands the switch over to Ansible. After that,
|
||||||
|
`play_switch.yml` configures identity, services, VLANs, and backups.
|
||||||
|
|
||||||
|
> **Do not connect the switch to the live makerspace network** until VLANs are configured
|
||||||
|
> (Task 7 in the implementation plan) — an unconfigured switch on the live net can cause
|
||||||
|
> loops or hand out the wrong VLAN.
|
||||||
946
docs/superpowers/plans/2026-06-07-mikrotik-crs310-ansible.md
Normal file
946
docs/superpowers/plans/2026-06-07-mikrotik-crs310-ansible.md
Normal file
|
|
@ -0,0 +1,946 @@
|
||||||
|
# MakerFLOSS_Mikrotik — CRS310 Ansible Management — Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Build a fresh Ansible repo that configures the makerspace MikroTik CRS310-8G+2S+IN switch over SSH (identity, services, users/keys, VLANs, backups, firmware), idempotently and version-controlled.
|
||||||
|
|
||||||
|
**Architecture:** One role `makerfloss.mikrotik_switch` with per-domain task files gated by enable-flags, driven by `community.routeros` over `network_cli`. Real values live in `host_vars`; connection vars in `group_vars/mikrotik.yml`; mechanism + placeholders in role defaults. A one-time `play_bootstrap.yml` (password auth) imports the operator SSH key; day-2 `play_switch.yml` runs key-only.
|
||||||
|
|
||||||
|
**Tech Stack:** Ansible 10.x / ansible-core 2.17, `community.routeros` (+ `ansible.netcommon`), RouterOS 7.x, SSH key auth, ansible-vault (`makerfloss` identity), ansible-lint + yamllint.
|
||||||
|
|
||||||
|
**Working directory:** `~/Projects/MakerFLOSS_Mikrotik` (git repo already initialised, `main`, with the design spec committed).
|
||||||
|
|
||||||
|
**Reference spec:** `docs/superpowers/specs/2026-06-07-mikrotik-crs310-ansible-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How verification works in this plan
|
||||||
|
|
||||||
|
There is no unit-test framework for RouterOS config. "Tests" in this plan are:
|
||||||
|
- **Static:** `yamllint .`, `ansible-lint`, `ansible-playbook --syntax-check`.
|
||||||
|
- **Connectivity:** `ansible -m community.routeros.command -a "commands='/system/resource/print'" <switch>`.
|
||||||
|
- **Idempotency:** run the play twice; the **second run must report `changed=0`** for that domain.
|
||||||
|
|
||||||
|
Device-dependent tasks (Phase 4 onward) require the switch to be prepared and reachable
|
||||||
|
(Phase 0). Until then, only static checks pass — that is expected and fine.
|
||||||
|
|
||||||
|
> ⚠️ **Lockout safety:** When applying VLAN/bridge or service changes, keep an independent
|
||||||
|
> recovery channel open (WinBox MAC-telnet, or serial console) so a mistake in management
|
||||||
|
> reachability doesn't strand the switch. Enable `vlan-filtering` **last**, after the
|
||||||
|
> management path is proven.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 0: Prerequisites (manual, out-of-band — do before Phase 4)
|
||||||
|
|
||||||
|
These are not code tasks; they gate the device-dependent phases.
|
||||||
|
|
||||||
|
- [ ] **0.1 Create the empty repo** `MakerFLOSS_Mikrotik` on `forgejo.makerfloss.eu` (no README/license, so the first push isn't rejected).
|
||||||
|
- [ ] **0.2 Confirm boot OS is RouterOS** (not SwOS) on the CRS310. Switch in RouterBOOT if needed.
|
||||||
|
- [ ] **0.3 Upgrade + pin firmware:** update RouterOS and RouterBOOT to a known-good stable (e.g. latest 7.x stable). Record the exact version string for `firmware_target` (used in Task 9).
|
||||||
|
- [ ] **0.4 Factory-reset to NO default configuration** (`/system/reset-configuration no-defaults=yes` or Netinstall). Ansible will own the whole config.
|
||||||
|
- [ ] **0.5 First contact:** connect a laptop, reach the switch (default has no IP after no-defaults reset — assign a temporary IP via WinBox MAC session, e.g. `/ip/address/add address=192.168.88.1/24 interface=ether1`, and `/ip/service/enable ssh`). Confirm `ssh admin@<ip>` works.
|
||||||
|
- [ ] **0.6 Record identity facts** into a scratch note for Task 3 host_vars: serial, base MAC, model, RouterOS version, the temporary mgmt IP/port.
|
||||||
|
- [ ] **0.7 Physical:** fit the SFP+ module/DAC for the 10G uplink; confirm PSU/mounting.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Repo scaffolding (no device required)
|
||||||
|
|
||||||
|
### Task 1: direnv, ansible.cfg, lint configs, requirements
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `.envrc`, `ansible.cfg`, `.ansible-lint`, `.yamllint`, `requirements.txt`, `requirements.yml`, `.gitignore`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create `.gitignore`**
|
||||||
|
|
||||||
|
```gitignore
|
||||||
|
.venv/
|
||||||
|
*.retry
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.DS_Store
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Create `.envrc`** (verbatim from AnsibleBaobabV4)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create .venv automatically if it doesn't exist
|
||||||
|
if [ ! -d .venv ]; then
|
||||||
|
python3 -m venv .venv
|
||||||
|
.venv/bin/python -m pip install -U pip setuptools wheel
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Activate the environment manually (avoids Python 3.13 deprecation warning)
|
||||||
|
export VIRTUAL_ENV=$PWD/.venv
|
||||||
|
PATH_add .venv/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Create `ansible.cfg`**
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[defaults]
|
||||||
|
inventory = inventories/prod/hosts.yml
|
||||||
|
roles_path = roles:~/.ansible/roles
|
||||||
|
collections_path = ~/.ansible/collections
|
||||||
|
host_key_checking = False
|
||||||
|
retry_files_enabled = False
|
||||||
|
interpreter_python = auto_silent
|
||||||
|
nocows = 1
|
||||||
|
timeout = 30
|
||||||
|
stdout_callback = yaml
|
||||||
|
bin_ansible_callbacks = True
|
||||||
|
vault_identity_list = makerfloss@~/.ansible/vault-keys/makerfloss.txt
|
||||||
|
|
||||||
|
[persistent_connection]
|
||||||
|
command_timeout = 60
|
||||||
|
connect_timeout = 60
|
||||||
|
|
||||||
|
[ssh_connection]
|
||||||
|
pipelining = True
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Create `requirements.txt`**
|
||||||
|
|
||||||
|
```text
|
||||||
|
# Core Ansible
|
||||||
|
ansible==10.3.0
|
||||||
|
|
||||||
|
# Linting & validation
|
||||||
|
ansible-lint==24.7.0
|
||||||
|
yamllint==1.35.1
|
||||||
|
|
||||||
|
# Network connection plugins / SCP for SSH key transfer to RouterOS
|
||||||
|
paramiko>=3.4.0
|
||||||
|
scp>=0.15.0
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Create `requirements.yml`**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
collections:
|
||||||
|
- name: community.routeros
|
||||||
|
version: ">=3.0.0,<4.0.0"
|
||||||
|
- name: ansible.netcommon
|
||||||
|
version: ">=6.0.0,<8.0.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Create `.ansible-lint`**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
profile: production
|
||||||
|
skip_list:
|
||||||
|
- line-length
|
||||||
|
- no-changed-when
|
||||||
|
exclude_paths:
|
||||||
|
- .venv/
|
||||||
|
- backups/
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Create `.yamllint`**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
extends: default
|
||||||
|
rules:
|
||||||
|
line-length: disable
|
||||||
|
comments:
|
||||||
|
min-spaces-from-content: 1
|
||||||
|
truthy:
|
||||||
|
allowed-values: ["true", "false", "yes", "no"]
|
||||||
|
ignore: |
|
||||||
|
.venv/
|
||||||
|
backups/
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 8: Bootstrap the venv and install**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
cd ~/Projects/MakerFLOSS_Mikrotik
|
||||||
|
direnv allow 2>/dev/null || python3 -m venv .venv
|
||||||
|
. .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
ansible-galaxy collection install -r requirements.yml
|
||||||
|
```
|
||||||
|
Expected: ansible, ansible-lint, yamllint installed; `community.routeros` + `ansible.netcommon` installed.
|
||||||
|
|
||||||
|
- [ ] **Step 9: Verify yamllint passes on the config files**
|
||||||
|
|
||||||
|
Run: `yamllint .`
|
||||||
|
Expected: no errors.
|
||||||
|
|
||||||
|
- [ ] **Step 10: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .gitignore .envrc ansible.cfg requirements.txt requirements.yml .ansible-lint .yamllint
|
||||||
|
git commit -m "chore: repo scaffolding (direnv, ansible.cfg, lint, requirements)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Vault identity + inventory + connection group_vars
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `~/.ansible/vault-keys/makerfloss.txt` (outside repo), `inventories/prod/hosts.yml`, `group_vars/mikrotik.yml`, `group_vars/all.yml`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the vault key (outside the repo)**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.ansible/vault-keys
|
||||||
|
( umask 077; openssl rand -base64 48 > ~/.ansible/vault-keys/makerfloss.txt )
|
||||||
|
chmod 600 ~/.ansible/vault-keys/makerfloss.txt
|
||||||
|
```
|
||||||
|
Expected: a 600-perm key file exists. (This is the `makerfloss` identity from `ansible.cfg`.)
|
||||||
|
|
||||||
|
- [ ] **Step 2: Create `inventories/prod/hosts.yml`**
|
||||||
|
|
||||||
|
> Replace `crs310-maker` and the `ansible_host` IP with the real values from Phase 0.6.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
all:
|
||||||
|
children:
|
||||||
|
mikrotik:
|
||||||
|
hosts:
|
||||||
|
crs310-maker:
|
||||||
|
ansible_host: 192.168.88.1 # temp mgmt IP until Task 4 sets the real one
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Create `group_vars/mikrotik.yml`** (connection/platform vars)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
ansible_connection: ansible.netcommon.network_cli
|
||||||
|
ansible_network_os: community.routeros.routeros
|
||||||
|
ansible_user: admin
|
||||||
|
ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
|
||||||
|
|
||||||
|
# Domain enable-flags (day-2 play). Override per-host if needed.
|
||||||
|
switch_identity_enabled: true
|
||||||
|
switch_users_enabled: true
|
||||||
|
switch_vlans_enabled: true
|
||||||
|
switch_backup_enabled: true
|
||||||
|
switch_firmware_enabled: false # opt-in; upgrades are disruptive
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Create `group_vars/all.yml`** (placeholder for shared, non-secret defaults)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
# Shared non-secret defaults across all hosts go here.
|
||||||
|
# Secrets live in the vault (see host_vars / a vaulted file), not in this file.
|
||||||
|
org_name: "MakerFLOSS"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Verify inventory parses**
|
||||||
|
|
||||||
|
Run: `ansible-inventory --graph`
|
||||||
|
Expected: shows `@mikrotik` → `crs310-maker`.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add inventories group_vars
|
||||||
|
git commit -m "feat: inventory, connection group_vars, makerfloss vault identity"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Role skeleton
|
||||||
|
|
||||||
|
### Task 3: Role skeleton + host_vars + meta
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `roles/makerfloss.mikrotik_switch/defaults/main.yml`
|
||||||
|
- Create: `roles/makerfloss.mikrotik_switch/meta/main.yml`
|
||||||
|
- Create: `roles/makerfloss.mikrotik_switch/tasks/main.yml`
|
||||||
|
- Create: `host_vars/crs310-maker.yml`
|
||||||
|
- Create: `play_switch.yml`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create `roles/makerfloss.mikrotik_switch/meta/main.yml`**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
galaxy_info:
|
||||||
|
role_name: mikrotik_switch
|
||||||
|
namespace: makerfloss
|
||||||
|
author: sjat
|
||||||
|
description: Configure a MikroTik RouterOS switch (CRS310) over SSH.
|
||||||
|
license: MIT
|
||||||
|
min_ansible_version: "2.17"
|
||||||
|
platforms: []
|
||||||
|
dependencies: []
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Create `roles/makerfloss.mikrotik_switch/defaults/main.yml`** (mechanism + PLACEHOLDER topology)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
# ----- Identity / management -----
|
||||||
|
switch_identity_name: "{{ inventory_hostname }}"
|
||||||
|
switch_mgmt_vlan_id: 99
|
||||||
|
switch_mgmt_address: "192.168.88.1/24" # PLACEHOLDER — override in host_vars
|
||||||
|
switch_mgmt_gateway: "192.168.88.254" # PLACEHOLDER — override in host_vars
|
||||||
|
switch_dns_servers: "192.168.88.254"
|
||||||
|
switch_ntp_servers: "192.168.88.254"
|
||||||
|
|
||||||
|
# Services to disable for hardening (winbox kept on by default for recovery)
|
||||||
|
switch_disabled_services:
|
||||||
|
- telnet
|
||||||
|
- ftp
|
||||||
|
- www
|
||||||
|
- www-ssl
|
||||||
|
- api
|
||||||
|
- api-ssl
|
||||||
|
switch_ssh_port: 22
|
||||||
|
|
||||||
|
# ----- Users -----
|
||||||
|
switch_admin_user: "sjat"
|
||||||
|
switch_admin_group: "full"
|
||||||
|
switch_admin_ssh_pubkey_file: "~/.ssh/id_ed25519.pub"
|
||||||
|
switch_disable_default_admin: true
|
||||||
|
|
||||||
|
# ----- VLAN / bridge / ports (PLACEHOLDER example) -----
|
||||||
|
# Real topology is defined in host_vars/<switch>.yml.
|
||||||
|
switch_bridge_name: "bridge"
|
||||||
|
switch_vlans:
|
||||||
|
- { id: 99, name: "mgmt" }
|
||||||
|
- { id: 10, name: "members" }
|
||||||
|
switch_bridge_ports:
|
||||||
|
# ether1..ether8 = 2.5GbE access ports; sfp-sfpplus1/2 = 10G uplinks
|
||||||
|
- { interface: "ether1", pvid: 10, mode: access }
|
||||||
|
- { interface: "sfp-sfpplus1", pvid: 1, mode: trunk, tagged_vlans: [99, 10] }
|
||||||
|
|
||||||
|
# ----- Firmware -----
|
||||||
|
switch_firmware_target: "" # set in host_vars when opting into upgrades
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Create `roles/makerfloss.mikrotik_switch/tasks/main.yml`** (domain dispatch)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
- name: Identity, management and services
|
||||||
|
ansible.builtin.import_tasks: identity.yml
|
||||||
|
when: switch_identity_enabled | bool
|
||||||
|
tags: [identity]
|
||||||
|
|
||||||
|
- name: Users and SSH keys
|
||||||
|
ansible.builtin.import_tasks: users.yml
|
||||||
|
when: switch_users_enabled | bool
|
||||||
|
tags: [users]
|
||||||
|
|
||||||
|
- name: VLANs, bridge and ports
|
||||||
|
ansible.builtin.import_tasks: vlans.yml
|
||||||
|
when: switch_vlans_enabled | bool
|
||||||
|
tags: [vlans]
|
||||||
|
|
||||||
|
- name: Backup configuration
|
||||||
|
ansible.builtin.import_tasks: backup.yml
|
||||||
|
when: switch_backup_enabled | bool
|
||||||
|
tags: [backup]
|
||||||
|
|
||||||
|
- name: Firmware upgrade
|
||||||
|
ansible.builtin.import_tasks: firmware.yml
|
||||||
|
when: switch_firmware_enabled | bool
|
||||||
|
tags: [firmware]
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Create stub `identity.yml`, `users.yml`, `vlans.yml`, `backup.yml`, `firmware.yml`**
|
||||||
|
|
||||||
|
Each stub (replaced in later tasks) is just:
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
- name: Placeholder
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "not yet implemented"
|
||||||
|
```
|
||||||
|
Create all five files in `roles/makerfloss.mikrotik_switch/tasks/` with that content.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Create `host_vars/crs310-maker.yml`** (REAL values from Phase 0.6)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
# Identity facts recorded during Phase 0.6 (edit to match the device)
|
||||||
|
switch_identity_name: "crs310-maker"
|
||||||
|
switch_mgmt_vlan_id: 99
|
||||||
|
switch_mgmt_address: "10.0.99.2/24" # EDIT: real mgmt IP
|
||||||
|
switch_mgmt_gateway: "10.0.99.1" # EDIT: real gateway
|
||||||
|
switch_dns_servers: "10.0.99.1"
|
||||||
|
switch_ntp_servers: "10.0.99.1"
|
||||||
|
|
||||||
|
switch_admin_user: "sjat"
|
||||||
|
|
||||||
|
# Real VLAN/port topology (EDIT to the makerspace plan when known)
|
||||||
|
switch_vlans:
|
||||||
|
- { id: 99, name: "mgmt" }
|
||||||
|
- { id: 10, name: "members" }
|
||||||
|
switch_bridge_ports:
|
||||||
|
- { interface: "ether1", pvid: 10, mode: access }
|
||||||
|
- { interface: "ether2", pvid: 10, mode: access }
|
||||||
|
- { interface: "sfp-sfpplus1", pvid: 1, mode: trunk, tagged_vlans: [99, 10] }
|
||||||
|
|
||||||
|
# Firmware (opt-in)
|
||||||
|
# switch_firmware_enabled: true
|
||||||
|
# switch_firmware_target: "7.x.y" # EDIT to the version pinned in Phase 0.3
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Create `play_switch.yml`**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
- name: Configure MikroTik switches (day-2, key auth)
|
||||||
|
hosts: mikrotik
|
||||||
|
gather_facts: false
|
||||||
|
roles:
|
||||||
|
- makerfloss.mikrotik_switch
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Verify syntax + lint**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
ansible-playbook play_switch.yml --syntax-check
|
||||||
|
yamllint .
|
||||||
|
ansible-lint
|
||||||
|
```
|
||||||
|
Expected: syntax OK; yamllint clean; ansible-lint clean (fix any findings).
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add roles host_vars play_switch.yml
|
||||||
|
git commit -m "feat: role skeleton, host_vars, day-2 play (stubbed domains)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: Bootstrap play (device-dependent — needs Phase 0 done)
|
||||||
|
|
||||||
|
### Task 4: First-contact bootstrap — create user, import SSH key
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `play_bootstrap.yml`
|
||||||
|
- Create: `group_vars/mikrotik.vault.yml` (vaulted admin password)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the vaulted admin password file**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
ansible-vault create group_vars/mikrotik.vault.yml
|
||||||
|
```
|
||||||
|
Put in it:
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
vault_switch_admin_password: "CHOOSE-A-STRONG-PASSWORD"
|
||||||
|
```
|
||||||
|
Expected: file is encrypted (`head -1` shows `$ANSIBLE_VAULT`).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Create `play_bootstrap.yml`**
|
||||||
|
|
||||||
|
> Run with password auth the first time: `ansible-playbook play_bootstrap.yml -e ansible_user=admin --ask-pass`.
|
||||||
|
> `net_put` copies the public key file to the device over SCP, then RouterOS imports it.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
- name: Bootstrap MikroTik switch (first contact, password auth)
|
||||||
|
hosts: mikrotik
|
||||||
|
gather_facts: false
|
||||||
|
vars:
|
||||||
|
pubkey_local: "{{ switch_admin_ssh_pubkey_file | default('~/.ssh/id_ed25519.pub') }}"
|
||||||
|
pubkey_remote: "id_ansible.pub"
|
||||||
|
tasks:
|
||||||
|
- name: Create named admin user (idempotent)
|
||||||
|
community.routeros.command:
|
||||||
|
commands:
|
||||||
|
- >-
|
||||||
|
:if ([:len [/user find name="{{ switch_admin_user }}"]] = 0) do={
|
||||||
|
/user add name="{{ switch_admin_user }}" group="{{ switch_admin_group }}"
|
||||||
|
password="{{ vault_switch_admin_password }}" }
|
||||||
|
register: user_create
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Copy operator public key to the switch
|
||||||
|
ansible.netcommon.net_put:
|
||||||
|
src: "{{ pubkey_local }}"
|
||||||
|
dest: "{{ pubkey_remote }}"
|
||||||
|
|
||||||
|
- name: Import the SSH public key for the admin user
|
||||||
|
community.routeros.command:
|
||||||
|
commands:
|
||||||
|
- /user/ssh-keys/import public-key-file="{{ pubkey_remote }}" user="{{ switch_admin_user }}"
|
||||||
|
register: key_import
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Ensure SSH service is enabled
|
||||||
|
community.routeros.command:
|
||||||
|
commands:
|
||||||
|
- /ip/service/set ssh disabled=no port={{ switch_ssh_port | default(22) }}
|
||||||
|
changed_when: true
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Syntax check**
|
||||||
|
|
||||||
|
Run: `ansible-playbook play_bootstrap.yml --syntax-check`
|
||||||
|
Expected: OK.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run bootstrap against the switch (password auth)**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
ansible-playbook play_bootstrap.yml -e ansible_user=admin --ask-pass
|
||||||
|
```
|
||||||
|
Expected: user created, key file copied, key imported, SSH enabled. (Keep a WinBox MAC session open per the lockout note.)
|
||||||
|
|
||||||
|
- [ ] **Step 5: Prove key login works**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
ansible -m community.routeros.command -a "commands='/user/print'" crs310-maker
|
||||||
|
```
|
||||||
|
Expected: succeeds using `~/.ssh/id_ed25519` (no password prompt), and lists your named user.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add play_bootstrap.yml group_vars/mikrotik.vault.yml
|
||||||
|
git commit -m "feat: first-contact bootstrap play (named admin + SSH key import)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: Domain tasks (device-dependent — idempotency-verified)
|
||||||
|
|
||||||
|
> For every task below: run `ansible-playbook play_switch.yml --tags <domain> --limit crs310-maker`
|
||||||
|
> **twice**; the second run must report `changed=0` (or all `changed_when: false`).
|
||||||
|
> RouterOS `:if ([:len [... find ...]] = 0)` guards make `add` idempotent.
|
||||||
|
|
||||||
|
### Task 5: identity.yml — identity, mgmt IP, DNS/NTP, service hardening
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `roles/makerfloss.mikrotik_switch/tasks/identity.yml`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace `identity.yml` with the real implementation**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
- name: Set system identity
|
||||||
|
community.routeros.command:
|
||||||
|
commands:
|
||||||
|
- /system/identity/set name="{{ switch_identity_name }}"
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Configure DNS
|
||||||
|
community.routeros.command:
|
||||||
|
commands:
|
||||||
|
- /ip/dns/set servers="{{ switch_dns_servers }}" allow-remote-requests=no
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Configure NTP client
|
||||||
|
community.routeros.command:
|
||||||
|
commands:
|
||||||
|
- /system/ntp/client/set enabled=yes servers="{{ switch_ntp_servers }}"
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Disable unused services
|
||||||
|
community.routeros.command:
|
||||||
|
commands: >-
|
||||||
|
{{ switch_disabled_services
|
||||||
|
| map('regex_replace', '^(.*)$', '/ip/service/set \1 disabled=yes')
|
||||||
|
| list }}
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Set SSH service port
|
||||||
|
community.routeros.command:
|
||||||
|
commands:
|
||||||
|
- /ip/service/set ssh disabled=no port={{ switch_ssh_port }}
|
||||||
|
changed_when: false
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run twice, assert idempotent**
|
||||||
|
|
||||||
|
Run (twice):
|
||||||
|
```bash
|
||||||
|
ansible-playbook play_switch.yml --tags identity --limit crs310-maker
|
||||||
|
```
|
||||||
|
Expected: completes cleanly both runs; `/system/identity/print` shows the new name; disabled services show `X` (disabled).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add roles/makerfloss.mikrotik_switch/tasks/identity.yml
|
||||||
|
git commit -m "feat(identity): identity, DNS, NTP, service hardening"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: users.yml — ensure admin user, key, disable default admin
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `roles/makerfloss.mikrotik_switch/tasks/users.yml`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace `users.yml`**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
- name: Ensure named admin user exists
|
||||||
|
community.routeros.command:
|
||||||
|
commands:
|
||||||
|
- >-
|
||||||
|
:if ([:len [/user find name="{{ switch_admin_user }}"]] = 0) do={
|
||||||
|
/user add name="{{ switch_admin_user }}" group="{{ switch_admin_group }}" }
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Disable the default admin user
|
||||||
|
community.routeros.command:
|
||||||
|
commands:
|
||||||
|
- >-
|
||||||
|
:if ([:len [/user find name="admin"]] > 0) do={
|
||||||
|
/user/set admin disabled=yes }
|
||||||
|
when: switch_disable_default_admin | bool
|
||||||
|
changed_when: false
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run twice, assert idempotent**
|
||||||
|
|
||||||
|
Run (twice):
|
||||||
|
```bash
|
||||||
|
ansible-playbook play_switch.yml --tags users --limit crs310-maker
|
||||||
|
```
|
||||||
|
Expected: clean both runs. **Before this lands, confirm key login as the named user works** (Task 4 Step 5), or disabling `admin` could lock you out.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add roles/makerfloss.mikrotik_switch/tasks/users.yml
|
||||||
|
git commit -m "feat(users): ensure named admin, disable default admin"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: vlans.yml — VLAN-aware bridge, ports, mgmt interface
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `roles/makerfloss.mikrotik_switch/tasks/vlans.yml`
|
||||||
|
|
||||||
|
> Ordering matters to avoid lockout: create bridge (filtering OFF) → add ports → define
|
||||||
|
> VLANs → add mgmt VLAN interface + IP → enable `vlan-filtering` LAST.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace `vlans.yml`**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
- name: Create VLAN-aware bridge (filtering off initially)
|
||||||
|
community.routeros.command:
|
||||||
|
commands:
|
||||||
|
- >-
|
||||||
|
:if ([:len [/interface/bridge/find name="{{ switch_bridge_name }}"]] = 0) do={
|
||||||
|
/interface/bridge/add name="{{ switch_bridge_name }}" vlan-filtering=no }
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Add bridge ports with PVIDs
|
||||||
|
community.routeros.command:
|
||||||
|
commands:
|
||||||
|
- >-
|
||||||
|
:if ([:len [/interface/bridge/port/find interface="{{ item.interface }}"]] = 0) do={
|
||||||
|
/interface/bridge/port/add bridge="{{ switch_bridge_name }}"
|
||||||
|
interface="{{ item.interface }}" pvid={{ item.pvid }} }
|
||||||
|
else={ /interface/bridge/port/set
|
||||||
|
[find interface="{{ item.interface }}"] pvid={{ item.pvid }} }
|
||||||
|
loop: "{{ switch_bridge_ports }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.interface }}"
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Define bridge VLANs (tagged/untagged membership)
|
||||||
|
community.routeros.command:
|
||||||
|
commands:
|
||||||
|
- >-
|
||||||
|
:if ([:len [/interface/bridge/vlan/find vlan-ids={{ item.id }}]] = 0) do={
|
||||||
|
/interface/bridge/vlan/add bridge="{{ switch_bridge_name }}" vlan-ids={{ item.id }}
|
||||||
|
tagged="{{ ([switch_bridge_name] + (switch_bridge_ports
|
||||||
|
| selectattr('mode','equalto','trunk')
|
||||||
|
| selectattr('tagged_vlans','defined')
|
||||||
|
| selectattr('tagged_vlans','contains', item.id)
|
||||||
|
| map(attribute='interface') | list)) | join(',') }}"
|
||||||
|
untagged="{{ switch_bridge_ports
|
||||||
|
| selectattr('mode','equalto','access')
|
||||||
|
| selectattr('pvid','equalto', item.id)
|
||||||
|
| map(attribute='interface') | list | join(',') }}" }
|
||||||
|
loop: "{{ switch_vlans }}"
|
||||||
|
loop_control:
|
||||||
|
label: "vlan {{ item.id }}"
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Create management VLAN interface
|
||||||
|
community.routeros.command:
|
||||||
|
commands:
|
||||||
|
- >-
|
||||||
|
:if ([:len [/interface/vlan/find name="vlan-mgmt"]] = 0) do={
|
||||||
|
/interface/vlan/add name="vlan-mgmt" interface="{{ switch_bridge_name }}"
|
||||||
|
vlan-id={{ switch_mgmt_vlan_id }} }
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Assign management IP address
|
||||||
|
community.routeros.command:
|
||||||
|
commands:
|
||||||
|
- >-
|
||||||
|
:if ([:len [/ip/address/find interface="vlan-mgmt"]] = 0) do={
|
||||||
|
/ip/address/add address="{{ switch_mgmt_address }}" interface="vlan-mgmt" }
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Set default gateway route
|
||||||
|
community.routeros.command:
|
||||||
|
commands:
|
||||||
|
- >-
|
||||||
|
:if ([:len [/ip/route/find dst-address="0.0.0.0/0"]] = 0) do={
|
||||||
|
/ip/route/add dst-address=0.0.0.0/0 gateway="{{ switch_mgmt_gateway }}" }
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Enable VLAN filtering (LAST — verify mgmt reachability first)
|
||||||
|
community.routeros.command:
|
||||||
|
commands:
|
||||||
|
- /interface/bridge/set "{{ switch_bridge_name }}" vlan-filtering=yes
|
||||||
|
changed_when: false
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run twice, assert idempotent — WITH a recovery channel open**
|
||||||
|
|
||||||
|
Run (twice), keeping WinBox MAC session open:
|
||||||
|
```bash
|
||||||
|
ansible-playbook play_switch.yml --tags vlans --limit crs310-maker
|
||||||
|
```
|
||||||
|
Expected: clean both runs. Verify `/interface/bridge/vlan/print` shows correct tagged/untagged sets and you can still reach the mgmt IP after `vlan-filtering=yes`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add roles/makerfloss.mikrotik_switch/tasks/vlans.yml
|
||||||
|
git commit -m "feat(vlans): VLAN-aware bridge, ports, mgmt interface"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: backup.yml — export + binary backup, fetch into repo
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `roles/makerfloss.mikrotik_switch/tasks/backup.yml`
|
||||||
|
- Create: `play_backup.yml`
|
||||||
|
- Create: `backups/.gitkeep`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace `backup.yml`**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
- name: Generate a config export on the device
|
||||||
|
community.routeros.command:
|
||||||
|
commands:
|
||||||
|
- /export file=export
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Generate a binary system backup on the device
|
||||||
|
community.routeros.command:
|
||||||
|
commands:
|
||||||
|
- /system/backup/save name=backup dont-encrypt=yes
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Fetch the export file into the repo
|
||||||
|
ansible.netcommon.net_get:
|
||||||
|
src: "export.rsc"
|
||||||
|
dest: "{{ playbook_dir }}/backups/{{ inventory_hostname }}/export.rsc"
|
||||||
|
|
||||||
|
- name: Fetch the binary backup into the repo
|
||||||
|
ansible.netcommon.net_get:
|
||||||
|
src: "backup.backup"
|
||||||
|
dest: "{{ playbook_dir }}/backups/{{ inventory_hostname }}/backup.backup"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Create `play_backup.yml`**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
- name: Back up MikroTik switch configuration
|
||||||
|
hosts: mikrotik
|
||||||
|
gather_facts: false
|
||||||
|
tasks:
|
||||||
|
- name: Ensure local backup directory exists
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ playbook_dir }}/backups/{{ inventory_hostname }}"
|
||||||
|
state: directory
|
||||||
|
mode: "0755"
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
- name: Run backup tasks
|
||||||
|
ansible.builtin.include_role:
|
||||||
|
name: makerfloss.mikrotik_switch
|
||||||
|
tasks_from: backup.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Create `backups/.gitkeep`** (empty file) so the dir exists.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the backup play**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
ansible-playbook play_backup.yml --limit crs310-maker
|
||||||
|
```
|
||||||
|
Expected: `backups/crs310-maker/export.rsc` and `backup.backup` appear locally and are non-empty.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit (export only — binary backup may contain secrets)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo 'backups/**/*.backup' >> .gitignore
|
||||||
|
git add roles/makerfloss.mikrotik_switch/tasks/backup.yml play_backup.yml backups/crs310-maker/export.rsc .gitignore
|
||||||
|
git commit -m "feat(backup): export + binary backup, fetch into repo"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 9: firmware.yml — RouterOS/RouterBOOT upgrade to pinned target
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `roles/makerfloss.mikrotik_switch/tasks/firmware.yml`
|
||||||
|
|
||||||
|
> Opt-in only (`switch_firmware_enabled: true` + `switch_firmware_target` set in host_vars).
|
||||||
|
> Upgrades reboot the switch — run deliberately, with a recovery channel.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace `firmware.yml`**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
- name: Assert a firmware target is set
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- switch_firmware_target | length > 0
|
||||||
|
fail_msg: "switch_firmware_target must be set in host_vars to run firmware upgrades."
|
||||||
|
|
||||||
|
- name: Read current RouterOS version
|
||||||
|
community.routeros.facts:
|
||||||
|
register: ros_facts
|
||||||
|
|
||||||
|
- name: Upgrade RouterOS to the target channel and reboot
|
||||||
|
community.routeros.command:
|
||||||
|
commands:
|
||||||
|
- /system/package/update/set channel=stable
|
||||||
|
- /system/package/update/install
|
||||||
|
when: ansible_net_version is version(switch_firmware_target, '<')
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Pause for device reboot
|
||||||
|
ansible.builtin.wait_for_connection:
|
||||||
|
delay: 30
|
||||||
|
timeout: 300
|
||||||
|
when: ansible_net_version is version(switch_firmware_target, '<')
|
||||||
|
|
||||||
|
- name: Upgrade RouterBOOT firmware to match RouterOS
|
||||||
|
community.routeros.command:
|
||||||
|
commands:
|
||||||
|
- /system/routerboard/upgrade
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Reboot to apply RouterBOOT upgrade
|
||||||
|
community.routeros.command:
|
||||||
|
commands:
|
||||||
|
- /system/reboot
|
||||||
|
changed_when: true
|
||||||
|
ignore_errors: true # connection drops on reboot; expected
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Syntax + lint only (do NOT auto-run upgrades in CI)**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
ansible-playbook play_switch.yml --syntax-check
|
||||||
|
ansible-lint
|
||||||
|
```
|
||||||
|
Expected: clean.
|
||||||
|
|
||||||
|
- [ ] **Step 3: (Manual, optional) run the upgrade deliberately**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
ansible-playbook play_switch.yml --tags firmware --limit crs310-maker \
|
||||||
|
-e switch_firmware_enabled=true
|
||||||
|
```
|
||||||
|
Expected: upgrades only if current `< switch_firmware_target`; switch reboots and comes back.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add roles/makerfloss.mikrotik_switch/tasks/firmware.yml
|
||||||
|
git commit -m "feat(firmware): opt-in RouterOS + RouterBOOT upgrade to pinned target"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: Docs and publish
|
||||||
|
|
||||||
|
### Task 10: README, role README, CLAUDE.md, push to Forgejo
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `README.md`, `roles/makerfloss.mikrotik_switch/README.md`, `CLAUDE.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create `README.md`** covering: purpose, prerequisites (Phase 0 checklist), setup (`direnv allow`, `pip install`, `ansible-galaxy install`), bootstrap (`play_bootstrap.yml --ask-pass`), day-2 (`play_switch.yml`), backup (`play_backup.yml`), and the lockout-safety note.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Create `roles/makerfloss.mikrotik_switch/README.md`** documenting every variable in `defaults/main.yml`, the enable-flags, and the `switch_bridge_ports`/`switch_vlans` data shapes with an example.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Create `CLAUDE.md`** — short project guide: tech stack, structure, essential commands (lint, syntax-check, bootstrap, day-2, backup), the idempotency rule, and the lockout-safety rule.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Final static verification**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
yamllint . && ansible-lint && ansible-playbook play_switch.yml --syntax-check
|
||||||
|
```
|
||||||
|
Expected: all clean.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Add remote and push**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
git remote add origin git@forgejo.makerfloss.eu:<owner>/MakerFLOSS_Mikrotik.git
|
||||||
|
git add README.md roles/makerfloss.mikrotik_switch/README.md CLAUDE.md
|
||||||
|
git commit -m "docs: README, role README, CLAUDE.md"
|
||||||
|
git push -u origin main
|
||||||
|
```
|
||||||
|
Expected: repo populated on `forgejo.makerfloss.eu`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-review checklist (run before execution)
|
||||||
|
|
||||||
|
- [ ] **Spec coverage:** identity/services (Task 5), users/keys (Tasks 4,6), VLANs/bridge/ports (Task 7), backups (Task 8), firmware (Task 9), bring-over conventions (Tasks 1–2), separate vault (Task 2), placeholder topology overridable in host_vars (Tasks 3,7). ✔
|
||||||
|
- [ ] **Open items** from the spec are surfaced in the plan: firmware target (Phase 0.3 / Task 9), winbox on/off (`switch_disabled_services` default keeps winbox), admin username (`switch_admin_user`), backup scheduling (on-demand `play_backup.yml`; RouterOS scheduler left as a future enhancement).
|
||||||
|
- [ ] **Idempotency** is explicitly tested (run-twice) on every device-touching task.
|
||||||
|
- [ ] **Lockout safety** called out at the top and on Tasks 6 and 7.
|
||||||
|
|
||||||
|
## Notes / risks to validate during execution
|
||||||
|
|
||||||
|
- **RouterOS version drift:** exact CLI syntax (NTP `servers=` property, `ssh-keys/import` path) is RouterOS-7 specific; verify against the pinned version from Phase 0.3 and adjust.
|
||||||
|
- **`net_put`/`net_get` over `network_cli`:** depends on SCP being available on the RouterOS SSH service; if it fails, fall back to importing the key by pasting its contents via `/user/ssh-keys/...` or enabling SCP.
|
||||||
|
- **`changed_when: false`** is used widely because the `command` module can't detect RouterOS state changes; idempotency comes from the `:if [find]` guards. Revisit if you want accurate change reporting (parse command output).
|
||||||
|
|
||||||
|
## Carry-over notes from the skeleton code review (Tasks 1–3, done 2026-06-07)
|
||||||
|
|
||||||
|
The no-device tasks (1–3) are implemented, reviewed, and committed on branch
|
||||||
|
`feat/initial-scaffolding`. The code-quality review of the role skeleton raised these
|
||||||
|
points to handle WHEN the device task files (Tasks 5–9) are written:
|
||||||
|
|
||||||
|
- **`switch_ssh_port` (default 22):** the identity task will *set* the SSH port. If the
|
||||||
|
device was manually moved to a non-standard port before Ansible manages it, the first
|
||||||
|
run resets it to 22 and the connection drops. Confirm the live port matches before the
|
||||||
|
identity task runs, or override `switch_ssh_port` in host_vars.
|
||||||
|
- **`switch_bridge_name` / `switch_admin_group`:** these default to the CRS310 factory
|
||||||
|
values (`bridge` / `full`) and are NOT overridden in host_vars. Correct for this one
|
||||||
|
device; if the bridge/group name ever differs, the VLAN and users tasks silently target
|
||||||
|
the wrong object. Add explicit host_vars overrides if a second device is ever onboarded.
|
||||||
|
- **Trunk `pvid: 1` (sfp-sfpplus1):** untagged frames on the uplink land in VLAN 1. In a
|
||||||
|
hardened VLAN design VLAN 1 is usually unused — when writing `vlans.yml`, decide
|
||||||
|
deliberately whether the trunk should accept untagged traffic at all, and comment intent.
|
||||||
|
- **host_vars `# EDIT:` placeholders:** `switch_mgmt_address/gateway/dns/ntp` in
|
||||||
|
`host_vars/crs310-maker.yml` hold plausible `10.0.99.x` placeholders. Replace with the
|
||||||
|
real values from the field guide (Step 7) and remove the `# EDIT` comments so it's
|
||||||
|
unambiguous they were updated.
|
||||||
4
group_vars/all.yml
Normal file
4
group_vars/all.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
# Shared non-secret defaults across all hosts go here.
|
||||||
|
# Secrets live in the vault (see host_vars / a vaulted file), not in this file.
|
||||||
|
org_name: "MakerFLOSS"
|
||||||
8
group_vars/mikrotik.vault.yml
Normal file
8
group_vars/mikrotik.vault.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
$ANSIBLE_VAULT;1.2;AES256;makerfloss
|
||||||
|
65633363353761306465316563336137323966313330313238633661313938633939653330383561
|
||||||
|
3936363934636563383032646631336464363534613366360a666162626432303066383863376530
|
||||||
|
34616565613837326661323565306263636661396637313263613433366438653934383266343664
|
||||||
|
6538656135366336630a303536663139396364643539636532616165386533616635313166366564
|
||||||
|
31303762313063353734666632623262616562383833353765376263333732386336616336383934
|
||||||
|
61623334666230356661636433613633653439353662393730313663656664663962346139666639
|
||||||
|
396431396664316165663030633732656632
|
||||||
14
group_vars/mikrotik.yml
Normal file
14
group_vars/mikrotik.yml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
---
|
||||||
|
ansible_connection: ansible.netcommon.network_cli
|
||||||
|
ansible_network_os: community.routeros.routeros
|
||||||
|
# Bootstrap default. play_bootstrap.yml creates a named admin user and imports the
|
||||||
|
# operator SSH key; thereafter override ansible_user to that named user (host_vars).
|
||||||
|
ansible_user: admin
|
||||||
|
ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
|
||||||
|
|
||||||
|
# Domain enable-flags (day-2 play). Override per-host if needed.
|
||||||
|
switch_identity_enabled: true
|
||||||
|
switch_users_enabled: true
|
||||||
|
switch_vlans_enabled: true
|
||||||
|
switch_backup_enabled: true
|
||||||
|
switch_firmware_enabled: false # opt-in; upgrades are disruptive
|
||||||
33
host_vars/crs310-maker.yml
Normal file
33
host_vars/crs310-maker.yml
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
---
|
||||||
|
# Device facts (recorded on-site 2026-06-08):
|
||||||
|
# model: CRS310-8G+2S+IN
|
||||||
|
# serial: HM40B8TDNDD
|
||||||
|
# base MAC (ether1): D0:EA:11:24:F4:AA
|
||||||
|
# RouterOS: 7.19.6 stable (bootloader already current) -> pinned target below
|
||||||
|
#
|
||||||
|
# Bootstrap status (2026-06-08): identity set; user `sjat` (full) created with the
|
||||||
|
# operator ed25519 key imported + a vaulted password (vault_switch_admin_password in
|
||||||
|
# group_vars/mikrotik.vault.yml). Key login verified. Default `admin` still enabled
|
||||||
|
# (not yet hardened). Switch currently on the bench at 192.168.88.1 (defconf, not yet
|
||||||
|
# reset/VLAN-configured). Real mgmt addressing below is the FUTURE production plan.
|
||||||
|
switch_identity_name: "crs310-maker"
|
||||||
|
switch_mgmt_vlan_id: 99
|
||||||
|
switch_mgmt_address: "10.0.99.2/24" # EDIT: real mgmt IP
|
||||||
|
switch_mgmt_gateway: "10.0.99.1" # EDIT: real gateway
|
||||||
|
switch_dns_servers: "10.0.99.1"
|
||||||
|
switch_ntp_servers: "10.0.99.1"
|
||||||
|
|
||||||
|
switch_admin_user: "sjat"
|
||||||
|
|
||||||
|
# Real VLAN/port topology (EDIT to the makerspace plan when known)
|
||||||
|
switch_vlans:
|
||||||
|
- {id: 99, name: "mgmt"}
|
||||||
|
- {id: 10, name: "members"}
|
||||||
|
switch_bridge_ports:
|
||||||
|
- {interface: "ether1", pvid: 10, mode: access}
|
||||||
|
- {interface: "ether2", pvid: 10, mode: access}
|
||||||
|
- {interface: "sfp-sfpplus1", pvid: 1, mode: trunk, tagged_vlans: [99, 10]}
|
||||||
|
|
||||||
|
# Firmware: pinned at the version already installed (no upgrade planned now).
|
||||||
|
switch_firmware_target: "7.19.6"
|
||||||
|
# switch_firmware_enabled: true # opt-in only when you actually want to upgrade
|
||||||
7
inventories/prod/hosts.yml
Normal file
7
inventories/prod/hosts.yml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
all:
|
||||||
|
children:
|
||||||
|
mikrotik:
|
||||||
|
hosts:
|
||||||
|
crs310-maker:
|
||||||
|
ansible_host: 192.168.88.1 # temp mgmt IP until Task 4 sets the real one
|
||||||
6
play_switch.yml
Normal file
6
play_switch.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
- name: Configure MikroTik switches (day-2, key auth)
|
||||||
|
hosts: mikrotik
|
||||||
|
gather_facts: false
|
||||||
|
roles:
|
||||||
|
- makerfloss.mikrotik_switch
|
||||||
10
requirements.txt
Normal file
10
requirements.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Core Ansible
|
||||||
|
ansible==10.3.0
|
||||||
|
|
||||||
|
# Linting & validation
|
||||||
|
ansible-lint==24.7.0
|
||||||
|
yamllint==1.35.1
|
||||||
|
|
||||||
|
# Network connection plugins / SCP for SSH key transfer to RouterOS
|
||||||
|
paramiko>=3.4.0
|
||||||
|
scp>=0.15.0
|
||||||
6
requirements.yml
Normal file
6
requirements.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
collections:
|
||||||
|
- name: community.routeros
|
||||||
|
version: ">=3.0.0,<4.0.0"
|
||||||
|
- name: ansible.netcommon
|
||||||
|
version: ">=6.0.0,<8.0.0"
|
||||||
60
roles/makerfloss.mikrotik_switch/README.md
Normal file
60
roles/makerfloss.mikrotik_switch/README.md
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
# makerfloss.mikrotik_switch
|
||||||
|
|
||||||
|
Configure a MikroTik RouterOS switch (CRS310) over SSH with `community.routeros`.
|
||||||
|
The role provides the *mechanism*; real values live in `host_vars`. Each domain is
|
||||||
|
gated by an enable-flag (defined in `group_vars/mikrotik.yml`) so you can apply a
|
||||||
|
subset with `--tags`.
|
||||||
|
|
||||||
|
## Domains (enable-flags)
|
||||||
|
|
||||||
|
| Flag | Task file | Tag | Does |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `switch_identity_enabled` | `identity.yml` | `identity` | identity, mgmt IP, DNS/NTP, SSH on, disable unused services |
|
||||||
|
| `switch_users_enabled` | `users.yml` | `users` | named admin user, import SSH key, disable default `admin` |
|
||||||
|
| `switch_vlans_enabled` | `vlans.yml` | `vlans` | VLAN-aware bridge, access/trunk ports, mgmt VLAN iface |
|
||||||
|
| `switch_backup_enabled` | `backup.yml` | `backup` | `/export` + binary backup, fetched into the repo |
|
||||||
|
| `switch_firmware_enabled` | `firmware.yml` | `firmware` | RouterOS + RouterBOOT upgrade to `switch_firmware_target` (opt-in) |
|
||||||
|
|
||||||
|
> The per-domain task files are currently **stubs** pending implementation (see the
|
||||||
|
> plan in `docs/superpowers/plans/`).
|
||||||
|
|
||||||
|
## Variables (`defaults/main.yml`)
|
||||||
|
|
||||||
|
| Variable | Default | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `switch_identity_name` | `{{ inventory_hostname }}` | system identity |
|
||||||
|
| `switch_mgmt_vlan_id` | `99` | management VLAN id |
|
||||||
|
| `switch_mgmt_address` | placeholder | mgmt IP `addr/cidr` (override in host_vars) |
|
||||||
|
| `switch_mgmt_gateway` | placeholder | default gateway |
|
||||||
|
| `switch_dns_servers` | placeholder | DNS server(s) |
|
||||||
|
| `switch_ntp_servers` | placeholder | NTP server(s) |
|
||||||
|
| `switch_disabled_services` | telnet,ftp,www,www-ssl,api,api-ssl | services to disable (winbox kept for recovery) |
|
||||||
|
| `switch_ssh_port` | `22` | SSH service port |
|
||||||
|
| `switch_admin_user` | `sjat` | named admin user |
|
||||||
|
| `switch_admin_group` | `full` | RouterOS group for the admin user |
|
||||||
|
| `switch_admin_ssh_pubkey_file` | `~/.ssh/id_ed25519.pub` | operator public key to import |
|
||||||
|
| `switch_disable_default_admin` | `true` | disable the built-in `admin` after key login works |
|
||||||
|
| `switch_bridge_name` | `bridge` | bridge to manage |
|
||||||
|
| `switch_vlans` | example | list of `{id, name}` |
|
||||||
|
| `switch_bridge_ports` | example | list of port definitions (see below) |
|
||||||
|
| `switch_firmware_target` | `""` | RouterOS version to pin/upgrade to |
|
||||||
|
|
||||||
|
### Data shapes
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
switch_vlans:
|
||||||
|
- {id: 99, name: "mgmt"}
|
||||||
|
- {id: 10, name: "members"}
|
||||||
|
|
||||||
|
switch_bridge_ports:
|
||||||
|
# access port: untagged member of one VLAN (pvid)
|
||||||
|
- {interface: "ether1", pvid: 10, mode: access}
|
||||||
|
# trunk port: carries tagged VLANs; pvid sets the untagged/native VLAN
|
||||||
|
- {interface: "sfp-sfpplus1", pvid: 1, mode: trunk, tagged_vlans: [99, 10]}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Idempotency
|
||||||
|
|
||||||
|
RouterOS has no rich declarative module set over `network_cli`, so tasks use
|
||||||
|
`community.routeros.command` with `:if ([:len [... find ...]] = 0) do={ ... }` guards.
|
||||||
|
Always run twice and confirm the second run is a no-op.
|
||||||
38
roles/makerfloss.mikrotik_switch/defaults/main.yml
Normal file
38
roles/makerfloss.mikrotik_switch/defaults/main.yml
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
# ----- Identity / management -----
|
||||||
|
switch_identity_name: "{{ inventory_hostname }}"
|
||||||
|
switch_mgmt_vlan_id: 99
|
||||||
|
switch_mgmt_address: "192.168.88.1/24" # PLACEHOLDER — override in host_vars
|
||||||
|
switch_mgmt_gateway: "192.168.88.254" # PLACEHOLDER — override in host_vars
|
||||||
|
switch_dns_servers: "192.168.88.254"
|
||||||
|
switch_ntp_servers: "192.168.88.254"
|
||||||
|
|
||||||
|
# Services to disable for hardening (winbox kept on by default for recovery)
|
||||||
|
switch_disabled_services:
|
||||||
|
- telnet
|
||||||
|
- ftp
|
||||||
|
- www
|
||||||
|
- www-ssl
|
||||||
|
- api
|
||||||
|
- api-ssl
|
||||||
|
switch_ssh_port: 22
|
||||||
|
|
||||||
|
# ----- Users -----
|
||||||
|
switch_admin_user: "sjat"
|
||||||
|
switch_admin_group: "full"
|
||||||
|
switch_admin_ssh_pubkey_file: "~/.ssh/id_ed25519.pub"
|
||||||
|
switch_disable_default_admin: true
|
||||||
|
|
||||||
|
# ----- VLAN / bridge / ports (PLACEHOLDER example) -----
|
||||||
|
# Real topology is defined in host_vars/<switch>.yml.
|
||||||
|
switch_bridge_name: "bridge"
|
||||||
|
switch_vlans:
|
||||||
|
- {id: 99, name: "mgmt"}
|
||||||
|
- {id: 10, name: "members"}
|
||||||
|
switch_bridge_ports:
|
||||||
|
# ether1..ether8 = 2.5GbE access ports; sfp-sfpplus1/2 = 10G uplinks
|
||||||
|
- {interface: "ether1", pvid: 10, mode: access}
|
||||||
|
- {interface: "sfp-sfpplus1", pvid: 1, mode: trunk, tagged_vlans: [99, 10]}
|
||||||
|
|
||||||
|
# ----- Firmware -----
|
||||||
|
switch_firmware_target: "" # set in host_vars when opting into upgrades
|
||||||
10
roles/makerfloss.mikrotik_switch/meta/main.yml
Normal file
10
roles/makerfloss.mikrotik_switch/meta/main.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
galaxy_info:
|
||||||
|
role_name: mikrotik_switch
|
||||||
|
namespace: makerfloss
|
||||||
|
author: sjat
|
||||||
|
description: Configure a MikroTik RouterOS switch (CRS310) over SSH.
|
||||||
|
license: MIT
|
||||||
|
min_ansible_version: "2.17"
|
||||||
|
platforms: []
|
||||||
|
dependencies: []
|
||||||
4
roles/makerfloss.mikrotik_switch/tasks/backup.yml
Normal file
4
roles/makerfloss.mikrotik_switch/tasks/backup.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
- name: Placeholder
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "not yet implemented"
|
||||||
4
roles/makerfloss.mikrotik_switch/tasks/firmware.yml
Normal file
4
roles/makerfloss.mikrotik_switch/tasks/firmware.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
- name: Placeholder
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "not yet implemented"
|
||||||
4
roles/makerfloss.mikrotik_switch/tasks/identity.yml
Normal file
4
roles/makerfloss.mikrotik_switch/tasks/identity.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
- name: Placeholder
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "not yet implemented"
|
||||||
25
roles/makerfloss.mikrotik_switch/tasks/main.yml
Normal file
25
roles/makerfloss.mikrotik_switch/tasks/main.yml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
- name: Identity, management and services
|
||||||
|
ansible.builtin.import_tasks: identity.yml
|
||||||
|
when: switch_identity_enabled | bool
|
||||||
|
tags: [identity]
|
||||||
|
|
||||||
|
- name: Users and SSH keys
|
||||||
|
ansible.builtin.import_tasks: users.yml
|
||||||
|
when: switch_users_enabled | bool
|
||||||
|
tags: [users]
|
||||||
|
|
||||||
|
- name: VLANs, bridge and ports
|
||||||
|
ansible.builtin.import_tasks: vlans.yml
|
||||||
|
when: switch_vlans_enabled | bool
|
||||||
|
tags: [vlans]
|
||||||
|
|
||||||
|
- name: Backup configuration
|
||||||
|
ansible.builtin.import_tasks: backup.yml
|
||||||
|
when: switch_backup_enabled | bool
|
||||||
|
tags: [backup]
|
||||||
|
|
||||||
|
- name: Firmware upgrade
|
||||||
|
ansible.builtin.import_tasks: firmware.yml
|
||||||
|
when: switch_firmware_enabled | bool
|
||||||
|
tags: [firmware]
|
||||||
4
roles/makerfloss.mikrotik_switch/tasks/users.yml
Normal file
4
roles/makerfloss.mikrotik_switch/tasks/users.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
- name: Placeholder
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "not yet implemented"
|
||||||
4
roles/makerfloss.mikrotik_switch/tasks/vlans.yml
Normal file
4
roles/makerfloss.mikrotik_switch/tasks/vlans.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
- name: Placeholder
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "not yet implemented"
|
||||||
Loading…
Add table
Reference in a new issue