20260526 增加持续soak监控与本地回放工具
This commit is contained in:
parent
cda7fdb135
commit
7e1c24fcc3
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
target/
|
||||
Cargo.lock
|
||||
perf.*
|
||||
specs/* copy.excalidraw
|
||||
|
||||
131
monitor/README.md
Normal file
131
monitor/README.md
Normal file
@ -0,0 +1,131 @@
|
||||
# Ours RP Prometheus / Grafana Monitor
|
||||
|
||||
本目录提供本地开发监控栈,用于采集 `rpki_artifact_metrics` 暴露的 ours RP soak 指标。
|
||||
|
||||
## 前置条件
|
||||
|
||||
1. Docker + Docker Compose v2;
|
||||
2. 宿主机已启动 `rpki_artifact_metrics`,并监听 Docker 网桥可访问的地址,例如 `0.0.0.0:9556`;
|
||||
3. Prometheus 容器通过 `host.docker.internal:9556` 访问宿主 sidecar。
|
||||
|
||||
Linux Docker 下 compose 已配置:
|
||||
|
||||
```yaml
|
||||
extra_hosts:
|
||||
- host.docker.internal:host-gateway
|
||||
```
|
||||
|
||||
## 启动
|
||||
|
||||
```bash
|
||||
cd rpki_2/rpki/monitor
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
默认镜像使用官方 Docker Hub 镜像:
|
||||
|
||||
```text
|
||||
prom/prometheus:v2.55.1
|
||||
grafana/grafana:11.3.1
|
||||
```
|
||||
|
||||
如需切到其它镜像源:
|
||||
|
||||
```bash
|
||||
PROMETHEUS_IMAGE=<mirror>/prom/prometheus:v2.55.1 \
|
||||
GRAFANA_IMAGE=<mirror>/grafana/grafana:11.3.1 \
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
默认端口:
|
||||
|
||||
- Prometheus: <http://localhost:9090>
|
||||
- Grafana: <http://localhost:3000>
|
||||
- Grafana 默认账号密码:`admin` / `admin`
|
||||
|
||||
如端口冲突:
|
||||
|
||||
```bash
|
||||
PROMETHEUS_PORT=19090 GRAFANA_PORT=13000 docker compose up -d
|
||||
```
|
||||
|
||||
## 停止
|
||||
|
||||
```bash
|
||||
cd rpki_2/rpki/monitor
|
||||
docker compose down
|
||||
```
|
||||
|
||||
保留数据 volume。若要清理数据:
|
||||
|
||||
```bash
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
## 典型本地联调命令
|
||||
|
||||
先启动 APNIC soak 和 metrics sidecar,例如:
|
||||
|
||||
```bash
|
||||
# soak .env 关键配置
|
||||
MAX_RUNS=-1
|
||||
RIRS=apnic
|
||||
RETAIN_RUNS=5
|
||||
INTERVAL_SECS=0
|
||||
|
||||
# metrics sidecar
|
||||
rpki_artifact_metrics \
|
||||
--run-root /path/to/portable-soak \
|
||||
--listen 0.0.0.0:9556 \
|
||||
--poll-secs 5 \
|
||||
--instance local-apnic-continuous
|
||||
```
|
||||
|
||||
再启动监控栈:
|
||||
|
||||
```bash
|
||||
cd rpki_2/rpki/monitor
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
Prometheus target:
|
||||
|
||||
```bash
|
||||
curl -s 'http://localhost:9090/api/v1/targets' | python3 -m json.tool
|
||||
```
|
||||
|
||||
Prometheus query:
|
||||
|
||||
```bash
|
||||
curl -G 'http://localhost:9090/api/v1/query' \
|
||||
--data-urlencode 'query=up{job="ours-rp-artifact-metrics"}'
|
||||
|
||||
curl -G 'http://localhost:9090/api/v1/query' \
|
||||
--data-urlencode 'query=ours_rp_run_completed_total{status="success"}'
|
||||
```
|
||||
|
||||
Grafana health:
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:3000/api/health | python3 -m json.tool
|
||||
```
|
||||
|
||||
Grafana dashboard:
|
||||
|
||||
- 打开 <http://localhost:3000/d/ours-rp-soak-overview/ours-rp-soak-overview>
|
||||
|
||||
## 主要指标
|
||||
|
||||
- `ours_rp_metrics_service_up`
|
||||
- `ours_rp_run_completed_total`
|
||||
- `ours_rp_run_duration_seconds`
|
||||
- `ours_rp_run_max_rss_bytes`
|
||||
- `ours_rp_vrps`
|
||||
- `ours_rp_vaps`
|
||||
- `ours_rp_publication_points`
|
||||
- `ours_rp_repo_sync_phase_count`
|
||||
- `ours_rp_large_publication_points{object_count_gt="10|50|100|..."}`
|
||||
- `ours_rp_cir_objects`
|
||||
- `ours_rp_ccr_state_items`
|
||||
38
monitor/docker-compose.yml
Normal file
38
monitor/docker-compose.yml
Normal file
@ -0,0 +1,38 @@
|
||||
services:
|
||||
prometheus:
|
||||
image: ${PROMETHEUS_IMAGE:-prom/prometheus:v2.55.1}
|
||||
container_name: ours-rp-prometheus
|
||||
command:
|
||||
- --config.file=/etc/prometheus/prometheus.yml
|
||||
- --storage.tsdb.path=/prometheus
|
||||
- --storage.tsdb.retention.time=${PROMETHEUS_RETENTION:-7d}
|
||||
- --web.enable-lifecycle
|
||||
extra_hosts:
|
||||
- host.docker.internal:host-gateway
|
||||
ports:
|
||||
- "${PROMETHEUS_PORT:-9090}:9090"
|
||||
volumes:
|
||||
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- prometheus-data:/prometheus
|
||||
restart: unless-stopped
|
||||
|
||||
grafana:
|
||||
image: ${GRAFANA_IMAGE:-grafana/grafana:11.3.1}
|
||||
container_name: ours-rp-grafana
|
||||
depends_on:
|
||||
- prometheus
|
||||
ports:
|
||||
- "${GRAFANA_PORT:-3000}:3000"
|
||||
environment:
|
||||
GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin}
|
||||
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-admin}
|
||||
GF_USERS_ALLOW_SIGN_UP: "false"
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
||||
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
prometheus-data:
|
||||
grafana-data:
|
||||
632
monitor/grafana/dashboards/ours-rp-repo-sync.json
Normal file
632
monitor/grafana/dashboards/ours-rp-repo-sync.json
Normal file
@ -0,0 +1,632 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 1,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_publication_points",
|
||||
"legendFormat": "publication points",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Publication Points",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_repo_sync_phase_count{phase=\"rrdp_ok\"}",
|
||||
"legendFormat": "rrdp ok",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "RRDP OK Points",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 6,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"id": 3,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_repo_sync_phase_count{phase=\"rrdp_failed_rsync_ok\"}",
|
||||
"legendFormat": "fallback",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Rsync Fallback Points",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 6,
|
||||
"x": 18,
|
||||
"y": 0
|
||||
},
|
||||
"id": 4,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_repo_terminal_state_count{terminal_state=\"failed_no_cache\"}",
|
||||
"legendFormat": "failed no cache",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Failed No Cache Points",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "s"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 4
|
||||
},
|
||||
"id": 5,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"displayMode": "table",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_run_stage_duration_seconds{stage=\"repo_sync_total\"}",
|
||||
"legendFormat": "repo sync total",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "ours_rp_run_stage_duration_seconds{stage=\"rrdp_download_total\"}",
|
||||
"legendFormat": "rrdp download",
|
||||
"refId": "B"
|
||||
},
|
||||
{
|
||||
"expr": "ours_rp_run_stage_duration_seconds{stage=\"rsync_download_total\"}",
|
||||
"legendFormat": "rsync download",
|
||||
"refId": "C"
|
||||
}
|
||||
],
|
||||
"title": "Repo Sync Download Durations",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"x": 0,
|
||||
"y": 12,
|
||||
"w": 12,
|
||||
"h": 8
|
||||
},
|
||||
"id": 6,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"displayMode": "table",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_repo_sync_phase_count",
|
||||
"legendFormat": "{{phase}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Repo Sync Phase Counts",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"x": 12,
|
||||
"y": 12,
|
||||
"w": 12,
|
||||
"h": 8
|
||||
},
|
||||
"id": 7,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"displayMode": "table",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_repo_sync_phase_count{phase=\"rrdp_failed_rsync_ok\"}",
|
||||
"legendFormat": "rrdp failed, rsync ok",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "ours_rp_repo_sync_phase_count{phase=\"rrdp_failed_rsync_failed\"}",
|
||||
"legendFormat": "rrdp failed, rsync failed",
|
||||
"refId": "B"
|
||||
},
|
||||
{
|
||||
"expr": "ours_rp_repo_terminal_state_count{terminal_state=\"failed_no_cache\"}",
|
||||
"legendFormat": "failed no cache",
|
||||
"refId": "C"
|
||||
},
|
||||
{
|
||||
"expr": "ours_rp_tree_instances{state=\"failed\"}",
|
||||
"legendFormat": "tree failed",
|
||||
"refId": "D"
|
||||
}
|
||||
],
|
||||
"title": "Repo Failure / Fallback Counts",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "s"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"x": 0,
|
||||
"y": 20,
|
||||
"w": 12,
|
||||
"h": 8
|
||||
},
|
||||
"id": 8,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"displayMode": "table",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_repo_sync_phase_duration_seconds_total{phase=\"rrdp_failed_rsync_ok\"}",
|
||||
"legendFormat": "rsync fallback duration",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "ours_rp_repo_sync_phase_duration_seconds_total{phase=\"rrdp_failed_rsync_failed\"}",
|
||||
"legendFormat": "failed duration",
|
||||
"refId": "B"
|
||||
},
|
||||
{
|
||||
"expr": "ours_rp_repo_terminal_state_duration_seconds_total{terminal_state=\"failed_no_cache\"}",
|
||||
"legendFormat": "failed no cache duration",
|
||||
"refId": "C"
|
||||
}
|
||||
],
|
||||
"title": "Repo Failure / Fallback Durations",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "none"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"x": 0,
|
||||
"y": 29,
|
||||
"w": 12,
|
||||
"h": 9
|
||||
},
|
||||
"id": 9,
|
||||
"options": {
|
||||
"showHeader": true,
|
||||
"cellHeight": "sm",
|
||||
"footer": {
|
||||
"show": false,
|
||||
"reducer": [
|
||||
"sum"
|
||||
],
|
||||
"countRows": false,
|
||||
"fields": ""
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_rrdp_rsync_failed_repository_duration_seconds",
|
||||
"format": "table",
|
||||
"instant": true,
|
||||
"legendFormat": "",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "RRDP + Rsync Failed Repositories",
|
||||
"type": "table",
|
||||
"transformations": [
|
||||
{
|
||||
"id": "organize",
|
||||
"options": {
|
||||
"excludeByName": {
|
||||
"job": true,
|
||||
"terminal_state": true,
|
||||
"rank": true,
|
||||
"transport": true,
|
||||
"__name__": true,
|
||||
"publication_points": true,
|
||||
"instance": true,
|
||||
"repo_id": true,
|
||||
"pp_id": true,
|
||||
"exported_instance": true,
|
||||
"rp": true,
|
||||
"source": true
|
||||
},
|
||||
"indexByName": {
|
||||
"Time": 0,
|
||||
"host": 1,
|
||||
"phase": 2,
|
||||
"uri": 3,
|
||||
"Value": 4
|
||||
},
|
||||
"renameByName": {
|
||||
"Value": "duration"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "none"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"x": 0,
|
||||
"y": 38,
|
||||
"w": 24,
|
||||
"h": 9
|
||||
},
|
||||
"id": 10,
|
||||
"options": {
|
||||
"showHeader": true,
|
||||
"cellHeight": "sm",
|
||||
"footer": {
|
||||
"show": false,
|
||||
"reducer": [
|
||||
"sum"
|
||||
],
|
||||
"countRows": false,
|
||||
"fields": ""
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "topk(20, ours_rp_top_publication_point_object_count)",
|
||||
"format": "table",
|
||||
"instant": true,
|
||||
"legendFormat": "",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Top Publication Points by Objects",
|
||||
"type": "table",
|
||||
"transformations": [
|
||||
{
|
||||
"id": "organize",
|
||||
"options": {
|
||||
"excludeByName": {
|
||||
"job": true,
|
||||
"__name__": true,
|
||||
"publication_points": true,
|
||||
"instance": true,
|
||||
"repo_id": true,
|
||||
"phase": true,
|
||||
"pp_id": true,
|
||||
"exported_instance": true,
|
||||
"rp": true,
|
||||
"source": true
|
||||
},
|
||||
"indexByName": {
|
||||
"Time": 0,
|
||||
"host": 1,
|
||||
"rank": 2,
|
||||
"terminal_state": 3,
|
||||
"transport": 4,
|
||||
"uri": 5,
|
||||
"Value": 6
|
||||
},
|
||||
"renameByName": {}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "none"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"x": 12,
|
||||
"y": 29,
|
||||
"w": 12,
|
||||
"h": 9
|
||||
},
|
||||
"id": 11,
|
||||
"options": {
|
||||
"showHeader": true,
|
||||
"cellHeight": "sm",
|
||||
"footer": {
|
||||
"show": false,
|
||||
"reducer": [
|
||||
"sum"
|
||||
],
|
||||
"countRows": false,
|
||||
"fields": ""
|
||||
}
|
||||
},
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "topk(20, ours_rp_top_repository_sync_duration_seconds)",
|
||||
"format": "table",
|
||||
"instant": true,
|
||||
"legendFormat": "",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Top 20 Repositories by Sync Duration",
|
||||
"type": "table",
|
||||
"transformations": [
|
||||
{
|
||||
"id": "organize",
|
||||
"options": {
|
||||
"excludeByName": {
|
||||
"job": true,
|
||||
"terminal_state": true,
|
||||
"__name__": true,
|
||||
"publication_points": true,
|
||||
"instance": true,
|
||||
"repo_id": true,
|
||||
"phase": true,
|
||||
"pp_id": true,
|
||||
"exported_instance": true,
|
||||
"rp": true,
|
||||
"source": true
|
||||
},
|
||||
"indexByName": {
|
||||
"Time": 0,
|
||||
"host": 1,
|
||||
"rank": 2,
|
||||
"transport": 3,
|
||||
"uri": 4,
|
||||
"Value": 5
|
||||
},
|
||||
"renameByName": {
|
||||
"Value": "value"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"refresh": "5s",
|
||||
"schemaVersion": 40,
|
||||
"tags": [
|
||||
"ours-rp",
|
||||
"rpki",
|
||||
"soak",
|
||||
"repo-sync"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-30m",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "browser",
|
||||
"title": "Ours RP Repo Sync",
|
||||
"uid": "ours-rp-repo-sync",
|
||||
"version": 3,
|
||||
"weekStart": ""
|
||||
}
|
||||
582
monitor/grafana/dashboards/ours-rp-soak-overview.json
Normal file
582
monitor/grafana/dashboards/ours-rp-soak-overview.json
Normal file
@ -0,0 +1,582 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 6,
|
||||
"h": 4
|
||||
},
|
||||
"id": 1,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_cir_trust_anchors",
|
||||
"legendFormat": "RIRs",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Current Run RIRs",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "s"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
"w": 6,
|
||||
"h": 4
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_run_duration_seconds",
|
||||
"legendFormat": "wall",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Latest Wall Time",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "bytes"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"x": 12,
|
||||
"y": 0,
|
||||
"w": 6,
|
||||
"h": 4
|
||||
},
|
||||
"id": 3,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_run_max_rss_bytes",
|
||||
"legendFormat": "rss",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Latest Max RSS",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"x": 18,
|
||||
"y": 0,
|
||||
"w": 6,
|
||||
"h": 4
|
||||
},
|
||||
"id": 4,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_publication_points",
|
||||
"legendFormat": "publication points",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Publication Points",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "s"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"x": 0,
|
||||
"y": 8,
|
||||
"w": 12,
|
||||
"h": 8
|
||||
},
|
||||
"id": 5,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"displayMode": "table",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_run_duration_seconds",
|
||||
"legendFormat": "wall",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "ours_rp_stage_duration_seconds{stage=\"validation\"}",
|
||||
"legendFormat": "validation",
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Run / Validation Duration",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"x": 12,
|
||||
"y": 8,
|
||||
"w": 12,
|
||||
"h": 8
|
||||
},
|
||||
"id": 6,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"displayMode": "table",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_vrps",
|
||||
"legendFormat": "VRPs",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "ours_rp_vaps",
|
||||
"legendFormat": "VAPs",
|
||||
"refId": "B"
|
||||
},
|
||||
{
|
||||
"expr": "ours_rp_cir_objects",
|
||||
"legendFormat": "CIR objects",
|
||||
"refId": "C"
|
||||
}
|
||||
],
|
||||
"title": "Output and Input Sizes",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"x": 0,
|
||||
"y": 16,
|
||||
"w": 12,
|
||||
"h": 8
|
||||
},
|
||||
"id": 8,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"displayMode": "table",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_large_publication_points",
|
||||
"legendFormat": "> {{object_count_gt}} objects",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Large Publication Points by Object Count",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"x": 0,
|
||||
"y": 4,
|
||||
"w": 6,
|
||||
"h": 4
|
||||
},
|
||||
"id": 9,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_run_sequence",
|
||||
"legendFormat": "seq",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Latest Run Sequence",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"x": 6,
|
||||
"y": 4,
|
||||
"w": 6,
|
||||
"h": 4
|
||||
},
|
||||
"id": 10,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_run_success",
|
||||
"legendFormat": "success",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Latest Run Success",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"x": 12,
|
||||
"y": 4,
|
||||
"w": 6,
|
||||
"h": 4
|
||||
},
|
||||
"id": 11,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_vrps",
|
||||
"legendFormat": "VRPs",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "VRPs",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"x": 18,
|
||||
"y": 4,
|
||||
"w": 6,
|
||||
"h": 4
|
||||
},
|
||||
"id": 12,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto",
|
||||
"wideLayout": true
|
||||
},
|
||||
"pluginVersion": "11.3.1",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_vaps",
|
||||
"legendFormat": "VAPs",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "VAPs",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "Prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "s"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"x": 12,
|
||||
"y": 16,
|
||||
"w": 12,
|
||||
"h": 8
|
||||
},
|
||||
"id": 13,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"displayMode": "table",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "ours_rp_run_stage_duration_seconds{stage=\"validation\"}",
|
||||
"legendFormat": "validation",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "ours_rp_run_stage_duration_seconds{stage=\"report_write\"}",
|
||||
"legendFormat": "report write",
|
||||
"refId": "E"
|
||||
},
|
||||
{
|
||||
"expr": "ours_rp_run_stage_duration_seconds{stage=\"ccr_write\"}",
|
||||
"legendFormat": "ccr write",
|
||||
"refId": "F"
|
||||
},
|
||||
{
|
||||
"expr": "ours_rp_run_stage_duration_seconds{stage=\"cir_write\"}",
|
||||
"legendFormat": "cir write",
|
||||
"refId": "G"
|
||||
}
|
||||
],
|
||||
"title": "Output Stage Durations",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"refresh": "5s",
|
||||
"schemaVersion": 40,
|
||||
"tags": [
|
||||
"ours-rp",
|
||||
"rpki",
|
||||
"soak"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-30m",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "browser",
|
||||
"title": "Ours RP Soak Overview",
|
||||
"uid": "ours-rp-soak-overview",
|
||||
"version": 4,
|
||||
"weekStart": ""
|
||||
}
|
||||
12
monitor/grafana/provisioning/dashboards/dashboard.yml
Normal file
12
monitor/grafana/provisioning/dashboards/dashboard.yml
Normal file
@ -0,0 +1,12 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: ours-rp
|
||||
orgId: 1
|
||||
folder: Ours RP
|
||||
type: file
|
||||
disableDeletion: false
|
||||
updateIntervalSeconds: 10
|
||||
allowUiUpdates: true
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
10
monitor/grafana/provisioning/datasources/prometheus.yml
Normal file
10
monitor/grafana/provisioning/datasources/prometheus.yml
Normal file
@ -0,0 +1,10 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
uid: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
editable: true
|
||||
13
monitor/prometheus/prometheus.yml
Normal file
13
monitor/prometheus/prometheus.yml
Normal file
@ -0,0 +1,13 @@
|
||||
global:
|
||||
scrape_interval: 5s
|
||||
evaluation_interval: 5s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: ours-rp-artifact-metrics
|
||||
metrics_path: /metrics
|
||||
static_configs:
|
||||
- targets:
|
||||
- host.docker.internal:9556
|
||||
labels:
|
||||
rp: ours-rp
|
||||
source: artifact-sidecar
|
||||
79
scripts/local_repo_replay/build_local_repo_replay_package.sh
Executable file
79
scripts/local_repo_replay/build_local_repo_replay_package.sh
Executable file
@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
build_local_repo_replay_package.sh --out <path> [--tar-gz]
|
||||
|
||||
Build a standalone local repository tree replay package for Routinator and
|
||||
rpki-client. The package does not include repository data and does not include
|
||||
materialize tooling.
|
||||
EOF
|
||||
}
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
SRC_DIR="$ROOT_DIR/scripts/local_repo_replay"
|
||||
OUT=""
|
||||
TAR_GZ=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--out) OUT="$2"; shift 2 ;;
|
||||
--tar-gz) TAR_GZ=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$OUT" ]] || { usage >&2; exit 2; }
|
||||
|
||||
if [[ "$TAR_GZ" -eq 1 ]]; then
|
||||
PACKAGE_DIR="$(mktemp -d)"
|
||||
TARGET_DIR="$PACKAGE_DIR/local-repo-replay-package"
|
||||
else
|
||||
TARGET_DIR="$OUT"
|
||||
rm -rf "$TARGET_DIR"
|
||||
fi
|
||||
|
||||
mkdir -p "$TARGET_DIR/scripts" "$TARGET_DIR/docs" "$TARGET_DIR/examples"
|
||||
|
||||
install -m 0755 "$SRC_DIR/run_routinator_from_local_tree.sh" "$TARGET_DIR/scripts/"
|
||||
install -m 0755 "$SRC_DIR/run_rpki_client_from_local_tree.sh" "$TARGET_DIR/scripts/"
|
||||
install -m 0755 "$SRC_DIR/run_dual_local_tree_replay.sh" "$TARGET_DIR/scripts/"
|
||||
install -m 0755 "$SRC_DIR/prepare_tals.py" "$TARGET_DIR/scripts/"
|
||||
install -m 0755 "$SRC_DIR/normalize_rp_outputs.py" "$TARGET_DIR/scripts/"
|
||||
install -m 0755 "$SRC_DIR/compare_normalized_sets.py" "$TARGET_DIR/scripts/"
|
||||
install -m 0755 "$SRC_DIR/summarize_replay.py" "$TARGET_DIR/scripts/"
|
||||
install -m 0755 "$ROOT_DIR/scripts/cir/cir-rsync-wrapper" "$TARGET_DIR/scripts/"
|
||||
install -m 0755 "$ROOT_DIR/scripts/cir/cir-local-link-sync.py" "$TARGET_DIR/scripts/"
|
||||
install -m 0644 "$SRC_DIR/templates/README.md" "$TARGET_DIR/README.md"
|
||||
install -m 0644 "$SRC_DIR/templates/docs/input_tree_requirements.md" "$TARGET_DIR/docs/"
|
||||
install -m 0644 "$SRC_DIR/templates/docs/offline_replay_limits.md" "$TARGET_DIR/docs/"
|
||||
install -m 0644 "$SRC_DIR/templates/docs/output_files.md" "$TARGET_DIR/docs/"
|
||||
install -m 0755 "$SRC_DIR/templates/examples/routinator_example.sh" "$TARGET_DIR/examples/"
|
||||
install -m 0755 "$SRC_DIR/templates/examples/rpki_client_example.sh" "$TARGET_DIR/examples/"
|
||||
install -m 0755 "$SRC_DIR/templates/examples/dual_compare_example.sh" "$TARGET_DIR/examples/"
|
||||
cat > "$TARGET_DIR/env.example" <<'EOF'
|
||||
# 本地目录树 replay 示例配置。目录树由使用者提前准备,不包含在 package 中。
|
||||
TAL_DIR=/data/replay/tals
|
||||
MIRROR_ROOT=/data/replay/mirror
|
||||
ROUTINATOR_BIN=/opt/routinator/target/release/routinator
|
||||
RPKI_CLIENT_BIN=/opt/rpki-client/src/rpki-client
|
||||
RPKI_CLIENT_CACHE_DIR=/data/replay/work/rpki-client-cache
|
||||
VALIDATION_TIME=2026-05-23T00:00:00Z
|
||||
EOF
|
||||
|
||||
if grep -R -E 'cir_materialize|repo-bytes\\.db|\\.cir' "$TARGET_DIR/scripts" >/dev/null; then
|
||||
echo "package contains forbidden materialize/repo-bytes implementation references" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$TAR_GZ" -eq 1 ]]; then
|
||||
mkdir -p "$(dirname "$OUT")"
|
||||
tar -C "$PACKAGE_DIR" -czf "$OUT" local-repo-replay-package
|
||||
rm -rf "$PACKAGE_DIR"
|
||||
echo "$OUT"
|
||||
else
|
||||
echo "$TARGET_DIR"
|
||||
fi
|
||||
51
scripts/local_repo_replay/compare_normalized_sets.py
Executable file
51
scripts/local_repo_replay/compare_normalized_sets.py
Executable file
@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def read_set(path: Path) -> set[str]:
|
||||
if not path.is_file():
|
||||
return set()
|
||||
return {line.strip() for line in path.read_text(encoding="utf-8", errors="replace").splitlines() if line.strip()}
|
||||
|
||||
|
||||
def compare(left: set[str], right: set[str]) -> dict[str, object]:
|
||||
union = left | right
|
||||
inter = left & right
|
||||
return {
|
||||
"left": len(left),
|
||||
"right": len(right),
|
||||
"intersection": len(inter),
|
||||
"onlyLeft": len(left - right),
|
||||
"onlyRight": len(right - left),
|
||||
"jaccard": round(len(inter) / len(union), 8) if union else 1.0,
|
||||
"onlyLeftSamples": sorted(left - right)[:20],
|
||||
"onlyRightSamples": sorted(right - left)[:20],
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--left", type=Path, required=True)
|
||||
parser.add_argument("--right", type=Path, required=True)
|
||||
parser.add_argument("--left-name", default="left")
|
||||
parser.add_argument("--right-name", default="right")
|
||||
parser.add_argument("--out", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = {
|
||||
"leftName": args.left_name,
|
||||
"rightName": args.right_name,
|
||||
"vrps": compare(read_set(args.left / "vrps.normalized.txt"), read_set(args.right / "vrps.normalized.txt")),
|
||||
"vaps": compare(read_set(args.left / "vaps.normalized.txt"), read_set(args.right / "vaps.normalized.txt")),
|
||||
}
|
||||
args.out.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.out.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
print(args.out)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
123
scripts/local_repo_replay/normalize_rp_outputs.py
Executable file
123
scripts/local_repo_replay/normalize_rp_outputs.py
Executable file
@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def normalize_asn(value: Any) -> str:
|
||||
text = str(value).strip().upper()
|
||||
if text.startswith("AS"):
|
||||
text = text[2:]
|
||||
return f"AS{int(text)}"
|
||||
|
||||
|
||||
def write_set(path: Path, rows: set[str]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("\n".join(sorted(rows)) + ("\n" if rows else ""), encoding="utf-8")
|
||||
|
||||
|
||||
def load_json(path: Path) -> Any:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def normalize_vrps_from_json(path: Path) -> set[str]:
|
||||
data = load_json(path)
|
||||
rows: set[str] = set()
|
||||
objects: list[dict[str, Any]] = []
|
||||
if isinstance(data, dict):
|
||||
for key in ("roas", "routeOrigins", "valid_roas"):
|
||||
value = data.get(key)
|
||||
if isinstance(value, list):
|
||||
objects.extend(item for item in value if isinstance(item, dict))
|
||||
elif isinstance(data, list):
|
||||
objects = [item for item in data if isinstance(item, dict)]
|
||||
for item in objects:
|
||||
asn = item.get("asn") or item.get("asID") or item.get("origin")
|
||||
prefix = item.get("prefix")
|
||||
max_length = item.get("maxLength") or item.get("max_length") or item.get("maxlen") or item.get("maxLengthPrefix")
|
||||
if asn is None or prefix is None or max_length is None:
|
||||
continue
|
||||
rows.add(f"{normalize_asn(asn)}|{prefix}|{int(max_length)}")
|
||||
return rows
|
||||
|
||||
|
||||
def normalize_vaps_from_json(path: Path) -> set[str]:
|
||||
data = load_json(path)
|
||||
rows: set[str] = set()
|
||||
objects: list[dict[str, Any]] = []
|
||||
if isinstance(data, dict):
|
||||
for key in ("aspas", "aspaAssertions", "vaps"):
|
||||
value = data.get(key)
|
||||
if isinstance(value, list):
|
||||
objects.extend(item for item in value if isinstance(item, dict))
|
||||
elif isinstance(data, list):
|
||||
objects = [item for item in data if isinstance(item, dict)]
|
||||
for item in objects:
|
||||
customer = (
|
||||
item.get("customer")
|
||||
or item.get("customer_asid")
|
||||
or item.get("customerASID")
|
||||
or item.get("customerAsid")
|
||||
or item.get("customerAsn")
|
||||
)
|
||||
providers = item.get("providers") or item.get("provider_asns") or item.get("providerASNs") or []
|
||||
if customer is None:
|
||||
continue
|
||||
provider_asns = [normalize_asn(provider) for provider in providers]
|
||||
rows.add(f"{normalize_asn(customer)}|{','.join(sorted(set(provider_asns), key=lambda value: int(value[2:])))}")
|
||||
return rows
|
||||
|
||||
|
||||
def normalize_vrps_from_csv(path: Path) -> set[str]:
|
||||
rows: set[str] = set()
|
||||
with path.open(newline="", encoding="utf-8", errors="replace") as handle:
|
||||
for row in csv.DictReader(handle):
|
||||
asn = row.get("ASN") or row.get("asn") or row.get("AS")
|
||||
prefix = row.get("IP Prefix") or row.get("prefix") or row.get("Prefix")
|
||||
max_length = row.get("Max Length") or row.get("maxLength") or row.get("max_length")
|
||||
if asn and prefix and max_length:
|
||||
rows.add(f"{normalize_asn(asn)}|{prefix}|{int(max_length)}")
|
||||
return rows
|
||||
|
||||
|
||||
def normalize_routinator(input_path: Path) -> tuple[set[str], set[str]]:
|
||||
return normalize_vrps_from_json(input_path), normalize_vaps_from_json(input_path)
|
||||
|
||||
|
||||
def normalize_rpki_client(input_path: Path) -> tuple[set[str], set[str]]:
|
||||
json_path = input_path / "json" if input_path.is_dir() else input_path
|
||||
csv_path = input_path / "csv" if input_path.is_dir() else input_path
|
||||
vrps: set[str] = set()
|
||||
vaps: set[str] = set()
|
||||
if json_path.is_file():
|
||||
vrps |= normalize_vrps_from_json(json_path)
|
||||
vaps |= normalize_vaps_from_json(json_path)
|
||||
if csv_path.is_file():
|
||||
vrps |= normalize_vrps_from_csv(csv_path)
|
||||
return vrps, vaps
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--kind", choices=["routinator", "rpki-client"], required=True)
|
||||
parser.add_argument("--input", type=Path, required=True)
|
||||
parser.add_argument("--vrps-out", type=Path, required=True)
|
||||
parser.add_argument("--vaps-out", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.kind == "routinator":
|
||||
vrps, vaps = normalize_routinator(args.input)
|
||||
else:
|
||||
vrps, vaps = normalize_rpki_client(args.input)
|
||||
write_set(args.vrps_out, vrps)
|
||||
write_set(args.vaps_out, vaps)
|
||||
print(json.dumps({"vrps": len(vrps), "vaps": len(vaps)}, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
84
scripts/local_repo_replay/prepare_tals.py
Executable file
84
scripts/local_repo_replay/prepare_tals.py
Executable file
@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def split_tal(text: str) -> tuple[list[str], list[str]]:
|
||||
lines = text.splitlines()
|
||||
for index, line in enumerate(lines):
|
||||
if line.strip() == "":
|
||||
return lines[:index], lines[index + 1 :]
|
||||
return lines, []
|
||||
|
||||
|
||||
def rsync_only_tal(source: Path) -> tuple[str, dict[str, object]]:
|
||||
text = source.read_text(encoding="utf-8", errors="replace")
|
||||
uri_lines, key_lines = split_tal(text)
|
||||
uris = [
|
||||
line.strip()
|
||||
for line in uri_lines
|
||||
if line.strip() and not line.lstrip().startswith("#")
|
||||
]
|
||||
rsync_uris = [
|
||||
uri for uri in uris
|
||||
if uri.lower().startswith("rsync://")
|
||||
]
|
||||
if not rsync_uris:
|
||||
return text if text.endswith("\n") else text + "\n", {
|
||||
"file": source.name,
|
||||
"mode": "unchanged_no_rsync_uri",
|
||||
"uris": len(uris),
|
||||
"rsyncUris": 0,
|
||||
}
|
||||
out = "\n".join(rsync_uris) + "\n\n" + "\n".join(key_lines).strip() + "\n"
|
||||
return out, {
|
||||
"file": source.name,
|
||||
"mode": "rsync_only",
|
||||
"uris": len(uris),
|
||||
"rsyncUris": len(rsync_uris),
|
||||
}
|
||||
|
||||
|
||||
def collect_tals(tal_dir: Path | None, tals: list[Path]) -> list[Path]:
|
||||
paths: list[Path] = []
|
||||
if tal_dir is not None:
|
||||
paths.extend(sorted(tal_dir.glob("*.tal")))
|
||||
paths.extend(tals)
|
||||
unique: dict[str, Path] = {}
|
||||
for path in paths:
|
||||
unique[path.name] = path
|
||||
return [unique[name] for name in sorted(unique)]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Prepare TALs for local rsync-tree replay.")
|
||||
parser.add_argument("--tal-dir", type=Path)
|
||||
parser.add_argument("--tal", type=Path, action="append", default=[])
|
||||
parser.add_argument("--out-dir", type=Path, required=True)
|
||||
parser.add_argument("--summary", type=Path)
|
||||
args = parser.parse_args()
|
||||
|
||||
sources = collect_tals(args.tal_dir, args.tal)
|
||||
if not sources:
|
||||
raise SystemExit("no TAL files provided")
|
||||
|
||||
args.out_dir.mkdir(parents=True, exist_ok=True)
|
||||
rows = []
|
||||
for source in sources:
|
||||
if not source.is_file():
|
||||
raise SystemExit(f"TAL file not found: {source}")
|
||||
content, row = rsync_only_tal(source)
|
||||
(args.out_dir / source.name).write_text(content, encoding="utf-8")
|
||||
rows.append(row)
|
||||
|
||||
if args.summary is not None:
|
||||
args.summary.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.summary.write_text(json.dumps(rows, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
76
scripts/local_repo_replay/run_dual_local_tree_replay.sh
Executable file
76
scripts/local_repo_replay/run_dual_local_tree_replay.sh
Executable file
@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
run_dual_local_tree_replay.sh \
|
||||
--routinator-bin <path> --routinator-mirror-root <dir> \
|
||||
--rpki-client-bin <path> --rpki-client-mirror-root <dir> \
|
||||
--tal-dir <dir> --out-dir <dir> [--validation-time <RFC3339>]
|
||||
EOF
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROUTINATOR_BIN=""
|
||||
ROUTINATOR_MIRROR_ROOT=""
|
||||
RPKI_CLIENT_BIN=""
|
||||
RPKI_CLIENT_MIRROR_ROOT=""
|
||||
RPKI_CLIENT_CACHE_DIR=""
|
||||
TAL_DIR=""
|
||||
OUT_DIR=""
|
||||
VALIDATION_TIME=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--routinator-bin) ROUTINATOR_BIN="$2"; shift 2 ;;
|
||||
--routinator-mirror-root) ROUTINATOR_MIRROR_ROOT="$2"; shift 2 ;;
|
||||
--rpki-client-bin) RPKI_CLIENT_BIN="$2"; shift 2 ;;
|
||||
--rpki-client-mirror-root) RPKI_CLIENT_MIRROR_ROOT="$2"; shift 2 ;;
|
||||
--rpki-client-cache-dir) RPKI_CLIENT_CACHE_DIR="$2"; shift 2 ;;
|
||||
--tal-dir) TAL_DIR="$2"; shift 2 ;;
|
||||
--out-dir) OUT_DIR="$2"; shift 2 ;;
|
||||
--validation-time) VALIDATION_TIME="$2"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$RPKI_CLIENT_MIRROR_ROOT" ]]; then
|
||||
RPKI_CLIENT_MIRROR_ROOT="$ROUTINATOR_MIRROR_ROOT"
|
||||
fi
|
||||
[[ -n "$ROUTINATOR_BIN" && -n "$ROUTINATOR_MIRROR_ROOT" && -n "$RPKI_CLIENT_BIN" && -n "$RPKI_CLIENT_MIRROR_ROOT" && -n "$TAL_DIR" && -n "$OUT_DIR" ]] || { usage >&2; exit 2; }
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
TIME_ARGS=()
|
||||
if [[ -n "$VALIDATION_TIME" ]]; then
|
||||
TIME_ARGS=(--validation-time "$VALIDATION_TIME")
|
||||
fi
|
||||
CACHE_ARGS=()
|
||||
if [[ -n "$RPKI_CLIENT_CACHE_DIR" ]]; then
|
||||
CACHE_ARGS=(--cache-dir "$RPKI_CLIENT_CACHE_DIR")
|
||||
fi
|
||||
|
||||
"$SCRIPT_DIR/run_routinator_from_local_tree.sh" \
|
||||
--routinator-bin "$ROUTINATOR_BIN" \
|
||||
--mirror-root "$ROUTINATOR_MIRROR_ROOT" \
|
||||
--tal-dir "$TAL_DIR" \
|
||||
--out-dir "$OUT_DIR/routinator" \
|
||||
--enable-aspa \
|
||||
"${TIME_ARGS[@]}"
|
||||
|
||||
"$SCRIPT_DIR/run_rpki_client_from_local_tree.sh" \
|
||||
--rpki-client-bin "$RPKI_CLIENT_BIN" \
|
||||
--mirror-root "$RPKI_CLIENT_MIRROR_ROOT" \
|
||||
--tal-dir "$TAL_DIR" \
|
||||
--out-dir "$OUT_DIR/rpki-client" \
|
||||
"${CACHE_ARGS[@]}" \
|
||||
"${TIME_ARGS[@]}"
|
||||
|
||||
python3 "$SCRIPT_DIR/compare_normalized_sets.py" \
|
||||
--left "$OUT_DIR/routinator" \
|
||||
--right "$OUT_DIR/rpki-client" \
|
||||
--left-name routinator \
|
||||
--right-name rpki-client \
|
||||
--out "$OUT_DIR/compare-summary.json"
|
||||
echo "done: $OUT_DIR"
|
||||
131
scripts/local_repo_replay/run_routinator_from_local_tree.sh
Executable file
131
scripts/local_repo_replay/run_routinator_from_local_tree.sh
Executable file
@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
run_routinator_from_local_tree.sh \
|
||||
--routinator-bin <path> \
|
||||
--mirror-root <local-rsync-mirror-root> \
|
||||
--tal-dir <dir> | --tal <file>... \
|
||||
--out-dir <path> \
|
||||
[--validation-time <RFC3339>] \
|
||||
[--real-rsync-bin <path>] \
|
||||
[--enable-aspa]
|
||||
|
||||
The input mirror is prepared by the caller. This script does not generate it.
|
||||
Pass --validation-time only if FAKETIME_LIB points to a working libfaketime
|
||||
library; otherwise Routinator will validate at wall-clock time.
|
||||
Example:
|
||||
export FAKETIME_LIB=/usr/lib/x86_64-linux-gnu/faketime/libfaketime.so.1
|
||||
EOF
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROUTINATOR_BIN=""
|
||||
MIRROR_ROOT=""
|
||||
OUT_DIR=""
|
||||
VALIDATION_TIME=""
|
||||
REAL_RSYNC_BIN="${REAL_RSYNC_BIN:-/usr/bin/rsync}"
|
||||
ENABLE_ASPA=0
|
||||
TAL_DIR=""
|
||||
TALS=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--routinator-bin) ROUTINATOR_BIN="$2"; shift 2 ;;
|
||||
--mirror-root) MIRROR_ROOT="$2"; shift 2 ;;
|
||||
--tal-dir) TAL_DIR="$2"; shift 2 ;;
|
||||
--tal) TALS+=("$2"); shift 2 ;;
|
||||
--out-dir) OUT_DIR="$2"; shift 2 ;;
|
||||
--validation-time) VALIDATION_TIME="$2"; shift 2 ;;
|
||||
--real-rsync-bin) REAL_RSYNC_BIN="$2"; shift 2 ;;
|
||||
--enable-aspa) ENABLE_ASPA=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$ROUTINATOR_BIN" && -n "$MIRROR_ROOT" && -n "$OUT_DIR" ]] || { usage >&2; exit 2; }
|
||||
[[ -x "$ROUTINATOR_BIN" ]] || { echo "routinator binary not executable: $ROUTINATOR_BIN" >&2; exit 2; }
|
||||
[[ -d "$MIRROR_ROOT" ]] || { echo "mirror root not found: $MIRROR_ROOT" >&2; exit 2; }
|
||||
if [[ -z "$TAL_DIR" && "${#TALS[@]}" -eq 0 ]]; then
|
||||
echo "provide --tal-dir or at least one --tal" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
WORK_DIR="$OUT_DIR/work"
|
||||
REPO_DIR="$WORK_DIR/repository"
|
||||
EXTRA_TALS="$WORK_DIR/tals"
|
||||
CONFIG_FILE="$WORK_DIR/routinator.conf"
|
||||
rm -rf "$WORK_DIR"
|
||||
mkdir -p "$REPO_DIR" "$EXTRA_TALS"
|
||||
|
||||
PREPARE_TAL_ARGS=(--out-dir "$EXTRA_TALS" --summary "$OUT_DIR/prepared-tals.json")
|
||||
if [[ -n "$TAL_DIR" ]]; then
|
||||
PREPARE_TAL_ARGS+=(--tal-dir "$TAL_DIR")
|
||||
fi
|
||||
for tal in "${TALS[@]}"; do
|
||||
PREPARE_TAL_ARGS+=(--tal "$tal")
|
||||
done
|
||||
python3 "$SCRIPT_DIR/prepare_tals.py" "${PREPARE_TAL_ARGS[@]}"
|
||||
|
||||
cat > "$CONFIG_FILE" <<EOF
|
||||
repository-dir = "$REPO_DIR"
|
||||
no-rir-tals = true
|
||||
extra-tals-dir = "$EXTRA_TALS"
|
||||
disable-rrdp = true
|
||||
rrdp-fallback = "never"
|
||||
rsync-command = "$SCRIPT_DIR/cir-rsync-wrapper"
|
||||
EOF
|
||||
|
||||
export CIR_MIRROR_ROOT="$(cd "$MIRROR_ROOT" && pwd)"
|
||||
export REAL_RSYNC_BIN="$REAL_RSYNC_BIN"
|
||||
export CIR_LOCAL_LINK_MODE=1
|
||||
export LOCAL_REPO_REPLAY_VALIDATION_TIME="$VALIDATION_TIME"
|
||||
export LOCAL_REPO_REPLAY_OUT_DIR="$OUT_DIR"
|
||||
|
||||
ARGS=(
|
||||
"$ROUTINATOR_BIN"
|
||||
--config "$CONFIG_FILE"
|
||||
--repository-dir "$REPO_DIR"
|
||||
--disable-rrdp
|
||||
--rrdp-fallback never
|
||||
--rsync-command "$SCRIPT_DIR/cir-rsync-wrapper"
|
||||
--no-rir-tals
|
||||
--extra-tals-dir "$EXTRA_TALS"
|
||||
)
|
||||
if [[ "$ENABLE_ASPA" -eq 1 ]]; then
|
||||
echo 'enable-aspa = true' >> "$CONFIG_FILE"
|
||||
ARGS+=(--enable-aspa)
|
||||
fi
|
||||
if [[ -n "$VALIDATION_TIME" && -z "${FAKETIME_LIB:-}" ]]; then
|
||||
echo "warning: --validation-time is ignored for Routinator because FAKETIME_LIB is not set" >&2
|
||||
fi
|
||||
VRP_ARGS=(vrps -o "$OUT_DIR/vrps.csv")
|
||||
JSON_ARGS=(vrps -f jsonext -o "$OUT_DIR/routinator.json")
|
||||
|
||||
/usr/bin/time -v -o "$OUT_DIR/process-time.txt" bash -c '
|
||||
set -euo pipefail
|
||||
if [[ -n "$LOCAL_REPO_REPLAY_VALIDATION_TIME" && -n "${FAKETIME_LIB:-}" ]]; then
|
||||
faketime_value="$(python3 - <<'"'"'PY'"'"' "$LOCAL_REPO_REPLAY_VALIDATION_TIME"
|
||||
from datetime import datetime, timezone
|
||||
import sys
|
||||
dt = datetime.fromisoformat(sys.argv[1].replace("Z", "+00:00")).astimezone(timezone.utc)
|
||||
print("@" + dt.strftime("%Y-%m-%d %H:%M:%S"))
|
||||
PY
|
||||
)"
|
||||
export LD_PRELOAD="$FAKETIME_LIB"
|
||||
export TZ=UTC
|
||||
unset FAKETIME_FMT
|
||||
export FAKETIME="$faketime_value"
|
||||
export FAKETIME_DONT_FAKE_MONOTONIC=1
|
||||
fi
|
||||
"$@" update --complete >"$LOCAL_REPO_REPLAY_OUT_DIR/update.log" 2>&1 || true
|
||||
"$@" vrps -o "$LOCAL_REPO_REPLAY_OUT_DIR/vrps.csv" >"$LOCAL_REPO_REPLAY_OUT_DIR/vrps.log" 2>&1
|
||||
"$@" vrps -f jsonext -o "$LOCAL_REPO_REPLAY_OUT_DIR/routinator.json" >"$LOCAL_REPO_REPLAY_OUT_DIR/json.log" 2>&1
|
||||
' local_repo_replay "${ARGS[@]}"
|
||||
python3 "$SCRIPT_DIR/normalize_rp_outputs.py" --kind routinator --input "$OUT_DIR/routinator.json" --vrps-out "$OUT_DIR/vrps.normalized.txt" --vaps-out "$OUT_DIR/vaps.normalized.txt"
|
||||
python3 "$SCRIPT_DIR/summarize_replay.py" --rp routinator --out-dir "$OUT_DIR" --summary "$OUT_DIR/summary.json"
|
||||
echo "done: $OUT_DIR"
|
||||
107
scripts/local_repo_replay/run_rpki_client_from_local_tree.sh
Executable file
107
scripts/local_repo_replay/run_rpki_client_from_local_tree.sh
Executable file
@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
run_rpki_client_from_local_tree.sh \
|
||||
--rpki-client-bin <path> \
|
||||
--mirror-root <local-rsync-mirror-root> \
|
||||
--tal-dir <dir> | --tal <file>... \
|
||||
--out-dir <path> \
|
||||
[--cache-dir <work-cache-dir>] \
|
||||
[--validation-time <RFC3339>] \
|
||||
[--real-rsync-bin <path>] \
|
||||
[--parser-workers <n>]
|
||||
|
||||
The input mirror is prepared by the caller. This script disables RRDP, points
|
||||
rpki-client's rsync command at a local URI mapper, and fetches only from the
|
||||
local filesystem tree.
|
||||
EOF
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
RPKI_CLIENT_BIN=""
|
||||
CACHE_DIR=""
|
||||
MIRROR_ROOT=""
|
||||
OUT_DIR=""
|
||||
TAL_DIR=""
|
||||
VALIDATION_TIME=""
|
||||
PARSER_WORKERS="${PARSER_WORKERS:-4}"
|
||||
REAL_RSYNC_BIN="${REAL_RSYNC_BIN:-/usr/bin/rsync}"
|
||||
TALS=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--rpki-client-bin) RPKI_CLIENT_BIN="$2"; shift 2 ;;
|
||||
--mirror-root) MIRROR_ROOT="$2"; shift 2 ;;
|
||||
--cache-dir) CACHE_DIR="$2"; shift 2 ;;
|
||||
--tal-dir) TAL_DIR="$2"; shift 2 ;;
|
||||
--tal) TALS+=("$2"); shift 2 ;;
|
||||
--out-dir) OUT_DIR="$2"; shift 2 ;;
|
||||
--validation-time) VALIDATION_TIME="$2"; shift 2 ;;
|
||||
--real-rsync-bin) REAL_RSYNC_BIN="$2"; shift 2 ;;
|
||||
--parser-workers) PARSER_WORKERS="$2"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$RPKI_CLIENT_BIN" && -n "$MIRROR_ROOT" && -n "$OUT_DIR" ]] || { usage >&2; exit 2; }
|
||||
[[ -x "$RPKI_CLIENT_BIN" ]] || { echo "rpki-client binary not executable: $RPKI_CLIENT_BIN" >&2; exit 2; }
|
||||
[[ -d "$MIRROR_ROOT" ]] || { echo "mirror root not found: $MIRROR_ROOT" >&2; exit 2; }
|
||||
if [[ -z "$TAL_DIR" && "${#TALS[@]}" -eq 0 ]]; then
|
||||
echo "provide --tal-dir or at least one --tal" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
if [[ -z "$CACHE_DIR" ]]; then
|
||||
CACHE_DIR="$OUT_DIR/cache"
|
||||
fi
|
||||
mkdir -p "$CACHE_DIR" "$OUT_DIR/out"
|
||||
chmod a+rwx "$OUT_DIR" "$CACHE_DIR" "$OUT_DIR/out"
|
||||
WORK_DIR="$OUT_DIR/work"
|
||||
PREPARED_TALS="$WORK_DIR/tals"
|
||||
rm -rf "$WORK_DIR"
|
||||
mkdir -p "$PREPARED_TALS"
|
||||
PREPARE_TAL_ARGS=(--out-dir "$PREPARED_TALS" --summary "$OUT_DIR/prepared-tals.json")
|
||||
if [[ -n "$TAL_DIR" ]]; then
|
||||
PREPARE_TAL_ARGS+=(--tal-dir "$TAL_DIR")
|
||||
fi
|
||||
for tal in "${TALS[@]}"; do
|
||||
PREPARE_TAL_ARGS+=(--tal "$tal")
|
||||
done
|
||||
python3 "$SCRIPT_DIR/prepare_tals.py" "${PREPARE_TAL_ARGS[@]}"
|
||||
|
||||
CLIENT_TAL_ARGS=()
|
||||
while IFS= read -r tal; do
|
||||
CLIENT_TAL_ARGS+=(-t "$tal")
|
||||
done < <(find "$PREPARED_TALS" -maxdepth 1 -type f -name '*.tal' | sort)
|
||||
|
||||
TIME_ARGS=()
|
||||
if [[ -n "$VALIDATION_TIME" ]]; then
|
||||
epoch="$(python3 - <<'PY' "$VALIDATION_TIME"
|
||||
from datetime import datetime, timezone
|
||||
import sys
|
||||
print(int(datetime.fromisoformat(sys.argv[1].replace("Z", "+00:00")).astimezone(timezone.utc).timestamp()))
|
||||
PY
|
||||
)"
|
||||
TIME_ARGS=(-P "$epoch")
|
||||
fi
|
||||
|
||||
export CIR_MIRROR_ROOT="$(cd "$MIRROR_ROOT" && pwd)"
|
||||
export REAL_RSYNC_BIN="$REAL_RSYNC_BIN"
|
||||
export CIR_LOCAL_LINK_MODE=1
|
||||
|
||||
/usr/bin/time -v -o "$OUT_DIR/process-time.txt" \
|
||||
"$RPKI_CLIENT_BIN" \
|
||||
-R -e "$SCRIPT_DIR/cir-rsync-wrapper" -j -c -p "$PARSER_WORKERS" \
|
||||
"${TIME_ARGS[@]}" \
|
||||
"${CLIENT_TAL_ARGS[@]}" \
|
||||
-d "$CACHE_DIR" \
|
||||
"$OUT_DIR/out" >"$OUT_DIR/stdout.log" 2>"$OUT_DIR/stderr.log"
|
||||
|
||||
python3 "$SCRIPT_DIR/normalize_rp_outputs.py" --kind rpki-client --input "$OUT_DIR/out" --vrps-out "$OUT_DIR/vrps.normalized.txt" --vaps-out "$OUT_DIR/vaps.normalized.txt"
|
||||
python3 "$SCRIPT_DIR/summarize_replay.py" --rp rpki-client --out-dir "$OUT_DIR" --summary "$OUT_DIR/summary.json"
|
||||
echo "done: $OUT_DIR"
|
||||
47
scripts/local_repo_replay/summarize_replay.py
Executable file
47
scripts/local_repo_replay/summarize_replay.py
Executable file
@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def count_lines(path: Path) -> int:
|
||||
if not path.is_file():
|
||||
return 0
|
||||
return sum(1 for line in path.read_text(encoding="utf-8", errors="replace").splitlines() if line.strip())
|
||||
|
||||
|
||||
def parse_time(path: Path) -> dict[str, object]:
|
||||
if not path.is_file():
|
||||
return {}
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
elapsed = re.search(r"Elapsed \(wall clock\) time .*: (.+)", text)
|
||||
rss = re.search(r"Maximum resident set size \(kbytes\): (\d+)", text)
|
||||
return {
|
||||
"elapsed": elapsed.group(1).strip() if elapsed else "",
|
||||
"maxRssKb": int(rss.group(1)) if rss else 0,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--rp", required=True)
|
||||
parser.add_argument("--out-dir", type=Path, required=True)
|
||||
parser.add_argument("--summary", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
summary = {
|
||||
"rp": args.rp,
|
||||
"vrps": count_lines(args.out_dir / "vrps.normalized.txt"),
|
||||
"vaps": count_lines(args.out_dir / "vaps.normalized.txt"),
|
||||
"time": parse_time(args.out_dir / "process-time.txt"),
|
||||
"artifacts": sorted(path.name for path in args.out_dir.iterdir() if path.is_file()),
|
||||
}
|
||||
args.summary.write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
print(args.summary)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
118
scripts/local_repo_replay/templates/README.md
Normal file
118
scripts/local_repo_replay/templates/README.md
Normal file
@ -0,0 +1,118 @@
|
||||
# Local Repository Tree Replay Package
|
||||
|
||||
This package replays already prepared local RPKI repository trees with
|
||||
Routinator and rpki-client.
|
||||
|
||||
It is intentionally independent from CIR:
|
||||
|
||||
- it does not read `.cir`;
|
||||
- it does not read `repo-bytes.db`;
|
||||
- it does not call `cir_materialize`;
|
||||
- it does not generate a local repository tree.
|
||||
|
||||
The caller must prepare the local repository/cache tree before running these
|
||||
scripts.
|
||||
|
||||
## Contents
|
||||
|
||||
```text
|
||||
local-repo-replay-package/
|
||||
scripts/
|
||||
run_routinator_from_local_tree.sh
|
||||
run_rpki_client_from_local_tree.sh
|
||||
run_dual_local_tree_replay.sh
|
||||
prepare_tals.py
|
||||
cir-rsync-wrapper
|
||||
cir-local-link-sync.py
|
||||
normalize_rp_outputs.py
|
||||
compare_normalized_sets.py
|
||||
summarize_replay.py
|
||||
docs/
|
||||
input_tree_requirements.md
|
||||
offline_replay_limits.md
|
||||
output_files.md
|
||||
examples/
|
||||
routinator_example.sh
|
||||
rpki_client_example.sh
|
||||
dual_compare_example.sh
|
||||
env.example
|
||||
```
|
||||
|
||||
## Routinator replay
|
||||
|
||||
```bash
|
||||
./scripts/run_routinator_from_local_tree.sh \
|
||||
--routinator-bin /opt/routinator/target/release/routinator \
|
||||
--mirror-root /data/replay/mirror \
|
||||
--tal-dir /data/replay/tals \
|
||||
--out-dir /data/replay/out/routinator \
|
||||
--enable-aspa
|
||||
```
|
||||
|
||||
The script uses `--disable-rrdp`, `--rsync-command ./scripts/cir-rsync-wrapper`,
|
||||
and the local mirror root to satisfy rsync fetches from the local filesystem.
|
||||
The wrapper name is historical; in this package it is only a generic
|
||||
`rsync://` to local-path mapper.
|
||||
|
||||
If `--validation-time` is needed for Routinator, set `FAKETIME_LIB` to a working
|
||||
libfaketime shared library. Otherwise Routinator validates at wall-clock time.
|
||||
|
||||
On Ubuntu, install and use faketime like this:
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y libfaketime
|
||||
export FAKETIME_LIB=/usr/lib/x86_64-linux-gnu/faketime/libfaketime.so.1
|
||||
./scripts/run_routinator_from_local_tree.sh \
|
||||
--routinator-bin /opt/routinator/target/release/routinator \
|
||||
--mirror-root /data/replay/mirror \
|
||||
--tal-dir /data/replay/tals \
|
||||
--out-dir /data/replay/out/routinator \
|
||||
--validation-time 2026-05-14T06:48:00Z \
|
||||
--enable-aspa
|
||||
```
|
||||
|
||||
Without `FAKETIME_LIB`, old local trees can produce empty or smaller output
|
||||
because Routinator validates manifests and CRLs against current wall-clock time.
|
||||
|
||||
## rpki-client replay
|
||||
|
||||
```bash
|
||||
./scripts/run_rpki_client_from_local_tree.sh \
|
||||
--rpki-client-bin /opt/rpki-client/src/rpki-client \
|
||||
--mirror-root /data/replay/mirror \
|
||||
--tal-dir /data/replay/tals \
|
||||
--out-dir /data/replay/out/rpki-client \
|
||||
--parser-workers 4
|
||||
```
|
||||
|
||||
The script uses `rpki-client -R -e ./scripts/cir-rsync-wrapper` so RRDP is
|
||||
disabled and rsync fetches are served from the local mirror. `--cache-dir` is an
|
||||
optional working cache directory used by rpki-client during this local replay.
|
||||
|
||||
## Dual replay
|
||||
|
||||
```bash
|
||||
./scripts/run_dual_local_tree_replay.sh \
|
||||
--routinator-bin /opt/routinator/target/release/routinator \
|
||||
--routinator-mirror-root /data/replay/mirror \
|
||||
--rpki-client-bin /opt/rpki-client/src/rpki-client \
|
||||
--rpki-client-mirror-root /data/replay/mirror \
|
||||
--tal-dir /data/replay/tals \
|
||||
--out-dir /data/replay/out/dual
|
||||
```
|
||||
|
||||
If `--validation-time` is passed to dual replay, remember to export
|
||||
`FAKETIME_LIB` first so Routinator and rpki-client use the same logical
|
||||
validation time.
|
||||
|
||||
## Outputs
|
||||
|
||||
Each run writes normalized output:
|
||||
|
||||
- `vrps.normalized.txt`
|
||||
- `vaps.normalized.txt`
|
||||
- `summary.json`
|
||||
- raw RP output and logs
|
||||
- `process-time.txt`
|
||||
|
||||
See `docs/output_files.md`.
|
||||
@ -0,0 +1,39 @@
|
||||
# Input Tree Requirements
|
||||
|
||||
The input tree is not part of this package. The caller must prepare it before
|
||||
running replay.
|
||||
|
||||
## Mirror root
|
||||
|
||||
The mirror root must map rsync URIs to local paths:
|
||||
|
||||
```text
|
||||
rsync://rpki.example.net/repo/a/b/c.roa
|
||||
=> <mirror-root>/rpki.example.net/repo/a/b/c.roa
|
||||
```
|
||||
|
||||
The tree must contain all objects needed by the selected TALs: TA certificates,
|
||||
manifests, CRLs, ROAs, ASPAs, router certs, and child CA certificates.
|
||||
|
||||
Both Routinator and rpki-client scripts consume this same mirror root through a
|
||||
local rsync command wrapper.
|
||||
|
||||
## rpki-client working cache
|
||||
|
||||
For rpki-client replay, `--cache-dir` is only rpki-client's working cache
|
||||
directory for this local run. It is not the input dataset. The authoritative
|
||||
input is `--mirror-root`.
|
||||
|
||||
## TALs
|
||||
|
||||
Provide either `--tal-dir <dir>` or repeated `--tal <file>`.
|
||||
|
||||
The scripts prepare a replay-local TAL copy that prefers `rsync://` TA
|
||||
certificate URIs. This prevents a TAL with an HTTPS URI listed first from
|
||||
escaping to the network during local replay. The TAL set should match the local
|
||||
tree. Mixing a tree from one run with different TALs may produce meaningless
|
||||
differences.
|
||||
|
||||
## No generation
|
||||
|
||||
This package does not generate the tree and does not repair missing objects.
|
||||
@ -0,0 +1,38 @@
|
||||
# Offline Replay Limits
|
||||
|
||||
## Routinator
|
||||
|
||||
The Routinator script disables RRDP and uses an rsync command wrapper to map
|
||||
rsync URLs to local paths. It still runs Routinator's normal validation logic.
|
||||
|
||||
If the local mirror does not contain required objects, validation can fail or
|
||||
produce fewer outputs.
|
||||
|
||||
## rpki-client
|
||||
|
||||
The rpki-client script uses `-R` to disable RRDP and `-e` to point rsync at the
|
||||
local mapper. rpki-client still builds its normal working cache, but every
|
||||
rsync source is rewritten to the local mirror.
|
||||
|
||||
If the mirror was incomplete or produced by a different TAL set, replay results
|
||||
may differ from the original run.
|
||||
|
||||
## Validation time
|
||||
|
||||
rpki-client supports `-P <posix-seconds>`. Routinator does not expose the same
|
||||
simple command-line evaluation-time option in the tested version; if `FAKETIME_LIB`
|
||||
is configured, the script can run Routinator under faketime. Without
|
||||
`FAKETIME_LIB`, `--validation-time` is intentionally ignored for Routinator and
|
||||
current wall-clock validation can reject stale manifests or CRLs.
|
||||
|
||||
Ubuntu example:
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y libfaketime
|
||||
export FAKETIME_LIB=/usr/lib/x86_64-linux-gnu/faketime/libfaketime.so.1
|
||||
```
|
||||
|
||||
The script sets `TZ=UTC` and converts RFC3339 validation time to libfaketime
|
||||
absolute UTC format, for example `2026-05-14T06:48:00Z` becomes
|
||||
`@2026-05-14 06:48:00`. Setting `TZ=UTC` is required because libfaketime parses
|
||||
absolute timestamps in the process timezone.
|
||||
16
scripts/local_repo_replay/templates/docs/output_files.md
Normal file
16
scripts/local_repo_replay/templates/docs/output_files.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Output Files
|
||||
|
||||
Each replay output directory can contain:
|
||||
|
||||
- `vrps.normalized.txt`: one normalized VRP per line.
|
||||
- `vaps.normalized.txt`: one normalized VAP/ASPA per line.
|
||||
- `summary.json`: counts and resource summary.
|
||||
- `process-time.txt`: `/usr/bin/time -v` output.
|
||||
- RP-specific raw output:
|
||||
- Routinator: `routinator.json`, `vrps.csv`.
|
||||
- rpki-client: `out/json`, `out/csv`, and other native files.
|
||||
- logs:
|
||||
- `stdout.log` / `stderr.log` for rpki-client.
|
||||
- `update.log`, `vrps.log`, `json.log` for Routinator.
|
||||
|
||||
Dual replay additionally writes `compare-summary.json`.
|
||||
11
scripts/local_repo_replay/templates/examples/dual_compare_example.sh
Executable file
11
scripts/local_repo_replay/templates/examples/dual_compare_example.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
./scripts/run_dual_local_tree_replay.sh \
|
||||
--routinator-bin "${ROUTINATOR_BIN:-/opt/routinator/target/release/routinator}" \
|
||||
--routinator-mirror-root "${ROUTINATOR_MIRROR_ROOT:-/data/replay/mirror}" \
|
||||
--rpki-client-bin "${RPKI_CLIENT_BIN:-/opt/rpki-client/src/rpki-client}" \
|
||||
--rpki-client-mirror-root "${RPKI_CLIENT_MIRROR_ROOT:-/data/replay/mirror}" \
|
||||
--rpki-client-cache-dir "${RPKI_CLIENT_CACHE_DIR:-/data/replay/work/rpki-client-cache}" \
|
||||
--tal-dir "${TAL_DIR:-/data/replay/tals}" \
|
||||
--out-dir "${OUT_DIR:-/data/replay/out/dual}"
|
||||
9
scripts/local_repo_replay/templates/examples/routinator_example.sh
Executable file
9
scripts/local_repo_replay/templates/examples/routinator_example.sh
Executable file
@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
./scripts/run_routinator_from_local_tree.sh \
|
||||
--routinator-bin "${ROUTINATOR_BIN:-/opt/routinator/target/release/routinator}" \
|
||||
--mirror-root "${MIRROR_ROOT:-/data/replay/mirror}" \
|
||||
--tal-dir "${TAL_DIR:-/data/replay/tals}" \
|
||||
--out-dir "${OUT_DIR:-/data/replay/out/routinator}" \
|
||||
--enable-aspa
|
||||
10
scripts/local_repo_replay/templates/examples/rpki_client_example.sh
Executable file
10
scripts/local_repo_replay/templates/examples/rpki_client_example.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
./scripts/run_rpki_client_from_local_tree.sh \
|
||||
--rpki-client-bin "${RPKI_CLIENT_BIN:-/opt/rpki-client/src/rpki-client}" \
|
||||
--mirror-root "${MIRROR_ROOT:-/data/replay/mirror}" \
|
||||
--cache-dir "${RPKI_CLIENT_CACHE_DIR:-/data/replay/work/rpki-client-cache}" \
|
||||
--tal-dir "${TAL_DIR:-/data/replay/tals}" \
|
||||
--out-dir "${OUT_DIR:-/data/replay/out/rpki-client}" \
|
||||
--parser-workers "${PARSER_WORKERS:-4}"
|
||||
@ -2,6 +2,7 @@
|
||||
# 复制为 .env 后可以在远端直接调整;所有路径默认相对 package 根目录。
|
||||
|
||||
# 最大运行轮次。重复执行 run_soak.sh 时会从已有最后一轮之后继续编号。
|
||||
# 正整数表示固定运行轮次;负数(例如 -1)表示持续运行不自动停止;0 非法。
|
||||
MAX_RUNS=3
|
||||
|
||||
# 两轮之间等待秒数。做连续无等待验收时设置为 0。
|
||||
@ -15,7 +16,7 @@ RIRS=afrinic,apnic,arin,lacnic,ripe
|
||||
# 运行根目录。默认使用 package 根目录;如需把产物写到独立数据盘,可改成绝对路径。
|
||||
RUN_ROOT="${PACKAGE_ROOT}"
|
||||
|
||||
# 保留最近多少轮 run 目录。旧 run 会由 rpki_daemon 自身或后续脚本策略清理。
|
||||
# 保留最近多少轮 run 目录。持续运行模式建议设置为 5 或按磁盘容量评估。
|
||||
RETAIN_RUNS=10
|
||||
|
||||
# 是否输出 compact report JSON。1 表示启用,0 表示关闭。
|
||||
|
||||
@ -77,6 +77,11 @@ validate_non_negative_int() {
|
||||
[[ "$value" =~ ^[0-9]+$ ]] || die "$name must be an integer: $value"
|
||||
}
|
||||
|
||||
validate_max_runs() {
|
||||
[[ "$MAX_RUNS" =~ ^-?[0-9]+$ ]] || die "MAX_RUNS must be an integer: $MAX_RUNS"
|
||||
[[ "$MAX_RUNS" != "0" ]] || die "MAX_RUNS must be non-zero; use a positive value for fixed runs or -1 for continuous mode"
|
||||
}
|
||||
|
||||
validate_rsync_scope() {
|
||||
case "$RSYNC_SCOPE" in
|
||||
publication-point|module-root)
|
||||
@ -486,20 +491,30 @@ PY
|
||||
|
||||
apply_outer_retention() {
|
||||
local dirs=()
|
||||
local retain_limit="$RETAIN_RUNS"
|
||||
local keep_run="${1:-}"
|
||||
local run_dir
|
||||
shopt -s nullglob
|
||||
for run_dir in "$RUNS_ROOT"/run_[0-9][0-9][0-9][0-9]; do
|
||||
[[ -d "$run_dir" ]] && dirs+=("$run_dir")
|
||||
done
|
||||
shopt -u nullglob
|
||||
if (( ${#dirs[@]} <= RETAIN_RUNS )); then
|
||||
if (( ${#dirs[@]} <= retain_limit )); then
|
||||
return 0
|
||||
fi
|
||||
mapfile -t dirs < <(printf '%s\n' "${dirs[@]}" | sort)
|
||||
local remove_count=$(( ${#dirs[@]} - RETAIN_RUNS ))
|
||||
local index
|
||||
for (( index = 0; index < remove_count; index++ )); do
|
||||
rm -rf "${dirs[$index]}"
|
||||
local remove_count=$(( ${#dirs[@]} - retain_limit ))
|
||||
local removed_count=0
|
||||
local candidate
|
||||
for candidate in "${dirs[@]}"; do
|
||||
if [[ -n "$keep_run" && "$(basename "$candidate")" == "$keep_run" ]]; then
|
||||
continue
|
||||
fi
|
||||
rm -rf "$candidate"
|
||||
removed_count=$((removed_count + 1))
|
||||
if (( removed_count >= remove_count )); then
|
||||
break
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
@ -519,6 +534,7 @@ run_one_round() {
|
||||
local summary_state
|
||||
|
||||
mkdir -p "$run_dir" "$daemon_state_root"
|
||||
apply_outer_retention "$run_id"
|
||||
started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
write_run_meta "$run_dir/run-meta.json" "running" "$run_index" "$run_id" "$sync_mode" \
|
||||
"$snapshot_reason" "$previous_run_id" "$previous_success_value" "$started_at" "" \
|
||||
@ -573,7 +589,7 @@ main() {
|
||||
require_command python3
|
||||
require_command date
|
||||
require_command find
|
||||
validate_positive_int "MAX_RUNS" "$MAX_RUNS"
|
||||
validate_max_runs
|
||||
validate_non_negative_int "INTERVAL_SECS" "$INTERVAL_SECS"
|
||||
validate_positive_int "RETAIN_RUNS" "$RETAIN_RUNS"
|
||||
validate_rsync_scope
|
||||
@ -599,12 +615,20 @@ main() {
|
||||
|
||||
local max_index
|
||||
local next_index
|
||||
local run_forever=0
|
||||
local stop_index=0
|
||||
max_index="$(max_existing_run_index)"
|
||||
next_index=$((max_index + 1))
|
||||
local stop_index=$((max_index + MAX_RUNS))
|
||||
if (( MAX_RUNS < 0 )); then
|
||||
run_forever=1
|
||||
echo "run_soak mode=continuous max_existing_run_index=$max_index next_run=$(printf 'run_%04d' "$next_index")"
|
||||
else
|
||||
stop_index=$((max_index + MAX_RUNS))
|
||||
echo "run_soak mode=fixed max_existing_run_index=$max_index next_run=$(printf 'run_%04d' "$next_index") stop_run=$(printf 'run_%04d' "$stop_index")"
|
||||
fi
|
||||
local any_failed=0
|
||||
|
||||
while (( next_index <= stop_index )); do
|
||||
while (( run_forever == 1 || next_index <= stop_index )); do
|
||||
INVALID_DB_PATH=""
|
||||
INVALID_STATE_PATH=""
|
||||
INVALID_TMP_PATH=""
|
||||
@ -649,7 +673,7 @@ main() {
|
||||
echo "completed run $(printf 'run_%04d' "$next_index") status=failed" >&2
|
||||
any_failed=1
|
||||
fi
|
||||
if (( next_index < stop_index && INTERVAL_SECS > 0 )); then
|
||||
if (( (run_forever == 1 || next_index < stop_index) && INTERVAL_SECS > 0 )); then
|
||||
sleep "$INTERVAL_SECS"
|
||||
fi
|
||||
next_index=$((next_index + 1))
|
||||
|
||||
2037
src/bin/rpki_artifact_metrics.rs
Normal file
2037
src/bin/rpki_artifact_metrics.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@ pub mod decode;
|
||||
pub mod encode;
|
||||
#[cfg(feature = "full")]
|
||||
pub mod export;
|
||||
#[cfg(feature = "full")]
|
||||
pub mod materialize;
|
||||
pub mod model;
|
||||
pub mod sequence;
|
||||
@ -15,6 +16,7 @@ pub use export::{
|
||||
CirExportError, CirExportSummary, CirTrustAnchorBinding, build_cir_from_run,
|
||||
build_cir_from_run_multi, export_cir_from_run, export_cir_from_run_multi, write_cir_file,
|
||||
};
|
||||
#[cfg(feature = "full")]
|
||||
pub use materialize::{
|
||||
CirMaterializeError, CirMaterializeSummary, materialize_cir, materialize_cir_from_raw_store,
|
||||
materialize_cir_from_repo_bytes, mirror_relative_path_for_rsync_uri, resolve_static_pool_file,
|
||||
|
||||
@ -12,6 +12,7 @@ pub mod audit_downloads;
|
||||
pub mod audit_trace;
|
||||
#[cfg(feature = "full")]
|
||||
pub mod blob_store;
|
||||
#[cfg(feature = "full")]
|
||||
pub mod cli;
|
||||
#[cfg(feature = "full")]
|
||||
pub mod current_repo_index;
|
||||
|
||||
@ -589,6 +589,48 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
struct FailRrdpThenFailRsyncExecutor {
|
||||
rrdp_count: Arc<AtomicUsize>,
|
||||
rsync_count: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl RepoTransportExecutor for FailRrdpThenFailRsyncExecutor {
|
||||
fn execute_transport(&self, task: RepoTransportTask) -> RepoTransportResultEnvelope {
|
||||
match task.mode {
|
||||
RepoTransportMode::Rrdp => {
|
||||
self.rrdp_count.fetch_add(1, Ordering::SeqCst);
|
||||
RepoTransportResultEnvelope {
|
||||
dedup_key: task.dedup_key,
|
||||
repo_identity: task.repo_identity,
|
||||
mode: RepoTransportMode::Rrdp,
|
||||
tal_id: task.tal_id,
|
||||
rir_id: task.rir_id,
|
||||
timing_ms: 10,
|
||||
result: RepoTransportResultKind::Failed {
|
||||
detail: "rrdp failed".to_string(),
|
||||
warnings: vec![Warning::new("rrdp failed")],
|
||||
},
|
||||
}
|
||||
}
|
||||
RepoTransportMode::Rsync => {
|
||||
self.rsync_count.fetch_add(1, Ordering::SeqCst);
|
||||
RepoTransportResultEnvelope {
|
||||
dedup_key: task.dedup_key,
|
||||
repo_identity: task.repo_identity,
|
||||
mode: RepoTransportMode::Rsync,
|
||||
tal_id: task.tal_id,
|
||||
rir_id: task.rir_id,
|
||||
timing_ms: 12,
|
||||
result: RepoTransportResultKind::Failed {
|
||||
detail: "rsync failed".to_string(),
|
||||
warnings: vec![Warning::new("rsync failed")],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phase1_runtime_waits_for_rrdp_transport_and_returns_rrdp_outcome() {
|
||||
let coordinator = GlobalRunCoordinator::new(
|
||||
@ -852,6 +894,42 @@ mod tests {
|
||||
assert_eq!(rsync_count.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phase1_runtime_terminal_failure_keeps_rsync_failure_duration() {
|
||||
let rrdp_count = Arc::new(AtomicUsize::new(0));
|
||||
let rsync_count = Arc::new(AtomicUsize::new(0));
|
||||
let coordinator = GlobalRunCoordinator::new(
|
||||
ParallelPhase1Config::default(),
|
||||
vec![TalInputSpec::from_url("https://example.test/arin.tal")],
|
||||
);
|
||||
let pool = RepoTransportWorkerPool::new(
|
||||
RepoWorkerPoolConfig { max_workers: 1 },
|
||||
FailRrdpThenFailRsyncExecutor {
|
||||
rrdp_count: Arc::clone(&rrdp_count),
|
||||
rsync_count: Arc::clone(&rsync_count),
|
||||
},
|
||||
)
|
||||
.expect("pool");
|
||||
let runtime = Phase1RepoSyncRuntime::new(
|
||||
coordinator,
|
||||
pool,
|
||||
Arc::new(|_base: &str| "rsync://example.test/module/".to_string()),
|
||||
SyncPreference::RrdpThenRsync,
|
||||
);
|
||||
|
||||
let outcome = runtime
|
||||
.sync_publication_point_repo(&sample_ca("rsync://example.test/repo/root.mft"))
|
||||
.expect("sync repo");
|
||||
assert!(!outcome.repo_sync_ok);
|
||||
assert_eq!(
|
||||
outcome.repo_sync_phase.as_deref(),
|
||||
Some("rrdp_failed_rsync_failed")
|
||||
);
|
||||
assert_eq!(outcome.repo_sync_duration_ms, 12);
|
||||
assert_eq!(rrdp_count.load(Ordering::SeqCst), 1);
|
||||
assert_eq!(rsync_count.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phase1_runtime_prefetch_submits_transport_task_before_consumption() {
|
||||
let rrdp_count = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
@ -433,6 +433,7 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
|
||||
}
|
||||
|
||||
let repo_sync_started = std::time::Instant::now();
|
||||
let mut runtime_repo_sync_duration_ms = None;
|
||||
let (repo_sync_ok, repo_sync_err, repo_sync_source, repo_sync_phase): (
|
||||
bool,
|
||||
Option<String>,
|
||||
@ -444,9 +445,10 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
|
||||
repo_sync_err,
|
||||
repo_sync_source,
|
||||
repo_sync_phase,
|
||||
repo_sync_duration_ms: _,
|
||||
repo_sync_duration_ms,
|
||||
warnings: repo_warnings,
|
||||
} = runtime.sync_publication_point_repo(ca)?;
|
||||
runtime_repo_sync_duration_ms = Some(repo_sync_duration_ms);
|
||||
warnings.extend(repo_warnings);
|
||||
(
|
||||
repo_sync_ok,
|
||||
@ -575,7 +577,11 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
|
||||
}
|
||||
}
|
||||
};
|
||||
let repo_sync_duration_ms = repo_sync_started.elapsed().as_millis() as u64;
|
||||
let repo_sync_duration_ms = effective_repo_sync_duration_ms(
|
||||
repo_sync_started.elapsed().as_millis() as u64,
|
||||
runtime_repo_sync_duration_ms,
|
||||
repo_sync_ok,
|
||||
);
|
||||
crate::progress_log::emit(
|
||||
"publication_point_repo_sync_done",
|
||||
serde_json::json!({
|
||||
@ -1640,6 +1646,19 @@ fn repo_sync_source_label(source: crate::sync::repo::RepoSyncSource) -> &'static
|
||||
}
|
||||
}
|
||||
|
||||
fn effective_repo_sync_duration_ms(
|
||||
elapsed_ms: u64,
|
||||
runtime_reported_duration_ms: Option<u64>,
|
||||
repo_sync_ok: bool,
|
||||
) -> u64 {
|
||||
if repo_sync_ok {
|
||||
return elapsed_ms;
|
||||
}
|
||||
runtime_reported_duration_ms
|
||||
.map(|runtime_ms| elapsed_ms.max(runtime_ms))
|
||||
.unwrap_or(elapsed_ms)
|
||||
}
|
||||
|
||||
fn kind_from_vcir_artifact_kind(kind: VcirArtifactKind) -> AuditObjectKind {
|
||||
match kind {
|
||||
VcirArtifactKind::Mft => AuditObjectKind::Manifest,
|
||||
@ -6463,6 +6482,15 @@ authorityKeyIdentifier = keyid:always
|
||||
assert!(audit.objects.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_repo_sync_duration_uses_runtime_duration_for_failures() {
|
||||
assert_eq!(effective_repo_sync_duration_ms(0, Some(12), false), 12);
|
||||
assert_eq!(effective_repo_sync_duration_ms(5, Some(12), false), 12);
|
||||
assert_eq!(effective_repo_sync_duration_ms(20, Some(12), false), 20);
|
||||
assert_eq!(effective_repo_sync_duration_ms(5, None, false), 5);
|
||||
assert_eq!(effective_repo_sync_duration_ms(5, Some(12), true), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconstruct_snapshot_from_vcir_reports_missing_manifest_and_related_raw_bytes() {
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user