Add nodes management and initial setup pages
- Implemented nodes management functionality in `nodes.php` including create, update, and delete actions. - Added form validation and error handling for node operations. - Created a new setup page in `setup.php` for initial administrator account creation. - Included user feedback messages for successful operations and errors. - Designed user interface for both nodes management and setup processes.
This commit is contained in:
610
pathvector-admin/pages/asns.php
Normal file
610
pathvector-admin/pages/asns.php
Normal file
@@ -0,0 +1,610 @@
|
||||
<?php
|
||||
/**
|
||||
* ASNs Management Page
|
||||
*/
|
||||
|
||||
$asnManager = new ASN($db);
|
||||
$validator = new Validator();
|
||||
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
$id = $_GET['id'] ?? null;
|
||||
$message = '';
|
||||
$error = '';
|
||||
|
||||
// Handle form submissions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!$auth->verifyCsrfToken($_POST['csrf_token'] ?? '')) {
|
||||
$error = 'Invalid security token. Please try again.';
|
||||
} else {
|
||||
$postAction = $_POST['action'] ?? '';
|
||||
|
||||
switch ($postAction) {
|
||||
case 'create':
|
||||
if (!hasPermission('create_peers')) {
|
||||
$error = 'You do not have permission to create ASNs.';
|
||||
break;
|
||||
}
|
||||
|
||||
$asnNumber = trim($_POST['asn'] ?? '');
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
|
||||
// Validate ASN
|
||||
if (!$validator->validateASN($asnNumber)) {
|
||||
$error = 'Invalid ASN number.';
|
||||
break;
|
||||
}
|
||||
|
||||
if (empty($name)) {
|
||||
$error = 'Name is required.';
|
||||
break;
|
||||
}
|
||||
|
||||
// Build defaults from form
|
||||
$defaults = buildDefaultsFromForm($_POST);
|
||||
|
||||
$result = $asnManager->create($asnNumber, $name, $description, $defaults);
|
||||
|
||||
if ($result['success']) {
|
||||
$logger->log('asn', 'Created ASN', ['asn' => $asnNumber, 'name' => $name]);
|
||||
header('Location: ?page=asns&message=ASN created successfully');
|
||||
exit;
|
||||
} else {
|
||||
$error = $result['message'];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
if (!hasPermission('edit_peers')) {
|
||||
$error = 'You do not have permission to edit ASNs.';
|
||||
break;
|
||||
}
|
||||
|
||||
$id = $_POST['id'] ?? '';
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
|
||||
if (empty($name)) {
|
||||
$error = 'Name is required.';
|
||||
break;
|
||||
}
|
||||
|
||||
$defaults = buildDefaultsFromForm($_POST);
|
||||
|
||||
$result = $asnManager->update($id, [
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
'defaults' => $defaults,
|
||||
]);
|
||||
|
||||
if ($result['success']) {
|
||||
$logger->log('asn', 'Updated ASN', ['id' => $id, 'name' => $name]);
|
||||
header('Location: ?page=asns&message=ASN updated successfully');
|
||||
exit;
|
||||
} else {
|
||||
$error = $result['message'];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
if (!hasPermission('delete_peers')) {
|
||||
$error = 'You do not have permission to delete ASNs.';
|
||||
break;
|
||||
}
|
||||
|
||||
$id = $_POST['id'] ?? '';
|
||||
$asn = $asnManager->get($id);
|
||||
|
||||
if (!$asn) {
|
||||
$error = 'ASN not found.';
|
||||
break;
|
||||
}
|
||||
|
||||
$result = $asnManager->delete($id);
|
||||
|
||||
if ($result['success']) {
|
||||
$logger->log('asn', 'Deleted ASN', ['asn' => $asn['asn'], 'name' => $asn['name']]);
|
||||
header('Location: ?page=asns&message=ASN deleted successfully');
|
||||
exit;
|
||||
} else {
|
||||
$error = $result['message'];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get message from query string
|
||||
if (isset($_GET['message'])) {
|
||||
$message = $_GET['message'];
|
||||
}
|
||||
|
||||
// Helper function to build defaults from form
|
||||
function buildDefaultsFromForm(array $post): array {
|
||||
$defaults = [];
|
||||
|
||||
// String fields
|
||||
$stringFields = [
|
||||
'router_id', 'source4', 'source6', 'irr_server', 'rtr_server',
|
||||
'bgpq_path', 'bgpq_args', 'bird_directory', 'bird_socket',
|
||||
'cache_directory', 'log_file', 'hostname', 'peeringdb_api_key',
|
||||
'peeringdb_cache_file', 'peeringdb_query_timeout', 'kernel_table',
|
||||
];
|
||||
|
||||
foreach ($stringFields as $field) {
|
||||
if (isset($post[$field]) && $post[$field] !== '') {
|
||||
$defaults[$field] = trim($post[$field]);
|
||||
}
|
||||
}
|
||||
|
||||
// Boolean fields
|
||||
$booleanFields = [
|
||||
'keep_filtered', 'merge_paths', 'rpki_filter', 'irr_filter',
|
||||
'transit_locking', 'graceful_shutdown', 'no_announce',
|
||||
];
|
||||
|
||||
foreach ($booleanFields as $field) {
|
||||
if (isset($post[$field])) {
|
||||
$defaults[$field] = $post[$field] === '1';
|
||||
}
|
||||
}
|
||||
|
||||
// Integer fields
|
||||
$intFields = [
|
||||
'default_route_limit4', 'default_route_limit6', 'pref_src4_placeholder',
|
||||
'pref_src6_placeholder', 'kernel_learn', 'kernel_export',
|
||||
];
|
||||
|
||||
foreach ($intFields as $field) {
|
||||
if (isset($post[$field]) && $post[$field] !== '') {
|
||||
$defaults[$field] = (int) $post[$field];
|
||||
}
|
||||
}
|
||||
|
||||
// Array fields (comma-separated)
|
||||
$arrayFields = [
|
||||
'prefixes4' => 'prefixes4',
|
||||
'prefixes6' => 'prefixes6',
|
||||
'communities_blackhole' => 'blackhole',
|
||||
'communities_nopeer' => 'nopeer',
|
||||
];
|
||||
|
||||
foreach ($arrayFields as $postField => $defaultField) {
|
||||
if (isset($post[$postField]) && $post[$postField] !== '') {
|
||||
$values = array_filter(array_map('trim', explode(',', $post[$postField])));
|
||||
if (!empty($values)) {
|
||||
$defaults[$defaultField] = $values;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
// Get ASN for edit
|
||||
$editAsn = null;
|
||||
if ($action === 'edit' && $id) {
|
||||
$editAsn = $asnManager->get($id);
|
||||
if (!$editAsn) {
|
||||
$error = 'ASN not found.';
|
||||
$action = 'list';
|
||||
}
|
||||
}
|
||||
|
||||
// Get all ASNs for list view
|
||||
$asns = $asnManager->getAll();
|
||||
?>
|
||||
|
||||
<?php if ($action === 'list'): ?>
|
||||
<!-- List View -->
|
||||
<div class="page-header">
|
||||
<div class="d-flex flex-items-center flex-justify-between">
|
||||
<div>
|
||||
<h1 class="h2 mb-1">ASNs</h1>
|
||||
<p class="color-fg-muted mb-0">Manage your Autonomous System Numbers</p>
|
||||
</div>
|
||||
<?php if (hasPermission('create_peers')): ?>
|
||||
<a href="?page=asns&action=create" class="btn btn-primary">
|
||||
<svg class="octicon mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.75 2a.75.75 0 01.75.75V7h4.25a.75.75 0 110 1.5H8.5v4.25a.75.75 0 11-1.5 0V8.5H2.75a.75.75 0 010-1.5H7V2.75A.75.75 0 017.75 2z"/></svg>
|
||||
New ASN
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
<?php if ($message): ?>
|
||||
<div class="flash flash-success mb-3">
|
||||
<?= e($message) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="flash flash-error mb-3">
|
||||
<?= e($error) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (empty($asns)): ?>
|
||||
<div class="blankslate">
|
||||
<svg class="octicon blankslate-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="24" height="24"><path fill-rule="evenodd" d="M1.5 14.25c0 .138.112.25.25.25H4v-1.25a.75.75 0 01.75-.75h2.5a.75.75 0 01.75.75v1.25h2.25a.25.25 0 00.25-.25V1.75a.25.25 0 00-.25-.25h-8.5a.25.25 0 00-.25.25v12.5z"/></svg>
|
||||
<h3 class="blankslate-heading">No ASNs configured</h3>
|
||||
<p>Get started by creating your first ASN.</p>
|
||||
<?php if (hasPermission('create_peers')): ?>
|
||||
<a href="?page=asns&action=create" class="btn btn-primary">Create ASN</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="Box">
|
||||
<div class="overflow-auto">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ASN</th>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Router ID</th>
|
||||
<th>Nodes</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$nodeManager = new Node($db);
|
||||
foreach ($asns as $asn):
|
||||
$asnNodes = $nodeManager->getByAsn($asn['id']);
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="Label Label--accent">AS<?= e($asn['asn']) ?></span>
|
||||
</td>
|
||||
<td class="text-bold"><?= e($asn['name']) ?></td>
|
||||
<td class="color-fg-muted"><?= e($asn['description'] ?? '-') ?></td>
|
||||
<td>
|
||||
<code><?= e($asn['defaults']['router_id'] ?? '-') ?></code>
|
||||
</td>
|
||||
<td>
|
||||
<span class="Counter"><?= count($asnNodes) ?></span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a href="?page=asns&action=edit&id=<?= e($asn['id']) ?>" class="btn btn-sm">Edit</a>
|
||||
<?php if (hasPermission('delete_peers')): ?>
|
||||
<form method="POST" class="d-inline" onsubmit="return confirmDelete('Are you sure you want to delete AS<?= e($asn['asn']) ?>?')">
|
||||
<?= csrfField() ?>
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="id" value="<?= e($asn['id']) ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php elseif ($action === 'create' || $action === 'edit'): ?>
|
||||
<!-- Create/Edit Form -->
|
||||
<div class="page-header">
|
||||
<div class="d-flex flex-items-center">
|
||||
<a href="?page=asns" class="btn-octicon mr-2">
|
||||
<svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.78 12.53a.75.75 0 01-1.06 0L2.47 8.28a.75.75 0 010-1.06l4.25-4.25a.75.75 0 011.06 1.06L4.81 7h7.44a.75.75 0 010 1.5H4.81l2.97 2.97a.75.75 0 010 1.06z"/></svg>
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="h2 mb-1"><?= $action === 'edit' ? 'Edit ASN' : 'New ASN' ?></h1>
|
||||
<p class="color-fg-muted mb-0">
|
||||
<?= $action === 'edit' ? 'AS' . e($editAsn['asn']) . ' - ' . e($editAsn['name']) : 'Configure a new Autonomous System Number' ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
<?php if ($error): ?>
|
||||
<div class="flash flash-error mb-3">
|
||||
<?= e($error) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" action="">
|
||||
<?= csrfField() ?>
|
||||
<input type="hidden" name="action" value="<?= $action === 'edit' ? 'update' : 'create' ?>">
|
||||
<?php if ($editAsn): ?>
|
||||
<input type="hidden" name="id" value="<?= e($editAsn['id']) ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="Box mb-4">
|
||||
<div class="Box-header">
|
||||
<h3 class="Box-title">Basic Information</h3>
|
||||
</div>
|
||||
<div class="Box-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="asn">ASN Number *</label>
|
||||
<input type="number"
|
||||
id="asn"
|
||||
name="asn"
|
||||
class="form-control"
|
||||
style="max-width: 200px;"
|
||||
value="<?= e($editAsn['asn'] ?? '') ?>"
|
||||
min="1"
|
||||
max="4294967295"
|
||||
<?= $editAsn ? 'readonly' : 'required' ?>>
|
||||
<p class="form-hint">Your Autonomous System Number (e.g., 65530)</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="name">Name *</label>
|
||||
<input type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
class="form-control form-input-wide"
|
||||
value="<?= e($editAsn['name'] ?? '') ?>"
|
||||
required>
|
||||
<p class="form-hint">A friendly name for this ASN</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="description">Description</label>
|
||||
<textarea id="description"
|
||||
name="description"
|
||||
class="form-control form-input-wide"
|
||||
rows="2"><?= e($editAsn['description'] ?? '') ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="Box mb-4">
|
||||
<div class="Box-header">
|
||||
<h3 class="Box-title">Network Configuration</h3>
|
||||
</div>
|
||||
<div class="Box-body">
|
||||
<div class="d-flex flex-wrap" style="gap: 16px;">
|
||||
<div class="form-group flex-1" style="min-width: 200px;">
|
||||
<label class="form-label" for="router_id">Router ID</label>
|
||||
<input type="text"
|
||||
id="router_id"
|
||||
name="router_id"
|
||||
class="form-control"
|
||||
value="<?= e($editAsn['defaults']['router_id'] ?? '') ?>"
|
||||
placeholder="e.g., 192.0.2.1">
|
||||
<p class="form-hint">Default router ID for all nodes</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group flex-1" style="min-width: 200px;">
|
||||
<label class="form-label" for="source4">Source IPv4</label>
|
||||
<input type="text"
|
||||
id="source4"
|
||||
name="source4"
|
||||
class="form-control"
|
||||
value="<?= e($editAsn['defaults']['source4'] ?? '') ?>"
|
||||
placeholder="e.g., 192.0.2.1">
|
||||
<p class="form-hint">Default source address for IPv4</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group flex-1" style="min-width: 200px;">
|
||||
<label class="form-label" for="source6">Source IPv6</label>
|
||||
<input type="text"
|
||||
id="source6"
|
||||
name="source6"
|
||||
class="form-control"
|
||||
value="<?= e($editAsn['defaults']['source6'] ?? '') ?>"
|
||||
placeholder="e.g., 2001:db8::1">
|
||||
<p class="form-hint">Default source address for IPv6</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="prefixes4">IPv4 Prefixes</label>
|
||||
<input type="text"
|
||||
id="prefixes4"
|
||||
name="prefixes4"
|
||||
class="form-control form-input-wide"
|
||||
value="<?= e(implode(', ', $editAsn['defaults']['prefixes'] ?? [])) ?>"
|
||||
placeholder="192.0.2.0/24, 198.51.100.0/24">
|
||||
<p class="form-hint">Comma-separated list of IPv4 prefixes you originate</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="prefixes6">IPv6 Prefixes</label>
|
||||
<input type="text"
|
||||
id="prefixes6"
|
||||
name="prefixes6"
|
||||
class="form-control form-input-wide"
|
||||
value="<?= e(implode(', ', $editAsn['defaults']['prefixes6'] ?? [])) ?>"
|
||||
placeholder="2001:db8::/32">
|
||||
<p class="form-hint">Comma-separated list of IPv6 prefixes you originate</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="Box mb-4">
|
||||
<div class="Box-header">
|
||||
<h3 class="Box-title">Filtering Options</h3>
|
||||
</div>
|
||||
<div class="Box-body">
|
||||
<div class="d-flex flex-wrap" style="gap: 16px;">
|
||||
<div class="form-group flex-1" style="min-width: 200px;">
|
||||
<label class="form-label" for="irr_server">IRR Server</label>
|
||||
<input type="text"
|
||||
id="irr_server"
|
||||
name="irr_server"
|
||||
class="form-control"
|
||||
value="<?= e($editAsn['defaults']['irr_server'] ?? 'rr.ntt.net') ?>"
|
||||
placeholder="rr.ntt.net">
|
||||
</div>
|
||||
|
||||
<div class="form-group flex-1" style="min-width: 200px;">
|
||||
<label class="form-label" for="rtr_server">RTR Server (RPKI)</label>
|
||||
<input type="text"
|
||||
id="rtr_server"
|
||||
name="rtr_server"
|
||||
class="form-control"
|
||||
value="<?= e($editAsn['defaults']['rtr_server'] ?? '') ?>"
|
||||
placeholder="rtr.example.com:8282">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap" style="gap: 24px;">
|
||||
<div class="form-checkbox">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
name="rpki_filter"
|
||||
value="1"
|
||||
<?= ($editAsn['defaults']['rpki_filter'] ?? false) ? 'checked' : '' ?>>
|
||||
Enable RPKI filtering
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-checkbox">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
name="irr_filter"
|
||||
value="1"
|
||||
<?= ($editAsn['defaults']['irr_filter'] ?? false) ? 'checked' : '' ?>>
|
||||
Enable IRR filtering
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-checkbox">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
name="keep_filtered"
|
||||
value="1"
|
||||
<?= ($editAsn['defaults']['keep_filtered'] ?? false) ? 'checked' : '' ?>>
|
||||
Keep filtered routes
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap mt-3" style="gap: 16px;">
|
||||
<div class="form-group flex-1" style="min-width: 150px;">
|
||||
<label class="form-label" for="default_route_limit4">Default Route Limit (IPv4)</label>
|
||||
<input type="number"
|
||||
id="default_route_limit4"
|
||||
name="default_route_limit4"
|
||||
class="form-control"
|
||||
value="<?= e($editAsn['defaults']['default_route_limit4'] ?? '') ?>"
|
||||
min="0">
|
||||
</div>
|
||||
|
||||
<div class="form-group flex-1" style="min-width: 150px;">
|
||||
<label class="form-label" for="default_route_limit6">Default Route Limit (IPv6)</label>
|
||||
<input type="number"
|
||||
id="default_route_limit6"
|
||||
name="default_route_limit6"
|
||||
class="form-control"
|
||||
value="<?= e($editAsn['defaults']['default_route_limit6'] ?? '') ?>"
|
||||
min="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="Box mb-4">
|
||||
<div class="Box-header">
|
||||
<h3 class="Box-title">Pathvector Paths</h3>
|
||||
</div>
|
||||
<div class="Box-body">
|
||||
<div class="d-flex flex-wrap" style="gap: 16px;">
|
||||
<div class="form-group flex-1" style="min-width: 200px;">
|
||||
<label class="form-label" for="bird_directory">BIRD Directory</label>
|
||||
<input type="text"
|
||||
id="bird_directory"
|
||||
name="bird_directory"
|
||||
class="form-control"
|
||||
value="<?= e($editAsn['defaults']['bird_directory'] ?? '/etc/bird') ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group flex-1" style="min-width: 200px;">
|
||||
<label class="form-label" for="bird_socket">BIRD Socket</label>
|
||||
<input type="text"
|
||||
id="bird_socket"
|
||||
name="bird_socket"
|
||||
class="form-control"
|
||||
value="<?= e($editAsn['defaults']['bird_socket'] ?? '/run/bird/bird.ctl') ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap" style="gap: 16px;">
|
||||
<div class="form-group flex-1" style="min-width: 200px;">
|
||||
<label class="form-label" for="cache_directory">Cache Directory</label>
|
||||
<input type="text"
|
||||
id="cache_directory"
|
||||
name="cache_directory"
|
||||
class="form-control"
|
||||
value="<?= e($editAsn['defaults']['cache_directory'] ?? '/var/cache/pathvector') ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group flex-1" style="min-width: 200px;">
|
||||
<label class="form-label" for="bgpq_path">BGPQ4 Path</label>
|
||||
<input type="text"
|
||||
id="bgpq_path"
|
||||
name="bgpq_path"
|
||||
class="form-control"
|
||||
value="<?= e($editAsn['defaults']['bgpq_path'] ?? 'bgpq4') ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="Box mb-4">
|
||||
<div class="Box-header">
|
||||
<h3 class="Box-title">Advanced Options</h3>
|
||||
</div>
|
||||
<div class="Box-body">
|
||||
<div class="d-flex flex-wrap" style="gap: 24px;">
|
||||
<div class="form-checkbox">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
name="merge_paths"
|
||||
value="1"
|
||||
<?= ($editAsn['defaults']['merge_paths'] ?? false) ? 'checked' : '' ?>>
|
||||
Enable merge paths (ADD-PATH)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-checkbox">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
name="transit_locking"
|
||||
value="1"
|
||||
<?= ($editAsn['defaults']['transit_locking'] ?? false) ? 'checked' : '' ?>>
|
||||
Enable transit locking
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-checkbox">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
name="graceful_shutdown"
|
||||
value="1"
|
||||
<?= ($editAsn['defaults']['graceful_shutdown'] ?? false) ? 'checked' : '' ?>>
|
||||
Enable graceful shutdown
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-checkbox">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
name="no_announce"
|
||||
value="1"
|
||||
<?= ($editAsn['defaults']['no_announce'] ?? false) ? 'checked' : '' ?>>
|
||||
No announce (disable route announcements)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-items-center" style="gap: 8px;">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<?= $action === 'edit' ? 'Update ASN' : 'Create ASN' ?>
|
||||
</button>
|
||||
<a href="?page=asns" class="btn">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
314
pathvector-admin/pages/dashboard.php
Normal file
314
pathvector-admin/pages/dashboard.php
Normal file
@@ -0,0 +1,314 @@
|
||||
<?php
|
||||
/**
|
||||
* Dashboard Page
|
||||
* Main overview with statistics and quick actions
|
||||
*/
|
||||
|
||||
// Initialize managers
|
||||
$asnManager = new ASN($db);
|
||||
$nodeManager = new Node($db);
|
||||
$peerManager = new Peer($db);
|
||||
$templateManager = new Template($db);
|
||||
$hostManager = new Host($db);
|
||||
|
||||
// Get statistics
|
||||
$asns = $asnManager->getAll();
|
||||
$nodes = $nodeManager->getAll();
|
||||
$peers = $peerManager->getAll();
|
||||
$templates = $templateManager->getAll();
|
||||
$hosts = $hostManager->getAll();
|
||||
|
||||
// Count by status
|
||||
$peersByStatus = [];
|
||||
foreach ($peers as $peer) {
|
||||
$status = $peer['enabled'] ?? true ? 'enabled' : 'disabled';
|
||||
$peersByStatus[$status] = ($peersByStatus[$status] ?? 0) + 1;
|
||||
}
|
||||
|
||||
// Recent activity
|
||||
$recentLogs = $logger->search([], 10);
|
||||
?>
|
||||
|
||||
<div class="page-header">
|
||||
<div class="d-flex flex-items-center flex-justify-between">
|
||||
<div>
|
||||
<h1 class="h2 mb-1">Dashboard</h1>
|
||||
<p class="color-fg-muted mb-0">Overview of your BGP configuration</p>
|
||||
</div>
|
||||
<div>
|
||||
<?php if (hasPermission('execute_commands')): ?>
|
||||
<a href="?page=execute" class="btn btn-primary">
|
||||
<svg class="octicon mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zM6.379 5.227A.25.25 0 006 5.442v5.117a.25.25 0 00.379.214l4.264-2.559a.25.25 0 000-.428L6.379 5.227z"/></svg>
|
||||
Execute
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
<!-- Statistics Cards -->
|
||||
<div class="d-flex flex-wrap" style="gap: 16px; margin-bottom: 24px;">
|
||||
<div class="stat-card flex-1" style="min-width: 200px;">
|
||||
<div class="d-flex flex-items-center">
|
||||
<div class="mr-3">
|
||||
<svg class="octicon color-fg-muted" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="32" height="32"><path fill-rule="evenodd" d="M1.5 14.25c0 .138.112.25.25.25H4v-1.25a.75.75 0 01.75-.75h2.5a.75.75 0 01.75.75v1.25h2.25a.25.25 0 00.25-.25V1.75a.25.25 0 00-.25-.25h-8.5a.25.25 0 00-.25.25v12.5zM1.75 16A1.75 1.75 0 010 14.25V1.75C0 .784.784 0 1.75 0h8.5C11.216 0 12 .784 12 1.75v12.5c0 .085-.006.168-.018.25h2.268a.25.25 0 00.25-.25V8.285a.25.25 0 00-.111-.208l-1.055-.703a.75.75 0 11.832-1.248l1.055.703c.487.325.779.871.779 1.456v5.965A1.75 1.75 0 0114.25 16h-3.5a.75.75 0 01-.197-.026c-.099.017-.2.026-.303.026h-3a.75.75 0 01-.75-.75V14h-1v1.25a.75.75 0 01-.75.75h-3z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="stat-value"><?= count($asns) ?></div>
|
||||
<div class="stat-label">ASNs</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card flex-1" style="min-width: 200px;">
|
||||
<div class="d-flex flex-items-center">
|
||||
<div class="mr-3">
|
||||
<svg class="octicon color-fg-muted" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="32" height="32"><path fill-rule="evenodd" d="M1.75 1A1.75 1.75 0 000 2.75v4c0 .372.116.717.314 1a1.742 1.742 0 00-.314 1v4c0 .966.784 1.75 1.75 1.75h12.5A1.75 1.75 0 0016 12.75v-4c0-.372-.116-.717-.314-1 .198-.283.314-.628.314-1v-4A1.75 1.75 0 0014.25 1H1.75z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="stat-value"><?= count($nodes) ?></div>
|
||||
<div class="stat-label">Nodes</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card flex-1" style="min-width: 200px;">
|
||||
<div class="d-flex flex-items-center">
|
||||
<div class="mr-3">
|
||||
<svg class="octicon color-fg-muted" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="32" height="32"><path fill-rule="evenodd" d="M11.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122V6A2.5 2.5 0 0110 8.5H6a1 1 0 00-1 1v1.128a2.251 2.251 0 11-1.5 0V5.372a2.25 2.25 0 111.5 0v1.836A2.492 2.492 0 016 7h4a1 1 0 001-1v-.628A2.25 2.25 0 019.5 3.25z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="stat-value"><?= count($peers) ?></div>
|
||||
<div class="stat-label">Peers</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card flex-1" style="min-width: 200px;">
|
||||
<div class="d-flex flex-items-center">
|
||||
<div class="mr-3">
|
||||
<svg class="octicon color-fg-muted" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="32" height="32"><path fill-rule="evenodd" d="M4 1.75C4 .784 4.784 0 5.75 0h5.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v8.586A1.75 1.75 0 0114.25 15h-9a.75.75 0 010-1.5h9a.25.25 0 00.25-.25V6h-2.75A1.75 1.75 0 0110 4.25V1.5H5.75a.25.25 0 00-.25.25v2.5a.75.75 0 01-1.5 0v-2.5z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="stat-value"><?= count($templates) ?></div>
|
||||
<div class="stat-label">Templates</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card flex-1" style="min-width: 200px;">
|
||||
<div class="d-flex flex-items-center">
|
||||
<div class="mr-3">
|
||||
<svg class="octicon color-fg-muted" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="32" height="32"><path fill-rule="evenodd" d="M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0114.25 15H1.75A1.75 1.75 0 010 13.25V2.75z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="stat-value"><?= count($hosts) ?></div>
|
||||
<div class="stat-label">Hosts</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap" style="gap: 24px;">
|
||||
<!-- Quick Actions -->
|
||||
<div class="flex-1" style="min-width: 300px;">
|
||||
<div class="Box">
|
||||
<div class="Box-header">
|
||||
<h3 class="Box-title">Quick Actions</h3>
|
||||
</div>
|
||||
<div class="Box-body">
|
||||
<div class="d-flex flex-column" style="gap: 8px;">
|
||||
<?php if (hasPermission('create_peers')): ?>
|
||||
<a href="?page=asns&action=create" class="btn btn-outline btn-block text-left">
|
||||
<svg class="octicon mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.75 2a.75.75 0 01.75.75V7h4.25a.75.75 0 110 1.5H8.5v4.25a.75.75 0 11-1.5 0V8.5H2.75a.75.75 0 010-1.5H7V2.75A.75.75 0 017.75 2z"/></svg>
|
||||
Add New ASN
|
||||
</a>
|
||||
<a href="?page=nodes&action=create" class="btn btn-outline btn-block text-left">
|
||||
<svg class="octicon mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.75 2a.75.75 0 01.75.75V7h4.25a.75.75 0 110 1.5H8.5v4.25a.75.75 0 11-1.5 0V8.5H2.75a.75.75 0 010-1.5H7V2.75A.75.75 0 017.75 2z"/></svg>
|
||||
Add New Node
|
||||
</a>
|
||||
<a href="?page=peers&action=create" class="btn btn-outline btn-block text-left">
|
||||
<svg class="octicon mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.75 2a.75.75 0 01.75.75V7h4.25a.75.75 0 110 1.5H8.5v4.25a.75.75 0 11-1.5 0V8.5H2.75a.75.75 0 010-1.5H7V2.75A.75.75 0 017.75 2z"/></svg>
|
||||
Add New Peer
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<a href="?page=config" class="btn btn-outline btn-block text-left">
|
||||
<svg class="octicon mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.679 7.932c.412-.621 1.242-1.75 2.366-2.717C5.175 4.242 6.527 3.5 8 3.5c1.473 0 2.824.742 3.955 1.715 1.124.967 1.954 2.096 2.366 2.717a.119.119 0 010 .136c-.412.621-1.242 1.75-2.366 2.717C10.825 11.758 9.473 12.5 8 12.5c-1.473 0-2.824-.742-3.955-1.715C2.92 9.818 2.09 8.69 1.679 8.068a.119.119 0 010-.136zM8 2c-1.981 0-3.67.992-4.933 2.078C1.797 5.169.88 6.423.43 7.1a1.619 1.619 0 000 1.798c.45.678 1.367 1.932 2.637 3.024C4.329 13.008 6.019 14 8 14c1.981 0 3.67-.992 4.933-2.078 1.27-1.091 2.187-2.345 2.637-3.023a1.619 1.619 0 000-1.798c-.45-.678-1.367-1.932-2.637-3.023C11.671 2.992 9.981 2 8 2zm0 8a2 2 0 100-4 2 2 0 000 4z"/></svg>
|
||||
Preview Configuration
|
||||
</a>
|
||||
<?php if (hasPermission('execute_commands')): ?>
|
||||
<a href="?page=execute&action=validate" class="btn btn-outline btn-block text-left">
|
||||
<svg class="octicon mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/></svg>
|
||||
Validate Configuration
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent ASNs -->
|
||||
<div class="flex-1" style="min-width: 300px;">
|
||||
<div class="Box">
|
||||
<div class="Box-header d-flex flex-items-center flex-justify-between">
|
||||
<h3 class="Box-title">ASNs</h3>
|
||||
<a href="?page=asns" class="Link--primary text-small">View all</a>
|
||||
</div>
|
||||
<?php if (empty($asns)): ?>
|
||||
<div class="Box-body color-fg-muted">
|
||||
No ASNs configured yet.
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<ul class="list-style-none">
|
||||
<?php foreach (array_slice($asns, 0, 5) as $asn): ?>
|
||||
<li class="Box-row d-flex flex-items-center">
|
||||
<span class="Label Label--secondary mr-2">AS<?= e($asn['asn']) ?></span>
|
||||
<span class="flex-1"><?= e($asn['name']) ?></span>
|
||||
<a href="?page=asns&action=edit&id=<?= e($asn['id']) ?>" class="Link--secondary text-small">Edit</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap mt-4" style="gap: 24px;">
|
||||
<!-- Recent Nodes -->
|
||||
<div class="flex-1" style="min-width: 300px;">
|
||||
<div class="Box">
|
||||
<div class="Box-header d-flex flex-items-center flex-justify-between">
|
||||
<h3 class="Box-title">Nodes</h3>
|
||||
<a href="?page=nodes" class="Link--primary text-small">View all</a>
|
||||
</div>
|
||||
<?php if (empty($nodes)): ?>
|
||||
<div class="Box-body color-fg-muted">
|
||||
No nodes configured yet.
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<ul class="list-style-none">
|
||||
<?php foreach (array_slice($nodes, 0, 5) as $node): ?>
|
||||
<?php
|
||||
$nodeAsn = $asnManager->get($node['asn_id']);
|
||||
$nodePeers = array_filter($peers, fn($p) => $p['node_id'] === $node['id']);
|
||||
?>
|
||||
<li class="Box-row d-flex flex-items-center">
|
||||
<div class="flex-1">
|
||||
<div class="text-bold"><?= e($node['name']) ?></div>
|
||||
<div class="text-small color-fg-muted">
|
||||
<?= $nodeAsn ? 'AS' . e($nodeAsn['asn']) : 'Unknown ASN' ?> •
|
||||
<?= count($nodePeers) ?> peers
|
||||
</div>
|
||||
</div>
|
||||
<a href="?page=nodes&action=edit&id=<?= e($node['id']) ?>" class="Link--secondary text-small">Edit</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="flex-1" style="min-width: 300px;">
|
||||
<div class="Box">
|
||||
<div class="Box-header d-flex flex-items-center flex-justify-between">
|
||||
<h3 class="Box-title">Recent Activity</h3>
|
||||
<a href="?page=logs" class="Link--primary text-small">View all</a>
|
||||
</div>
|
||||
<?php if (empty($recentLogs)): ?>
|
||||
<div class="Box-body color-fg-muted">
|
||||
No recent activity.
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<ul class="list-style-none">
|
||||
<?php foreach (array_slice($recentLogs, 0, 5) as $log): ?>
|
||||
<li class="Box-row">
|
||||
<div class="d-flex flex-items-start">
|
||||
<span class="status-dot <?= $log['level'] === 'error' ? 'danger' : ($log['level'] === 'warning' ? 'warning' : 'success') ?> mt-1"></span>
|
||||
<div class="flex-1 min-width-0">
|
||||
<div class="text-small text-truncate"><?= e($log['message']) ?></div>
|
||||
<div class="text-small color-fg-muted">
|
||||
<?= e($log['category']) ?> • <?= formatDate($log['timestamp']) ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Peers Summary -->
|
||||
<div class="mt-4">
|
||||
<div class="Box">
|
||||
<div class="Box-header d-flex flex-items-center flex-justify-between">
|
||||
<h3 class="Box-title">Recent Peers</h3>
|
||||
<a href="?page=peers" class="Link--primary text-small">View all</a>
|
||||
</div>
|
||||
<?php if (empty($peers)): ?>
|
||||
<div class="Box-body color-fg-muted">
|
||||
No peers configured yet.
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="overflow-auto">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Peer Name</th>
|
||||
<th>ASN</th>
|
||||
<th>Neighbor</th>
|
||||
<th>Node</th>
|
||||
<th>Template</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach (array_slice($peers, 0, 10) as $peer): ?>
|
||||
<?php
|
||||
$peerNode = $nodeManager->get($peer['node_id']);
|
||||
$peerAsn = $peerNode ? $asnManager->get($peerNode['asn_id']) : null;
|
||||
?>
|
||||
<tr>
|
||||
<td class="text-bold"><?= e($peer['name']) ?></td>
|
||||
<td><?= isset($peer['config']['asn']) ? 'AS' . e($peer['config']['asn']) : '-' ?></td>
|
||||
<td>
|
||||
<?php if (!empty($peer['config']['neighbors'])): ?>
|
||||
<?= e(implode(', ', array_slice($peer['config']['neighbors'], 0, 2))) ?>
|
||||
<?php if (count($peer['config']['neighbors']) > 2): ?>
|
||||
<span class="color-fg-muted">+<?= count($peer['config']['neighbors']) - 2 ?></span>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= $peerNode ? e($peerNode['name']) : '-' ?></td>
|
||||
<td>
|
||||
<?php if (!empty($peer['config']['template'])): ?>
|
||||
<span class="Label Label--secondary"><?= e($peer['config']['template']) ?></span>
|
||||
<?php else: ?>
|
||||
-
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($peer['enabled'] ?? true): ?>
|
||||
<span class="Label Label--success">Enabled</span>
|
||||
<?php else: ?>
|
||||
<span class="Label Label--secondary">Disabled</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<a href="?page=peers&action=edit&id=<?= e($peer['id']) ?>" class="btn btn-sm">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
586
pathvector-admin/pages/nodes.php
Normal file
586
pathvector-admin/pages/nodes.php
Normal file
@@ -0,0 +1,586 @@
|
||||
<?php
|
||||
/**
|
||||
* Nodes Management Page
|
||||
*/
|
||||
|
||||
$asnManager = new ASN($db);
|
||||
$nodeManager = new Node($db);
|
||||
$validator = new Validator();
|
||||
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
$id = $_GET['id'] ?? null;
|
||||
$message = '';
|
||||
$error = '';
|
||||
|
||||
// Handle form submissions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!$auth->verifyCsrfToken($_POST['csrf_token'] ?? '')) {
|
||||
$error = 'Invalid security token. Please try again.';
|
||||
} else {
|
||||
$postAction = $_POST['action'] ?? '';
|
||||
|
||||
switch ($postAction) {
|
||||
case 'create':
|
||||
if (!hasPermission('create_peers')) {
|
||||
$error = 'You do not have permission to create nodes.';
|
||||
break;
|
||||
}
|
||||
|
||||
$asnId = $_POST['asn_id'] ?? '';
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
|
||||
if (empty($asnId)) {
|
||||
$error = 'Please select an ASN.';
|
||||
break;
|
||||
}
|
||||
|
||||
if (empty($name)) {
|
||||
$error = 'Name is required.';
|
||||
break;
|
||||
}
|
||||
|
||||
$config = buildNodeConfigFromForm($_POST);
|
||||
|
||||
$result = $nodeManager->create($asnId, $name, $description, $config);
|
||||
|
||||
if ($result['success']) {
|
||||
$logger->log('node', 'Created node', ['name' => $name, 'asn_id' => $asnId]);
|
||||
header('Location: ?page=nodes&message=Node created successfully');
|
||||
exit;
|
||||
} else {
|
||||
$error = $result['message'];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
if (!hasPermission('edit_peers')) {
|
||||
$error = 'You do not have permission to edit nodes.';
|
||||
break;
|
||||
}
|
||||
|
||||
$id = $_POST['id'] ?? '';
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
|
||||
if (empty($name)) {
|
||||
$error = 'Name is required.';
|
||||
break;
|
||||
}
|
||||
|
||||
$config = buildNodeConfigFromForm($_POST);
|
||||
|
||||
$result = $nodeManager->update($id, [
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
'config' => $config,
|
||||
]);
|
||||
|
||||
if ($result['success']) {
|
||||
$logger->log('node', 'Updated node', ['id' => $id, 'name' => $name]);
|
||||
header('Location: ?page=nodes&message=Node updated successfully');
|
||||
exit;
|
||||
} else {
|
||||
$error = $result['message'];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
if (!hasPermission('delete_peers')) {
|
||||
$error = 'You do not have permission to delete nodes.';
|
||||
break;
|
||||
}
|
||||
|
||||
$id = $_POST['id'] ?? '';
|
||||
$node = $nodeManager->get($id);
|
||||
|
||||
if (!$node) {
|
||||
$error = 'Node not found.';
|
||||
break;
|
||||
}
|
||||
|
||||
$result = $nodeManager->delete($id);
|
||||
|
||||
if ($result['success']) {
|
||||
$logger->log('node', 'Deleted node', ['name' => $node['name']]);
|
||||
header('Location: ?page=nodes&message=Node deleted successfully');
|
||||
exit;
|
||||
} else {
|
||||
$error = $result['message'];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get message from query string
|
||||
if (isset($_GET['message'])) {
|
||||
$message = $_GET['message'];
|
||||
}
|
||||
|
||||
// Helper function to build config from form
|
||||
function buildNodeConfigFromForm(array $post): array {
|
||||
$config = [];
|
||||
|
||||
// String fields
|
||||
$stringFields = [
|
||||
'router_id', 'source4', 'source6', 'hostname', 'bird_directory',
|
||||
'bird_socket', 'cache_directory', 'irr_server', 'rtr_server',
|
||||
];
|
||||
|
||||
foreach ($stringFields as $field) {
|
||||
if (isset($post[$field]) && $post[$field] !== '') {
|
||||
$config[$field] = trim($post[$field]);
|
||||
}
|
||||
}
|
||||
|
||||
// Boolean fields
|
||||
$booleanFields = [
|
||||
'keep_filtered', 'rpki_filter', 'irr_filter',
|
||||
];
|
||||
|
||||
foreach ($booleanFields as $field) {
|
||||
if (isset($post[$field])) {
|
||||
$config[$field] = $post[$field] === '1';
|
||||
}
|
||||
}
|
||||
|
||||
// Prefixes
|
||||
if (!empty($post['prefixes4'])) {
|
||||
$config['prefixes'] = array_filter(array_map('trim', explode(',', $post['prefixes4'])));
|
||||
}
|
||||
|
||||
if (!empty($post['prefixes6'])) {
|
||||
$config['prefixes6'] = array_filter(array_map('trim', explode(',', $post['prefixes6'])));
|
||||
}
|
||||
|
||||
// Templates
|
||||
if (!empty($post['templates'])) {
|
||||
$config['templates'] = array_filter(array_map('trim', explode(',', $post['templates'])));
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
// Get node for edit
|
||||
$editNode = null;
|
||||
if ($action === 'edit' && $id) {
|
||||
$editNode = $nodeManager->get($id);
|
||||
if (!$editNode) {
|
||||
$error = 'Node not found.';
|
||||
$action = 'list';
|
||||
}
|
||||
}
|
||||
|
||||
// Get all ASNs and nodes
|
||||
$asns = $asnManager->getAll();
|
||||
$nodes = $nodeManager->getAll();
|
||||
|
||||
// Filter by ASN if specified
|
||||
$filterAsnId = $_GET['asn_id'] ?? '';
|
||||
if ($filterAsnId) {
|
||||
$nodes = array_filter($nodes, fn($n) => $n['asn_id'] === $filterAsnId);
|
||||
}
|
||||
?>
|
||||
|
||||
<?php if ($action === 'list'): ?>
|
||||
<!-- List View -->
|
||||
<div class="page-header">
|
||||
<div class="d-flex flex-items-center flex-justify-between">
|
||||
<div>
|
||||
<h1 class="h2 mb-1">Nodes</h1>
|
||||
<p class="color-fg-muted mb-0">Manage your router nodes</p>
|
||||
</div>
|
||||
<?php if (hasPermission('create_peers')): ?>
|
||||
<a href="?page=nodes&action=create" class="btn btn-primary">
|
||||
<svg class="octicon mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.75 2a.75.75 0 01.75.75V7h4.25a.75.75 0 110 1.5H8.5v4.25a.75.75 0 11-1.5 0V8.5H2.75a.75.75 0 010-1.5H7V2.75A.75.75 0 017.75 2z"/></svg>
|
||||
New Node
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
<?php if ($message): ?>
|
||||
<div class="flash flash-success mb-3">
|
||||
<?= e($message) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="flash flash-error mb-3">
|
||||
<?= e($error) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Filter -->
|
||||
<?php if (!empty($asns)): ?>
|
||||
<div class="mb-3">
|
||||
<form method="GET" class="d-flex flex-items-center" style="gap: 8px;">
|
||||
<input type="hidden" name="page" value="nodes">
|
||||
<select name="asn_id" class="form-select" onchange="this.form.submit()">
|
||||
<option value="">All ASNs</option>
|
||||
<?php foreach ($asns as $asn): ?>
|
||||
<option value="<?= e($asn['id']) ?>" <?= $filterAsnId === $asn['id'] ? 'selected' : '' ?>>
|
||||
AS<?= e($asn['asn']) ?> - <?= e($asn['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (empty($nodes)): ?>
|
||||
<div class="blankslate">
|
||||
<svg class="octicon blankslate-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="24" height="24"><path fill-rule="evenodd" d="M1.75 1A1.75 1.75 0 000 2.75v4c0 .372.116.717.314 1a1.742 1.742 0 00-.314 1v4c0 .966.784 1.75 1.75 1.75h12.5A1.75 1.75 0 0016 12.75v-4c0-.372-.116-.717-.314-1 .198-.283.314-.628.314-1v-4A1.75 1.75 0 0014.25 1H1.75z"/></svg>
|
||||
<h3 class="blankslate-heading">No nodes configured</h3>
|
||||
<p>Get started by creating your first node.</p>
|
||||
<?php if (hasPermission('create_peers')): ?>
|
||||
<a href="?page=nodes&action=create" class="btn btn-primary">Create Node</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="Box">
|
||||
<div class="overflow-auto">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>ASN</th>
|
||||
<th>Router ID</th>
|
||||
<th>Description</th>
|
||||
<th>Peers</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$peerManager = new Peer($db);
|
||||
foreach ($nodes as $node):
|
||||
$nodeAsn = $asnManager->get($node['asn_id']);
|
||||
$nodePeers = $peerManager->getByNode($node['id']);
|
||||
?>
|
||||
<tr>
|
||||
<td class="text-bold"><?= e($node['name']) ?></td>
|
||||
<td>
|
||||
<?php if ($nodeAsn): ?>
|
||||
<span class="Label Label--secondary">AS<?= e($nodeAsn['asn']) ?></span>
|
||||
<?php else: ?>
|
||||
<span class="color-fg-muted">-</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<code><?= e($node['config']['router_id'] ?? '-') ?></code>
|
||||
</td>
|
||||
<td class="color-fg-muted"><?= e($node['description'] ?? '-') ?></td>
|
||||
<td>
|
||||
<span class="Counter"><?= count($nodePeers) ?></span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<a href="?page=peers&node_id=<?= e($node['id']) ?>" class="btn btn-sm">View Peers</a>
|
||||
<a href="?page=nodes&action=edit&id=<?= e($node['id']) ?>" class="btn btn-sm">Edit</a>
|
||||
<?php if (hasPermission('delete_peers')): ?>
|
||||
<form method="POST" class="d-inline" onsubmit="return confirmDelete('Are you sure you want to delete <?= e($node['name']) ?>?')">
|
||||
<?= csrfField() ?>
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="id" value="<?= e($node['id']) ?>">
|
||||
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php elseif ($action === 'create' || $action === 'edit'): ?>
|
||||
<!-- Create/Edit Form -->
|
||||
<div class="page-header">
|
||||
<div class="d-flex flex-items-center">
|
||||
<a href="?page=nodes" class="btn-octicon mr-2">
|
||||
<svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.78 12.53a.75.75 0 01-1.06 0L2.47 8.28a.75.75 0 010-1.06l4.25-4.25a.75.75 0 011.06 1.06L4.81 7h7.44a.75.75 0 010 1.5H4.81l2.97 2.97a.75.75 0 010 1.06z"/></svg>
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="h2 mb-1"><?= $action === 'edit' ? 'Edit Node' : 'New Node' ?></h1>
|
||||
<p class="color-fg-muted mb-0">
|
||||
<?= $action === 'edit' ? e($editNode['name']) : 'Configure a new router node' ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
<?php if (empty($asns)): ?>
|
||||
<div class="flash flash-warn mb-3">
|
||||
<strong>No ASNs found.</strong> You need to create an ASN before creating nodes.
|
||||
<a href="?page=asns&action=create" class="Link--primary">Create ASN</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="flash flash-error mb-3">
|
||||
<?= e($error) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" action="">
|
||||
<?= csrfField() ?>
|
||||
<input type="hidden" name="action" value="<?= $action === 'edit' ? 'update' : 'create' ?>">
|
||||
<?php if ($editNode): ?>
|
||||
<input type="hidden" name="id" value="<?= e($editNode['id']) ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="Box mb-4">
|
||||
<div class="Box-header">
|
||||
<h3 class="Box-title">Basic Information</h3>
|
||||
</div>
|
||||
<div class="Box-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="asn_id">ASN *</label>
|
||||
<select id="asn_id"
|
||||
name="asn_id"
|
||||
class="form-select"
|
||||
style="max-width: 400px;"
|
||||
<?= $editNode ? 'disabled' : 'required' ?>>
|
||||
<option value="">Select an ASN</option>
|
||||
<?php foreach ($asns as $asn): ?>
|
||||
<option value="<?= e($asn['id']) ?>" <?= ($editNode['asn_id'] ?? '') === $asn['id'] ? 'selected' : '' ?>>
|
||||
AS<?= e($asn['asn']) ?> - <?= e($asn['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if ($editNode): ?>
|
||||
<input type="hidden" name="asn_id" value="<?= e($editNode['asn_id']) ?>">
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="name">Name *</label>
|
||||
<input type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
class="form-control form-input-wide"
|
||||
value="<?= e($editNode['name'] ?? '') ?>"
|
||||
placeholder="e.g., router1.example.com"
|
||||
required>
|
||||
<p class="form-hint">A unique name for this node (usually the hostname)</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="description">Description</label>
|
||||
<textarea id="description"
|
||||
name="description"
|
||||
class="form-control form-input-wide"
|
||||
rows="2"><?= e($editNode['description'] ?? '') ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="Box mb-4">
|
||||
<div class="Box-header">
|
||||
<h3 class="Box-title">Network Configuration</h3>
|
||||
</div>
|
||||
<div class="Box-body">
|
||||
<p class="color-fg-muted mb-3">These settings override the ASN defaults for this specific node.</p>
|
||||
|
||||
<div class="d-flex flex-wrap" style="gap: 16px;">
|
||||
<div class="form-group flex-1" style="min-width: 200px;">
|
||||
<label class="form-label" for="router_id">Router ID</label>
|
||||
<input type="text"
|
||||
id="router_id"
|
||||
name="router_id"
|
||||
class="form-control"
|
||||
value="<?= e($editNode['config']['router_id'] ?? '') ?>"
|
||||
placeholder="e.g., 192.0.2.1">
|
||||
</div>
|
||||
|
||||
<div class="form-group flex-1" style="min-width: 200px;">
|
||||
<label class="form-label" for="hostname">Hostname</label>
|
||||
<input type="text"
|
||||
id="hostname"
|
||||
name="hostname"
|
||||
class="form-control"
|
||||
value="<?= e($editNode['config']['hostname'] ?? '') ?>"
|
||||
placeholder="router1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap" style="gap: 16px;">
|
||||
<div class="form-group flex-1" style="min-width: 200px;">
|
||||
<label class="form-label" for="source4">Source IPv4</label>
|
||||
<input type="text"
|
||||
id="source4"
|
||||
name="source4"
|
||||
class="form-control"
|
||||
value="<?= e($editNode['config']['source4'] ?? '') ?>"
|
||||
placeholder="e.g., 192.0.2.1">
|
||||
</div>
|
||||
|
||||
<div class="form-group flex-1" style="min-width: 200px;">
|
||||
<label class="form-label" for="source6">Source IPv6</label>
|
||||
<input type="text"
|
||||
id="source6"
|
||||
name="source6"
|
||||
class="form-control"
|
||||
value="<?= e($editNode['config']['source6'] ?? '') ?>"
|
||||
placeholder="e.g., 2001:db8::1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="prefixes4">IPv4 Prefixes</label>
|
||||
<input type="text"
|
||||
id="prefixes4"
|
||||
name="prefixes4"
|
||||
class="form-control form-input-wide"
|
||||
value="<?= e(implode(', ', $editNode['config']['prefixes'] ?? [])) ?>"
|
||||
placeholder="192.0.2.0/24, 198.51.100.0/24">
|
||||
<p class="form-hint">Node-specific IPv4 prefixes (overrides ASN defaults)</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="prefixes6">IPv6 Prefixes</label>
|
||||
<input type="text"
|
||||
id="prefixes6"
|
||||
name="prefixes6"
|
||||
class="form-control form-input-wide"
|
||||
value="<?= e(implode(', ', $editNode['config']['prefixes6'] ?? [])) ?>"
|
||||
placeholder="2001:db8::/32">
|
||||
<p class="form-hint">Node-specific IPv6 prefixes (overrides ASN defaults)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="Box mb-4">
|
||||
<div class="Box-header">
|
||||
<h3 class="Box-title">BIRD Configuration</h3>
|
||||
</div>
|
||||
<div class="Box-body">
|
||||
<div class="d-flex flex-wrap" style="gap: 16px;">
|
||||
<div class="form-group flex-1" style="min-width: 200px;">
|
||||
<label class="form-label" for="bird_directory">BIRD Directory</label>
|
||||
<input type="text"
|
||||
id="bird_directory"
|
||||
name="bird_directory"
|
||||
class="form-control"
|
||||
value="<?= e($editNode['config']['bird_directory'] ?? '') ?>"
|
||||
placeholder="/etc/bird">
|
||||
</div>
|
||||
|
||||
<div class="form-group flex-1" style="min-width: 200px;">
|
||||
<label class="form-label" for="bird_socket">BIRD Socket</label>
|
||||
<input type="text"
|
||||
id="bird_socket"
|
||||
name="bird_socket"
|
||||
class="form-control"
|
||||
value="<?= e($editNode['config']['bird_socket'] ?? '') ?>"
|
||||
placeholder="/run/bird/bird.ctl">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cache_directory">Cache Directory</label>
|
||||
<input type="text"
|
||||
id="cache_directory"
|
||||
name="cache_directory"
|
||||
class="form-control"
|
||||
style="max-width: 400px;"
|
||||
value="<?= e($editNode['config']['cache_directory'] ?? '') ?>"
|
||||
placeholder="/var/cache/pathvector">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="Box mb-4">
|
||||
<div class="Box-header">
|
||||
<h3 class="Box-title">Filtering Options</h3>
|
||||
</div>
|
||||
<div class="Box-body">
|
||||
<div class="d-flex flex-wrap" style="gap: 16px;">
|
||||
<div class="form-group flex-1" style="min-width: 200px;">
|
||||
<label class="form-label" for="irr_server">IRR Server</label>
|
||||
<input type="text"
|
||||
id="irr_server"
|
||||
name="irr_server"
|
||||
class="form-control"
|
||||
value="<?= e($editNode['config']['irr_server'] ?? '') ?>"
|
||||
placeholder="rr.ntt.net">
|
||||
</div>
|
||||
|
||||
<div class="form-group flex-1" style="min-width: 200px;">
|
||||
<label class="form-label" for="rtr_server">RTR Server (RPKI)</label>
|
||||
<input type="text"
|
||||
id="rtr_server"
|
||||
name="rtr_server"
|
||||
class="form-control"
|
||||
value="<?= e($editNode['config']['rtr_server'] ?? '') ?>"
|
||||
placeholder="rtr.example.com:8282">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap" style="gap: 24px;">
|
||||
<div class="form-checkbox">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
name="rpki_filter"
|
||||
value="1"
|
||||
<?= ($editNode['config']['rpki_filter'] ?? false) ? 'checked' : '' ?>>
|
||||
Enable RPKI filtering
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-checkbox">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
name="irr_filter"
|
||||
value="1"
|
||||
<?= ($editNode['config']['irr_filter'] ?? false) ? 'checked' : '' ?>>
|
||||
Enable IRR filtering
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-checkbox">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
name="keep_filtered"
|
||||
value="1"
|
||||
<?= ($editNode['config']['keep_filtered'] ?? false) ? 'checked' : '' ?>>
|
||||
Keep filtered routes
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="Box mb-4">
|
||||
<div class="Box-header">
|
||||
<h3 class="Box-title">Templates</h3>
|
||||
</div>
|
||||
<div class="Box-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="templates">Default Templates</label>
|
||||
<input type="text"
|
||||
id="templates"
|
||||
name="templates"
|
||||
class="form-control form-input-wide"
|
||||
value="<?= e(implode(', ', $editNode['config']['templates'] ?? [])) ?>"
|
||||
placeholder="upstream, peer, downstream">
|
||||
<p class="form-hint">Comma-separated list of default templates to use for this node</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-items-center" style="gap: 8px;">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<?= $action === 'edit' ? 'Update Node' : 'Create Node' ?>
|
||||
</button>
|
||||
<a href="?page=nodes" class="btn">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
Reference in New Issue
Block a user