- 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.
690 lines
23 KiB
PHP
690 lines
23 KiB
PHP
<?php
|
|
/**
|
|
* Peer Class
|
|
*
|
|
* Manages BGP peer configurations with full Pathvector options support
|
|
*/
|
|
|
|
require_once __DIR__ . '/FlatFileDB.php';
|
|
require_once __DIR__ . '/Logger.php';
|
|
require_once __DIR__ . '/Validator.php';
|
|
|
|
class Peer {
|
|
private FlatFileDB $db;
|
|
private Logger $logger;
|
|
private Validator $validator;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param string $dataFile Path to peers 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('peers')) {
|
|
$this->db->set('peers', []);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all peers
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getAll(): array {
|
|
return $this->db->get('peers') ?? [];
|
|
}
|
|
|
|
/**
|
|
* Get peers by ASN
|
|
*
|
|
* @param int $asn
|
|
* @return array
|
|
*/
|
|
public function getByAsn(int $asn): array {
|
|
$peers = $this->db->get('peers') ?? [];
|
|
return array_filter($peers, fn($peer) => ($peer['asn'] ?? 0) === $asn);
|
|
}
|
|
|
|
/**
|
|
* Get peers by node
|
|
*
|
|
* @param string $nodeId
|
|
* @return array
|
|
*/
|
|
public function getByNode(string $nodeId): array {
|
|
$peers = $this->db->get('peers') ?? [];
|
|
return array_filter($peers, fn($peer) => ($peer['node'] ?? '') === $nodeId);
|
|
}
|
|
|
|
/**
|
|
* Get peer by ID
|
|
*
|
|
* @param string $id
|
|
* @return array|null
|
|
*/
|
|
public function get(string $id): ?array {
|
|
$peers = $this->db->get('peers') ?? [];
|
|
return $peers[$id] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Create new peer
|
|
*
|
|
* @param array $data
|
|
* @return array
|
|
*/
|
|
public function create(array $data): array {
|
|
$this->validator->clear();
|
|
|
|
// Validate required fields
|
|
if (empty($data['id'])) {
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Peer ID is required',
|
|
'errors' => ['id' => 'Peer ID is required'],
|
|
];
|
|
}
|
|
|
|
if (empty($data['asn'])) {
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Local ASN is required',
|
|
'errors' => ['asn' => 'Local ASN is required'],
|
|
];
|
|
}
|
|
|
|
if (empty($data['remote_asn'])) {
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Remote ASN is required',
|
|
'errors' => ['remote_asn' => 'Remote ASN is required'],
|
|
];
|
|
}
|
|
|
|
if (!$this->validator->validateAsn($data['asn'], 'asn') ||
|
|
!$this->validator->validateAsn($data['remote_asn'], 'remote_asn')) {
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Invalid ASN',
|
|
'errors' => $this->validator->getErrors(),
|
|
];
|
|
}
|
|
|
|
$peers = $this->db->get('peers') ?? [];
|
|
$id = $this->sanitizeId($data['id']);
|
|
|
|
if (isset($peers[$id])) {
|
|
return [
|
|
'success' => false,
|
|
'message' => "Peer $id already exists",
|
|
'errors' => ['id' => 'Peer ID already exists'],
|
|
];
|
|
}
|
|
|
|
// Validate neighbors
|
|
$neighbors = $data['neighbors'] ?? [];
|
|
if (!is_array($neighbors)) {
|
|
$neighbors = array_filter(array_map('trim', explode("\n", $neighbors)));
|
|
}
|
|
|
|
foreach ($neighbors as $neighbor) {
|
|
if (!$this->validator->validateIP($neighbor, 'neighbors')) {
|
|
return [
|
|
'success' => false,
|
|
'message' => "Invalid neighbor IP: $neighbor",
|
|
'errors' => $this->validator->getErrors(),
|
|
];
|
|
}
|
|
}
|
|
|
|
$peerData = $this->buildPeerData($id, $data, $neighbors);
|
|
$peers[$id] = $peerData;
|
|
|
|
if ($this->db->set('peers', $peers)) {
|
|
$this->logger->success('peer', "Created peer: $id (AS{$peerData['remote_asn']}) on node {$peerData['node']}");
|
|
return [
|
|
'success' => true,
|
|
'message' => "Peer $id created successfully",
|
|
'data' => $peerData,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Failed to save peer',
|
|
'errors' => ['database' => 'Failed to save peer'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Update peer
|
|
*
|
|
* @param string $id
|
|
* @param array $data
|
|
* @return array
|
|
*/
|
|
public function update(string $id, array $data): array {
|
|
$peers = $this->db->get('peers') ?? [];
|
|
|
|
if (!isset($peers[$id])) {
|
|
return [
|
|
'success' => false,
|
|
'message' => "Peer $id not found",
|
|
'errors' => ['id' => 'Peer not found'],
|
|
];
|
|
}
|
|
|
|
$peerData = $peers[$id];
|
|
|
|
// Handle neighbors
|
|
if (isset($data['neighbors'])) {
|
|
$neighbors = $data['neighbors'];
|
|
if (!is_array($neighbors)) {
|
|
$neighbors = array_filter(array_map('trim', explode("\n", $neighbors)));
|
|
}
|
|
|
|
foreach ($neighbors as $neighbor) {
|
|
if (!$this->validator->validateIP($neighbor, 'neighbors')) {
|
|
return [
|
|
'success' => false,
|
|
'message' => "Invalid neighbor IP: $neighbor",
|
|
'errors' => $this->validator->getErrors(),
|
|
];
|
|
}
|
|
}
|
|
$peerData['neighbors'] = array_values($neighbors);
|
|
}
|
|
|
|
// Update Pathvector options
|
|
if (isset($data['pathvector'])) {
|
|
$peerData['pathvector'] = $this->buildPathvectorOptions(
|
|
array_merge($peerData['pathvector'], $data['pathvector'])
|
|
);
|
|
}
|
|
|
|
// Update basic fields
|
|
$basicFields = [
|
|
'name', 'description', 'remote_asn', 'node', 'template',
|
|
'afi', 'templates', 'is_active', 'tags'
|
|
];
|
|
|
|
foreach ($basicFields as $field) {
|
|
if (isset($data[$field])) {
|
|
$peerData[$field] = $data[$field];
|
|
}
|
|
}
|
|
|
|
$peerData['updated_at'] = date('c');
|
|
$peers[$id] = $peerData;
|
|
|
|
if ($this->db->set('peers', $peers)) {
|
|
$this->logger->info('peer', "Updated peer: $id");
|
|
return [
|
|
'success' => true,
|
|
'message' => "Peer $id updated successfully",
|
|
'data' => $peerData,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Failed to update peer',
|
|
'errors' => ['database' => 'Failed to save peer'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Delete peer
|
|
*
|
|
* @param string $id
|
|
* @return array
|
|
*/
|
|
public function delete(string $id): array {
|
|
$peers = $this->db->get('peers') ?? [];
|
|
|
|
if (!isset($peers[$id])) {
|
|
return [
|
|
'success' => false,
|
|
'message' => "Peer $id not found",
|
|
];
|
|
}
|
|
|
|
$peerName = $peers[$id]['name'];
|
|
$peerAsn = $peers[$id]['remote_asn'];
|
|
unset($peers[$id]);
|
|
|
|
if ($this->db->set('peers', $peers)) {
|
|
$this->logger->warning('peer', "Deleted peer: $id ($peerName - AS$peerAsn)");
|
|
return [
|
|
'success' => true,
|
|
'message' => "Peer $id deleted successfully",
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Failed to delete peer',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Build peer data structure
|
|
*
|
|
* @param string $id
|
|
* @param array $data
|
|
* @param array $neighbors
|
|
* @return array
|
|
*/
|
|
private function buildPeerData(string $id, array $data, array $neighbors): array {
|
|
return [
|
|
'id' => $id,
|
|
'name' => $data['name'] ?? $id,
|
|
'description' => $data['description'] ?? '',
|
|
'asn' => (int) $data['asn'],
|
|
'remote_asn' => (int) $data['remote_asn'],
|
|
'node' => $data['node'] ?? '',
|
|
'neighbors' => array_values($neighbors),
|
|
'afi' => $data['afi'] ?? ['ipv4', 'ipv6'],
|
|
'template' => $data['template'] ?? '',
|
|
'templates' => $data['templates'] ?? [],
|
|
'tags' => $data['tags'] ?? [],
|
|
'pathvector' => $this->buildPathvectorOptions($data['pathvector'] ?? []),
|
|
'is_active' => $data['is_active'] ?? true,
|
|
'created_at' => date('c'),
|
|
'updated_at' => date('c'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Build Pathvector options structure with ALL supported options
|
|
*
|
|
* @param array $data
|
|
* @return array
|
|
*/
|
|
private function buildPathvectorOptions(array $data): array {
|
|
return [
|
|
// Session control
|
|
'disabled' => $data['disabled'] ?? false,
|
|
'import' => $data['import'] ?? true,
|
|
'export' => $data['export'] ?? true,
|
|
|
|
// BGP attributes
|
|
'local_asn' => $data['local_asn'] ?? null,
|
|
'prepends' => $data['prepends'] ?? 0,
|
|
'prepend_path' => $data['prepend_path'] ?? [],
|
|
'clear_path' => $data['clear_path'] ?? false,
|
|
'local_pref' => $data['local_pref'] ?? 100,
|
|
'local_pref4' => $data['local_pref4'] ?? null,
|
|
'local_pref6' => $data['local_pref6'] ?? null,
|
|
'set_local_pref' => $data['set_local_pref'] ?? false,
|
|
'multihop' => $data['multihop'] ?? null,
|
|
|
|
// Listening
|
|
'listen4' => $data['listen4'] ?? '',
|
|
'listen6' => $data['listen6'] ?? '',
|
|
'local_port' => $data['local_port'] ?? 179,
|
|
'neighbor_port' => $data['neighbor_port'] ?? 179,
|
|
'passive' => $data['passive'] ?? false,
|
|
'direct' => $data['direct'] ?? false,
|
|
|
|
// Next-hop
|
|
'next_hop_self' => $data['next_hop_self'] ?? false,
|
|
'next_hop_self_ebgp' => $data['next_hop_self_ebgp'] ?? false,
|
|
'next_hop_self_ibgp' => $data['next_hop_self_ibgp'] ?? false,
|
|
'import_next_hop' => $data['import_next_hop'] ?? '',
|
|
'export_next_hop' => $data['export_next_hop'] ?? '',
|
|
'enforce_peer_nexthop' => $data['enforce_peer_nexthop'] ?? true,
|
|
'force_peer_nexthop' => $data['force_peer_nexthop'] ?? false,
|
|
|
|
// BFD
|
|
'bfd' => $data['bfd'] ?? false,
|
|
|
|
// Authentication
|
|
'password' => $data['password'] ?? '',
|
|
|
|
// Route server / reflector
|
|
'rs_client' => $data['rs_client'] ?? false,
|
|
'rr_client' => $data['rr_client'] ?? false,
|
|
|
|
// AS path manipulation
|
|
'remove_private_asns' => $data['remove_private_asns'] ?? true,
|
|
'allow_local_as' => $data['allow_local_as'] ?? false,
|
|
'enforce_first_as' => $data['enforce_first_as'] ?? true,
|
|
|
|
// Multi-protocol
|
|
'mp_unicast_46' => $data['mp_unicast_46'] ?? false,
|
|
|
|
// Add-path
|
|
'add_path_tx' => $data['add_path_tx'] ?? false,
|
|
'add_path_rx' => $data['add_path_rx'] ?? false,
|
|
|
|
// Confederation
|
|
'confederation' => $data['confederation'] ?? null,
|
|
'confederation_member' => $data['confederation_member'] ?? false,
|
|
|
|
// TTL Security
|
|
'ttl_security' => $data['ttl_security'] ?? false,
|
|
|
|
// Timers and limits
|
|
'receive_limit4' => $data['receive_limit4'] ?? null,
|
|
'receive_limit6' => $data['receive_limit6'] ?? null,
|
|
'receive_limit_violation' => $data['receive_limit_violation'] ?? 'disable',
|
|
'export_limit4' => $data['export_limit4'] ?? null,
|
|
'export_limit6' => $data['export_limit6'] ?? null,
|
|
'export_limit_violation' => $data['export_limit_violation'] ?? 'disable',
|
|
|
|
// Session options
|
|
'interpret_communities' => $data['interpret_communities'] ?? true,
|
|
'default_local_pref' => $data['default_local_pref'] ?? null,
|
|
'advertise_hostname' => $data['advertise_hostname'] ?? false,
|
|
'disable_after_error' => $data['disable_after_error'] ?? false,
|
|
'prefer_older_routes' => $data['prefer_older_routes'] ?? false,
|
|
|
|
// IRR
|
|
'irr_accept_child_prefixes' => $data['irr_accept_child_prefixes'] ?? false,
|
|
|
|
// Communities
|
|
'add_on_import' => $data['add_on_import'] ?? [],
|
|
'add_on_export' => $data['add_on_export'] ?? [],
|
|
'announce' => $data['announce'] ?? [],
|
|
'remove_communities' => $data['remove_communities'] ?? [],
|
|
'remove_all_communities' => $data['remove_all_communities'] ?? null,
|
|
|
|
// AS preferences
|
|
'as_prefs' => $data['as_prefs'] ?? [],
|
|
'community_prefs' => $data['community_prefs'] ?? [],
|
|
'large_community_prefs' => $data['large_community_prefs'] ?? [],
|
|
|
|
// AS-SET
|
|
'as_set' => $data['as_set'] ?? '',
|
|
|
|
// Blackhole
|
|
'allow_blackhole_community' => $data['allow_blackhole_community'] ?? false,
|
|
'blackhole_in' => $data['blackhole_in'] ?? false,
|
|
'blackhole_out' => $data['blackhole_out'] ?? false,
|
|
|
|
// Filtering
|
|
'filter_irr' => $data['filter_irr'] ?? false,
|
|
'filter_rpki' => $data['filter_rpki'] ?? true,
|
|
'strict_rpki' => $data['strict_rpki'] ?? false,
|
|
'filter_max_prefix' => $data['filter_max_prefix'] ?? true,
|
|
'filter_bogon_routes' => $data['filter_bogon_routes'] ?? true,
|
|
'filter_bogon_asns' => $data['filter_bogon_asns'] ?? true,
|
|
'filter_transit_asns' => $data['filter_transit_asns'] ?? false,
|
|
'filter_prefix_length' => $data['filter_prefix_length'] ?? true,
|
|
'filter_never_via_route_servers' => $data['filter_never_via_route_servers'] ?? false,
|
|
'filter_as_set' => $data['filter_as_set'] ?? false,
|
|
'filter_aspa' => $data['filter_aspa'] ?? false,
|
|
'filter_blocklist' => $data['filter_blocklist'] ?? true,
|
|
|
|
// Transit lock
|
|
'transit_lock' => $data['transit_lock'] ?? [],
|
|
|
|
// Announcement control
|
|
'dont_announce' => $data['dont_announce'] ?? [],
|
|
'only_announce' => $data['only_announce'] ?? [],
|
|
'prefix_communities' => $data['prefix_communities'] ?? [],
|
|
|
|
// Auto-config
|
|
'auto_import_limits' => $data['auto_import_limits'] ?? false,
|
|
'auto_as_set' => $data['auto_as_set'] ?? false,
|
|
'auto_as_set_members' => $data['auto_as_set_members'] ?? false,
|
|
|
|
// Graceful shutdown
|
|
'honor_graceful_shutdown' => $data['honor_graceful_shutdown'] ?? true,
|
|
|
|
// Prefixes
|
|
'prefixes' => $data['prefixes'] ?? [],
|
|
'as_set_members' => $data['as_set_members'] ?? [],
|
|
|
|
// BGP Role (RFC 9234)
|
|
'role' => $data['role'] ?? '',
|
|
'require_roles' => $data['require_roles'] ?? false,
|
|
|
|
// Export options
|
|
'announce_default' => $data['announce_default'] ?? false,
|
|
'announce_originated' => $data['announce_originated'] ?? true,
|
|
'announce_all' => $data['announce_all'] ?? false,
|
|
|
|
// Custom config
|
|
'session_global' => $data['session_global'] ?? '',
|
|
'pre_import_filter' => $data['pre_import_filter'] ?? '',
|
|
'post_import_filter' => $data['post_import_filter'] ?? '',
|
|
'pre_import_accept' => $data['pre_import_accept'] ?? '',
|
|
'pre_export' => $data['pre_export'] ?? '',
|
|
'pre_export_final' => $data['pre_export_final'] ?? '',
|
|
|
|
// Route optimization
|
|
'probe_sources' => $data['probe_sources'] ?? [],
|
|
'optimize_inbound' => $data['optimize_inbound'] ?? false,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Sanitize peer 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 peers
|
|
*
|
|
* @return int
|
|
*/
|
|
public function count(): int {
|
|
$peers = $this->db->get('peers') ?? [];
|
|
return count($peers);
|
|
}
|
|
|
|
/**
|
|
* Search peers
|
|
*
|
|
* @param string $query
|
|
* @return array
|
|
*/
|
|
public function search(string $query): array {
|
|
$peers = $this->db->get('peers') ?? [];
|
|
$query = strtolower($query);
|
|
|
|
return array_filter($peers, function($peer) use ($query) {
|
|
return strpos(strtolower($peer['id']), $query) !== false ||
|
|
strpos(strtolower($peer['name']), $query) !== false ||
|
|
strpos((string) $peer['remote_asn'], $query) !== false ||
|
|
strpos(strtolower($peer['description'] ?? ''), $query) !== false;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Export peer to YAML format
|
|
*
|
|
* @param string $id
|
|
* @return string|null
|
|
*/
|
|
public function exportYaml(string $id): ?string {
|
|
$peer = $this->get($id);
|
|
|
|
if (!$peer) {
|
|
return null;
|
|
}
|
|
|
|
$pv = $peer['pathvector'];
|
|
$config = [];
|
|
|
|
// Required fields
|
|
$config['asn'] = $peer['remote_asn'];
|
|
|
|
if (!empty($peer['template'])) {
|
|
$config['template'] = $peer['template'];
|
|
}
|
|
|
|
if (!empty($peer['neighbors'])) {
|
|
$config['neighbors'] = $peer['neighbors'];
|
|
}
|
|
|
|
// Add description if set
|
|
if (!empty($peer['description'])) {
|
|
$config['description'] = $peer['description'];
|
|
}
|
|
|
|
// Add all non-default Pathvector options
|
|
$this->addNonDefaultOptions($config, $pv);
|
|
|
|
// Build YAML with peer name as key
|
|
$peerName = $peer['name'];
|
|
$yaml = " $peerName:\n";
|
|
|
|
foreach ($config as $key => $value) {
|
|
$yamlKey = str_replace('_', '-', $key);
|
|
$yaml .= $this->formatYamlEntry($yamlKey, $value, 2);
|
|
}
|
|
|
|
return $yaml;
|
|
}
|
|
|
|
/**
|
|
* Add non-default options to config array
|
|
*
|
|
* @param array &$config
|
|
* @param array $pv
|
|
*/
|
|
private function addNonDefaultOptions(array &$config, array $pv): void {
|
|
$defaults = $this->buildPathvectorOptions([]);
|
|
|
|
foreach ($pv as $key => $value) {
|
|
// Skip if same as default
|
|
if (isset($defaults[$key]) && $defaults[$key] === $value) {
|
|
continue;
|
|
}
|
|
|
|
// Skip empty values
|
|
if ($value === '' || $value === null || $value === []) {
|
|
continue;
|
|
}
|
|
|
|
// Skip certain internal fields
|
|
if (in_array($key, ['template', 'neighbors'])) {
|
|
continue;
|
|
}
|
|
|
|
$config[$key] = $value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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)) {
|
|
// Sequential array
|
|
$yaml = "$prefix$key:\n";
|
|
foreach ($value as $item) {
|
|
$yaml .= "$prefix - " . $this->formatYamlValue($item) . "\n";
|
|
}
|
|
return $yaml;
|
|
} else {
|
|
// Associative array
|
|
$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;
|
|
}
|
|
|
|
/**
|
|
* Get peers grouped by type (based on template or ASN type)
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getGroupedByType(): array {
|
|
$peers = $this->db->get('peers') ?? [];
|
|
$grouped = [
|
|
'upstream' => [],
|
|
'downstream' => [],
|
|
'peer' => [],
|
|
'routeserver' => [],
|
|
'ibgp' => [],
|
|
'other' => [],
|
|
];
|
|
|
|
foreach ($peers as $id => $peer) {
|
|
$template = strtolower($peer['template'] ?? '');
|
|
|
|
if (strpos($template, 'upstream') !== false) {
|
|
$grouped['upstream'][$id] = $peer;
|
|
} elseif (strpos($template, 'downstream') !== false) {
|
|
$grouped['downstream'][$id] = $peer;
|
|
} elseif (strpos($template, 'routeserver') !== false || strpos($template, 'rs') !== false) {
|
|
$grouped['routeserver'][$id] = $peer;
|
|
} elseif ($peer['asn'] === $peer['remote_asn']) {
|
|
$grouped['ibgp'][$id] = $peer;
|
|
} elseif (strpos($template, 'peer') !== false) {
|
|
$grouped['peer'][$id] = $peer;
|
|
} else {
|
|
$grouped['other'][$id] = $peer;
|
|
}
|
|
}
|
|
|
|
return $grouped;
|
|
}
|
|
|
|
/**
|
|
* Backup peers data
|
|
*
|
|
* @param string $backupDir
|
|
* @return string|false
|
|
*/
|
|
public function backup(string $backupDir): string|false {
|
|
return $this->db->backup($backupDir);
|
|
}
|
|
}
|