This commit is contained in:
Nicolas Debrigode 2024-01-26 15:18:21 +01:00
parent e81b652597
commit fdc24a00c2
21 changed files with 438 additions and 17 deletions

3
.env
View File

@ -17,3 +17,6 @@ APP_ENV=prod
APP_SECRET=644192852c93fabc1bb219ad6458fe25
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
PEERINGDB_HOST='https://peeringdb.com'
PEERINGDB_URI='/api/'

View File

@ -3,6 +3,7 @@ config:
daystatsfile: '/home/nidebr/data/DEV/asstats/stats/asstats_day.txt'
knownlinksfile: '/home/nidebr/data/DEV/asstats/conf/knownlinks'
asinfofile: 'ressources/asinfo.db'
myasn: 34863
graph:
showv6: true
outispositive: false

View File

@ -15,6 +15,7 @@
"symfony/flex": "^2",
"symfony/form": "7.0.*",
"symfony/framework-bundle": "7.0.*",
"symfony/http-client": "7.0.*",
"symfony/process": "7.0.*",
"symfony/property-access": "7.0.*",
"symfony/runtime": "7.0.*",

View File

@ -27,3 +27,8 @@ services:
App\Application\ConfigApplication:
arguments:
- '%kernel.environment%'
## peeringdb client
App\Client\PeeringDbClient:
$host: '%env(PEERINGDB_HOST)%'
$uri: '%env(PEERINGDB_URI)%'

View File

@ -15,3 +15,4 @@ parameters:
ignoreErrors:
- '#Only booleans are allowed#'
- '#Cannot cast mixed to int.#'

View File

@ -66,6 +66,19 @@ class ConfigApplication
return self::getAsStatsConfig()['top'];
}
public static function getAsStatsConfigMyAsn(): ?int
{
if (false === \array_key_exists('myasn', self::getAsStatsConfig())) {
return null;
}
if (!self::getAsStatsConfig()['myasn']) {
return null;
}
return self::getAsStatsConfig()['myasn'];
}
/**
* @throws ConfigErrorException
*/

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Client;
use App\Util\EndWithFunction;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class PeeringDbClient extends AbstractController
{
public function __construct(
readonly string $host,
readonly string $uri,
private HttpClientInterface $client,
) {
if (!EndWithFunction::endsWith($uri, '/')) {
$uri = \sprintf('%s/', $uri);
}
$this->client = $client->withOptions([
'base_uri' => \sprintf('%s%s', $host, $uri),
'verify_host' => false,
'verify_peer' => false,
'timeout' => 30,
'max_duration' => 30,
]);
}
/**
* @throws TransportExceptionInterface
*/
public function get(string $url): array
{
$response = $this->client->request('GET', $url, [
'headers' => [
'Content-Type' => 'application/json',
],
]);
try {
return [
'status_code' => $response->getStatusCode(),
'response' => $response->toArray(),
];
} catch (ClientExceptionInterface|TransportExceptionInterface|DecodingExceptionInterface|ServerExceptionInterface|RedirectionExceptionInterface $e) {
if (429 === $e->getCode()) {
$this->addFlash('warning', 'Request was throttled by peeringdb.com API server.');
}
return [
'status_code' => $e->getCode(),
'message' => $e->getMessage(),
'response' => [],
];
}
}
}

View File

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Application\ConfigApplication;
use App\Exception\ConfigErrorException;
use App\Exception\DbErrorException;
use App\Form\SelectMyIXForm;
use App\Repository\GetAsDataRepository;
use App\Repository\KnowlinksRepository;
use App\Repository\PeeringDBRepository;
use App\Util\Annotation\Menu;
use Doctrine\DBAL\Exception;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
#[Route(
path: '/ix',
)]
#[Menu('view_ix')]
class IXStatsController extends BaseController
{
protected array $data = [];
/**
* @throws ConfigErrorException
* @throws DbErrorException
* @throws Exception
*/
#[Route(
path: '/my-ix',
name: 'my_ix',
methods: ['GET|POST'],
)]
public function history(
Request $request,
PeeringDBRepository $peeringDBRepository,
GetAsDataRepository $asDataRepository,
ConfigApplication $Config,
): Response {
$this->base_data['content_wrapper']['titre'] = 'My IX Stats';
$form = $this->createForm(SelectMyIXForm::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$ixInfo = $peeringDBRepository->getIXInfo((int) $form->get('myix')->getData());
$this->base_data['content_wrapper']['titre'] = \sprintf(
'Top %s (%s)',
$this->base_data['top'],
'24 hours'
);
$this->base_data['content_wrapper']['small'] = $ixInfo['name'];
$this->data['data'] = $asDataRepository::get(
$this->base_data['top'],
'',
[],
$peeringDBRepository->getIXMembers((int) $form->get('myix')->getData()),
);
$this->data['start'] = time() - 24 * 3600;
$this->data['end'] = time();
$this->data['graph_size'] = [
'width' => $Config::getAsStatsConfigGraph()['top_graph_width'],
'height' => $Config::getAsStatsConfigGraph()['top_graph_height'],
];
$this->data['selectedLinks'] = [];
return $this->render('pages/ix/my_ix/show.html.twig', [
'base_data' => $this->base_data,
'data' => $this->data,
'knownlinks' => KnowlinksRepository::get(),
'form' => $form->createView(),
]);
}
return $this->render('pages/ix/my_ix/index.html.twig', [
'base_data' => $this->base_data,
'form' => $form->createView(),
]);
}
}

View File

@ -46,7 +46,7 @@ class IndexController extends BaseController
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->data['data'] = $asDataRepository::get($this->base_data['top'], null, (array) $form->getData());
$this->data['data'] = $asDataRepository::get($this->base_data['top'], '', (array) $form->getData());
$this->data['selectedLinks'] = KnowlinksRepository::select((array) $form->getData());
} else {
$this->data['data'] = $asDataRepository::get($this->base_data['top']);

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Form;
use App\Application\ConfigApplication;
use App\Client\PeeringDbClient;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class SelectMyIXForm extends AbstractType
{
public function __construct(private PeeringDbClient $peeringDbClient)
{
}
/**
* @param array<string, mixed> $options
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$peering_data = $this->peeringDbClient->get(\sprintf('netixlan?asn=%s', ConfigApplication::getAsStatsConfigMyAsn()));
$data = [];
if (200 === $peering_data['status_code']) {
foreach ($peering_data['response']['data'] as $myix) {
$data[$myix['name']] = $myix['ix_id'];
}
}
ksort($data);
$builder
->add('myix', ChoiceType::class, [
'label' => false,
'choices' => $data,
'choice_translation_domain' => false,
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'csrf_protection' => false,
]);
}
}

View File

@ -66,7 +66,7 @@ class DbAsStatsRepository
->setMaxResults($ntop)
->fetchAllAssociative();
}
} catch (Exception) {
} catch (Exception $e) {
throw new DbErrorException(\sprintf('Problem with stats files %s', $this->dbname));
}

View File

@ -18,8 +18,9 @@ class GetAsDataRepository
*/
public static function get(
int $top,
?string $topInterval = null,
array $selectedLinks = []
string $topInterval = '',
array $selectedLinks = [],
array $listAsn = [],
): array {
if (0 === $top) {
return [];
@ -27,7 +28,7 @@ class GetAsDataRepository
$return = [];
if ($topInterval) {
if ('' !== $topInterval && '0' !== $topInterval) {
$dbName = ConfigApplication::getAsStatsConfigTopInterval()[$topInterval]['statsfile'];
} else {
$dbName = ConfigApplication::getAsStatsConfigDayStatsFile();
@ -36,7 +37,7 @@ class GetAsDataRepository
$data = new DbAsStatsRepository($dbName);
$asInfoRepository = new DbAsInfoRepository();
foreach ($data->getASStatsTop($top, KnowlinksRepository::select($selectedLinks)) as $as => $nbytes) {
foreach ($data->getASStatsTop($top, KnowlinksRepository::select($selectedLinks), $listAsn) as $as => $nbytes) {
$return['asinfo'][$as]['info'] = $asInfoRepository->getAsInfo($as);
$return['asinfo'][$as]['v4'] = [

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Client\PeeringDbClient;
class PeeringDBRepository
{
private PeeringDbClient $peeringDbClient;
public function __construct(PeeringDbClient $peeringDbClient)
{
$this->peeringDbClient = $peeringDbClient;
}
public function getIXMembers(int $ix_id): array
{
$data = $this->peeringDbClient->get(\sprintf('net?ix_id=%s', $ix_id));
if (200 !== $data['status_code']) {
return [];
}
$asn = [];
foreach ($data['response']['data'] as $list) {
$asn[] = $list['asn'];
}
return $asn;
}
public function getIXInfo(int $ix_id): array
{
$data = $this->peeringDbClient->get(\sprintf('ix?id=%s', $ix_id));
if (200 !== $data['status_code']) {
return [];
}
return $data['response']['data'][0];
}
}

View File

@ -23,12 +23,13 @@ class ConfigApplicationExtension extends AbstractExtension
{
return [
new TwigFunction('configapplication_graph', [$this, 'getConfigGraph']),
new TwigFunction('configapplication_myasn', [$this, 'getConfigMyAsn']),
];
}
public function getConfigGraph(string $key): mixed
{
if ($key === '' || $key === '0') {
if ('' === $key || '0' === $key) {
return '';
}
@ -38,4 +39,9 @@ class ConfigApplicationExtension extends AbstractExtension
return $this->configApplication::getAsStatsConfigGraph()[$key];
}
public function getConfigMyAsn(): ?int
{
return $this->configApplication::getAsStatsConfigMyAsn();
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Util;
class EndWithFunction
{
public static function endsWith(string $FullStr, string $needle): bool
{
$StrLen = strlen($needle);
$FullStrEnd = substr($FullStr, strlen($FullStr) - $StrLen);
return $FullStrEnd === $needle;
}
}

View File

@ -9,6 +9,7 @@ enum Icon: string
// Menu
case menu_home = 'ti-home icon';
case menu_view = 'ti-chart-histogram icon';
case menu_ix = 'ti-list-details icon';
// Form
case filter = 'ti-filter';

View File

@ -1,6 +1,7 @@
<div class="collapse navbar-collapse" id="navbar-menu">
<div class="d-flex flex-column flex-md-row flex-fill align-items-stretch align-items-md-center">
<ul class="navbar-nav">
{% if base_data.top_interval %}
<li class="nav-item dropdown {% if "top_as" == current_menu %}active{% endif %}">
<a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown" data-bs-auto-close="outside" role="button" aria-expanded="false" >
@ -34,6 +35,7 @@
</a>
</li>
{% endif %}
<li class="nav-item {% if "view_as" == current_menu %}active{% endif %}">
<a class="nav-link" href="{{ path('history') }}">
<span class="nav-link-icon d-md-none d-lg-inline-block">
@ -44,6 +46,24 @@
</span>
</a>
</li>
<li class="nav-item dropdown {% if "view_ix" == current_menu %}active{% endif %}">
<a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown" data-bs-auto-close="outside" role="button" aria-expanded="false" >
<span class="nav-link-icon d-md-none d-lg-inline-block">
{{ icon('menu_ix') }}
</span>
<span class="nav-link-title">
IX Stats
</span>
</a>
<div class="dropdown-menu">
{% if configapplication_myasn() %}
<a class="dropdown-item" href="{{ path('my_ix') }}">
My IX
</a>
{% endif %}
</div>
</li>
</ul>
</div>
</div>

View File

@ -0,0 +1,36 @@
<div class="sticky-top sticky-top-legend">
<div class="card">
<div class="card-body">
<h3 class="card-title">
<strong>Legend</strong>
</h3>
<div class="table-responsive">
<table class="table table-borderless table-sm">
<tbody>
{% if knownlinks is defined %}
{% for link in knownlinks %}
<tr>
<td class="align-middle">
<small>{{ link.descr }}</small>
</td>
<td>
<table class="table-legend">
<tr>
{% if configapplication_graph('brighten_negative') %}
<td class="table-legend-td table-legend-td-brighten" style="background-color: #{{ link.color }}">&nbsp;</td>
<td class="table-legend-td table-legend-td-brighten" style="opacity: 0.73; background-color: #{{ link.color }}">&nbsp;</td>
{% else %}
<td class="table-legend-td" style="background-color: #{{ link.color }}">&nbsp;</td>
{% endif %}
</tr>
</table>
</td>
</tr>
{% endfor %}
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,15 @@
{{ form_start(form) }}
<div class="card">
<div class="card-body">
<h3 class="card-title">My IX</h3>
<div class="row g2">
<div class="col">
{{ form_widget(form.myix) }}
</div>
<div class="col-auto">
<button type="submit" class="btn btn-icon">{{ icon('search', 'icon') }}</button>
</div>
</div>
</div>
</div>
{{ form_end(form) }}

View File

@ -0,0 +1,13 @@
{% extends "base/_layout.html.twig" %}
{% block content %}
<div class="row row-cards">
<div class="col-lg-2 col-sm-12 space-y">
{% include 'pages/ix/my_ix/_search.html.twig' %}
{% block legend %}{% endblock %}
</div>
<div class="col">
{% block graph %}{% endblock %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,44 @@
{% extends "pages/ix/my_ix/index.html.twig" %}
{% set counter = 0 %}
{% block legend %}
{% if knownlinks is defined %}
{% include 'core/legend_simple.html.twig' %}
{% endif %}
{% endblock %}
{% block graph %}
<div class="col">
<div class="row row-cards">
<div class="space-y">
{% for as, as_data in data.data.asinfo %}
{% set counter = counter + 1 %}
{% embed 'core/card_top.html.twig' with {as: as, as_data: as_data, data: data, counter: counter, hours: hours|default('24 hours')} only %}
{% block rank %}
<div class="display-6 fw-bold rank">
# {{ counter }}
</div>
{% endblock %}
{% block title %}
<h3 class="mb-0">
<span class="me-2 flag flag-{{ as_data.info.country|lower }}"></span>
<strong>AS{{ as }}:</strong>
<span class="small text-uppercase"><i>{{ as_data.info.name|default(as_data.info.description) }}</i></span>
</h3>
{% endblock %}
{#% block customlinks %}
<div class="col-md-auto">
<div class="mt-3 badges">
{{ custom_links_top(as)|raw }}
</div>
</div>
{% endblock %#}
{% endembed %}
{% endfor %}
</div>
</div>
</div>
{% endblock %}