commti after qwen start
This commit is contained in:
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
```
|
||||
|
||||
---
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user