Files
Joseph.Rawlings d8b76233c0 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.
2025-12-14 01:33:12 -05:00

563 lines
17 KiB
PHP

<?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);
}
}