Add full internet table injection with background worker and progress tracking
Generates realistic IPv4 routing tables (1K-900K prefixes) with DFZ-like prefix length distribution, varied AS paths, and transit ASN diversity. Background injection with progress API, CLI follow mode, and Vue UI component with preset sizes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1f0936763b
commit
d691b512f9
@ -41,6 +41,7 @@
|
|||||||
<AnnounceForm v-else-if="activeTab === 'inject'" @routes-changed="fetchRoutes" />
|
<AnnounceForm v-else-if="activeTab === 'inject'" @routes-changed="fetchRoutes" />
|
||||||
<PeerStatus v-else-if="activeTab === 'peers'" :peers="peers" />
|
<PeerStatus v-else-if="activeTab === 'peers'" :peers="peers" />
|
||||||
<ChurnControl v-else-if="activeTab === 'churn'" />
|
<ChurnControl v-else-if="activeTab === 'churn'" />
|
||||||
|
<FullTable v-else-if="activeTab === 'full-table'" @routes-changed="fetchRoutes" />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@ -63,6 +64,7 @@ import RouteTable from './components/RouteTable.vue'
|
|||||||
import AnnounceForm from './components/AnnounceForm.vue'
|
import AnnounceForm from './components/AnnounceForm.vue'
|
||||||
import PeerStatus from './components/PeerStatus.vue'
|
import PeerStatus from './components/PeerStatus.vue'
|
||||||
import ChurnControl from './components/ChurnControl.vue'
|
import ChurnControl from './components/ChurnControl.vue'
|
||||||
|
import FullTable from './components/FullTable.vue'
|
||||||
|
|
||||||
const health = ref(null)
|
const health = ref(null)
|
||||||
const routes = ref([])
|
const routes = ref([])
|
||||||
@ -75,6 +77,7 @@ const tabs = [
|
|||||||
{ id: 'inject', label: 'Inject' },
|
{ id: 'inject', label: 'Inject' },
|
||||||
{ id: 'peers', label: 'Peers' },
|
{ id: 'peers', label: 'Peers' },
|
||||||
{ id: 'churn', label: 'Churn' },
|
{ id: 'churn', label: 'Churn' },
|
||||||
|
{ id: 'full-table', label: 'Full Table' },
|
||||||
]
|
]
|
||||||
|
|
||||||
async function fetchHealth() {
|
async function fetchHealth() {
|
||||||
|
|||||||
477
exabgp-ui/src/components/FullTable.vue
Normal file
477
exabgp-ui/src/components/FullTable.vue
Normal file
@ -0,0 +1,477 @@
|
|||||||
|
<template>
|
||||||
|
<div class="full-table">
|
||||||
|
<h2 class="section-title">Full Table Injection</h2>
|
||||||
|
<p class="section-desc">
|
||||||
|
Inject a realistic IPv4 routing table into ExaBGP for stress testing.
|
||||||
|
Routes are generated with varied AS paths, prefix lengths, and communities matching real DFZ distribution.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="config-card">
|
||||||
|
<!-- Level selector -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Table Size</label>
|
||||||
|
<div class="level-grid">
|
||||||
|
<button
|
||||||
|
v-for="level in levels"
|
||||||
|
:key="level.count"
|
||||||
|
class="level-btn"
|
||||||
|
:class="{ selected: selectedCount === level.count }"
|
||||||
|
:disabled="injecting"
|
||||||
|
@click="selectedCount = level.count"
|
||||||
|
>
|
||||||
|
<span class="level-count">{{ level.label }}</span>
|
||||||
|
<span class="level-desc">{{ level.desc }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom count -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Custom Count</label>
|
||||||
|
<input
|
||||||
|
v-model.number="selectedCount"
|
||||||
|
type="number"
|
||||||
|
min="100"
|
||||||
|
max="950000"
|
||||||
|
step="1000"
|
||||||
|
:disabled="injecting"
|
||||||
|
class="mono-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action buttons -->
|
||||||
|
<div class="action-row">
|
||||||
|
<button v-if="!injecting" class="btn-start" @click="startInjection" :disabled="!selectedCount">
|
||||||
|
<span>▶</span> Inject {{ formatNum(selectedCount) }} Routes
|
||||||
|
</button>
|
||||||
|
<button v-else class="btn-stop" @click="stopInjection">
|
||||||
|
<span>■</span> Stop Injection
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="!injecting && lastCompleted"
|
||||||
|
class="btn-withdraw"
|
||||||
|
@click="withdrawAll"
|
||||||
|
:disabled="withdrawing"
|
||||||
|
>
|
||||||
|
{{ withdrawing ? 'Withdrawing...' : 'Withdraw All' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status display -->
|
||||||
|
<div v-if="injecting || statusMsg" class="status-card">
|
||||||
|
<div class="status-header">
|
||||||
|
<span class="status-dot" :class="injecting ? 'dot-active' : 'dot-idle'"></span>
|
||||||
|
<span class="status-text">{{ statusMsg || 'Idle' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress bar -->
|
||||||
|
<div v-if="state.total > 0" class="progress-section">
|
||||||
|
<div class="progress-labels">
|
||||||
|
<span>{{ formatNum(state.injected) }} / {{ formatNum(state.total) }}</span>
|
||||||
|
<span>{{ state.progress_pct || 0 }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-track">
|
||||||
|
<div class="progress-fill" :style="{ width: (state.progress_pct || 0) + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats row -->
|
||||||
|
<div v-if="state.total > 0" class="stats-row">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Rate</span>
|
||||||
|
<span class="stat-val">{{ formatNum(state.rate_pps || 0) }}/s</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Elapsed</span>
|
||||||
|
<span class="stat-val">{{ state.elapsed_sec || 0 }}s</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Active Routes</span>
|
||||||
|
<span class="stat-val">{{ formatNum(state.active_routes || 0) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error -->
|
||||||
|
<div v-if="state.error" class="inject-error">{{ state.error }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onUnmounted } from 'vue'
|
||||||
|
import { api } from '../api.js'
|
||||||
|
|
||||||
|
const emit = defineEmits(['routes-changed'])
|
||||||
|
|
||||||
|
const levels = [
|
||||||
|
{ count: 1000, label: '1K', desc: 'Quick test' },
|
||||||
|
{ count: 10000, label: '10K', desc: 'Light load' },
|
||||||
|
{ count: 50000, label: '50K', desc: 'Medium load' },
|
||||||
|
{ count: 100000, label: '100K', desc: 'Stress test' },
|
||||||
|
{ count: 500000, label: '500K', desc: 'Heavy load' },
|
||||||
|
{ count: 900000, label: '900K', desc: 'Full DFZ' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const selectedCount = ref(10000)
|
||||||
|
const injecting = ref(false)
|
||||||
|
const statusMsg = ref('')
|
||||||
|
const lastCompleted = ref(false)
|
||||||
|
const withdrawing = ref(false)
|
||||||
|
const state = ref({})
|
||||||
|
|
||||||
|
let pollTimer = null
|
||||||
|
|
||||||
|
function formatNum(n) {
|
||||||
|
if (n == null) return '0'
|
||||||
|
return Number(n).toLocaleString()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startInjection() {
|
||||||
|
try {
|
||||||
|
statusMsg.value = 'Starting injection...'
|
||||||
|
injecting.value = true
|
||||||
|
lastCompleted.value = false
|
||||||
|
state.value = {}
|
||||||
|
await api.fullTableStart(selectedCount.value, 1000)
|
||||||
|
startPolling()
|
||||||
|
} catch (e) {
|
||||||
|
statusMsg.value = `Start failed: ${e.message}`
|
||||||
|
injecting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopInjection() {
|
||||||
|
try {
|
||||||
|
await api.fullTableStop()
|
||||||
|
statusMsg.value = 'Stop requested...'
|
||||||
|
} catch (e) {
|
||||||
|
statusMsg.value = `Stop failed: ${e.message}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function withdrawAll() {
|
||||||
|
withdrawing.value = true
|
||||||
|
try {
|
||||||
|
const data = await api.withdrawAll()
|
||||||
|
statusMsg.value = `Withdrew ${data.count} routes`
|
||||||
|
lastCompleted.value = false
|
||||||
|
state.value = {}
|
||||||
|
emit('routes-changed')
|
||||||
|
} catch (e) {
|
||||||
|
statusMsg.value = `Withdraw failed: ${e.message}`
|
||||||
|
} finally {
|
||||||
|
withdrawing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPolling() {
|
||||||
|
stopPolling()
|
||||||
|
pollStatus()
|
||||||
|
pollTimer = setInterval(pollStatus, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPolling() {
|
||||||
|
if (pollTimer) {
|
||||||
|
clearInterval(pollTimer)
|
||||||
|
pollTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollStatus() {
|
||||||
|
try {
|
||||||
|
const data = await api.fullTableStatus()
|
||||||
|
state.value = data
|
||||||
|
|
||||||
|
if (data.active) {
|
||||||
|
statusMsg.value = `Injecting: ${formatNum(data.injected)} / ${formatNum(data.total)} (${data.rate_pps || 0}/s)`
|
||||||
|
} else if (data.error) {
|
||||||
|
statusMsg.value = `Error: ${data.error}`
|
||||||
|
injecting.value = false
|
||||||
|
stopPolling()
|
||||||
|
} else if (data.injected > 0) {
|
||||||
|
statusMsg.value = `Complete: ${formatNum(data.injected)} routes in ${data.elapsed_sec}s (${data.rate_pps}/s)`
|
||||||
|
injecting.value = false
|
||||||
|
lastCompleted.value = true
|
||||||
|
stopPolling()
|
||||||
|
emit('routes-changed')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// keep polling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopPolling()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.full-table {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18px;
|
||||||
|
max-width: 680px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-desc {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-top: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 12px 8px;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
color: var(--text);
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-btn:hover:not(:disabled) {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: rgba(79, 156, 249, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-btn.selected {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: rgba(79, 156, 249, 0.15);
|
||||||
|
box-shadow: 0 0 0 1px var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-count {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-desc {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mono-input {
|
||||||
|
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 7px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mono-input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
padding-top: 4px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-start {
|
||||||
|
padding: 9px 22px;
|
||||||
|
background: rgba(72, 187, 120, 0.15);
|
||||||
|
color: #48bb78;
|
||||||
|
border: 1px solid rgba(72, 187, 120, 0.3);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-start:hover:not(:disabled) {
|
||||||
|
background: rgba(72, 187, 120, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-stop {
|
||||||
|
padding: 9px 22px;
|
||||||
|
background: rgba(252, 129, 129, 0.15);
|
||||||
|
color: #fc8181;
|
||||||
|
border: 1px solid rgba(252, 129, 129, 0.3);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
animation: pulse-border 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-stop:hover {
|
||||||
|
background: rgba(252, 129, 129, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-withdraw {
|
||||||
|
padding: 9px 18px;
|
||||||
|
background: rgba(246, 173, 85, 0.15);
|
||||||
|
color: #f6ad55;
|
||||||
|
border: 1px solid rgba(246, 173, 85, 0.3);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-withdraw:hover:not(:disabled) {
|
||||||
|
background: rgba(246, 173, 85, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 16px 18px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-active {
|
||||||
|
background: #48bb78;
|
||||||
|
box-shadow: 0 0 8px #48bb78;
|
||||||
|
animation: pulse-dot 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot-idle {
|
||||||
|
background: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-labels {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-track {
|
||||||
|
height: 6px;
|
||||||
|
background: var(--border);
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--accent);
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-val {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text);
|
||||||
|
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inject-error {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #fc8181;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: rgba(252, 129, 129, 0.08);
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid rgba(252, 129, 129, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-dot {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-border {
|
||||||
|
0%, 100% { border-color: rgba(252, 129, 129, 0.3); }
|
||||||
|
50% { border-color: rgba(252, 129, 129, 0.6); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -48,11 +48,15 @@ peer_states = {}
|
|||||||
# ExaBGP command helpers
|
# ExaBGP command helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_quiet_mode = False
|
||||||
|
|
||||||
|
|
||||||
def _send(cmd: str):
|
def _send(cmd: str):
|
||||||
"""Write a command to ExaBGP via stdout."""
|
"""Write a command to ExaBGP via stdout."""
|
||||||
with _stdout_lock:
|
with _stdout_lock:
|
||||||
sys.stdout.write(cmd + '\n')
|
sys.stdout.write(cmd + '\n')
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
if not _quiet_mode:
|
||||||
log.info('→ ExaBGP: %s', cmd)
|
log.info('→ ExaBGP: %s', cmd)
|
||||||
|
|
||||||
|
|
||||||
@ -162,7 +166,22 @@ def api_withdraw_all():
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
sys.path.insert(0, '/exabgp')
|
sys.path.insert(0, '/exabgp')
|
||||||
from scenarios import SCENARIOS
|
from scenarios import SCENARIOS, generate_full_internet
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Full-table background injection
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_injection_state = {
|
||||||
|
'active': False,
|
||||||
|
'total': 0,
|
||||||
|
'injected': 0,
|
||||||
|
'elapsed_sec': 0,
|
||||||
|
'rate_pps': 0,
|
||||||
|
'error': None,
|
||||||
|
'stop_requested': False,
|
||||||
|
}
|
||||||
|
_injection_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
@app.route('/scenarios', methods=['GET'])
|
@app.route('/scenarios', methods=['GET'])
|
||||||
@ -223,6 +242,131 @@ def get_peers():
|
|||||||
return jsonify({'peers': peer_states})
|
return jsonify({'peers': peer_states})
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Full-table injection endpoints
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _injection_worker(count, batch_size):
|
||||||
|
"""Background thread: generate and inject full internet table."""
|
||||||
|
global _quiet_mode
|
||||||
|
try:
|
||||||
|
_quiet_mode = True # suppress per-route logging
|
||||||
|
log.info('Generating %d full-table prefixes...', count)
|
||||||
|
routes = generate_full_internet(count)
|
||||||
|
with _injection_lock:
|
||||||
|
_injection_state['total'] = len(routes)
|
||||||
|
log.info('Generated %d routes, starting injection at batch_size=%d', len(routes), batch_size)
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
for i, route in enumerate(routes):
|
||||||
|
with _injection_lock:
|
||||||
|
if _injection_state['stop_requested']:
|
||||||
|
log.info('Injection stopped by user at %d/%d', i, len(routes))
|
||||||
|
break
|
||||||
|
|
||||||
|
prefix = route['prefix']
|
||||||
|
announce_route(
|
||||||
|
prefix,
|
||||||
|
next_hop=route.get('next_hop', 'self'),
|
||||||
|
as_path=route.get('as_path', []),
|
||||||
|
communities=route.get('communities', []),
|
||||||
|
med=route.get('med'),
|
||||||
|
local_pref=route.get('local_pref'),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update progress periodically (every batch_size routes)
|
||||||
|
if (i + 1) % batch_size == 0:
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
with _injection_lock:
|
||||||
|
_injection_state['injected'] = i + 1
|
||||||
|
_injection_state['elapsed_sec'] = round(elapsed, 1)
|
||||||
|
_injection_state['rate_pps'] = round((i + 1) / elapsed, 1) if elapsed > 0 else 0
|
||||||
|
log.info('Injection progress: %d/%d (%.0f/s)',
|
||||||
|
i + 1, len(routes), (i + 1) / elapsed if elapsed > 0 else 0)
|
||||||
|
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
with _injection_lock:
|
||||||
|
_injection_state['injected'] = min(i + 1, len(routes))
|
||||||
|
_injection_state['elapsed_sec'] = round(elapsed, 1)
|
||||||
|
_injection_state['rate_pps'] = round(_injection_state['injected'] / elapsed, 1) if elapsed > 0 else 0
|
||||||
|
_injection_state['active'] = False
|
||||||
|
log.info('Injection complete: %d routes in %.1fs (%.0f/s)',
|
||||||
|
_injection_state['injected'], elapsed,
|
||||||
|
_injection_state['injected'] / elapsed if elapsed > 0 else 0)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error('Injection error: %s', e)
|
||||||
|
with _injection_lock:
|
||||||
|
_injection_state['error'] = str(e)
|
||||||
|
_injection_state['active'] = False
|
||||||
|
finally:
|
||||||
|
_quiet_mode = False
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/full-table/start', methods=['POST'])
|
||||||
|
def start_full_table():
|
||||||
|
"""Start background injection of a full IPv4 routing table.
|
||||||
|
|
||||||
|
POST body (all optional):
|
||||||
|
count: Number of prefixes (default 900000)
|
||||||
|
batch_size: Progress update interval (default 1000)
|
||||||
|
"""
|
||||||
|
with _injection_lock:
|
||||||
|
if _injection_state['active']:
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Injection already in progress',
|
||||||
|
'state': dict(_injection_state),
|
||||||
|
}), 409
|
||||||
|
|
||||||
|
data = request.get_json(force=True) if request.data else {}
|
||||||
|
count = int(data.get('count', 900000))
|
||||||
|
batch_size = int(data.get('batch_size', 1000))
|
||||||
|
|
||||||
|
with _injection_lock:
|
||||||
|
_injection_state.update({
|
||||||
|
'active': True,
|
||||||
|
'total': count,
|
||||||
|
'injected': 0,
|
||||||
|
'elapsed_sec': 0,
|
||||||
|
'rate_pps': 0,
|
||||||
|
'error': None,
|
||||||
|
'stop_requested': False,
|
||||||
|
})
|
||||||
|
|
||||||
|
t = threading.Thread(target=_injection_worker, args=(count, batch_size), daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
log.info('Started full-table injection: %d prefixes', count)
|
||||||
|
return jsonify({
|
||||||
|
'status': 'started',
|
||||||
|
'count': count,
|
||||||
|
'message': f'Generating and injecting {count} prefixes in background. GET /full-table/status to track progress.',
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/full-table/status', methods=['GET'])
|
||||||
|
def full_table_status():
|
||||||
|
"""Get current full-table injection progress."""
|
||||||
|
with _injection_lock:
|
||||||
|
state = dict(_injection_state)
|
||||||
|
if state['total'] > 0:
|
||||||
|
state['progress_pct'] = round(state['injected'] / state['total'] * 100, 1)
|
||||||
|
else:
|
||||||
|
state['progress_pct'] = 0
|
||||||
|
state['active_routes'] = len(active_routes)
|
||||||
|
return jsonify(state)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/full-table/stop', methods=['POST'])
|
||||||
|
def stop_full_table():
|
||||||
|
"""Stop an in-progress full-table injection."""
|
||||||
|
with _injection_lock:
|
||||||
|
if not _injection_state['active']:
|
||||||
|
return jsonify({'error': 'No injection in progress'}), 400
|
||||||
|
_injection_state['stop_requested'] = True
|
||||||
|
return jsonify({'status': 'stop_requested', 'injected_so_far': _injection_state['injected']})
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# ExaBGP event loop (main thread)
|
# ExaBGP event loop (main thread)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
116
exabgp/inject.py
116
exabgp/inject.py
@ -12,6 +12,9 @@ Usage:
|
|||||||
inject.py withdraw-all
|
inject.py withdraw-all
|
||||||
inject.py scenario <name>
|
inject.py scenario <name>
|
||||||
inject.py withdraw-scenario <name>
|
inject.py withdraw-scenario <name>
|
||||||
|
inject.py full-table [--count N] [--follow] # inject full IPv4 table (background)
|
||||||
|
inject.py full-table-status # show injection progress
|
||||||
|
inject.py full-table-stop # stop injection
|
||||||
inject.py churn [--count N] [--interval SEC] # cycle announce/withdraw for ip_rib_log population
|
inject.py churn [--count N] [--interval SEC] # cycle announce/withdraw for ip_rib_log population
|
||||||
inject.py monitor # live-refresh terminal view
|
inject.py monitor # live-refresh terminal view
|
||||||
|
|
||||||
@ -29,8 +32,8 @@ import requests
|
|||||||
API = os.environ.get('EXABGP_API', 'http://localhost:5050')
|
API = os.environ.get('EXABGP_API', 'http://localhost:5050')
|
||||||
|
|
||||||
|
|
||||||
def _post(path, data=None):
|
def _post(path, data=None, timeout=10):
|
||||||
r = requests.post(f'{API}{path}', json=data or {}, timeout=10)
|
r = requests.post(f'{API}{path}', json=data or {}, timeout=timeout)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
@ -174,6 +177,101 @@ def cmd_withdraw_scenario(args):
|
|||||||
print(f"Withdrew scenario '{args.name}': {data['count']} routes withdrawn")
|
print(f"Withdrew scenario '{args.name}': {data['count']} routes withdrawn")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_full_table(args):
|
||||||
|
"""Inject a full IPv4 routing table for stress testing."""
|
||||||
|
count = args.count
|
||||||
|
print(f"Starting full-table injection: {count} prefixes")
|
||||||
|
print("This generates routes in background. Use 'inject.py full-table-status' to track.\n")
|
||||||
|
|
||||||
|
data = _post('/full-table/start', {'count': count, 'batch_size': args.batch_size}, timeout=120)
|
||||||
|
print(data.get('message', 'Started'))
|
||||||
|
|
||||||
|
if args.follow:
|
||||||
|
print()
|
||||||
|
_follow_injection()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_full_table_status(args):
|
||||||
|
"""Show full-table injection progress."""
|
||||||
|
data = _get('/full-table/status')
|
||||||
|
active = data.get('active', False)
|
||||||
|
total = data.get('total', 0)
|
||||||
|
injected = data.get('injected', 0)
|
||||||
|
pct = data.get('progress_pct', 0)
|
||||||
|
rate = data.get('rate_pps', 0)
|
||||||
|
elapsed = data.get('elapsed_sec', 0)
|
||||||
|
error = data.get('error')
|
||||||
|
active_routes = data.get('active_routes', 0)
|
||||||
|
|
||||||
|
if error:
|
||||||
|
print(f"ERROR: {error}")
|
||||||
|
elif active:
|
||||||
|
bar_len = 40
|
||||||
|
filled = int(bar_len * pct / 100)
|
||||||
|
bar = '#' * filled + '-' * (bar_len - filled)
|
||||||
|
print(f"[{bar}] {pct:.1f}%")
|
||||||
|
print(f" Injected: {injected:,} / {total:,} ({rate:.0f} routes/s)")
|
||||||
|
print(f" Elapsed: {elapsed:.0f}s")
|
||||||
|
print(f" Active routes in ExaBGP: {active_routes:,}")
|
||||||
|
elif total > 0:
|
||||||
|
print(f"Injection complete: {injected:,} / {total:,} routes in {elapsed:.0f}s ({rate:.0f}/s)")
|
||||||
|
print(f"Active routes in ExaBGP: {active_routes:,}")
|
||||||
|
else:
|
||||||
|
print("No injection running or completed.")
|
||||||
|
print(f"Active routes: {active_routes:,}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_full_table_stop(args):
|
||||||
|
"""Stop an in-progress full-table injection."""
|
||||||
|
try:
|
||||||
|
data = _post('/full-table/stop')
|
||||||
|
print(f"Stop requested. Injected so far: {data.get('injected_so_far', '?'):,}")
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
if e.response.status_code == 400:
|
||||||
|
print("No injection in progress.")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _follow_injection():
|
||||||
|
"""Poll injection status until complete."""
|
||||||
|
import shutil
|
||||||
|
lines_printed = 0
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = _get('/full-table/status')
|
||||||
|
active = data.get('active', False)
|
||||||
|
total = data.get('total', 0)
|
||||||
|
injected = data.get('injected', 0)
|
||||||
|
pct = data.get('progress_pct', 0)
|
||||||
|
rate = data.get('rate_pps', 0)
|
||||||
|
elapsed = data.get('elapsed_sec', 0)
|
||||||
|
active_routes = data.get('active_routes', 0)
|
||||||
|
|
||||||
|
# Move cursor up to overwrite
|
||||||
|
if lines_printed > 0:
|
||||||
|
print(f"\033[{lines_printed}A", end='')
|
||||||
|
|
||||||
|
bar_len = 40
|
||||||
|
filled = int(bar_len * pct / 100)
|
||||||
|
bar = '#' * filled + '-' * (bar_len - filled)
|
||||||
|
output_lines = [
|
||||||
|
f" [{bar}] {pct:.1f}%",
|
||||||
|
f" Injected: {injected:,} / {total:,} ({rate:.0f} routes/s) elapsed: {elapsed:.0f}s",
|
||||||
|
f" Active routes: {active_routes:,}",
|
||||||
|
]
|
||||||
|
print('\n'.join(output_lines))
|
||||||
|
lines_printed = len(output_lines)
|
||||||
|
|
||||||
|
if not active:
|
||||||
|
print(f"\nDone! {injected:,} routes injected in {elapsed:.0f}s")
|
||||||
|
break
|
||||||
|
|
||||||
|
time.sleep(2)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\nFollowing stopped (injection continues in background).")
|
||||||
|
|
||||||
|
|
||||||
def cmd_churn(args):
|
def cmd_churn(args):
|
||||||
"""
|
"""
|
||||||
Cycle announce/withdraw on the 'churn' scenario to generate ip_rib_log
|
Cycle announce/withdraw on the 'churn' scenario to generate ip_rib_log
|
||||||
@ -236,6 +334,17 @@ def main():
|
|||||||
p = sub.add_parser('withdraw-scenario', help='Withdraw a named scenario')
|
p = sub.add_parser('withdraw-scenario', help='Withdraw a named scenario')
|
||||||
p.add_argument('name')
|
p.add_argument('name')
|
||||||
|
|
||||||
|
p = sub.add_parser('full-table', help='Inject full IPv4 routing table (background)')
|
||||||
|
p.add_argument('--count', type=int, default=900000, metavar='N',
|
||||||
|
help='Number of prefixes to inject (default: 900000)')
|
||||||
|
p.add_argument('--batch-size', type=int, default=1000, metavar='N',
|
||||||
|
help='Progress update interval (default: 1000)')
|
||||||
|
p.add_argument('--follow', '-f', action='store_true',
|
||||||
|
help='Follow progress until complete')
|
||||||
|
|
||||||
|
sub.add_parser('full-table-status', help='Show full-table injection progress')
|
||||||
|
sub.add_parser('full-table-stop', help='Stop full-table injection')
|
||||||
|
|
||||||
p = sub.add_parser('churn', help='Cycle announce/withdraw to populate ip_rib_log')
|
p = sub.add_parser('churn', help='Cycle announce/withdraw to populate ip_rib_log')
|
||||||
p.add_argument('--count', type=int, default=0, metavar='N',
|
p.add_argument('--count', type=int, default=0, metavar='N',
|
||||||
help='Number of cycles (0 = infinite)')
|
help='Number of cycles (0 = infinite)')
|
||||||
@ -255,6 +364,9 @@ def main():
|
|||||||
'withdraw-all': cmd_withdraw_all,
|
'withdraw-all': cmd_withdraw_all,
|
||||||
'scenario': cmd_scenario,
|
'scenario': cmd_scenario,
|
||||||
'withdraw-scenario': cmd_withdraw_scenario,
|
'withdraw-scenario': cmd_withdraw_scenario,
|
||||||
|
'full-table': cmd_full_table,
|
||||||
|
'full-table-status': cmd_full_table_status,
|
||||||
|
'full-table-stop': cmd_full_table_stop,
|
||||||
'churn': cmd_churn,
|
'churn': cmd_churn,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -441,6 +441,100 @@ _PATH_DIVERSITY_ROUTES = [
|
|||||||
# Registry
|
# Registry
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Full Internet Table Generator
|
||||||
|
# Generates realistic-looking IPv4 prefixes across the routable address space
|
||||||
|
# with varied AS paths, prefix lengths, origins, and communities.
|
||||||
|
# Configurable count: 10K (quick test) to 900K+ (full table stress test).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Well-known transit ASNs for realistic path construction
|
||||||
|
_TRANSIT_ASNS = [174, 701, 1299, 2914, 3257, 3356, 6461, 6762, 7018, 3491, 5400, 1239]
|
||||||
|
|
||||||
|
# Realistic origin ASNs (mix of large providers and small networks)
|
||||||
|
_ORIGIN_POOL = [
|
||||||
|
13335, 15169, 16509, 8075, 20940, 32934, 714, 54113, 13414, 7922,
|
||||||
|
36459, 46489, 14618, 16276, 24940, 47541, 35916, 49981, 9808, 4134,
|
||||||
|
4837, 9121, 12322, 3320, 6830, 5511, 1273, 6939, 4766, 9318,
|
||||||
|
23693, 38001, 45102, 58453, 10026, 18881, 28573, 7738, 26599, 8151,
|
||||||
|
11888, 17676, 4713, 7545, 9299, 50304, 51167, 60068, 41095, 34984,
|
||||||
|
]
|
||||||
|
|
||||||
|
# IANA-allocated first octets for routable IPv4 (subset for realism)
|
||||||
|
_ROUTABLE_FIRST_OCTETS = list(range(1, 56)) + list(range(57, 127)) + list(range(128, 224))
|
||||||
|
|
||||||
|
|
||||||
|
def generate_full_internet(count=900000):
|
||||||
|
"""Generate a realistic full IPv4 routing table.
|
||||||
|
|
||||||
|
Distributes prefixes across the IPv4 address space with realistic
|
||||||
|
prefix lengths (/8 through /24) and varied AS paths.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
count: Number of prefixes to generate (default 900K).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of route dicts.
|
||||||
|
"""
|
||||||
|
import random
|
||||||
|
rng = random.Random(42) # deterministic for reproducibility
|
||||||
|
|
||||||
|
routes = []
|
||||||
|
generated = set()
|
||||||
|
|
||||||
|
# Prefix length distribution (approximates real DFZ):
|
||||||
|
# /24: ~55%, /23: ~8%, /22: ~7%, /21: ~5%, /20: ~5%,
|
||||||
|
# /19: ~4%, /18: ~3%, /17: ~2%, /16: ~5%, /15-/8: ~6%
|
||||||
|
prefix_len_weights = {
|
||||||
|
24: 55, 23: 8, 22: 7, 21: 5, 20: 5,
|
||||||
|
19: 4, 18: 3, 17: 2, 16: 5, 15: 2,
|
||||||
|
14: 1, 13: 1, 12: 1, 11: 0.5, 10: 0.3,
|
||||||
|
9: 0.1, 8: 0.1,
|
||||||
|
}
|
||||||
|
plen_choices = list(prefix_len_weights.keys())
|
||||||
|
plen_weights = list(prefix_len_weights.values())
|
||||||
|
|
||||||
|
# AS path length distribution: 1-hop: 5%, 2-hop: 30%, 3-hop: 40%, 4-hop: 20%, 5-hop: 5%
|
||||||
|
path_len_weights = [5, 30, 40, 20, 5]
|
||||||
|
|
||||||
|
while len(routes) < count:
|
||||||
|
# Pick a routable first octet weighted by allocation density
|
||||||
|
first = rng.choice(_ROUTABLE_FIRST_OCTETS)
|
||||||
|
plen = rng.choices(plen_choices, weights=plen_weights, k=1)[0]
|
||||||
|
|
||||||
|
# Generate random prefix within this /8
|
||||||
|
if plen <= 8:
|
||||||
|
prefix = f'{first}.0.0.0/{plen}'
|
||||||
|
elif plen <= 16:
|
||||||
|
second = rng.randint(0, 255) & (0xFF << (16 - plen))
|
||||||
|
prefix = f'{first}.{second}.0.0/{plen}'
|
||||||
|
elif plen <= 24:
|
||||||
|
second = rng.randint(0, 255)
|
||||||
|
third = rng.randint(0, 255) & (0xFF << (24 - plen))
|
||||||
|
prefix = f'{first}.{second}.{third}.0/{plen}'
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if prefix in generated:
|
||||||
|
continue
|
||||||
|
generated.add(prefix)
|
||||||
|
|
||||||
|
# Build realistic AS path
|
||||||
|
path_len = rng.choices([1, 2, 3, 4, 5], weights=path_len_weights, k=1)[0]
|
||||||
|
origin = rng.choice(_ORIGIN_POOL) if rng.random() < 0.3 else (64512 + rng.randint(0, 65535 - 64512))
|
||||||
|
transits = rng.sample(_TRANSIT_ASNS, min(path_len - 1, len(_TRANSIT_ASNS)))
|
||||||
|
as_path = [65100] + transits[:path_len - 1] + [origin]
|
||||||
|
|
||||||
|
# Occasionally add communities (~20% of routes)
|
||||||
|
communities = []
|
||||||
|
if rng.random() < 0.2:
|
||||||
|
communities.append(f'65100:{rng.choice([100, 200, 300, 400, 500])}')
|
||||||
|
|
||||||
|
routes.append(_r(prefix, as_path, communities=communities or None))
|
||||||
|
|
||||||
|
return routes
|
||||||
|
|
||||||
|
|
||||||
SCENARIOS = {
|
SCENARIOS = {
|
||||||
'internet_sample': {
|
'internet_sample': {
|
||||||
'description': 'Partial internet table (~80 IPv4 + 14 IPv6 prefixes with realistic AS paths)',
|
'description': 'Partial internet table (~80 IPv4 + 14 IPv6 prefixes with realistic AS paths)',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user