Case studies in detail
VMware to Proxmox migrations on production estates
Context
A virtualisation platform costs you twice: the licence you renew, and the resources it ties up. I have migrated production servers from VMware to Proxmox on several estates, to get out of the first and get the second back. What runs on those machines is not negotiable: Active Directory, the business application, file servers. A migration that goes wrong there stops everyone’s working day, which is exactly why these projects keep getting postponed.
How it works
- Inventory first: what actually runs, what depends on what, what can stop for an hour and what cannot. That inventory decides the order of the switchover, not the calendar
- Backups verified by a real restore before touching anything. A backup you have never restored is not a backup, it is an intention
- Switchover in batches, with the way back prepared for each batch. As long as the original host is untouched, a batch that goes wrong is replayed instead of endured
- Validation by use and not by the console: you open a session, you reach the shares, you launch the application. A hypervisor showing green proves nothing
Results
- No business interruption observed on the migrations carried out
- Virtualisation licence cost removed on the migrated scope
- Resources recovered: consolidation gives back CPU and memory that the old platform was holding
- Faster restores than before, because a restore replays at the level of the virtual machine and because it is tested
Sentinel SRE: predictive monitoring and AI analysis (Gemini 2.5)
Context
Monitoring an estate of more than 50 Linux servers produces a volume of alerts that is hard to work through by hand every day. The aim was to replace classic reactive monitoring with a proactive system, without installing heavy agents on the machines, using AI to interpret the basic metrics.
How it works
- Light agent (Bash): a CRON script on each server pulls the vital signs (load, RAM, IO, security patches, uptime) using native commands
- n8n orchestration: JSON received through a webhook, then aggregation across the whole estate
- AI layer: the Gemini 2.5 model reads the overall picture, spots the bottlenecks (CPU load for instance) and correlates them with pending updates
Technical solution
#!/bin/bash
# Health script: collects SRE metrics for AI analysis
WEBHOOK_URL="https://xxxxxxxx.xxxx.xx/webhook/system-health-xxxx"
HOSTNAME=$(hostname | tr '[:upper:]' '[:lower:]')
IP_ADDR="10.0.x.x"
REBOOT_REQ=$( [ -f /var/run/reboot-required ] && echo "true" || echo "false" )
RAW_UPGRADE=$(apt-get -s upgrade 2>/dev/null)
PKG_LIST=$(echo "$RAW_UPGRADE" | grep "^Inst" | awk '{print $2}' | tr '\n' ',' | sed 's/,$//' | tr -d '[:cntrl:]"' )
APT_CHECK_TOOL="/usr/lib/update-notifier/apt-check"
if [ -f "$APT_CHECK_TOOL" ]; then
CHECK_DATA=$($APT_CHECK_TOOL 2>&1)
TOTAL_UPDATES=$(echo "$CHECK_DATA" | cut -d';' -f1 | grep -oE '[0-9]+$' | tail -n 1)
SECURITY_UPDATES=$(echo "$CHECK_DATA" | cut -d';' -f2 | grep -oE '[0-9]+$' | tail -n 1)
else
TOTAL_UPDATES=$(echo "$RAW_UPGRADE" | grep -c "^Inst" | tr -d '[:space:]')
SECURITY_UPDATES=$(echo "$RAW_UPGRADE" | grep -i "security" | grep -c "^Inst" | tr -d '[:space:]')
fi
RELEASE_CHECK=$(do-release-upgrade -c 2>/dev/null | grep -i "available" | wc -l)
OS_UPGRADE=$( [ "$RELEASE_CHECK" -gt 0 ] && echo "true" || echo "false" )
UPTIME=$(uptime -p | sed 's/up //' | tr -d '[:cntrl:]"' )
DISK_USAGE=$(df -h / | awk 'NR==2 {print $5}' | tr -d '[:space:]')
RAM_USAGE=$(free | grep Mem | awk '{printf "%.0f%%", $3/$2 * 100.0}' | tr -d '[:space:]')
CPU_LOAD=$(cat /proc/loadavg | awk '{print $1}' | tr -d '[:space:]')
LAST_USER=$(last -n 1 | grep -vE 'reboot|wtmp|^$' | awk '{print $1}' | head -n 1 | tr -d '[:space:]')
[ -z "$LAST_USER" ] && LAST_USER="System"
PAYLOAD=$(cat <<EOF
{
"hostname": "$HOSTNAME",
"ip": "$IP_ADDR",
"reboot_required": "$REBOOT_REQ",
"total_updates": "$TOTAL_UPDATES",
"security_updates": "$SECURITY_UPDATES",
"os_upgrade": "$OS_UPGRADE",
"package_list": "$PKG_LIST",
"uptime": "$UPTIME",
"disk_usage": "$DISK_USAGE",
"ram_usage": "$RAM_USAGE",
"cpu_load": "$CPU_LOAD",
"last_user": "$LAST_USER"
}
EOF
)
curl -s -X POST "$WEBHOOK_URL" -H "Content-Type: application/json" -d "$PAYLOAD"
Results
- IT governance: a clear, interpreted view of the health of the estate every morning for IT management
- Time saved: no more manual SSH checks on individual servers
Starlink IP reputation monitoring and blacklist prevention
Context
The incident: a full service outage on a production floor after a public Starlink IP address was blacklisted. The cause: Starlink addresses are dynamic, so you can inherit an address with a history of high fraud scores, which gets you blocked by partners’ application firewalls. What was at stake: keeping the business running by spotting the loss of reputation before it reaches the users.
How it works
- Distributed collection: a light Bash script on the gateways finds the real outbound IP with curl and sends it on securely
- Webhook trigger: n8n receives the data through a secured endpoint
- Enrichment: the AbuseIPDB API is queried to compute the confidence score
- Persistence: structured archiving in Google Sheets for forensic analysis and trend tracking
Technical solution
#!/bin/bash
# Script: sentinel-ip-check.sh
# Purpose: collect the public IP and send it to the automation instance
# Identifiers masked for security
WEBHOOK_URL="https://xxxxxxxx.xxxx.xx/webhook/xxxxxxxx-xxxx"
VLAN_ID=$(hostname)
# Get the outbound IP
PUBLIC_IP=$(curl -s https://icanhazip.com)
# Send the metrics securely
curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{\"ip\": \"$PUBLIC_IP\", \"vlan_name\": \"$VLAN_ID\"}"
Results
- Key indicator: 0 minutes of production downtime from a blacklisted IP since it went in
- Ahead of the problem: the IT team gets a consolidated report at 8:30, so IPs can be rotated before the score passes 10%
Shadow IT Scanner: software compliance audit
Context
Keeping the information system secure and the licences in order means auditing installed software regularly. The problem: a raw extract from inventory tools produces a volume of data nobody can use (name variations, endless version numbers), which makes spotting unauthorised software impossible by eye. The aim was a normalisation and categorisation pipeline.
How it works
- Trigger: a batch script paired with PDQ Inventory exports a CSV report and pushes it to an n8n webhook
- Processing and cleaning (data normalisation): a JavaScript node parses the strings, strips filler words and truncates versions with regex
- Categorisation: software is grouped by VLAN and department, tying each piece of software to the PC and the user concerned
Technical solution
#!/bin/bash
# Push script: PDQ Inventory export to n8n
REPORT_PATH="D:\ApplicationsHebdo\Applications.csv"
WEBHOOK_URL="https://xxxxxxxx.xxxx.xx/webhook/ApplicationsHebdo"
curl -X POST \
-H "Content-Type: multipart/form-data" \
-F "data=@%REPORT_PATH%" \
"%WEBHOOK_URL%"
Results
- Full visibility: a Google Sheets workbook generated automatically, one tab per VLAN
- Something to act on: for each piece of software the IT team immediately knows how many machines, which PC and which user
- Compliance: anomalies show up at once, which cuts shadow IT and vulnerability exposure
Email triage agent (n8n, Notion, Gemini)
Context
As an IT manager I receive hundreds of emails a day: server alerts, automatic notifications, and user requests. Sorting them by hand eats the morning, and the critical ones risk being buried. The aim was an agent able to filter out the noise (bulk monitoring) without burning AI tokens for nothing, so that only the tasks genuinely needing me reach my Notion.
How it works
- Pre-AI filtering: an "If" node drops known exception and monitoring emails, to save API calls
- LLM analysis: the model reads the body of the email to judge relevance and structure the information
- Task router: if the model confirms this is an action for me, it produces a summary and a priority
- Notion publishing: the structured task is pushed to my Notion database with title, priority, category and action plan
Technical solution
// Shape of the JSON expected out of the model
// The intelligence sits in the system prompt of the Gemini LLM node
{
"is_task": true,
"is_for_me": true,
"owner": "Mbolafinaritra ANDRIANARIMANANA",
"priority": "CRITICAL",
"category": "Infrastructure & Security",
"title": "Critical maintenance: Linux server estate health",
"summary": "SRE analysis: action needed on 9 servers (Docker, Zabbix, NXFilter, etc.). 25 critical security updates on DNS and 44 on Zabbix. Saturation alert: Docker CPU (1.61 load) and NXFilter disk (81%).",
"action_plan": [
"Schedule reboot: ansible, docker, glpi-v3, samba, squid, wazuh",
"Patching: 25 DNS fixes + 44 Zabbix fixes",
"Investigation: Docker CPU load & NXFilter disk IO"
],
"source": "[email protected]",
"timestamp": "2026-05-15T08:35:05Z"
}
Results
- A clean Notion inbox, categorised and prioritised on its own
- 80% less time spent reading emails that do not concern me
- AI tokens saved thanks to the pre-LLM filter (If node)
- Live visibility on critical tasks through the Notion dashboard
Automated helpdesk management reporting from GLPI
Context
To manage a helpdesk handling hundreds of tickets each month, I automated GLPI data extraction, ITIL KPI computation and workload analysis by technician. The monthly report highlights delays, recurring incidents and workload imbalances so management can decide where to act.
How it works
- Ingestion and data cleaning: an n8n webhook receives the GLPI exports. Several JavaScript nodes clean them thoroughly (logistics and facilities tickets are excluded so they do not distort the purely IT metrics).
- SLA computation: the JS code recomputes the time-to-own and time-to-resolve success rates per technician and pulls out the top 5 recurring incidents.
- Structured storage: the cleaned data is spread across several tabs of a Google Sheet (summary, technicians, delays, top analysis) to keep the history.
- Cognitive analysis: the whole set of figures goes to Gemini 2.5 Flash-lite with a system engineer and manager prompt, to write the qualitative assessment.
Technical solution
// Extract from the framing prompt given to the model (Gemini)
Analyse the GLPI data for the month of: {{ $json.mois }}
REQUIRED REPORT STRUCTURE:
1. Quantitative analysis: volume, resolution rate, time-to-own and time-to-resolve stats, priorities.
2. Qualitative analysis: identify 3 critical areas (Wazuh security alerts, locked AD accounts, and so on). Give the finding and the real business impact.
3. Team performance: top contributors, workload analysis.
4. Improvements: propose actions based on rebalancing the load and cutting recurring incidents.
Write in an executive register. Do not use raw Markdown.
Results
- Informed steering: a formatted HTML email every month (executive dashboard) setting out the health of the information system.
- Decisions on data: automatic detection of overloaded people (one technician absorbing 44% of tickets, for instance), so the load can be rebalanced before it becomes a problem.
- Cybersecurity: the model picks out spikes of SOC alerts (CVEs reported by Wazuh, for instance) buried in the ticket flow, and flags the risk of alert fatigue in the security team.
Delivery of a school information system into production
Context
Delivery of an information-system project for a private secondary school, from business requirements to production. The platform connects enrolment, user accounts, school administration, marks, messaging and re-enrolment in one multi-role system. I owned the architecture, API exchanges, access management, containerised deployment and operations.
How it works
- Decoupled full-stack architecture: Symfony 7 REST API behind Nginx plus a Next.js 16 / React 19 / TypeScript frontend, orchestrated with Docker Compose (PostgreSQL 16, Symfony, Nginx, Next.js)
- Security: JWT authentication (RSA keys) with a token blacklist, Symfony firewall and a JWT guard on the frontend through middleware
- BFF pattern (Backend-For-Frontend): the frontend proxies every API call through its own routes, with the JWT living in an httpOnly cookie that client JavaScript cannot read (XSS protection)
- Rich data model: around 35 entities covering identity and security, academic structure (years, cycles, levels, classes, subjects, teacher assignments), LMS, quizzes, marks, attendance and messaging
- Key business rule: coefficient and teaching hours are defined per Level plus Subject pair; a class inherits the subjects of its level dynamically, and a teacher assignment belongs to one class by subject by school year
- Automatic provisioning: approving an enrolment request creates the student account and shows a one-time temporary password
- Re-enrolment by QR code: a single-use token per student is printed as a QR code on the final report card; the parent scans it, lands on a pre-filled re-enrolment form and confirms in seconds; the request joins the admin approval queue
Technical solution
// BFF pattern: the Next.js frontend proxies the Symfony API.
// The JWT lives in an httpOnly cookie: invisible to client JS (anti-XSS),
// never exposed to the browser, injected on the server side only.
// middleware.ts: JWT guard at the entrance of authenticated areas
export function middleware(req: NextRequest) {
const token = req.cookies.get("auth_token")?.value
if (req.nextUrl.pathname.startsWith("/dashboard") && !token) {
return NextResponse.redirect(new URL("/login", req.url))
}
return NextResponse.next()
}
// app/api/grades/route.ts: BFF route, proxy to the internal Symfony API
export async function POST(req: Request) {
const token = (await cookies()).get("auth_token")?.value
const res = await fetch(`${process.env.BACKEND_INTERNAL_URL}/api/grades`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`, // added server side, never client side
},
body: await req.text(),
})
return new Response(await res.text(), { status: res.status })
}
Results
- Complete platform running in production through Docker Compose: public website plus online enrolment plus a multi-role digital workspace (student / teacher / admin / parent)
- Broad functional scope: LMS (courses, chapters, PDF/video/link resources, resume where you left off), timed multiple-choice quizzes with marking scales, marks and weighted averages, attendance (present/absent/late plus supporting documents), clash-free timetable, messaging and announcements targeted by role and class
- Administration back office: user and role CRUD, school configuration (years, cycles, levels, classes, subjects), enrolment handling, analytics dashboard and CSV exports (users, marks, attendance)
- Re-enrolment by QR code, with nothing retyped each year: the QR printed on the report card opens a pre-filled re-enrolment request, secured by a single-use token and approved by the admin
- On the structural side: around 35 Doctrine entities, around 30 REST controllers, JWT security (RSA plus blacklist), anti-XSS BFF pattern, TypeScript on the frontend