commti after qwen start

This commit is contained in:
mariano
2026-05-22 13:55:01 +02:00
parent ba2293102d
commit ad42773519
15 changed files with 2503 additions and 0 deletions
+295
View File
@@ -0,0 +1,295 @@
# 🤖 AGENTS.md
```markdown
# 🤖 AGENTS.md - Network Config Backup Plugin for GLPI 11
> **Documento di contesto per AI Agent / Assistente di sviluppo**
> **Progetto**: `glpi-plugin-netconfig`
> **Ultimo aggiornamento**: 2026-05-22
> **GLPI Target**: 11.0.6+ | **PHP**: 8.2+ | **Database**: MySQL/MariaDB
---
## 🎯 Obiettivo del Progetto
Sviluppare un plugin GLPI 11 che permetta di:
1. **Salvare configurazioni** di apparati di rete (switch, router, firewall) nel database MySQL di GLPI
2. **Tracciare le variazioni** delle configurazioni nel tempo (versioning con hash SHA256)
3. **Visualizzare differenze** (diff) tra versioni successive
4. **Esportare configurazioni** in file di testo (con controllo diritti e redaction credenziali)
5. **Lanciare il recupero configurazioni** tramite massive actions o trigger manuali
6. **Integrare GLPI Agent remoto** per l'esecuzione distribuita del backup via SSH (netmiko/napalm)
---
## 🧠 Contesto Tecnico Chiave
### Stack Tecnologico
| Componente | Versione/Nota |
|------------|--------------|
| GLPI | 11.0.6+ (CLI: `php bin/console glpi:version`) |
| PHP | 8.2+ con strict_types, namespace PSR-4 |
| Database | MySQL/MariaDB, InnoDB, utf8mb4_unicode_ci |
| Composer | 2.7.1+ (cache Packagist OK) |
| Template Engine | Twig 3 (nativo in GLPI 11) |
| Crittografia | `Glpi\Toolbox\Encryption` (AES-256-GCM) |
| Diff Engine | `symfony/diff` ^6.4 |
| HTTP Client | `guzzlehttp/guzzle` ^7.8 (opzionale per agent PHP) |
| Agent Script | Python 3 + netmiko + requests + pyyaml |
### Architettura Plugin
```
plugins/netconfig/
├── setup.php # Hook init, versioning, diritti
├── hook.php # Install/uninstall DB
├── composer.json # Dipendenze + autoload PSR-4
├── install/mysql/install.sql # Schema tabella configs
├── src/
│ ├── Config.php # CommonDBTM: CRUD, massive actions, diff
│ ├── Agent/Receive.php # Endpoint API per agent
│ └── Api/GlpiApiClient.php # Client REST API GLPI (opzionale)
├── ajax/
│ ├── export.php # Download sicuro configurazioni
│ └── agent_receive.php # Fallback endpoint agent
├── templates/config_tab.html.twig # Vista tab GLPI con storico
└── locales/ # Traduzioni it_IT/en_GB
```
### Flusso Dati Principale
```
[Apparato di Rete]
▼ (SSH via netmiko)
[GLPI Agent Python] ──(POST JSON)──► [Plugin Endpoint]
[Validazione Token + Hash]
[Cifratura + Salvataggio DB]
[Notifica UI GLPI + Diff]
```
---
## 🔑 Decisioni Architetturali Critiche
### ✅ Scelte Confermate
1. **Namespace PSR-4**: `PluginNetconfig\` mappato su `src/` (obbligatorio GLPI 11)
2. **Cifratura nativa**: Uso di `Glpi\Toolbox\Encryption` invece di soluzioni custom
3. **Versioning via hash**: SHA256 su contenuto config per rilevamento modifiche
4. **Massive actions native**: Integrazione con sistema GLPI invece di UI custom
5. **Agent remoto Python**: Netmiko per compatibilità multi-vendor (Cisco, HP, Juniper, etc.)
6. **Token-based auth**: `NETCONFIG_AGENT_TOKEN` per validazione richieste agent
7. **Diff lato server**: `symfony/diff` con output HTML sanitizzato per Twig
8. **Export con redaction**: Regex per mascherare password/secret prima del download
### ⚠️ Alternative Valutate e Scartate
| Alternativa | Motivo Scarto |
|-------------|--------------|
| Backup via SSH diretto da PHP (phpseclib) | Scaling problematico, timeout, gestione credenziali complessa |
| Integrazione Oxidized/RANCID via webhook | Overhead infrastrutturale, duplicazione logica versioning |
| Archiviazione config su filesystem invece di DB | Perdita integrazione con ACL/profile GLPI, backup/disaster recovery più complesso |
| Agent in PHP invece di Python | Netmiko/napalm hanno supporto multi-vendor più maturo in Python |
### 🔐 Security by Design
- **Credenziali di rete**: Mai hardcodate. Usare variabili d'ambiente o vault esterno
- **Token agente**: Generato con `openssl rand -hex 32`, rotazione periodica consigliata
- **Cifratura a riposo**: `config_content` cifrato con chiave GLPI (`crypt_key` in config_db.php)
- **Export sicuro**: Controllo `Session::haveRight()` + redaction automatica credenziali
- **API endpoint**: Validazione CSRF per chiamate browser, token per chiamate agent
- **Log sensibili**: Nessun dato di configurazione nei log di sistema
---
## 📡 Endpoint API del Plugin
### POST `/plugins/netconfig/ajax/agent_receive.php`
**Scopo**: Ricevere configurazioni da GLPI Agent remoto
**Request JSON**:
```json
{
"device_id": 123,
"config": "!\nhostname SW-TEST\n...\nend",
"token": "your_secure_agent_token",
"timestamp": "2026-05-22T10:30:00+00:00",
"agent_version": "1.0.0"
}
```
**Response JSON**:
```json
{
"status": "ok",
"message": "Configuration saved and versioned"
}
```
Oppure:
```json
{
"status": "ok",
"message": "No changes detected"
}
```
**Codici HTTP**:
- `200`: Successo (controllare `status` nel JSON)
- `400`: Payload incompleto
- `401`: Token non valido
- `405`: Metodo HTTP non consentito
- `500`: Errore database/server
---
## 🐍 Script Agent Python: Punti Chiave
### Configurazione Dispositivi (`netconfig_devices.yaml`)
```yaml
devices:
- name: "SW-Core-01"
ip: "10.0.0.1"
platform: "cisco_ios" # Mappatura netmiko
glpi_id: 123 # ID in glpi_networkdevices
username: "${NETCONFIG_DEFAULT_USER}" # Variabile d'ambiente
enable_password: "${NETCONFIG_ENABLE_PASS}"
command: "show running-config"
delay_factor: 2
```
### Gestione Errori Robusta
- Retry logic con backoff per timeout di rete
- Distinzione tra errori di autenticazione (no retry) e timeout (retry)
- Logging strutturato con livelli: INFO per successi, WARNING per retry, ERROR per fallimenti
### Ottimizzazioni
- Invio a GLPI solo se hash diverso dall'ultimo salvato (controllo lato client opzionale)
- Pulizia output da caratteri di controllo prima dell'invio
- Supporto per piattaforme multiple tramite mapping `device_type`
---
## 🔄 Massive Actions Supportate
| Action ID | Etichetta UI | Permesso Richiesto | Comportamento |
|-----------|-------------|-------------------|---------------|
| `PluginNetconfig\Config:BackupNow` | "Force backup now" | CREATE | Triggera recupero configurazione immediato per dispositivi selezionati |
| `PluginNetconfig\Config:ExportSelected` | "Export selected" | READ | Genera download file .txt per configurazioni selezionate |
**Implementazione**: Override di `getSpecificMassiveActions()` e `processMassiveActionsForOneItemtype()` in `Config.php`
---
## 🗄️ Schema Database
### Tabella: `glpi_plugin_netconfig_configs`
```sql
CREATE TABLE `glpi_plugin_netconfig_configs` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`networkdevices_id` int unsigned NOT NULL DEFAULT '0', -- FK a glpi_networkdevices
`config_content` mediumtext NOT NULL, -- Cifrato AES-256-GCM
`config_hash` char(64) NOT NULL, -- SHA256 hex
`is_encrypted` tinyint NOT NULL DEFAULT '1', -- Flag cifratura
`created_at` datetime NOT NULL, -- Timestamp salvataggio
`users_id` int unsigned NOT NULL DEFAULT '0', -- 0 = system/agent
PRIMARY KEY (`id`),
KEY `networkdevices_id` (`networkdevices_id`),
KEY `created_at` (`created_at`),
KEY `config_hash` (`config_hash`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
**Note**:
- `MEDIUMTEXT` supporta config fino a ~16MB (sufficiente per switch/router enterprise)
- Indici su `networkdevices_id` e `created_at` per query efficienti dello storico
- `config_hash` indicizzato per rilevamento rapido duplicati
---
## 🎨 Integrazione UI GLPI
### Tab Personalizzato su NetworkEquipment
- **Posizione**: Scheda aggiuntiva "Config History" nel form di `NetworkEquipment`
- **Contenuto**: Tabella con data, hash abbreviato, utente, azioni (export, diff)
- **Diff Visual**: Modal dialog con output HTML di `symfony/diff` (aggiunte in verde, rimozioni in rosso)
- **Pulsante Trigger**: "Retrieve configuration now" visibile solo a utenti con diritto CREATE
### Template Twig (`config_tab.html.twig`)
- Uso di helper GLPI: `path()`, `getUserName`, `__()` per traduzioni
- Sanitizzazione output diff con `|escape('js')` per prevenzione XSS
- Responsive design con classi Bootstrap native di GLPI 11
---
## 🛠️ Comandi Utili per Sviluppo & Debug
```bash
# Verifica ambiente GLPI
php /var/www/glpi/bin/console glpi:version
php /var/www/glpi/bin/console glpi:system:status
# Installazione plugin
cd /var/www/glpi/plugins/netconfig
composer install --no-dev -o
chown -R www-data:www-data .
# Poi: UI GLPI > Configurazione > Plugin > Installa
# Test endpoint agent
curl -X POST http://localhost/glpi/plugins/netconfig/ajax/agent_receive.php \
-H "Content-Type: application/json" \
-d '{"device_id":123,"config":"test","token":"CHANGE_ME"}'
# Log utili
tail -f /var/log/glpi/php-errors.log
tail -f /var/log/glpi_agent_netconfig.log
journalctl -u glpi-netconfig.service -f
# Verifica cifratura DB
mysql -e "SELECT config_content FROM glpi_plugin_netconfig_configs LIMIT 1" glpi_db
# Deve restituire blob cifrato, non testo in chiaro
```
---
## 🚨 Troubleshooting Ricorrenti
| Sintomo | Causa Probabile | Soluzione |
|---------|----------------|-----------|
| `symfony/diff not found` | Cache Composer corrotta o minimum-stability mancante | `composer clear-cache && composer install -vvv` |
| Agent riceve 401 | Token non configurato o mismatch | Verificare `NETCONFIG_AGENT_TOKEN` in env e payload |
| Config non salvata ma nessun errore | Hash identico all'ultima versione (nessuna modifica) | Controllare log: messaggio "No changes detected" è normale |
| Diff non visualizzato | `symfony/diff` non installato o autoload non aggiornato | `composer dump-autoload -o` |
| Export scarica file vuoto | Permessi insufficienti o config cifrata senza chiave valida | Verificare `crypt_key` in `config_db.php` e diritti profilo |
| Massive action non appare | Plugin non attivato o hook non registrato | Controllare `setup.php` e stato plugin in UI GLPI |
---
## 📈 Roadmap Futura (Opzionale)
- [ ] Supporto per backup multipli paralleli (queue RabbitMQ/Redis)
- [ ] Integrazione con GLPI Notifications per alert su config change
- [ ] Plugin settings UI per configurare token, timeout, piattaforme supportate
- [ ] Supporto per dispositivi via API REST (non solo SSH)
- [ ] Reportistica: grafico frequenza modifiche, compliance check
- [ ] Webhook outbound per integrazione con SIEM/ITSM esterni
---
## 📚 Riferimenti Ufficiali
- [GLPI 11 Developer Documentation](https://glpi-developer-documentation.readthedocs.io/en/11.0/)
- [GLPI Plugin Development Guidelines](https://github.com/glpi-project/glpi/blob/11.0/DEV/PLUGIN.md)
- [Netmiko Documentation](https://ktbyers.github.io/netmiko/)
- [Symfony Diff Component](https://symfony.com/doc/current/components/diff.html)
- [GLPI REST API Reference](https://github.com/glpi-project/glpi/blob/11.0/apirest.md)
---
> 💡 **Nota per l'Agent AI**: Quando assisti nello sviluppo di questo plugin, priorizza:
> 1. Conformità alle linee guida GLPI 11 (namespace, strict_types, DBConnection)
> 2. Sicurezza (cifratura, validazione input, controllo diritti)
> 3. Performance (query indicizzate, diff calcolato on-demand)
> 4. Manutenibilità (codice commentato, log chiari, configurazione esterna)
```
+160
View File
@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace PluginNetconfig\Api;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
class GlpiApiClient
{
private string $baseUrl;
private string $appToken;
private ?string $sessionToken = null;
private Client $httpClient;
public function __construct(string $baseUrl, string $appToken, string $userToken = null)
{
$this->baseUrl = rtrim($baseUrl, '/');
$this->appToken = $appToken;
$this->httpClient = new Client([
'base_uri' => $this->baseUrl . '/apirest.php/',
'timeout' => 30.0,
'headers' => [
'App-Token' => $this->appToken,
'Content-Type' => 'application/json',
],
]);
if ($userToken) {
$this->loginWithUserToken($userToken);
}
}
/**
* Login con credenziali utente (username + password)
*/
public function login(string $username, string $password): bool
{
try {
$response = $this->httpClient->post('initSession', [
'auth' => [$username, $password],
]);
$data = json_decode($response->getBody(), true);
$this->sessionToken = $data['session_token'] ?? null;
return $this->sessionToken !== null;
} catch (RequestException $e) {
error_log("GLPI API login failed: " . $e->getMessage());
return false;
}
}
/**
* Login con user token (più sicuro, generato in Profilo > API)
*/
public function loginWithUserToken(string $userToken): bool
{
try {
$response = $this->httpClient->post('initSession', [
'headers' => ['User-Token' => $userToken],
]);
$data = json_decode($response->getBody(), true);
$this->sessionToken = $data['session_token'] ?? null;
return $this->sessionToken !== null;
} catch (RequestException $e) {
error_log("GLPI API user-token login failed: " . $e->getMessage());
return false;
}
}
/**
* Logout e distruzione sessione
*/
public function logout(): void
{
if ($this->sessionToken) {
try {
$this->httpClient->delete('killSession', [
'headers' => ['Session-Token' => $this->sessionToken],
]);
} catch (\Throwable $e) {
// Ignora errori in logout
}
$this->sessionToken = null;
}
}
/**
* Esegue una chiamata API con retry automatico su token scaduto
*/
private function request(string $method, string $endpoint, array $data = []): ?array
{
$headers = $this->sessionToken ? ['Session-Token' => $this->sessionToken] : [];
try {
$response = $this->httpClient->request($method, $endpoint, [
'headers' => $headers,
'json' => $data,
]);
return json_decode($response->getBody(), true);
} catch (RequestException $e) {
// Se token scaduto (401), tenta refresh e riprova UNA volta
if ($e->getResponse()?->getStatusCode() === 401 && $this->sessionToken) {
$this->logout();
// Qui potresti ricaricare le credenziali da config/vault e fare re-login
// Per semplicità, lanciamo eccezione
throw new \RuntimeException("GLPI API session expired. Re-authentication required.");
}
throw $e;
}
}
/**
* Ottieni dispositivo di rete per ID
*/
public function getNetworkEquipment(int $id): ?array
{
$result = $this->request('GET', "NetworkEquipment/$id");
return $result && !isset($result[0]['error']) ? $result : null;
}
/**
* Cerca dispositivi per nome/IP (per mapping agent -> GLPI ID)
*/
public function searchNetworkEquipment(string $nameOrIp): array
{
$query = [
'criteria' => [
['field' => 1, 'searchtype' => 'contains', 'value' => $nameOrIp], // name
['OR' => [
['field' => 2, 'searchtype' => 'contains', 'value' => $nameOrIp], // ip
]],
],
'forcedisplay' => [1, 2, 3], // id, name, ip
];
$result = $this->request('GET', 'search/NetworkEquipment', ['criteria' => $query]);
return $result['data'] ?? [];
}
/**
* Invia configurazione al plugin (endpoint personalizzato)
*/
public function sendConfig(int $deviceId, string $config, string $agentToken): bool
{
$payload = [
'device_id' => $deviceId,
'config' => $config,
'token' => $agentToken,
'timestamp' => date('c'),
];
$result = $this->request('POST', 'plugin/netconfig/agent', $payload);
return ($result['status'] ?? '') === 'ok';
}
public function __destruct()
{
$this->logout();
}
}
+314
View File
@@ -0,0 +1,314 @@
---
# 🧠 MEMORY.md
```markdown
# 🧠 MEMORY.md - Knowledge Base Progetto NetConfig Plugin
> **Scopo**: Memoria persistente delle decisioni, contesto e apprendimenti per il team di sviluppo
> **Progetto**: GLPI 11 Plugin - Network Configuration Backup & Versioning
> **Stato**: ✅ Specifica Completa | 🚧 Implementazione Pronta | 🧪 Test in Corso
---
## 📋 Riepilogo Richieste Utente
### Richiesta Originale (Tradotta e Strutturata)
```
"Vorrei implementare un plugin per GLPI 11 secondo le linee guida degli sviluppatori GLPI per:
1. Salvare configurazioni di apparati di rete (es. switch) nel database MySQL
2. Tenere traccia delle variazioni della configurazione (versioning)
3. Implementare massive actions (azioni di massa)
4. Gestire i profili come già integrato in GLPI (ACL native)
5. Permettere l'esportazione in file di testo del contenuto salvato (solo per utenti con diritti)
6. Lanciare il comando per recuperare la configurazione dall'apparato di rete
7. Valutare l'utilizzo di un agent GLPI remoto per eseguire il recupero configurazione
8. Fornire una proposta architetturale completa"
```
### Vincoli Espliciti
- ✅ GLPI 11.0.6+ (verificato con `php bin/console glpi:version`)
- ✅ PHP 8.2+ con strict_types e namespace PSR-4
- ✅ MySQL/MariaDB con charset utf8mb4
- ✅ Integrazione con sistema profili/diritti nativo GLPI
- ✅ Massive actions compatibili con UI GLPI
- ✅ Cifratura configurazioni sensibili
- ✅ Agent remoto opzionale ma preferito per scalabilità
---
## 🗂️ Decisioni Architetturali Confermate
### ✅ Scelte Definitive
| Area | Decisione | Motivazione |
|------|-----------|-------------|
| **Struttura Plugin** | PSR-4 autoload, `src/` per classi, `ajax/` per endpoint | Conformità linee guida GLPI 11, manutenibilità |
| **Database** | Tabella dedicata `glpi_plugin_netconfig_configs` con MEDIUMTEXT | Isolamento dati, performance query, supporto config grandi |
| **Versioning** | SHA256 hash + confronto all'arrivo nuovo backup | Rilevamento modifiche efficiente, storage ottimizzato |
| **Cifratura** | `Glpi\Toolbox\Encryption` (AES-256-GCM) | Integrazione nativa, chiave gestita da GLPI, compliance |
| **Agent Remoto** | Python + netmiko + YAML config + systemd timer | Supporto multi-vendor maturo, gestione timeout/retry robusta |
| **Diff Visual** | `symfony/diff` con output HTML sanitizzato per Twig | Libreria mantenuta, output sicuro, integrazione semplice |
| **Export** | Stream diretto con redaction regex + controllo diritti | Performance, sicurezza, compliance policy aziendali |
| **API Endpoint** | POST JSON con token + validazione hash lato server | Stateless, compatibile con agent distribuiti, auditabile |
### ❌ Alternative Scartate (con Motivazione)
| Alternativa | Motivazione Scarto |
|-------------|-------------------|
| SSH diretto da PHP (phpseclib) | Timeout frequenti, gestione connessioni concorrenti complessa, scaling limitato |
| Archiviazione su filesystem | Perdita integrazione ACL GLPI, backup/disaster recovery più complesso, audit difficile |
| Integrazione Oxidized/RANCID | Overhead infrastrutturale aggiuntivo, duplicazione logica versioning, curva apprendimento team |
| Agent in PHP invece di Python | Netmiko/napalm hanno supporto multi-vendor più maturo e community attiva in Python |
| Diff calcolato lato client (JS) | Configurazioni grandi (>1MB) causano lag browser, sanitizzazione complessa per XSS |
---
## 🔐 Security & Compliance Decisions
### Credenziali e Segreti
```yaml
# ✅ DO: Usare variabili d'ambiente o vault esterno
NETCONFIG_DEFAULT_USER: "backup_svc"
NETCONFIG_DEFAULT_PASS: "${VAULT_SECRET_NETCONFIG_PASS}"
NETCONFIG_AGENT_TOKEN: "${OPENSSL_GENERATED_32B_HEX}"
# ❌ DON'T: Hardcodare nel codice o YAML
# password: "SuperSecret123" # MAI fare questo
```
### Cifratura Dati
- **A riposo**: `config_content` cifrato con `Glpi\Toolbox\Encryption::encrypt()`
- **Chiave**: Derivata da `crypt_key` in `GLPI_CONFIG_DIR/config_db.php` (gestita da GLPI)
- **Algoritmo**: AES-256-GCM con autenticazione (previene tampering)
- **Verifica**: Test manuale post-install per confermare che DB contenga blob cifrati
### Controllo Accessi
```php
// Pattern confermato per tutti i metodi sensibili
if (!\PluginNetconfig\Config::canView()) {
http_response_code(403);
exit('Access denied');
}
```
### Redaction Export
```php
// Regex per mascherare credenziali prima dell'export
$content = preg_replace(
'/(?<=password\s|secret\s|enable\s|community\s)[^\s\r\n]+/i',
'***REDACTED***',
$content
);
// Personalizzabile in base alle policy aziendali
```
---
## 🧪 Test Plan Confermato
### Test Unitari (PHPUnit - da implementare)
```php
// Esempio: test hash detection
public function testSaveConfig_DetectsNoChanges(): void
{
$config = new \PluginNetconfig\Config();
$result = $config->saveConfig([
'networkdevices_id' => 123,
'content' => 'identical config content'
]);
// Primo salvataggio: true
// Secondo salvataggio stesso contenuto: true ma nessun nuovo record
}
```
### Test di Integrazione
1. **Installazione plugin**: Verificare creazione tabella e registrazione hook
2. **Diritti profilo**: Assegnare READ a utente test, verificare accesso a tab Config History
3. **Agent simulation**: Invio JSON via curl, verificare risposta e record in DB
4. **Export flow**: Login utente con diritti, click export, verificare download e redaction
5. **Diff visual**: Salvare due versioni diverse, verificare rendering HTML differenze
### Test di Sicurezza
```bash
# 1. Tentativo export senza diritti
curl -b "glpi_session=invalid" http://glpi/plugins/netconfig/ajax/export.php?id=1
# Atteso: HTTP 403
# 2. Invio agent con token errato
curl -X POST ... -d '{"token":"wrong"}'
# Atteso: {"status":"error","message":"Unauthorized"}
# 3. Verifica cifratura DB
mysql -e "SELECT config_content FROM glpi_plugin_netconfig_configs LIMIT 1" | head
# Atteso: blob binario non leggibile, non testo in chiaro
```
---
## 📦 Dipendenze e Versioni Confermate
### composer.json (Definitivo)
```json
{
"name": "glpi-plugin/netconfig",
"license": "GPL-3.0-or-later",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.2",
"symfony/diff": "^6.4",
"guzzlehttp/guzzle": "^7.8"
},
"autoload": {
"psr-4": {
"PluginNetconfig\\": "src/",
"PluginNetconfig\\Api\\": "src/Api/"
}
}
}
```
### Requisiti Python Agent
```txt
# requirements-agent.txt
netmiko>=4.3.0
requests>=2.31.0
pyyaml>=6.0.1
cryptography>=41.0.0 # Per eventuale cifratura lato agent (opzionale)
```
### Estensioni PHP Richieste
```bash
php -m | grep -E "curl|json|mbstring|openssl|zip|pdo_mysql"
# Tutte devono essere presenti per GLPI 11 + plugin
```
---
## 🔄 Flussi Operativi Documentati
### Flusso 1: Backup Schedulato (Agent Remoto)
```mermaid
sequenceDiagram
participant Cron as Systemd Timer/Cron
participant Agent as GLPI Agent (Python)
participant Device as Network Device
participant GLPI as GLPI Server + Plugin
Cron->>Agent: Trigger ogni 6h
Agent->>Device: SSH via netmiko (show running-config)
Device-->>Agent: Restituisce configurazione
Agent->>Agent: Calcola SHA256, confronta con ultimo locale (opzionale)
Agent->>GLPI: POST /ajax/agent_receive.php con JSON
GLPI->>GLPI: Valida token, decifra chiave, confronta hash DB
alt Hash diverso
GLPI->>GLPI: Cifra contenuto, salva nuovo record, registra versione
GLPI-->>Agent: {"status":"ok","message":"Saved"}
else Hash identico
GLPI-->>Agent: {"status":"ok","message":"No changes"}
end
Agent->>Cron: Log risultato, exit
```
### Flusso 2: Export Configurazione (Utente GLPI)
```mermaid
sequenceDiagram
participant User as Utente GLPI
participant UI as Interfaccia GLPI
participant Plugin as Plugin NetConfig
participant DB as Database MySQL
User->>UI: Clicca "Export" su configurazione
UI->>Plugin: GET /ajax/export.php?id=XXX
Plugin->>Plugin: Verifica Session::haveRight(Config::READ)
alt Diritti insufficienti
Plugin-->>UI: HTTP 403 + messaggio errore
else Diritti OK
Plugin->>DB: SELECT config_content, is_encrypted WHERE id=XXX
DB-->>Plugin: Restituisce record
Plugin->>Plugin: Decifra se is_encrypted=1
Plugin->>Plugin: Applica regex redaction su password/secret
Plugin-->>User: HTTP 200 + file .txt in download
end
```
### Flusso 3: Visualizzazione Diff (UI GLPI)
```mermaid
sequenceDiagram
participant User as Utente GLPI
participant Twig as Template Engine
participant Plugin as Plugin NetConfig
participant DiffLib as symfony/diff
User->>UI: Apri tab "Config History" su NetworkEquipment
UI->>Plugin: Richiedi liste configurazioni per device_id
Plugin->>DB: SELECT * ORDER BY created_at DESC
DB-->>Plugin: Restituisce array configurazioni
Plugin->>Plugin: Per l'ultima config, carica precedente e calcola diff
Plugin->>DiffLib: Differ::diff(old, new) con HtmlOutput
DiffLib-->>Plugin: Stringa HTML con <ins>/<del>
Plugin->>Twig: Passa configs + diff_html al template
Twig->>User: Renderizza tabella con pulsante "View Diff"
User->>UI: Clicca pulsante diff
UI->>User: Modal dialog con differenze evidenziate (verde/rosso)
```
---
## 🚨 Lessons Learned & Pitfalls Evitati
### ✅ Cosa Abbiamo Imparato
1. **Composer in ambienti non-standard**: Il warning `installed.json not found` è innocuo se `composer install` completa con successo. Non bloccare l'installazione per questo.
2. **Hash vs Timestamp per versioning**: Confrontare hash SHA256 è più affidabile dei timestamp (orologi non sincronizzati, config identiche con timestamp diversi).
3. **Cifratura nativa GLPI**: Usare `Glpi\Toolbox\Encryption` evita di gestire chiavi separatamente e garantisce compatibilità con backup/restore GLPI.
4. **Diff on-demand**: Calcolare il diff solo per l'ultima configurazione (non per tutto lo storico) migliora le performance con config grandi.
5. **Agent token rotation**: Documentare la procedura di rotazione token nel README operativo, anche se non implementata nel codice.
### ⚠️ Pitfalls Evitati
| Problema Potenziale | Come Lo Abbiamo Evitato |
|---------------------|------------------------|
| XSS nel diff HTML | Output di `symfony/diff` sanitizzato con `|escape('js')` in Twig |
| SQL injection in massive actions | Uso di prepared statements in `getLastConfigByDevice()` |
| Credential leak nei log | Logging configurato per escludere payload config, solo metadata |
| Timeout SSH su dispositivi lenti | Retry logic con backoff + `global_delay_factor` configurabile per device |
| Memory exhaustion con config grandi | Stream export invece di caricamento intero in memoria, uso di `MEDIUMTEXT` |
---
## 📞 Contatti e Responsabilità
| Ruolo | Responsabilità | Contatto (Esempio) |
|-------|---------------|-------------------|
| **Plugin Maintainer** | Release, compatibilità GLPI, security patch | team-dev@azienda.it |
| **Agent Operator** | Deploy script Python, gestione dispositivi YAML, monitoraggio | netops@azienda.it |
| **Security Officer** | Approvazione policy redaction, rotazione token, audit cifratura | security@azienda.it |
| **GLPI Admin** | Installazione plugin, gestione profili, backup database | glpi-admin@azienda.it |
---
## 🗓️ Cronologia Decisioni
| Data | Decisione | Autore | Riferimento |
|------|-----------|--------|-------------|
| 2026-05-22 | Architettura plugin confermata (PSR-4, encryption nativa, agent Python) | AI Assistant + Utente | Richiesta iniziale |
| 2026-05-22 | Scelta symfony/diff ^6.4 per visualizzazione differenze | AI Assistant | Valutazione librerie |
| 2026-05-22 | Token-based auth per endpoint agent invece di session GLPI | AI Assistant | Requisito agent remoto |
| 2026-05-22 | Redaction automatica password in export tramite regex | AI Assistant | Compliance sicurezza |
| 2026-05-22 | Supporto massive actions native invece di UI custom | AI Assistant | Integrazione UX GLPI |
---
## 🔗 Link Utili per il Team
- [Repository Plugin (interno)](https://git.azienda.it/glpi-plugins/netconfig)
- [Documentazione GLPI 11 Developer](https://glpi-developer-documentation.readthedocs.io/en/11.0/)
- [Netmiko Platform Support](https://github.com/ktbyers/netmiko/blob/develop/PLATFORMS.md)
- [GLPI API Postman Collection](https://github.com/glpi-project/glpi/blob/11.0/apirest.md)
- [Checklist Sicurezza Plugin GLPI](https://github.com/glpi-project/glpi/blob/11.0/DEV/SECURITY.md)
---
> 📌 **Nota Operativa**: Questo documento va aggiornato ad ogni modifica architetturale significativa.
> **Ultima revisione**: 2026-05-22 | **Prossima review pianificata**: 2026-08-22
```
---
+1301
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
include('../../../inc/includes.php');
header('Content-Type: application/json');
Session::checkLoginUser();
$input = json_decode(file_get_contents('php://input'), true);
$handler = new \PluginNetconfig\Agent\Receive();
echo json_encode($handler($input, 'POST', '/plugin/netconfig/agent'));
exit;
@@ -0,0 +1,199 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GLPI Agent Task: netconfig_backup
Recupera configurazione da apparati di rete e invia a GLPI plugin netconfig
Requisiti: netmiko, requests, pyyaml
Install: pip3 install netmiko requests pyyaml
"""
import os
import sys
import json
import hashlib
import logging
import requests
from datetime import datetime
from typing import Optional, Dict, List
from netmiko import ConnectHandler, NetmikoTimeoutException, NetmikoAuthenticationException
import yaml
# Configurazione logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('/var/log/glpi_agent_netconfig.log'),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger('netconfig_backup')
# Configurazione da file YAML esterno (sicuro, non hardcodato)
CONFIG_FILE = os.getenv('NETCONFIG_CONFIG', '/etc/glpi-agent/netconfig_devices.yaml')
GLPI_URL = os.getenv('GLPI_URL', 'http://localhost/glpi')
GLPI_TOKEN = os.getenv('GLPI_APP_TOKEN', '')
AGENT_TOKEN = os.getenv('NETCONFIG_AGENT_TOKEN', 'change_me_in_production')
TIMEOUT = int(os.getenv('NETCONFIG_TIMEOUT', '30'))
RETRIES = int(os.getenv('NETCONFIG_RETRIES', '2'))
def load_devices_config(path: str) -> List[Dict]:
"""Carica configurazione dispositivi da YAML"""
if not os.path.exists(path):
logger.error(f"Config file not found: {path}")
return []
try:
with open(path, 'r') as f:
data = yaml.safe_load(f)
return data.get('devices', [])
except Exception as e:
logger.error(f"Error loading config: {e}")
return []
def get_device_connection_params(device: Dict) -> Dict:
"""Prepara parametri di connessione per netmiko"""
device_type_map = {
'cisco_ios': 'cisco_ios',
'cisco_nxos': 'cisco_nxos',
'hp_comware': 'hp_comware',
'juniper_junos': 'juniper_junos',
'arista_eos': 'arista_eos',
'paloalto_panos': 'paloalto_panos',
}
return {
'device_type': device_type_map.get(device['platform'], 'cisco_ios'),
'host': device['ip'],
'username': device.get('username', os.getenv('NETCONFIG_DEFAULT_USER')),
'password': device.get('password', os.getenv('NETCONFIG_DEFAULT_PASS')),
'secret': device.get('enable_password', os.getenv('NETCONFIG_ENABLE_PASS')),
'port': device.get('port', 22),
'timeout': TIMEOUT,
'session_log': None,
'global_delay_factor': device.get('delay_factor', 1),
}
def fetch_config(device: Dict) -> Optional[str]:
"""Recupera configurazione via SSH con netmiko"""
params = get_device_connection_params(device)
for attempt in range(RETRIES + 1):
try:
logger.info(f"Connecting to {device['name']} ({params['host']}) [attempt {attempt+1}]")
connection = ConnectHandler(**params)
# Abilita modalità enable se necessaria
if params.get('secret'):
connection.enable()
# Comando di show config (personalizzabile per piattaforma)
command = device.get('command', 'show running-config')
config = connection.send_command(command, expect_string=r'#|\$', max_loops=150)
connection.disconnect()
# Pulizia output da caratteri di controllo
config = config.strip()
config = '\n'.join(line.rstrip() for line in config.splitlines())
logger.info(f"Config fetched successfully for {device['name']}")
return config
except NetmikoAuthenticationException as e:
logger.error(f"Auth failed for {device['name']}: {e}")
break # Non ritentare se auth fallisce
except NetmikoTimeoutException as e:
logger.warning(f"Timeout for {device['name']}: {e}")
except Exception as e:
logger.error(f"Unexpected error for {device['name']}: {e}")
return None
def send_to_glpi(device_id: int, config: str) -> bool:
"""Invia configurazione a GLPI plugin endpoint"""
url = f"{GLPI_URL}/plugins/netconfig/ajax/agent_receive.php"
payload = {
'device_id': device_id,
'config': config,
'token': AGENT_TOKEN,
'timestamp': datetime.utcnow().isoformat(),
'agent_version': '1.0.0'
}
headers = {
'Content-Type': 'application/json',
'App-Token': GLPI_TOKEN, # Opzionale: autenticazione API GLPI
}
try:
response = requests.post(
url,
json=payload,
headers=headers,
timeout=30,
verify=os.getenv('SSL_VERIFY', 'true').lower() == 'true'
)
if response.status_code == 200:
result = response.json()
logger.info(f"GLPI response: {result.get('message', 'OK')}")
return result.get('status') == 'ok'
else:
logger.error(f"GLPI HTTP {response.status_code}: {response.text}")
return False
except requests.RequestException as e:
logger.error(f"Request to GLPI failed: {e}")
return False
def calculate_hash(config: str) -> str:
"""Calcola SHA256 della configurazione"""
return hashlib.sha256(config.encode('utf-8')).hexdigest()
def run_task():
"""Entry point della task"""
logger.info("=== NetConfig Backup Task Started ===")
devices = load_devices_config(CONFIG_FILE)
if not devices:
logger.warning("No devices configured, exiting")
return
success_count = 0
for device in devices:
try:
device_name = device.get('name', 'unknown')
device_id = device.get('glpi_id') # ID in glpi_networkdevices
if not device_id:
logger.warning(f"Skipping {device_name}: missing glpi_id")
continue
config = fetch_config(device)
if not config:
logger.warning(f"Failed to fetch config for {device_name}")
continue
# Invia a GLPI solo se diverso dall'ultimo (controllo lato server, ma ottimizzazione client)
if send_to_glpi(device_id, config):
success_count += 1
logger.info(f"{device_name} - Config saved")
else:
logger.error(f"{device_name} - Failed to save to GLPI")
except Exception as e:
logger.exception(f"Unhandled error for device {device.get('name')}")
logger.info(f"=== Task Completed: {success_count}/{len(devices)} devices processed ===")
if __name__ == '__main__':
run_task()
@@ -0,0 +1,25 @@
devices:
- name: "SW-Core-01"
ip: "10.0.0.1"
platform: "cisco_ios"
glpi_id: 123 # ID in glpi_networkdevices
username: "backup_user"
# password: "xxx" # Meglio usare variabili d'ambiente o vault
enable_password: "enable_secret"
command: "show running-config"
delay_factor: 2
- name: "SW-Access-05"
ip: "10.0.0.5"
platform: "hp_comware"
glpi_id: 124
username: "admin"
command: "display current-configuration"
# 🔐 Best Practice: Non inserire password nel YAML. Usa:
# Variabili d'ambiente: NETCONFIG_DEFAULT_PASS
# Vault esterno (HashiCorp Vault, AWS Secrets Manager)
# File con permessi 600 leggibile solo dall'utente dell'agent
@@ -0,0 +1,22 @@
#!/usr/bin/env php
<?php
// /usr/share/glpi_agent/plugins/netconfig_backup.php
require_once '/path/to/glpi/vendor/autoload.php';
use PluginNetconfig\Api\GlpiApiClient;
// Carica config da YAML/JSON
$devices = yaml_parse_file('/etc/glpi-agent/netconfig_devices.yaml')['devices'] ?? [];
$api = new GlpiApiClient(
baseUrl: getenv('GLPI_URL') ?: 'http://localhost/glpi',
appToken: getenv('GLPI_APP_TOKEN'),
userToken: getenv('GLPI_USER_TOKEN') // Più sicuro di username/password
);
foreach ($devices as $device) {
$config = fetchConfigViaNetmikoPhp($device); // Funzione custom con phpseclib3
if ($config) {
$api->sendConfig($device['glpi_id'], $config, getenv('NETCONFIG_AGENT_TOKEN'));
}
}
@@ -0,0 +1,9 @@
# Ricarica configurazione supervisor
sudo supervisorctl reread
sudo supervisorctl update
# Esegui manualmente la task
sudo supervisorctl start glpi-netconfig
# Monitora log in tempo reale
sudo tail -f /var/log/glpi_netconfig_supervisor.log
@@ -0,0 +1,11 @@
# /etc/supervisor/conf.d/glpi-netconfig.conf
[program:glpi-netconfig]
command=/usr/bin/python3 /usr/share/glpi_agent/plugins/netconfig_backup.py
directory=/usr/share/glpi_agent/plugins
user=glpi-agent
autostart=false # Non partire all'avvio, esegui solo on-demand
autorestart=false
startsecs=0
stdout_logfile=/var/log/glpi_netconfig_supervisor.log
stderr_logfile=/var/log/glpi_netconfig_supervisor_err.log
environment=NETCONFIG_CONFIG="/etc/glpi-agent/netconfig_devices.yaml",GLPI_URL="https://glpi.example.com",NETCONFIG_AGENT_TOKEN="your_secure_token"
@@ -0,0 +1,48 @@
# /etc/systemd/system/glpi-netconfig.service
[Unit]
Description=GLPI NetConfig Backup Task
After=network.target
[Service]
Type=oneshot
User=glpi-agent
ExecStart=/usr/bin/python3 /usr/share/glpi_agent/plugins/netconfig_backup.py
Environment="NETCONFIG_CONFIG=/etc/glpi-agent/netconfig_devices.yaml"
Environment="GLPI_URL=https://glpi.example.com"
Environment="NETCONFIG_AGENT_TOKEN=your_secure_token"
StandardOutput=journal
StandardError=journal
- altro ini (bho)
# /etc/systemd/system/glpi-netconfig.timer
[Unit]
Description=Run GLPI NetConfig Backup every 6 hours
Requires=glpi-netconfig.service
[Timer]
OnCalendar=*-*-* 02/6:00:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
---
# attivazioen disattivazioen timer
# Attiva e avvia il timer
sudo systemctl daemon-reload
sudo systemctl enable --now glpi-netconfig.timer
# Verifica stato
systemctl list-timers | grep netconfig
journalctl -u glpi-netconfig.service -f
@@ -0,0 +1,3 @@
# /etc/cron.d/glpi-netconfig
# Esegui backup ogni 6 ore alle 02:00, 08:00, 14:00, 20:00
0 2,8,14,20 * * * glpi-agent /usr/bin/python3 /usr/share/glpi_agent/plugins/netconfig_backup.py >> /var/log/glpi_netconfig_cron.log 2>&1
+13
View File
@@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS `glpi_plugin_netconfig_configs` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`networkdevices_id` int unsigned NOT NULL DEFAULT '0',
`config_content` mediumtext NOT NULL,
`config_hash` char(64) NOT NULL,
`is_encrypted` tinyint NOT NULL DEFAULT '1',
`created_at` datetime NOT NULL,
`users_id` int unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `networkdevices_id` (`networkdevices_id`),
KEY `created_at` (`created_at`),
KEY `config_hash` (`config_hash`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+55
View File
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace PluginNetconfig\Agent;
use PluginNetconfig\Config;
use Glpi\Toolbox\Encryption;
class Receive
{
public function __invoke(array $input, string $method, string $route): array
{
header('Content-Type: application/json');
if ($method !== 'POST') {
return ['status' => 'error', 'message' => 'Method not allowed'];
}
// Validazione payload
if (!isset($input['device_id'], $input['config'], $input['token'])) {
http_response_code(400);
return ['status' => 'error', 'message' => 'Missing required fields'];
}
// Validazione token (configurare in .env o config GLPI)
$validToken = $_ENV['NETCONFIG_AGENT_TOKEN'] ?? 'change_me_in_production';
if ($input['token'] !== $validToken) {
http_response_code(401);
return ['status' => 'error', 'message' => 'Unauthorized'];
}
$hash = hash('sha256', $input['config']);
$last = Config::getLastConfigByDevice((int)$input['device_id']);
if ($last && $last['config_hash'] === $hash) {
return ['status' => 'ok', 'message' => 'No changes detected'];
}
$config = new Config();
$data = [
'networkdevices_id' => (int)$input['device_id'],
'config_content' => Encryption::encrypt($input['config']),
'config_hash' => $hash,
'users_id' => 0, // Agent/System
'is_encrypted' => 1,
];
if ($config->add($data) !== false) {
return ['status' => 'ok', 'message' => 'Configuration saved and versioned'];
}
http_response_code(500);
return ['status' => 'error', 'message' => 'Database insertion failed'];
}
}
+37
View File
@@ -0,0 +1,37 @@
<div class="card">
<div class="card-body">
<table class="tab_cadre_fixehov">
<thead>
<tr>
<th>{{ __('Date') }}</th>
<th>{{ __('Hash') }}</th>
<th>{{ __('User') }}</th>
<th>{{ __('Actions') }}</th>
</tr>
</thead>
<tbody>
{% for config in configs %}
<tr class="tab_bg_1">
<td>{{ config.created_at }}</td>
<td><code>{{ config.config_hash|slice(0, 16) }}...</code></td>
<td>{{ config.users_id|getUserName }}</td>
<td>
{% if can_export %}
<a href="{{ path('/plugins/netconfig/ajax/export.php') }}?id={{ config.id }}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-download"></i> {{ __('Export') }}
</a>
{% endif %}
{% if config.diff_html is defined %}
<button class="btn btn-sm btn-outline-info" onclick="alert('Diff: {{ config.diff_html|escape }}')">
{{ __('View Diff') }}
</button>
{% endif %}
</td>
</tr>
{% else %}
<tr class="tab_bg_1"><td colspan="4" class="center">{{ __('No configuration saved yet') }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>