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('', $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); } }