Merge: initial scaffolding + field guide + role skeleton (Tasks 1-3)

No-device tasks complete and two-stage reviewed:
- repo scaffolding (direnv, ansible.cfg, lint, requirements)
- makerfloss vault identity, inventory, connection group_vars
- role skeleton makerfloss.mikrotik_switch (stubbed domains), host_vars, play_switch.yml
- on-site makerspace field guide; plan carry-over notes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
sjat 2026-06-08 18:26:08 +02:00
commit bdfde1644c
22 changed files with 402 additions and 0 deletions

8
.ansible-lint Normal file
View file

@ -0,0 +1,8 @@
---
profile: production
skip_list:
- line-length
- no-changed-when
exclude_paths:
- .venv/
- backups/

9
.envrc Normal file
View 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
View file

@ -0,0 +1,5 @@
.venv/
*.retry
__pycache__/
*.pyc
.DS_Store

11
.yamllint Normal file
View file

@ -0,0 +1,11 @@
---
extends: default
rules:
line-length: disable
comments:
min-spaces-from-content: 1
truthy:
allowed-values: ["true", "false", "yes", "no"]
ignore: |
.venv/
backups/

19
ansible.cfg Normal file
View 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

View 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: ~3045 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.

View file

@ -922,3 +922,25 @@ Expected: repo populated on `forgejo.makerfloss.eu`.
- **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. - **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. - **`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). - **`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 13, done 2026-06-07)
The no-device tasks (13) 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 59) 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
View 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"

14
group_vars/mikrotik.yml Normal file
View 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

View file

@ -0,0 +1,23 @@
---
# 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

View 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
View 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
View 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
View 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"

View 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

View 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: []

View file

@ -0,0 +1,4 @@
---
- name: Placeholder
ansible.builtin.debug:
msg: "not yet implemented"

View file

@ -0,0 +1,4 @@
---
- name: Placeholder
ansible.builtin.debug:
msg: "not yet implemented"

View file

@ -0,0 +1,4 @@
---
- name: Placeholder
ansible.builtin.debug:
msg: "not yet implemented"

View 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]

View file

@ -0,0 +1,4 @@
---
- name: Placeholder
ansible.builtin.debug:
msg: "not yet implemented"

View file

@ -0,0 +1,4 @@
---
- name: Placeholder
ansible.builtin.debug:
msg: "not yet implemented"