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:
562
pathvector-admin/lib/Template.php
Normal file
562
pathvector-admin/lib/Template.php
Normal file
@@ -0,0 +1,562 @@
|
||||
<?php
|
||||
/**
|
||||
* Template Class
|
||||
*
|
||||
* Manages reusable templates for ASNs, nodes, and peers
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/FlatFileDB.php';
|
||||
require_once __DIR__ . '/Logger.php';
|
||||
require_once __DIR__ . '/Validator.php';
|
||||
|
||||
class Template {
|
||||
private FlatFileDB $db;
|
||||
private Logger $logger;
|
||||
private Validator $validator;
|
||||
|
||||
// Template types
|
||||
public const TYPE_PEER = 'peer';
|
||||
public const TYPE_ASN = 'asn';
|
||||
public const TYPE_NODE = 'node';
|
||||
public const TYPE_POLICY = 'policy';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $dataFile Path to templates JSON file
|
||||
* @param Logger $logger Logger instance
|
||||
*/
|
||||
public function __construct(string $dataFile, Logger $logger) {
|
||||
$this->db = new FlatFileDB($dataFile);
|
||||
$this->logger = $logger;
|
||||
$this->validator = new Validator();
|
||||
|
||||
if (!$this->db->exists('templates')) {
|
||||
$this->db->set('templates', []);
|
||||
$this->initializeDefaultTemplates();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize default peer templates
|
||||
*/
|
||||
private function initializeDefaultTemplates(): void {
|
||||
$defaults = [
|
||||
'upstream' => [
|
||||
'id' => 'upstream',
|
||||
'name' => 'Upstream Provider',
|
||||
'type' => self::TYPE_PEER,
|
||||
'description' => 'Template for upstream transit providers',
|
||||
'pathvector' => [
|
||||
'allow_local_as' => true,
|
||||
'local_pref' => 80,
|
||||
'remove_all_communities' => null,
|
||||
'filter_rpki' => true,
|
||||
'filter_bogon_routes' => true,
|
||||
'filter_bogon_asns' => true,
|
||||
],
|
||||
'is_builtin' => true,
|
||||
'created_at' => date('c'),
|
||||
'updated_at' => date('c'),
|
||||
],
|
||||
'downstream' => [
|
||||
'id' => 'downstream',
|
||||
'name' => 'Downstream Customer',
|
||||
'type' => self::TYPE_PEER,
|
||||
'description' => 'Template for downstream customers',
|
||||
'pathvector' => [
|
||||
'filter_irr' => true,
|
||||
'filter_rpki' => true,
|
||||
'filter_transit_asns' => true,
|
||||
'auto_import_limits' => true,
|
||||
'auto_as_set' => true,
|
||||
'allow_blackhole_community' => true,
|
||||
'announce_default' => true,
|
||||
'local_pref' => 200,
|
||||
],
|
||||
'is_builtin' => true,
|
||||
'created_at' => date('c'),
|
||||
'updated_at' => date('c'),
|
||||
],
|
||||
'peer' => [
|
||||
'id' => 'peer',
|
||||
'name' => 'Peering Partner',
|
||||
'type' => self::TYPE_PEER,
|
||||
'description' => 'Template for bilateral peering sessions',
|
||||
'pathvector' => [
|
||||
'filter_irr' => true,
|
||||
'filter_rpki' => true,
|
||||
'filter_transit_asns' => true,
|
||||
'auto_import_limits' => true,
|
||||
'auto_as_set' => true,
|
||||
'local_pref' => 100,
|
||||
],
|
||||
'is_builtin' => true,
|
||||
'created_at' => date('c'),
|
||||
'updated_at' => date('c'),
|
||||
],
|
||||
'routeserver' => [
|
||||
'id' => 'routeserver',
|
||||
'name' => 'Route Server',
|
||||
'type' => self::TYPE_PEER,
|
||||
'description' => 'Template for IXP route servers',
|
||||
'pathvector' => [
|
||||
'filter_transit_asns' => true,
|
||||
'auto_import_limits' => true,
|
||||
'enforce_peer_nexthop' => false,
|
||||
'enforce_first_as' => false,
|
||||
'local_pref' => 90,
|
||||
],
|
||||
'is_builtin' => true,
|
||||
'created_at' => date('c'),
|
||||
'updated_at' => date('c'),
|
||||
],
|
||||
'ibgp' => [
|
||||
'id' => 'ibgp',
|
||||
'name' => 'Internal BGP',
|
||||
'type' => self::TYPE_PEER,
|
||||
'description' => 'Template for iBGP sessions within the same AS',
|
||||
'pathvector' => [
|
||||
'next_hop_self_ibgp' => true,
|
||||
'filter_rpki' => false,
|
||||
'filter_bogon_routes' => false,
|
||||
'filter_bogon_asns' => false,
|
||||
'remove_private_asns' => false,
|
||||
],
|
||||
'is_builtin' => true,
|
||||
'created_at' => date('c'),
|
||||
'updated_at' => date('c'),
|
||||
],
|
||||
];
|
||||
|
||||
$this->db->set('templates', $defaults);
|
||||
$this->logger->info('template', 'Initialized default templates');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all templates
|
||||
*
|
||||
* @param string|null $type Filter by type
|
||||
* @return array
|
||||
*/
|
||||
public function getAll(?string $type = null): array {
|
||||
$templates = $this->db->get('templates') ?? [];
|
||||
|
||||
if ($type !== null) {
|
||||
return array_filter($templates, fn($t) => ($t['type'] ?? '') === $type);
|
||||
}
|
||||
|
||||
return $templates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get template by ID
|
||||
*
|
||||
* @param string $id
|
||||
* @return array|null
|
||||
*/
|
||||
public function get(string $id): ?array {
|
||||
$templates = $this->db->get('templates') ?? [];
|
||||
return $templates[$id] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new template
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function create(array $data): array {
|
||||
if (empty($data['id'])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Template ID is required',
|
||||
'errors' => ['id' => 'Template ID is required'],
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($data['type']) || !in_array($data['type'], [self::TYPE_PEER, self::TYPE_ASN, self::TYPE_NODE, self::TYPE_POLICY])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Valid template type is required',
|
||||
'errors' => ['type' => 'Template type must be: peer, asn, node, or policy'],
|
||||
];
|
||||
}
|
||||
|
||||
$templates = $this->db->get('templates') ?? [];
|
||||
$id = $this->sanitizeId($data['id']);
|
||||
|
||||
if (isset($templates[$id])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Template $id already exists",
|
||||
'errors' => ['id' => 'Template ID already exists'],
|
||||
];
|
||||
}
|
||||
|
||||
$templateData = [
|
||||
'id' => $id,
|
||||
'name' => $data['name'] ?? $id,
|
||||
'type' => $data['type'],
|
||||
'description' => $data['description'] ?? '',
|
||||
'parent' => $data['parent'] ?? null, // For inheritance
|
||||
'pathvector' => $data['pathvector'] ?? [],
|
||||
'variables' => $data['variables'] ?? [], // Variable substitution
|
||||
'is_builtin' => false,
|
||||
'created_at' => date('c'),
|
||||
'updated_at' => date('c'),
|
||||
];
|
||||
|
||||
$templates[$id] = $templateData;
|
||||
|
||||
if ($this->db->set('templates', $templates)) {
|
||||
$this->logger->success('template', "Created template: $id ({$templateData['type']})");
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Template $id created successfully",
|
||||
'data' => $templateData,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to save template',
|
||||
'errors' => ['database' => 'Failed to save template'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update template
|
||||
*
|
||||
* @param string $id
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function update(string $id, array $data): array {
|
||||
$templates = $this->db->get('templates') ?? [];
|
||||
|
||||
if (!isset($templates[$id])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Template $id not found",
|
||||
'errors' => ['id' => 'Template not found'],
|
||||
];
|
||||
}
|
||||
|
||||
// Don't allow modifying builtin templates (only pathvector options)
|
||||
if ($templates[$id]['is_builtin'] ?? false) {
|
||||
$templateData = $templates[$id];
|
||||
|
||||
if (isset($data['pathvector'])) {
|
||||
$templateData['pathvector'] = array_merge(
|
||||
$templateData['pathvector'] ?? [],
|
||||
$data['pathvector']
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($data['description'])) {
|
||||
$templateData['description'] = $data['description'];
|
||||
}
|
||||
} else {
|
||||
$templateData = $templates[$id];
|
||||
|
||||
$allowedFields = ['name', 'description', 'parent', 'pathvector', 'variables'];
|
||||
|
||||
foreach ($allowedFields as $field) {
|
||||
if (isset($data[$field])) {
|
||||
if ($field === 'pathvector') {
|
||||
$templateData[$field] = array_merge(
|
||||
$templateData[$field] ?? [],
|
||||
$data[$field]
|
||||
);
|
||||
} else {
|
||||
$templateData[$field] = $data[$field];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$templateData['updated_at'] = date('c');
|
||||
$templates[$id] = $templateData;
|
||||
|
||||
if ($this->db->set('templates', $templates)) {
|
||||
$this->logger->info('template', "Updated template: $id");
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Template $id updated successfully",
|
||||
'data' => $templateData,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to update template',
|
||||
'errors' => ['database' => 'Failed to save template'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete template
|
||||
*
|
||||
* @param string $id
|
||||
* @return array
|
||||
*/
|
||||
public function delete(string $id): array {
|
||||
$templates = $this->db->get('templates') ?? [];
|
||||
|
||||
if (!isset($templates[$id])) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Template $id not found",
|
||||
];
|
||||
}
|
||||
|
||||
if ($templates[$id]['is_builtin'] ?? false) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Cannot delete builtin templates',
|
||||
];
|
||||
}
|
||||
|
||||
$templateName = $templates[$id]['name'];
|
||||
unset($templates[$id]);
|
||||
|
||||
if ($this->db->set('templates', $templates)) {
|
||||
$this->logger->warning('template', "Deleted template: $id ($templateName)");
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => "Template $id deleted successfully",
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Failed to delete template',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve template with inheritance
|
||||
*
|
||||
* @param string $id
|
||||
* @return array
|
||||
*/
|
||||
public function resolve(string $id): array {
|
||||
$template = $this->get($id);
|
||||
|
||||
if (!$template) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$resolved = $template['pathvector'] ?? [];
|
||||
|
||||
// Apply parent template if exists
|
||||
if (!empty($template['parent'])) {
|
||||
$parentResolved = $this->resolve($template['parent']);
|
||||
$resolved = array_merge($parentResolved, $resolved);
|
||||
}
|
||||
|
||||
return $resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply template to peer data with variable substitution
|
||||
*
|
||||
* @param array $peerData
|
||||
* @param string $templateId
|
||||
* @param array $variables
|
||||
* @return array
|
||||
*/
|
||||
public function applyToPeer(array $peerData, string $templateId, array $variables = []): array {
|
||||
$resolved = $this->resolve($templateId);
|
||||
|
||||
if (empty($resolved)) {
|
||||
return $peerData;
|
||||
}
|
||||
|
||||
// Apply variable substitution
|
||||
$resolved = $this->substituteVariables($resolved, $variables);
|
||||
|
||||
// Merge template with peer data (peer data takes precedence)
|
||||
$peerData['pathvector'] = array_merge(
|
||||
$resolved,
|
||||
$peerData['pathvector'] ?? []
|
||||
);
|
||||
|
||||
return $peerData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitute variables in template
|
||||
*
|
||||
* @param array $data
|
||||
* @param array $variables
|
||||
* @return array
|
||||
*/
|
||||
private function substituteVariables(array $data, array $variables): array {
|
||||
array_walk_recursive($data, function(&$value) use ($variables) {
|
||||
if (is_string($value)) {
|
||||
foreach ($variables as $key => $replacement) {
|
||||
$value = str_replace('{{' . $key . '}}', $replacement, $value);
|
||||
$value = str_replace('<pathvector.' . $key . '>', $replacement, $value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview template as YAML
|
||||
*
|
||||
* @param string $id
|
||||
* @return string|null
|
||||
*/
|
||||
public function previewYaml(string $id): ?string {
|
||||
$template = $this->get($id);
|
||||
|
||||
if (!$template || $template['type'] !== self::TYPE_PEER) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$resolved = $this->resolve($id);
|
||||
|
||||
$yaml = "# Template: {$template['name']}\n";
|
||||
$yaml .= "# Type: {$template['type']}\n";
|
||||
|
||||
if (!empty($template['description'])) {
|
||||
$yaml .= "# {$template['description']}\n";
|
||||
}
|
||||
|
||||
$yaml .= "\ntemplates:\n";
|
||||
$yaml .= " {$template['id']}:\n";
|
||||
|
||||
foreach ($resolved as $key => $value) {
|
||||
if ($value === null || $value === '' || $value === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$yamlKey = str_replace('_', '-', $key);
|
||||
$yaml .= $this->formatYamlEntry($yamlKey, $value, 2);
|
||||
}
|
||||
|
||||
return $yaml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format YAML entry
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param int $indent
|
||||
* @return string
|
||||
*/
|
||||
private function formatYamlEntry(string $key, $value, int $indent): string {
|
||||
$prefix = str_repeat(' ', $indent);
|
||||
|
||||
if (is_array($value)) {
|
||||
if (empty($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (array_keys($value) === range(0, count($value) - 1)) {
|
||||
$yaml = "$prefix$key:\n";
|
||||
foreach ($value as $item) {
|
||||
$yaml .= "$prefix - " . $this->formatYamlValue($item) . "\n";
|
||||
}
|
||||
return $yaml;
|
||||
} else {
|
||||
$yaml = "$prefix$key:\n";
|
||||
foreach ($value as $k => $v) {
|
||||
$yaml .= $this->formatYamlEntry($k, $v, $indent + 1);
|
||||
}
|
||||
return $yaml;
|
||||
}
|
||||
}
|
||||
|
||||
return "$prefix$key: " . $this->formatYamlValue($value) . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Format value for YAML
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return string
|
||||
*/
|
||||
private function formatYamlValue($value): string {
|
||||
if (is_bool($value)) {
|
||||
return $value ? 'true' : 'false';
|
||||
}
|
||||
|
||||
if (is_numeric($value)) {
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
if (is_string($value) && (strpos($value, ':') !== false || strpos($value, '#') !== false)) {
|
||||
return '"' . addslashes($value) . '"';
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize template ID
|
||||
*
|
||||
* @param string $id
|
||||
* @return string
|
||||
*/
|
||||
private function sanitizeId(string $id): string {
|
||||
$id = strtolower(trim($id));
|
||||
$id = preg_replace('/\s+/', '-', $id);
|
||||
$id = preg_replace('/[^a-z0-9-_]/', '', $id);
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count templates
|
||||
*
|
||||
* @param string|null $type
|
||||
* @return int
|
||||
*/
|
||||
public function count(?string $type = null): int {
|
||||
return count($this->getAll($type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone template
|
||||
*
|
||||
* @param string $sourceId
|
||||
* @param string $newId
|
||||
* @param string $newName
|
||||
* @return array
|
||||
*/
|
||||
public function cloneTemplate(string $sourceId, string $newId, string $newName = ''): array {
|
||||
$source = $this->get($sourceId);
|
||||
|
||||
if (!$source) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => "Source template $sourceId not found",
|
||||
];
|
||||
}
|
||||
|
||||
$data = $source;
|
||||
$data['id'] = $newId;
|
||||
$data['name'] = $newName ?: $source['name'] . ' (Copy)';
|
||||
$data['is_builtin'] = false;
|
||||
$data['parent'] = $sourceId; // Inherit from source
|
||||
unset($data['created_at'], $data['updated_at']);
|
||||
|
||||
return $this->create($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup templates data
|
||||
*
|
||||
* @param string $backupDir
|
||||
* @return string|false
|
||||
*/
|
||||
public function backup(string $backupDir): string|false {
|
||||
return $this->db->backup($backupDir);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user